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 ball_has_been_hit: Option<bool>,
18 pub kickoff_countdown_time: Option<i32>,
19 pub gameplay_phase: GameplayPhase,
20 pub is_live_play: bool,
21 pub modules: Modules,
22}
23
24pub type StatsSnapshotFrame = CapturedStatsFrame<Map<String, Value>>;
25
26#[derive(Debug, Clone, PartialEq, Serialize)]
27pub struct CapturedStatsData<Frame> {
28 pub replay_meta: ReplayMeta,
29 pub config: Map<String, Value>,
30 pub modules: Map<String, Value>,
31 pub frames: Vec<Frame>,
32}
33
34pub type StatsSnapshotData = CapturedStatsData<StatsSnapshotFrame>;
35
36impl<Modules> CapturedStatsFrame<Modules> {
37 pub fn map_modules<Mapped, F>(
38 self,
39 transform: F,
40 ) -> SubtrActorResult<CapturedStatsFrame<Mapped>>
41 where
42 F: FnOnce(Modules) -> SubtrActorResult<Mapped>,
43 {
44 Ok(CapturedStatsFrame {
45 frame_number: self.frame_number,
46 time: self.time,
47 dt: self.dt,
48 seconds_remaining: self.seconds_remaining,
49 game_state: self.game_state,
50 ball_has_been_hit: self.ball_has_been_hit,
51 kickoff_countdown_time: self.kickoff_countdown_time,
52 gameplay_phase: self.gameplay_phase,
53 is_live_play: self.is_live_play,
54 modules: transform(self.modules)?,
55 })
56 }
57}
58
59impl CapturedStatsData<StatsSnapshotFrame> {
60 pub fn into_legacy_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
61 self.to_legacy_replay_stats_timeline()
62 }
63
64 #[deprecated(
65 note = "use into_legacy_replay_stats_timeline for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
66 )]
67 pub fn into_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
68 self.into_legacy_replay_stats_timeline()
69 }
70
71 pub fn into_legacy_replay_stats_timeline_with_progress<F>(
72 self,
73 frame_interval: usize,
74 mut on_progress: F,
75 ) -> SubtrActorResult<ReplayStatsTimeline>
76 where
77 F: FnMut(usize, usize) -> SubtrActorResult<()>,
78 {
79 let frame_interval = frame_interval.max(1);
80 let total_frames = self.frames.len();
81 on_progress(0, total_frames)?;
82 let frames = self
83 .frames
84 .iter()
85 .enumerate()
86 .map(|(frame_index, frame)| {
87 let replay_frame = self.replay_stats_frame(frame)?;
88 let processed_frames = frame_index + 1;
89 if processed_frames == total_frames
90 || processed_frames.is_multiple_of(frame_interval)
91 {
92 on_progress(processed_frames, total_frames)?;
93 }
94 Ok(replay_frame)
95 })
96 .collect::<SubtrActorResult<Vec<_>>>()?;
97 self.to_replay_stats_timeline_with_frames(frames)
98 }
99
100 #[deprecated(
101 note = "use into_legacy_replay_stats_timeline_with_progress for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
102 )]
103 pub fn into_stats_timeline_with_progress<F>(
104 self,
105 frame_interval: usize,
106 on_progress: F,
107 ) -> SubtrActorResult<ReplayStatsTimeline>
108 where
109 F: FnMut(usize, usize) -> SubtrActorResult<()>,
110 {
111 self.into_legacy_replay_stats_timeline_with_progress(frame_interval, on_progress)
112 }
113
114 pub fn to_legacy_replay_stats_timeline(&self) -> SubtrActorResult<ReplayStatsTimeline> {
115 self.to_replay_stats_timeline_with_frames(
116 self.frames
117 .iter()
118 .map(|frame| self.replay_stats_frame(frame))
119 .collect::<SubtrActorResult<Vec<_>>>()?,
120 )
121 }
122
123 #[deprecated(
124 note = "use to_legacy_replay_stats_timeline for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
125 )]
126 pub fn to_stats_timeline(&self) -> SubtrActorResult<ReplayStatsTimeline> {
127 self.to_legacy_replay_stats_timeline()
128 }
129
130 pub(crate) fn into_replay_stats_timeline_with_frames(
131 self,
132 frames: Vec<ReplayStatsFrame>,
133 ) -> SubtrActorResult<ReplayStatsTimeline> {
134 self.to_replay_stats_timeline_with_frames(frames)
135 }
136
137 fn to_replay_stats_timeline_with_frames(
138 &self,
139 frames: Vec<ReplayStatsFrame>,
140 ) -> SubtrActorResult<ReplayStatsTimeline> {
141 Ok(ReplayStatsTimeline {
142 config: self.timeline_config(),
143 replay_meta: self.replay_meta.clone(),
144 events: self.timeline_event_sets_typed()?,
145 frames,
146 })
147 }
148
149 pub fn into_legacy_stats_timeline_value(self) -> SubtrActorResult<Value> {
150 self.to_legacy_stats_timeline_value()
151 }
152
153 #[deprecated(
154 note = "use into_legacy_stats_timeline_value for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
155 )]
156 pub fn into_stats_timeline_value(self) -> SubtrActorResult<Value> {
157 self.into_legacy_stats_timeline_value()
158 }
159
160 pub fn to_legacy_stats_timeline_value(&self) -> SubtrActorResult<Value> {
161 let mut timeline = Map::new();
162 timeline.insert("config".to_owned(), self.timeline_config_value()?);
163 timeline.insert(
164 "replay_meta".to_owned(),
165 serialize_to_json_value(&self.replay_meta)?,
166 );
167 timeline.insert("events".to_owned(), self.timeline_event_sets_value());
168 timeline.insert(
169 "frames".to_owned(),
170 Value::Array(
171 self.frames
172 .iter()
173 .map(|frame| self.timeline_frame_value(frame))
174 .collect::<SubtrActorResult<Vec<_>>>()?,
175 ),
176 );
177 Ok(Value::Object(timeline))
178 }
179
180 #[deprecated(
181 note = "use to_legacy_stats_timeline_value for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
182 )]
183 pub fn to_stats_timeline_value(&self) -> SubtrActorResult<Value> {
184 self.to_legacy_stats_timeline_value()
185 }
186
187 fn timeline_events(&self) -> Vec<Value> {
188 let mut events = self.module_array("core", "timeline");
189 events.extend(self.module_array("demo", "timeline"));
190 events.sort_by(|left, right| {
191 let left_time = left.get("time").and_then(Value::as_f64).unwrap_or(0.0);
192 let right_time = right.get("time").and_then(Value::as_f64).unwrap_or(0.0);
193 left_time.total_cmp(&right_time)
194 });
195 events
196 }
197
198 fn timeline_events_typed(&self) -> SubtrActorResult<Vec<TimelineEvent>> {
199 self.timeline_events()
200 .iter()
201 .map(parse_timeline_event)
202 .collect()
203 }
204
205 fn goal_tag_events_typed(&self) -> SubtrActorResult<Vec<GoalTagEvent>> {
206 let mut events = Vec::new();
207 for module_name in [
208 "aerial_goal",
209 "high_aerial_goal",
210 "long_distance_goal",
211 "own_half_goal",
212 "empty_net_goal",
213 "counter_attack_goal",
214 "flick_goal",
215 "double_tap_goal",
216 "one_timer_goal",
217 "passing_goal",
218 "air_dribble_goal",
219 "flip_reset_goal",
220 "half_volley_goal",
221 ] {
222 events.extend(self.module_player_events(
223 module_name,
224 "events",
225 parse_goal_tag_event,
226 )?);
227 }
228 events.sort_by(|left, right| {
229 left.time
230 .total_cmp(&right.time)
231 .then_with(|| left.frame.cmp(&right.frame))
232 .then_with(|| left.goal_index.cmp(&right.goal_index))
233 .then_with(|| format!("{:?}", left.kind).cmp(&format!("{:?}", right.kind)))
234 });
235 Ok(events)
236 }
237
238 fn mechanic_events_typed(&self) -> SubtrActorResult<Vec<MechanicEvent>> {
239 let mut events = Vec::new();
240
241 for (index, value) in self.module_array("ball_carry", "events").iter().enumerate() {
242 events.push(parse_ball_carry_mechanic_event(value, index)?);
243 }
244 for (index, value) in self
245 .module_array("ceiling_shot", "events")
246 .iter()
247 .enumerate()
248 {
249 let event = parse_ceiling_shot_event(value)?;
250 events.push(span_mechanic_event(
251 "ceiling_shot",
252 index,
253 event.ceiling_contact_frame,
254 event.frame,
255 event.ceiling_contact_time,
256 event.time,
257 event.player,
258 event.is_team_0,
259 ));
260 }
261 for (index, value) in self
262 .module_array("wall_aerial", "events")
263 .iter()
264 .enumerate()
265 {
266 let event = parse_wall_aerial_event(value)?;
267 let mut mechanic_event = span_mechanic_event(
268 "wall_aerial",
269 index,
270 event.wall_contact_frame,
271 event.frame,
272 event.wall_contact_time,
273 event.time,
274 event.player,
275 event.is_team_0,
276 );
277 mechanic_event.properties = vec![mechanic_event_text_property(
278 "wall",
279 event.wall.as_label_value(),
280 )];
281 events.push(mechanic_event);
282 }
283 for (index, value) in self
284 .module_array("wall_aerial_shot", "events")
285 .iter()
286 .enumerate()
287 {
288 let event = parse_wall_aerial_shot_event(value)?;
289 let mut mechanic_event = span_mechanic_event(
290 "wall_aerial_shot",
291 index,
292 event.wall_contact_frame,
293 event.frame,
294 event.wall_contact_time,
295 event.time,
296 event.player,
297 event.is_team_0,
298 );
299 mechanic_event.properties = vec![mechanic_event_text_property(
300 "wall",
301 event.wall.as_label_value(),
302 )];
303 events.push(mechanic_event);
304 }
305 for (index, value) in self.module_array("center", "events").iter().enumerate() {
306 let event = parse_center_event(value)?;
307 events.push(span_mechanic_event(
308 "center",
309 index,
310 event.start_frame,
311 event.frame,
312 event.start_time,
313 event.time,
314 event.player,
315 event.is_team_0,
316 ));
317 }
318 for (index, value) in self
319 .module_array("dodge_reset", "on_ball_events")
320 .iter()
321 .enumerate()
322 {
323 events.push(parse_dodge_reset_mechanic_event(value, index)?);
324 }
325 for (index, value) in self.module_array("double_tap", "events").iter().enumerate() {
326 let event = parse_double_tap_event(value)?;
327 events.push(span_mechanic_event(
328 "double_tap",
329 index,
330 event.backboard_frame,
331 event.frame,
332 event.backboard_time,
333 event.time,
334 event.player,
335 event.is_team_0,
336 ));
337 }
338 for (index, value) in self.module_array("flick", "events").iter().enumerate() {
339 events.push(parse_flick_mechanic_event(value, index)?);
340 }
341 for (index, value) in self
342 .module_array("musty_flick", "events")
343 .iter()
344 .enumerate()
345 {
346 events.push(parse_musty_flick_mechanic_event(value, index)?);
347 }
348 for (index, value) in self.module_array("one_timer", "events").iter().enumerate() {
349 let event = parse_one_timer_event(value)?;
350 events.push(span_mechanic_event(
351 "one_timer",
352 index,
353 event.pass_start_frame,
354 event.frame,
355 event.pass_start_time,
356 event.time,
357 event.player,
358 event.is_team_0,
359 ));
360 }
361 for (index, value) in self.module_array("pass", "events").iter().enumerate() {
362 let event = parse_pass_event(value)?;
363 events.push(span_mechanic_event(
364 "pass",
365 index,
366 event.start_frame,
367 event.frame,
368 event.start_time,
369 event.time,
370 event.passer,
371 event.is_team_0,
372 ));
373 }
374 for (index, value) in self.module_array("speed_flip", "events").iter().enumerate() {
375 let event = parse_speed_flip_event(value)?;
376 events.push(moment_mechanic_event(
377 "speed_flip",
378 index,
379 event.frame,
380 event.time,
381 event.player,
382 event.is_team_0,
383 ));
384 }
385 for (index, value) in self.module_array("half_flip", "events").iter().enumerate() {
386 let event = parse_half_flip_event(value)?;
387 events.push(moment_mechanic_event(
388 "half_flip",
389 index,
390 event.frame,
391 event.time,
392 event.player,
393 event.is_team_0,
394 ));
395 }
396 for (index, value) in self
397 .module_array("half_volley", "events")
398 .iter()
399 .enumerate()
400 {
401 let event = parse_half_volley_event(value)?;
402 events.push(moment_mechanic_event(
403 "half_volley",
404 index,
405 event.frame,
406 event.time,
407 event.player,
408 event.is_team_0,
409 ));
410 }
411 for (index, value) in self.module_array("wavedash", "events").iter().enumerate() {
412 let event = parse_wavedash_event(value)?;
413 events.push(span_mechanic_event(
414 "wavedash",
415 index,
416 event.dodge_frame,
417 event.frame,
418 event.dodge_time,
419 event.time,
420 event.player,
421 event.is_team_0,
422 ));
423 }
424 events.sort_by(|left, right| {
425 let left_time = mechanic_event_start_time(left);
426 let right_time = mechanic_event_start_time(right);
427 left_time
428 .total_cmp(&right_time)
429 .then_with(|| left.kind.cmp(&right.kind))
430 .then_with(|| left.id.cmp(&right.id))
431 });
432 Ok(events)
433 }
434
435 fn goal_tag_events_value(&self) -> Vec<Value> {
436 let mut events = Vec::new();
437 for module_name in [
438 "aerial_goal",
439 "high_aerial_goal",
440 "long_distance_goal",
441 "own_half_goal",
442 "empty_net_goal",
443 "counter_attack_goal",
444 "flick_goal",
445 "double_tap_goal",
446 "one_timer_goal",
447 "passing_goal",
448 "air_dribble_goal",
449 "flip_reset_goal",
450 "half_volley_goal",
451 ] {
452 events.extend(self.module_array(module_name, "events"));
453 }
454 events.sort_by(|left, right| {
455 let left_time = left.get("time").and_then(Value::as_f64).unwrap_or(0.0);
456 let right_time = right.get("time").and_then(Value::as_f64).unwrap_or(0.0);
457 left_time.total_cmp(&right_time)
458 });
459 events
460 }
461
462 fn timeline_event_sets_typed(&self) -> SubtrActorResult<ReplayStatsTimelineEvents> {
463 Ok(ReplayStatsTimelineEvents {
464 timeline: self.timeline_events_typed()?,
465 core_player: self.module_player_events(
466 "core",
467 "player_events",
468 parse_core_player_stats_event,
469 )?,
470 core_team: self.module_player_events(
471 "core",
472 "team_events",
473 parse_core_team_stats_event,
474 )?,
475 possession: self.module_player_events(
476 "possession",
477 "events",
478 parse_possession_event,
479 )?,
480 pressure: self.module_player_events("pressure", "events", parse_pressure_event)?,
481 movement: self.module_player_events("movement", "events", parse_movement_event)?,
482 positioning: self.module_player_events(
483 "positioning",
484 "events",
485 parse_positioning_event,
486 )?,
487 rotation_player: self.module_player_events(
488 "rotation",
489 "player_events",
490 parse_rotation_player_event,
491 )?,
492 rotation_team: self.module_player_events(
493 "rotation",
494 "team_events",
495 parse_rotation_team_event,
496 )?,
497 mechanics: self.mechanic_events_typed()?,
498 goal_context: self.module_player_events(
499 "core",
500 "goal_context",
501 parse_goal_context_event,
502 )?,
503 backboard: self.module_player_events("backboard", "events", parse_backboard_event)?,
504 ceiling_shot: self.module_player_events(
505 "ceiling_shot",
506 "events",
507 parse_ceiling_shot_event,
508 )?,
509 wall_aerial: self.module_player_events(
510 "wall_aerial",
511 "events",
512 parse_wall_aerial_event,
513 )?,
514 wall_aerial_shot: self.module_player_events(
515 "wall_aerial_shot",
516 "events",
517 parse_wall_aerial_shot_event,
518 )?,
519 center: self.module_player_events("center", "events", parse_center_event)?,
520 flick: self.module_player_events("flick", "events", parse_flick_event)?,
521 musty_flick: self.module_player_events(
522 "musty_flick",
523 "events",
524 parse_musty_flick_event,
525 )?,
526 dodge_reset: self.module_player_events(
527 "dodge_reset",
528 "events",
529 parse_dodge_reset_event,
530 )?,
531 double_tap: self.module_player_events(
532 "double_tap",
533 "events",
534 parse_double_tap_event,
535 )?,
536 one_timer: self.module_player_events("one_timer", "events", parse_one_timer_event)?,
537 fifty_fifty: self.module_player_events(
538 "fifty_fifty",
539 "events",
540 parse_fifty_fifty_event,
541 )?,
542 pass: self.module_player_events("pass", "events", parse_pass_event)?,
543 pass_last_completed: self.module_player_events(
544 "pass",
545 "last_completed_events",
546 parse_pass_last_completed_event,
547 )?,
548 ball_carry: self.module_player_events(
549 "ball_carry",
550 "events",
551 parse_ball_carry_event,
552 )?,
553 goal_tags: self.goal_tag_events_typed()?,
554 rush: self.module_typed_array("rush", "events")?,
555 speed_flip: self.module_player_events(
556 "speed_flip",
557 "events",
558 parse_speed_flip_event,
559 )?,
560 half_flip: self.module_player_events("half_flip", "events", parse_half_flip_event)?,
561 half_volley: self.module_player_events(
562 "half_volley",
563 "events",
564 parse_half_volley_event,
565 )?,
566 wavedash: self.module_player_events("wavedash", "events", parse_wavedash_event)?,
567 whiff: self.module_player_events("whiff", "events", parse_whiff_event)?,
568 powerslide: self.module_player_events(
569 "powerslide",
570 "events",
571 parse_powerslide_event,
572 )?,
573 touch: self.module_player_events("touch", "events", parse_touch_stats_event)?,
574 touch_ball_movement: self.module_player_events(
575 "touch",
576 "ball_movement_events",
577 parse_touch_ball_movement_event,
578 )?,
579 touch_last_touch: self.module_player_events(
580 "touch",
581 "last_touch_events",
582 parse_touch_last_touch_event,
583 )?,
584 boost_pickups: self.module_player_events(
585 "boost",
586 "events",
587 parse_boost_pickup_comparison_event,
588 )?,
589 boost_ledger: self.module_player_events(
590 "boost",
591 "ledger_events",
592 parse_boost_ledger_event,
593 )?,
594 boost_state: self.module_player_events(
595 "boost",
596 "state_events",
597 parse_boost_state_event,
598 )?,
599 bump: self.module_player_events("bump", "events", parse_bump_event)?,
600 })
601 }
602
603 fn timeline_event_sets_value(&self) -> Value {
604 let mut events = Map::new();
605 events.insert("timeline".to_owned(), Value::Array(self.timeline_events()));
606 events.insert(
607 "core_player".to_owned(),
608 Value::Array(self.module_array("core", "player_events")),
609 );
610 events.insert(
611 "core_team".to_owned(),
612 Value::Array(self.module_array("core", "team_events")),
613 );
614 events.insert(
615 "possession".to_owned(),
616 Value::Array(self.module_array("possession", "events")),
617 );
618 events.insert(
619 "pressure".to_owned(),
620 Value::Array(self.module_array("pressure", "events")),
621 );
622 events.insert(
623 "movement".to_owned(),
624 Value::Array(self.module_array("movement", "events")),
625 );
626 events.insert(
627 "positioning".to_owned(),
628 Value::Array(self.module_array("positioning", "events")),
629 );
630 events.insert(
631 "rotation_player".to_owned(),
632 Value::Array(self.module_array("rotation", "player_events")),
633 );
634 events.insert(
635 "rotation_team".to_owned(),
636 Value::Array(self.module_array("rotation", "team_events")),
637 );
638 events.insert("mechanics".to_owned(), Value::Array(Vec::new()));
639 events.insert(
640 "backboard".to_owned(),
641 Value::Array(self.module_array("backboard", "events")),
642 );
643 events.insert(
644 "ceiling_shot".to_owned(),
645 Value::Array(self.module_array("ceiling_shot", "events")),
646 );
647 events.insert(
648 "wall_aerial".to_owned(),
649 Value::Array(self.module_array("wall_aerial", "events")),
650 );
651 events.insert(
652 "wall_aerial_shot".to_owned(),
653 Value::Array(self.module_array("wall_aerial_shot", "events")),
654 );
655 events.insert(
656 "center".to_owned(),
657 Value::Array(self.module_array("center", "events")),
658 );
659 events.insert(
660 "double_tap".to_owned(),
661 Value::Array(self.module_array("double_tap", "events")),
662 );
663 events.insert(
664 "one_timer".to_owned(),
665 Value::Array(self.module_array("one_timer", "events")),
666 );
667 events.insert(
668 "pass".to_owned(),
669 Value::Array(self.module_array("pass", "events")),
670 );
671 events.insert(
672 "goal_tags".to_owned(),
673 Value::Array(self.goal_tag_events_value()),
674 );
675 events.insert(
676 "fifty_fifty".to_owned(),
677 Value::Array(self.module_array("fifty_fifty", "events")),
678 );
679 events.insert(
680 "rush".to_owned(),
681 Value::Array(self.module_array("rush", "events")),
682 );
683 events.insert(
684 "speed_flip".to_owned(),
685 Value::Array(self.module_array("speed_flip", "events")),
686 );
687 events.insert(
688 "half_flip".to_owned(),
689 Value::Array(self.module_array("half_flip", "events")),
690 );
691 events.insert(
692 "half_volley".to_owned(),
693 Value::Array(self.module_array("half_volley", "events")),
694 );
695 events.insert(
696 "wavedash".to_owned(),
697 Value::Array(self.module_array("wavedash", "events")),
698 );
699 events.insert(
700 "whiff".to_owned(),
701 Value::Array(self.module_array("whiff", "events")),
702 );
703 events.insert(
704 "touch".to_owned(),
705 Value::Array(self.module_array("touch", "events")),
706 );
707 events.insert(
708 "touch_ball_movement".to_owned(),
709 Value::Array(self.module_array("touch", "ball_movement_events")),
710 );
711 events.insert(
712 "touch_last_touch".to_owned(),
713 Value::Array(self.module_array("touch", "last_touch_events")),
714 );
715 events.insert(
716 "boost_pickups".to_owned(),
717 Value::Array(self.module_array("boost", "events")),
718 );
719 events.insert(
720 "boost_ledger".to_owned(),
721 Value::Array(self.module_array("boost", "ledger_events")),
722 );
723 events.insert(
724 "boost_state".to_owned(),
725 Value::Array(self.module_array("boost", "state_events")),
726 );
727 events.insert(
728 "bump".to_owned(),
729 Value::Array(self.module_array("bump", "events")),
730 );
731 Value::Object(events)
732 }
733
734 fn timeline_config(&self) -> StatsTimelineConfig {
735 let positioning_config = self.config.get("positioning").and_then(Value::as_object);
736 let pressure_config = self.config.get("pressure").and_then(Value::as_object);
737 let rotation_config = self.config.get("rotation").and_then(Value::as_object);
738 let rotation_defaults = RotationCalculatorConfig::default();
739 let rush_config = self.config.get("rush").and_then(Value::as_object);
740 let rush_defaults = RushCalculatorConfig::default();
741 let aerial_goal_config = self.config.get("aerial_goal").and_then(Value::as_object);
742 let high_aerial_goal_config = self
743 .config
744 .get("high_aerial_goal")
745 .and_then(Value::as_object);
746 let long_distance_goal_config = self
747 .config
748 .get("long_distance_goal")
749 .and_then(Value::as_object);
750 let own_half_goal_config = self.config.get("own_half_goal").and_then(Value::as_object);
751 let empty_net_goal_config = self.config.get("empty_net_goal").and_then(Value::as_object);
752 let flick_goal_config = self.config.get("flick_goal").and_then(Value::as_object);
753 let double_tap_goal_config = self
754 .config
755 .get("double_tap_goal")
756 .and_then(Value::as_object);
757 let one_timer_goal_config = self.config.get("one_timer_goal").and_then(Value::as_object);
758 let air_dribble_goal_config = self
759 .config
760 .get("air_dribble_goal")
761 .and_then(Value::as_object);
762 let flip_reset_goal_config = self
763 .config
764 .get("flip_reset_goal")
765 .and_then(Value::as_object);
766 let half_volley_config = self.config.get("half_volley").and_then(Value::as_object);
767 let half_volley_goal_config = self
768 .config
769 .get("half_volley_goal")
770 .and_then(Value::as_object);
771
772 StatsTimelineConfig {
773 most_back_forward_threshold_y: positioning_config
774 .and_then(|config| config.get("most_back_forward_threshold_y"))
775 .and_then(json_f32)
776 .unwrap_or(PositioningCalculatorConfig::default().most_back_forward_threshold_y),
777 level_ball_depth_margin: positioning_config
778 .and_then(|config| config.get("level_ball_depth_margin"))
779 .and_then(json_f32)
780 .unwrap_or(PositioningCalculatorConfig::default().level_ball_depth_margin),
781 pressure_neutral_zone_half_width_y: pressure_config
782 .and_then(|config| config.get("pressure_neutral_zone_half_width_y"))
783 .and_then(json_f32)
784 .unwrap_or(PressureCalculatorConfig::default().neutral_zone_half_width_y),
785 rotation_role_depth_margin: rotation_config
786 .and_then(|config| config.get("role_depth_margin"))
787 .and_then(json_f32)
788 .unwrap_or(rotation_defaults.role_depth_margin),
789 rotation_first_man_ambiguity_margin: rotation_config
790 .and_then(|config| config.get("first_man_ambiguity_margin"))
791 .and_then(json_f32)
792 .unwrap_or(rotation_defaults.first_man_ambiguity_margin),
793 rotation_first_man_debounce_seconds: rotation_config
794 .and_then(|config| config.get("first_man_debounce_seconds"))
795 .and_then(json_f32)
796 .unwrap_or(rotation_defaults.first_man_debounce_seconds),
797 rush_max_start_y: rush_config
798 .and_then(|config| config.get("rush_max_start_y"))
799 .and_then(json_f32)
800 .unwrap_or(rush_defaults.max_start_y),
801 rush_attack_support_distance_y: rush_config
802 .and_then(|config| config.get("rush_attack_support_distance_y"))
803 .and_then(json_f32)
804 .unwrap_or(rush_defaults.attack_support_distance_y),
805 rush_defender_distance_y: rush_config
806 .and_then(|config| config.get("rush_defender_distance_y"))
807 .and_then(json_f32)
808 .unwrap_or(rush_defaults.defender_distance_y),
809 rush_min_possession_retained_seconds: rush_config
810 .and_then(|config| config.get("rush_min_possession_retained_seconds"))
811 .and_then(json_f32)
812 .unwrap_or(rush_defaults.min_possession_retained_seconds),
813 aerial_goal_min_ball_z: aerial_goal_config
814 .and_then(|config| config.get("aerial_goal_min_ball_z"))
815 .and_then(json_f32)
816 .unwrap_or(AerialGoalCalculatorConfig::default().min_ball_z),
817 high_aerial_goal_min_ball_z: high_aerial_goal_config
818 .and_then(|config| config.get("high_aerial_goal_min_ball_z"))
819 .and_then(json_f32)
820 .unwrap_or(HighAerialGoalCalculatorConfig::default().min_ball_z),
821 long_distance_goal_max_attacking_y: long_distance_goal_config
822 .and_then(|config| config.get("long_distance_goal_max_attacking_y"))
823 .and_then(json_f32)
824 .unwrap_or(LongDistanceGoalCalculatorConfig::default().max_attacking_y),
825 own_half_goal_max_attacking_y: own_half_goal_config
826 .and_then(|config| config.get("own_half_goal_max_attacking_y"))
827 .and_then(json_f32)
828 .unwrap_or(OwnHalfGoalCalculatorConfig::default().max_attacking_y),
829 empty_net_min_defender_y_margin: empty_net_goal_config
830 .and_then(|config| config.get("empty_net_min_defender_y_margin"))
831 .and_then(json_f32)
832 .unwrap_or(EmptyNetGoalCalculatorConfig::default().min_defender_y_margin),
833 empty_net_min_defender_distance: empty_net_goal_config
834 .and_then(|config| config.get("empty_net_min_defender_distance"))
835 .and_then(json_f32)
836 .unwrap_or(EmptyNetGoalCalculatorConfig::default().min_defender_distance),
837 empty_net_max_touch_attacking_y: empty_net_goal_config
838 .and_then(|config| config.get("empty_net_max_touch_attacking_y"))
839 .and_then(json_f32)
840 .unwrap_or(EmptyNetGoalCalculatorConfig::default().max_touch_attacking_y),
841 flick_goal_max_event_to_goal_seconds: json_config_f32(
842 flick_goal_config,
843 "flick_goal_max_event_to_goal_seconds",
844 "flick_goal_max_event_to_touch_seconds",
845 )
846 .unwrap_or(FlickGoalCalculatorConfig::default().max_event_to_goal_seconds),
847 double_tap_goal_max_event_to_goal_seconds: json_config_f32(
848 double_tap_goal_config,
849 "double_tap_goal_max_event_to_goal_seconds",
850 "double_tap_goal_max_event_to_touch_seconds",
851 )
852 .unwrap_or(DoubleTapGoalCalculatorConfig::default().max_event_to_goal_seconds),
853 one_timer_goal_max_event_to_goal_seconds: json_config_f32(
854 one_timer_goal_config,
855 "one_timer_goal_max_event_to_goal_seconds",
856 "one_timer_goal_max_event_to_touch_seconds",
857 )
858 .unwrap_or(OneTimerGoalCalculatorConfig::default().max_event_to_goal_seconds),
859 air_dribble_goal_max_end_to_goal_seconds: json_config_f32(
860 air_dribble_goal_config,
861 "air_dribble_goal_max_end_to_goal_seconds",
862 "air_dribble_goal_max_end_to_touch_seconds",
863 )
864 .unwrap_or(AirDribbleGoalCalculatorConfig::default().max_end_to_goal_seconds),
865 flip_reset_goal_max_event_to_goal_seconds: json_config_f32(
866 flip_reset_goal_config,
867 "flip_reset_goal_max_event_to_goal_seconds",
868 "flip_reset_goal_max_event_to_touch_seconds",
869 )
870 .unwrap_or(FlipResetGoalCalculatorConfig::default().max_event_to_goal_seconds),
871 half_volley_max_bounce_to_touch_seconds: half_volley_config
872 .and_then(|config| config.get("half_volley_max_bounce_to_touch_seconds"))
873 .and_then(json_f32)
874 .unwrap_or(HalfVolleyCalculatorConfig::default().max_bounce_to_touch_seconds),
875 half_volley_min_ball_speed: half_volley_config
876 .and_then(|config| config.get("half_volley_min_ball_speed"))
877 .and_then(json_f32)
878 .unwrap_or(HalfVolleyCalculatorConfig::default().min_ball_speed),
879 half_volley_goal_max_touch_to_goal_seconds: half_volley_goal_config
880 .and_then(|config| config.get("half_volley_goal_max_touch_to_goal_seconds"))
881 .and_then(json_f32)
882 .unwrap_or(HalfVolleyGoalCalculatorConfig::default().max_touch_to_goal_seconds),
883 half_volley_goal_min_goal_alignment: half_volley_goal_config
884 .and_then(|config| config.get("half_volley_goal_min_goal_alignment"))
885 .and_then(json_f32)
886 .unwrap_or(HalfVolleyGoalCalculatorConfig::default().min_goal_alignment),
887 }
888 }
889
890 fn timeline_config_value(&self) -> SubtrActorResult<Value> {
891 let positioning_config = self.config.get("positioning").and_then(Value::as_object);
892 let pressure_config = self.config.get("pressure").and_then(Value::as_object);
893 let rotation_config = self.config.get("rotation").and_then(Value::as_object);
894 let rush_config = self.config.get("rush").and_then(Value::as_object);
895 let aerial_goal_config = self.config.get("aerial_goal").and_then(Value::as_object);
896 let high_aerial_goal_config = self
897 .config
898 .get("high_aerial_goal")
899 .and_then(Value::as_object);
900 let long_distance_goal_config = self
901 .config
902 .get("long_distance_goal")
903 .and_then(Value::as_object);
904 let own_half_goal_config = self.config.get("own_half_goal").and_then(Value::as_object);
905 let empty_net_goal_config = self.config.get("empty_net_goal").and_then(Value::as_object);
906 let flick_goal_config = self.config.get("flick_goal").and_then(Value::as_object);
907 let double_tap_goal_config = self
908 .config
909 .get("double_tap_goal")
910 .and_then(Value::as_object);
911 let one_timer_goal_config = self.config.get("one_timer_goal").and_then(Value::as_object);
912 let air_dribble_goal_config = self
913 .config
914 .get("air_dribble_goal")
915 .and_then(Value::as_object);
916 let flip_reset_goal_config = self
917 .config
918 .get("flip_reset_goal")
919 .and_then(Value::as_object);
920 let half_volley_config = self.config.get("half_volley").and_then(Value::as_object);
921 let half_volley_goal_config = self
922 .config
923 .get("half_volley_goal")
924 .and_then(Value::as_object);
925
926 let mut config = Map::new();
927 config.insert(
928 "most_back_forward_threshold_y".to_owned(),
929 serialize_to_json_value(
930 &positioning_config
931 .and_then(|config| config.get("most_back_forward_threshold_y"))
932 .and_then(Value::as_f64)
933 .unwrap_or(
934 PositioningCalculatorConfig::default().most_back_forward_threshold_y as f64,
935 ),
936 )?,
937 );
938 config.insert(
939 "level_ball_depth_margin".to_owned(),
940 serialize_to_json_value(
941 &positioning_config
942 .and_then(|config| config.get("level_ball_depth_margin"))
943 .and_then(Value::as_f64)
944 .unwrap_or(
945 PositioningCalculatorConfig::default().level_ball_depth_margin as f64,
946 ),
947 )?,
948 );
949 config.insert(
950 "pressure_neutral_zone_half_width_y".to_owned(),
951 serialize_to_json_value(
952 &pressure_config
953 .and_then(|config| config.get("pressure_neutral_zone_half_width_y"))
954 .and_then(Value::as_f64)
955 .unwrap_or(
956 PressureCalculatorConfig::default().neutral_zone_half_width_y as f64,
957 ),
958 )?,
959 );
960 let rotation_defaults = RotationCalculatorConfig::default();
961 for (key, default_value) in [
962 (
963 "rotation_role_depth_margin",
964 rotation_defaults.role_depth_margin,
965 ),
966 (
967 "rotation_first_man_ambiguity_margin",
968 rotation_defaults.first_man_ambiguity_margin,
969 ),
970 (
971 "rotation_first_man_debounce_seconds",
972 rotation_defaults.first_man_debounce_seconds,
973 ),
974 ] {
975 let source_key = key.strip_prefix("rotation_").unwrap_or(key);
976 config.insert(
977 key.to_owned(),
978 serialize_to_json_value(
979 &rotation_config
980 .and_then(|config| config.get(source_key))
981 .and_then(Value::as_f64)
982 .unwrap_or(default_value as f64),
983 )?,
984 );
985 }
986 let rush_defaults = RushCalculatorConfig::default();
987 config.insert(
988 "rush_max_start_y".to_owned(),
989 serialize_to_json_value(
990 &rush_config
991 .and_then(|config| config.get("rush_max_start_y"))
992 .and_then(Value::as_f64)
993 .unwrap_or(rush_defaults.max_start_y as f64),
994 )?,
995 );
996 config.insert(
997 "rush_attack_support_distance_y".to_owned(),
998 serialize_to_json_value(
999 &rush_config
1000 .and_then(|config| config.get("rush_attack_support_distance_y"))
1001 .and_then(Value::as_f64)
1002 .unwrap_or(rush_defaults.attack_support_distance_y as f64),
1003 )?,
1004 );
1005 config.insert(
1006 "rush_defender_distance_y".to_owned(),
1007 serialize_to_json_value(
1008 &rush_config
1009 .and_then(|config| config.get("rush_defender_distance_y"))
1010 .and_then(Value::as_f64)
1011 .unwrap_or(rush_defaults.defender_distance_y as f64),
1012 )?,
1013 );
1014 config.insert(
1015 "rush_min_possession_retained_seconds".to_owned(),
1016 serialize_to_json_value(
1017 &rush_config
1018 .and_then(|config| config.get("rush_min_possession_retained_seconds"))
1019 .and_then(Value::as_f64)
1020 .unwrap_or(rush_defaults.min_possession_retained_seconds as f64),
1021 )?,
1022 );
1023 for (module_config, key, default_value) in [
1024 (
1025 aerial_goal_config,
1026 "aerial_goal_min_ball_z",
1027 AerialGoalCalculatorConfig::default().min_ball_z,
1028 ),
1029 (
1030 high_aerial_goal_config,
1031 "high_aerial_goal_min_ball_z",
1032 HighAerialGoalCalculatorConfig::default().min_ball_z,
1033 ),
1034 (
1035 long_distance_goal_config,
1036 "long_distance_goal_max_attacking_y",
1037 LongDistanceGoalCalculatorConfig::default().max_attacking_y,
1038 ),
1039 (
1040 own_half_goal_config,
1041 "own_half_goal_max_attacking_y",
1042 OwnHalfGoalCalculatorConfig::default().max_attacking_y,
1043 ),
1044 (
1045 empty_net_goal_config,
1046 "empty_net_min_defender_y_margin",
1047 EmptyNetGoalCalculatorConfig::default().min_defender_y_margin,
1048 ),
1049 (
1050 empty_net_goal_config,
1051 "empty_net_min_defender_distance",
1052 EmptyNetGoalCalculatorConfig::default().min_defender_distance,
1053 ),
1054 (
1055 empty_net_goal_config,
1056 "empty_net_max_touch_attacking_y",
1057 EmptyNetGoalCalculatorConfig::default().max_touch_attacking_y,
1058 ),
1059 (
1060 flick_goal_config,
1061 "flick_goal_max_event_to_goal_seconds",
1062 FlickGoalCalculatorConfig::default().max_event_to_goal_seconds,
1063 ),
1064 (
1065 double_tap_goal_config,
1066 "double_tap_goal_max_event_to_goal_seconds",
1067 DoubleTapGoalCalculatorConfig::default().max_event_to_goal_seconds,
1068 ),
1069 (
1070 one_timer_goal_config,
1071 "one_timer_goal_max_event_to_goal_seconds",
1072 OneTimerGoalCalculatorConfig::default().max_event_to_goal_seconds,
1073 ),
1074 (
1075 air_dribble_goal_config,
1076 "air_dribble_goal_max_end_to_goal_seconds",
1077 AirDribbleGoalCalculatorConfig::default().max_end_to_goal_seconds,
1078 ),
1079 (
1080 flip_reset_goal_config,
1081 "flip_reset_goal_max_event_to_goal_seconds",
1082 FlipResetGoalCalculatorConfig::default().max_event_to_goal_seconds,
1083 ),
1084 (
1085 half_volley_config,
1086 "half_volley_max_bounce_to_touch_seconds",
1087 HalfVolleyCalculatorConfig::default().max_bounce_to_touch_seconds,
1088 ),
1089 (
1090 half_volley_config,
1091 "half_volley_min_ball_speed",
1092 HalfVolleyCalculatorConfig::default().min_ball_speed,
1093 ),
1094 (
1095 half_volley_goal_config,
1096 "half_volley_goal_max_touch_to_goal_seconds",
1097 HalfVolleyGoalCalculatorConfig::default().max_touch_to_goal_seconds,
1098 ),
1099 (
1100 half_volley_goal_config,
1101 "half_volley_goal_min_goal_alignment",
1102 HalfVolleyGoalCalculatorConfig::default().min_goal_alignment,
1103 ),
1104 ] {
1105 config.insert(
1106 key.to_owned(),
1107 serialize_to_json_value(
1108 &module_config
1109 .and_then(|config| config.get(key))
1110 .and_then(Value::as_f64)
1111 .unwrap_or(default_value as f64),
1112 )?,
1113 );
1114 }
1115 Ok(Value::Object(config))
1116 }
1117
1118 fn timeline_frame_value(&self, frame: &StatsSnapshotFrame) -> SubtrActorResult<Value> {
1119 let mut timeline = Map::new();
1120 timeline.insert(
1121 "frame_number".to_owned(),
1122 serialize_to_json_value(&frame.frame_number)?,
1123 );
1124 timeline.insert("time".to_owned(), serialize_to_json_value(&frame.time)?);
1125 timeline.insert("dt".to_owned(), serialize_to_json_value(&frame.dt)?);
1126 timeline.insert(
1127 "seconds_remaining".to_owned(),
1128 serialize_to_json_value(&frame.seconds_remaining)?,
1129 );
1130 timeline.insert(
1131 "game_state".to_owned(),
1132 serialize_to_json_value(&frame.game_state)?,
1133 );
1134 timeline.insert(
1135 "ball_has_been_hit".to_owned(),
1136 serialize_to_json_value(&frame.ball_has_been_hit)?,
1137 );
1138 timeline.insert(
1139 "kickoff_countdown_time".to_owned(),
1140 serialize_to_json_value(&frame.kickoff_countdown_time)?,
1141 );
1142 timeline.insert(
1143 "gameplay_phase".to_owned(),
1144 serialize_to_json_value(&frame.gameplay_phase)?,
1145 );
1146 timeline.insert(
1147 "is_live_play".to_owned(),
1148 serialize_to_json_value(&frame.is_live_play)?,
1149 );
1150 timeline.insert(
1151 "fifty_fifty".to_owned(),
1152 self.frame_stats_or_default::<FiftyFiftyStats>(frame, "fifty_fifty"),
1153 );
1154 timeline.insert(
1155 "possession".to_owned(),
1156 self.frame_stats_or_default::<PossessionStats>(frame, "possession"),
1157 );
1158 timeline.insert(
1159 "pressure".to_owned(),
1160 self.frame_stats_or_default::<PressureStats>(frame, "pressure"),
1161 );
1162 timeline.insert(
1163 "rush".to_owned(),
1164 self.frame_stats_or_default::<RushStats>(frame, "rush"),
1165 );
1166 timeline.insert(
1167 "team_zero".to_owned(),
1168 self.timeline_team_value(frame, "team_zero")?,
1169 );
1170 timeline.insert(
1171 "team_one".to_owned(),
1172 self.timeline_team_value(frame, "team_one")?,
1173 );
1174 timeline.insert(
1175 "players".to_owned(),
1176 Value::Array(
1177 self.replay_meta
1178 .player_order()
1179 .map(|player| self.timeline_player_value(frame, player))
1180 .collect::<SubtrActorResult<Vec<_>>>()?,
1181 ),
1182 );
1183 Ok(Value::Object(timeline))
1184 }
1185
1186 pub(crate) fn replay_stats_frame(
1187 &self,
1188 frame: &StatsSnapshotFrame,
1189 ) -> SubtrActorResult<ReplayStatsFrame> {
1190 Ok(ReplayStatsFrame {
1191 frame_number: frame.frame_number,
1192 time: frame.time,
1193 dt: frame.dt,
1194 seconds_remaining: frame.seconds_remaining,
1195 game_state: frame.game_state,
1196 ball_has_been_hit: frame.ball_has_been_hit,
1197 kickoff_countdown_time: frame.kickoff_countdown_time,
1198 gameplay_phase: frame.gameplay_phase,
1199 is_live_play: frame.is_live_play,
1200 team_zero: self.replay_team_stats(frame, "team_zero")?,
1201 team_one: self.replay_team_stats(frame, "team_one")?,
1202 players: self
1203 .replay_meta
1204 .player_order()
1205 .map(|player| self.replay_player_stats(frame, player))
1206 .collect::<SubtrActorResult<Vec<_>>>()?,
1207 })
1208 }
1209
1210 fn replay_team_stats(
1211 &self,
1212 frame: &StatsSnapshotFrame,
1213 team_key: &str,
1214 ) -> SubtrActorResult<TeamStatsSnapshot> {
1215 let is_team_zero = team_key == "team_zero";
1216 Ok(TeamStatsSnapshot {
1217 fifty_fifty: self
1218 .frame_stats_or_default_typed::<FiftyFiftyStats>(frame, "fifty_fifty")?
1219 .for_team(is_team_zero),
1220 possession: self
1221 .frame_stats_or_default_typed::<PossessionStats>(frame, "possession")?
1222 .for_team(is_team_zero),
1223 pressure: self
1224 .frame_stats_or_default_typed::<PressureStats>(frame, "pressure")?
1225 .for_team(is_team_zero),
1226 rotation: self.frame_team_stat_or_default_typed(frame, "rotation", team_key)?,
1227 rush: self
1228 .frame_stats_or_default_typed::<RushStats>(frame, "rush")?
1229 .for_team(is_team_zero),
1230 core: self.frame_team_stat_or_default_typed(frame, "core", team_key)?,
1231 backboard: self.frame_team_stat_or_default_typed(frame, "backboard", team_key)?,
1232 double_tap: self.frame_team_stat_or_default_typed(frame, "double_tap", team_key)?,
1233 one_timer: self.frame_team_stat_or_default_typed(frame, "one_timer", team_key)?,
1234 pass: self.frame_team_stat_or_default_typed(frame, "pass", team_key)?,
1235 ball_carry: self.frame_team_stat_or_default_typed(frame, "ball_carry", team_key)?,
1236 air_dribble: self.frame_team_stat_or_default_typed(frame, "air_dribble", team_key)?,
1237 boost: self.frame_team_stat_or_default_typed(frame, "boost", team_key)?,
1238 bump: self.frame_team_stat_or_default_typed(frame, "bump", team_key)?,
1239 half_volley: self.frame_team_stat_or_default_typed(frame, "half_volley", team_key)?,
1240 movement: self.frame_team_stat_or_default_typed(frame, "movement", team_key)?,
1241 powerslide: self.frame_team_stat_or_default_typed(frame, "powerslide", team_key)?,
1242 demo: self.frame_team_stat_or_default_typed(frame, "demo", team_key)?,
1243 })
1244 }
1245
1246 fn replay_player_stats(
1247 &self,
1248 frame: &StatsSnapshotFrame,
1249 player: &PlayerInfo,
1250 ) -> SubtrActorResult<PlayerStatsSnapshot> {
1251 let player_key = player_info_key(player)?;
1252 Ok(PlayerStatsSnapshot {
1253 player_id: player.remote_id.clone(),
1254 name: player.name.clone(),
1255 is_team_0: self.is_team_zero_player(player),
1256 core: self.frame_core_player_stat_or_default_by_key(frame, &player_key)?,
1257 backboard: self.frame_player_stat_or_default_typed_by_key(
1258 frame,
1259 "backboard",
1260 &player_key,
1261 )?,
1262 ceiling_shot: self.frame_player_stat_or_default_typed_by_key(
1263 frame,
1264 "ceiling_shot",
1265 &player_key,
1266 )?,
1267 wall_aerial: self.frame_player_stat_or_default_typed_by_key(
1268 frame,
1269 "wall_aerial",
1270 &player_key,
1271 )?,
1272 wall_aerial_shot: self.frame_player_stat_or_default_typed_by_key(
1273 frame,
1274 "wall_aerial_shot",
1275 &player_key,
1276 )?,
1277 double_tap: self.frame_player_stat_or_default_typed_by_key(
1278 frame,
1279 "double_tap",
1280 &player_key,
1281 )?,
1282 one_timer: self.frame_player_stat_or_default_typed_by_key(
1283 frame,
1284 "one_timer",
1285 &player_key,
1286 )?,
1287 pass: self.frame_player_stat_or_default_typed_by_key(frame, "pass", &player_key)?,
1288 fifty_fifty: self.frame_player_stat_or_default_typed_by_key(
1289 frame,
1290 "fifty_fifty",
1291 &player_key,
1292 )?,
1293 speed_flip: self.frame_player_stat_or_default_typed_by_key(
1294 frame,
1295 "speed_flip",
1296 &player_key,
1297 )?,
1298 half_flip: self.frame_player_stat_or_default_typed_by_key(
1299 frame,
1300 "half_flip",
1301 &player_key,
1302 )?,
1303 wavedash: self.frame_player_stat_or_default_typed_by_key(
1304 frame,
1305 "wavedash",
1306 &player_key,
1307 )?,
1308 touch: if frame.modules.contains_key("touch") {
1309 self.frame_player_stat_or_default_with_by_key(frame, "touch", &player_key, || {
1310 TouchStats::default().with_complete_labeled_touch_counts()
1311 })?
1312 } else {
1313 self.frame_player_stat_or_default_typed_by_key(frame, "touch", &player_key)?
1314 },
1315 whiff: self.frame_player_stat_or_default_typed_by_key(frame, "whiff", &player_key)?,
1316 flick: self.frame_player_stat_or_default_typed_by_key(frame, "flick", &player_key)?,
1317 musty_flick: self.frame_player_stat_or_default_typed_by_key(
1318 frame,
1319 "musty_flick",
1320 &player_key,
1321 )?,
1322 dodge_reset: self.frame_player_stat_or_default_typed_by_key(
1323 frame,
1324 "dodge_reset",
1325 &player_key,
1326 )?,
1327 ball_carry: self.frame_player_stat_or_default_typed_by_key(
1328 frame,
1329 "ball_carry",
1330 &player_key,
1331 )?,
1332 air_dribble: self.frame_player_stat_or_default_typed_by_key(
1333 frame,
1334 "air_dribble",
1335 &player_key,
1336 )?,
1337 boost: self.frame_player_stat_or_default_typed_by_key(frame, "boost", &player_key)?,
1338 bump: self.frame_player_stat_or_default_typed_by_key(frame, "bump", &player_key)?,
1339 half_volley: self.frame_player_stat_or_default_typed_by_key(
1340 frame,
1341 "half_volley",
1342 &player_key,
1343 )?,
1344 movement: self.frame_player_stat_or_default_with_by_key(
1345 frame,
1346 "movement",
1347 &player_key,
1348 || MovementStats::default().with_complete_labeled_tracked_time(),
1349 )?,
1350 positioning: self.frame_player_stat_or_default_typed_by_key(
1351 frame,
1352 "positioning",
1353 &player_key,
1354 )?,
1355 rotation: self.frame_player_stat_or_default_typed_by_key(
1356 frame,
1357 "rotation",
1358 &player_key,
1359 )?,
1360 powerslide: self.frame_player_stat_or_default_typed_by_key(
1361 frame,
1362 "powerslide",
1363 &player_key,
1364 )?,
1365 demo: self.frame_player_stat_or_default_typed_by_key(frame, "demo", &player_key)?,
1366 })
1367 }
1368
1369 fn is_team_zero_player(&self, player: &PlayerInfo) -> bool {
1370 self.replay_meta
1371 .team_zero
1372 .iter()
1373 .any(|team_player| team_player.remote_id == player.remote_id)
1374 }
1375
1376 fn timeline_team_value(
1377 &self,
1378 frame: &StatsSnapshotFrame,
1379 team_key: &str,
1380 ) -> SubtrActorResult<Value> {
1381 let is_team_zero = team_key == "team_zero";
1382 let mut team = Map::new();
1383 team.insert(
1384 "fifty_fifty".to_owned(),
1385 serialize_to_json_value(
1386 &self
1387 .frame_stats_or_default_typed::<FiftyFiftyStats>(frame, "fifty_fifty")?
1388 .for_team(is_team_zero),
1389 )?,
1390 );
1391 team.insert(
1392 "possession".to_owned(),
1393 serialize_to_json_value(
1394 &self
1395 .frame_stats_or_default_typed::<PossessionStats>(frame, "possession")?
1396 .for_team(is_team_zero),
1397 )?,
1398 );
1399 team.insert(
1400 "pressure".to_owned(),
1401 serialize_to_json_value(
1402 &self
1403 .frame_stats_or_default_typed::<PressureStats>(frame, "pressure")?
1404 .for_team(is_team_zero),
1405 )?,
1406 );
1407 team.insert(
1408 "rotation".to_owned(),
1409 self.frame_team_stat_or_default::<RotationTeamStats>(frame, "rotation", team_key),
1410 );
1411 team.insert(
1412 "rush".to_owned(),
1413 serialize_to_json_value(
1414 &self
1415 .frame_stats_or_default_typed::<RushStats>(frame, "rush")?
1416 .for_team(is_team_zero),
1417 )?,
1418 );
1419 team.insert(
1420 "core".to_owned(),
1421 self.frame_team_stat_or_default::<CoreTeamStats>(frame, "core", team_key),
1422 );
1423 team.insert(
1424 "backboard".to_owned(),
1425 self.frame_team_stat_or_default::<BackboardTeamStats>(frame, "backboard", team_key),
1426 );
1427 team.insert(
1428 "double_tap".to_owned(),
1429 self.frame_team_stat_or_default::<DoubleTapTeamStats>(frame, "double_tap", team_key),
1430 );
1431 team.insert(
1432 "one_timer".to_owned(),
1433 self.frame_team_stat_or_default::<OneTimerTeamStats>(frame, "one_timer", team_key),
1434 );
1435 team.insert(
1436 "pass".to_owned(),
1437 self.frame_team_stat_or_default::<PassTeamStats>(frame, "pass", team_key),
1438 );
1439 team.insert(
1440 "ball_carry".to_owned(),
1441 self.frame_team_stat_or_default::<BallCarryStats>(frame, "ball_carry", team_key),
1442 );
1443 team.insert(
1444 "air_dribble".to_owned(),
1445 self.frame_team_stat_or_default::<AirDribbleStats>(frame, "air_dribble", team_key),
1446 );
1447 team.insert(
1448 "boost".to_owned(),
1449 self.frame_team_stat_or_default::<BoostStats>(frame, "boost", team_key),
1450 );
1451 team.insert(
1452 "bump".to_owned(),
1453 self.frame_team_stat_or_default::<BumpTeamStats>(frame, "bump", team_key),
1454 );
1455 team.insert(
1456 "half_volley".to_owned(),
1457 self.frame_team_stat_or_default::<HalfVolleyTeamStats>(frame, "half_volley", team_key),
1458 );
1459 team.insert(
1460 "movement".to_owned(),
1461 self.frame_team_stat_or_default::<MovementStats>(frame, "movement", team_key),
1462 );
1463 team.insert(
1464 "powerslide".to_owned(),
1465 self.frame_team_stat_or_default::<PowerslideStats>(frame, "powerslide", team_key),
1466 );
1467 team.insert(
1468 "demo".to_owned(),
1469 self.frame_team_stat_or_default::<DemoTeamStats>(frame, "demo", team_key),
1470 );
1471 Ok(Value::Object(team))
1472 }
1473
1474 fn timeline_player_value(
1475 &self,
1476 frame: &StatsSnapshotFrame,
1477 player: &PlayerInfo,
1478 ) -> SubtrActorResult<Value> {
1479 let player_key = player_info_key(player)?;
1480 let mut player_value = Map::new();
1481 player_value.insert(
1482 "player_id".to_owned(),
1483 serialize_to_json_value(&player.remote_id)?,
1484 );
1485 player_value.insert("name".to_owned(), serialize_to_json_value(&player.name)?);
1486 player_value.insert(
1487 "is_team_0".to_owned(),
1488 serialize_to_json_value(
1489 &self
1490 .replay_meta
1491 .team_zero
1492 .iter()
1493 .any(|team_player| team_player.remote_id == player.remote_id),
1494 )?,
1495 );
1496 player_value.insert(
1497 "core".to_owned(),
1498 self.frame_player_stat_or_default_by_key::<CorePlayerStats>(
1499 frame,
1500 "core",
1501 &player_key,
1502 )?,
1503 );
1504 player_value.insert(
1505 "backboard".to_owned(),
1506 self.frame_player_stat_or_default_by_key::<BackboardPlayerStats>(
1507 frame,
1508 "backboard",
1509 &player_key,
1510 )?,
1511 );
1512 player_value.insert(
1513 "ceiling_shot".to_owned(),
1514 self.frame_player_stat_or_default_by_key::<CeilingShotStats>(
1515 frame,
1516 "ceiling_shot",
1517 &player_key,
1518 )?,
1519 );
1520 player_value.insert(
1521 "wall_aerial".to_owned(),
1522 self.frame_player_stat_or_default_by_key::<WallAerialStats>(
1523 frame,
1524 "wall_aerial",
1525 &player_key,
1526 )?,
1527 );
1528 player_value.insert(
1529 "wall_aerial_shot".to_owned(),
1530 self.frame_player_stat_or_default_by_key::<WallAerialShotStats>(
1531 frame,
1532 "wall_aerial_shot",
1533 &player_key,
1534 )?,
1535 );
1536 player_value.insert(
1537 "double_tap".to_owned(),
1538 self.frame_player_stat_or_default_by_key::<DoubleTapPlayerStats>(
1539 frame,
1540 "double_tap",
1541 &player_key,
1542 )?,
1543 );
1544 player_value.insert(
1545 "one_timer".to_owned(),
1546 self.frame_player_stat_or_default_by_key::<OneTimerPlayerStats>(
1547 frame,
1548 "one_timer",
1549 &player_key,
1550 )?,
1551 );
1552 player_value.insert(
1553 "pass".to_owned(),
1554 self.frame_player_stat_or_default_by_key::<PassPlayerStats>(
1555 frame,
1556 "pass",
1557 &player_key,
1558 )?,
1559 );
1560 player_value.insert(
1561 "fifty_fifty".to_owned(),
1562 self.frame_player_stat_or_default_by_key::<FiftyFiftyPlayerStats>(
1563 frame,
1564 "fifty_fifty",
1565 &player_key,
1566 )?,
1567 );
1568 player_value.insert(
1569 "speed_flip".to_owned(),
1570 self.frame_player_stat_or_default_by_key::<SpeedFlipStats>(
1571 frame,
1572 "speed_flip",
1573 &player_key,
1574 )?,
1575 );
1576 player_value.insert(
1577 "half_flip".to_owned(),
1578 self.frame_player_stat_or_default_by_key::<HalfFlipStats>(
1579 frame,
1580 "half_flip",
1581 &player_key,
1582 )?,
1583 );
1584 player_value.insert(
1585 "half_volley".to_owned(),
1586 self.frame_player_stat_or_default_by_key::<HalfVolleyPlayerStats>(
1587 frame,
1588 "half_volley",
1589 &player_key,
1590 )?,
1591 );
1592 player_value.insert(
1593 "wavedash".to_owned(),
1594 self.frame_player_stat_or_default_by_key::<WavedashStats>(
1595 frame,
1596 "wavedash",
1597 &player_key,
1598 )?,
1599 );
1600 player_value.insert(
1601 "touch".to_owned(),
1602 self.frame_player_stat_or_value_by_key(
1603 frame,
1604 "touch",
1605 &player_key,
1606 if frame.modules.contains_key("touch") {
1607 serialize_to_json_value(
1608 &TouchStats::default().with_complete_labeled_touch_counts(),
1609 )?
1610 } else {
1611 default_json_value::<TouchStats>()
1612 },
1613 )?,
1614 );
1615 player_value.insert(
1616 "whiff".to_owned(),
1617 self.frame_player_stat_or_default_by_key::<WhiffStats>(frame, "whiff", &player_key)?,
1618 );
1619 player_value.insert(
1620 "flick".to_owned(),
1621 self.frame_player_stat_or_default_by_key::<FlickStats>(frame, "flick", &player_key)?,
1622 );
1623 player_value.insert(
1624 "musty_flick".to_owned(),
1625 self.frame_player_stat_or_default_by_key::<MustyFlickStats>(
1626 frame,
1627 "musty_flick",
1628 &player_key,
1629 )?,
1630 );
1631 player_value.insert(
1632 "dodge_reset".to_owned(),
1633 self.frame_player_stat_or_default_by_key::<DodgeResetStats>(
1634 frame,
1635 "dodge_reset",
1636 &player_key,
1637 )?,
1638 );
1639 player_value.insert(
1640 "ball_carry".to_owned(),
1641 self.frame_player_stat_or_default_by_key::<BallCarryStats>(
1642 frame,
1643 "ball_carry",
1644 &player_key,
1645 )?,
1646 );
1647 player_value.insert(
1648 "air_dribble".to_owned(),
1649 self.frame_player_stat_or_default_by_key::<AirDribbleStats>(
1650 frame,
1651 "air_dribble",
1652 &player_key,
1653 )?,
1654 );
1655 player_value.insert(
1656 "boost".to_owned(),
1657 self.frame_player_stat_or_default_by_key::<BoostStats>(frame, "boost", &player_key)?,
1658 );
1659 player_value.insert(
1660 "bump".to_owned(),
1661 self.frame_player_stat_or_default_by_key::<BumpPlayerStats>(
1662 frame,
1663 "bump",
1664 &player_key,
1665 )?,
1666 );
1667 player_value.insert(
1668 "movement".to_owned(),
1669 self.frame_player_stat_or_value_by_key(
1670 frame,
1671 "movement",
1672 &player_key,
1673 if frame.modules.contains_key("movement") {
1674 serialize_to_json_value(
1675 &MovementStats::default().with_complete_labeled_tracked_time(),
1676 )?
1677 } else {
1678 default_json_value::<MovementStats>()
1679 },
1680 )?,
1681 );
1682 player_value.insert(
1683 "positioning".to_owned(),
1684 self.frame_player_stat_or_default_by_key::<PositioningStats>(
1685 frame,
1686 "positioning",
1687 &player_key,
1688 )?,
1689 );
1690 player_value.insert(
1691 "rotation".to_owned(),
1692 self.frame_player_stat_or_default_by_key::<RotationPlayerStats>(
1693 frame,
1694 "rotation",
1695 &player_key,
1696 )?,
1697 );
1698 player_value.insert(
1699 "powerslide".to_owned(),
1700 self.frame_player_stat_or_default_by_key::<PowerslideStats>(
1701 frame,
1702 "powerslide",
1703 &player_key,
1704 )?,
1705 );
1706 player_value.insert(
1707 "demo".to_owned(),
1708 self.frame_player_stat_or_default_by_key::<DemoPlayerStats>(
1709 frame,
1710 "demo",
1711 &player_key,
1712 )?,
1713 );
1714 Ok(Value::Object(player_value))
1715 }
1716
1717 fn frame_stats_or_default<T>(&self, frame: &StatsSnapshotFrame, module_name: &str) -> Value
1718 where
1719 T: Default + Serialize,
1720 {
1721 frame
1722 .modules
1723 .get(module_name)
1724 .and_then(Value::as_object)
1725 .and_then(|module| module.get("stats"))
1726 .cloned()
1727 .unwrap_or_else(|| default_json_value::<T>())
1728 }
1729
1730 fn frame_team_stat_or_default<T>(
1731 &self,
1732 frame: &StatsSnapshotFrame,
1733 module_name: &str,
1734 team_key: &str,
1735 ) -> Value
1736 where
1737 T: Default + Serialize,
1738 {
1739 frame
1740 .modules
1741 .get(module_name)
1742 .and_then(Value::as_object)
1743 .and_then(|module| module.get(team_key))
1744 .cloned()
1745 .unwrap_or_else(|| default_json_value::<T>())
1746 }
1747
1748 fn frame_player_stat_or_default_by_key<T>(
1749 &self,
1750 frame: &StatsSnapshotFrame,
1751 module_name: &str,
1752 player_key: &str,
1753 ) -> SubtrActorResult<Value>
1754 where
1755 T: Default + Serialize,
1756 {
1757 self.frame_player_stat_or_value_by_key(
1758 frame,
1759 module_name,
1760 player_key,
1761 default_json_value::<T>(),
1762 )
1763 }
1764
1765 fn frame_player_stat_or_value_by_key(
1766 &self,
1767 frame: &StatsSnapshotFrame,
1768 module_name: &str,
1769 player_key: &str,
1770 default_value: Value,
1771 ) -> SubtrActorResult<Value> {
1772 Ok(
1773 player_stats_value_for_key(frame.modules.get(module_name), player_key)?
1774 .cloned()
1775 .unwrap_or(default_value),
1776 )
1777 }
1778
1779 fn frame_stats_or_default_typed<T>(
1780 &self,
1781 frame: &StatsSnapshotFrame,
1782 module_name: &str,
1783 ) -> SubtrActorResult<T>
1784 where
1785 T: Default + DeserializeOwned + Serialize,
1786 {
1787 decode_json_value(self.frame_stats_or_default::<T>(frame, module_name))
1788 }
1789
1790 fn frame_team_stat_or_default_typed<T>(
1791 &self,
1792 frame: &StatsSnapshotFrame,
1793 module_name: &str,
1794 team_key: &str,
1795 ) -> SubtrActorResult<T>
1796 where
1797 T: Default + DeserializeOwned + Serialize,
1798 {
1799 decode_json_value(self.frame_team_stat_or_default::<T>(frame, module_name, team_key))
1800 }
1801
1802 fn frame_player_stat_or_default_typed_by_key<T>(
1803 &self,
1804 frame: &StatsSnapshotFrame,
1805 module_name: &str,
1806 player_key: &str,
1807 ) -> SubtrActorResult<T>
1808 where
1809 T: Default + DeserializeOwned + Serialize,
1810 {
1811 self.frame_player_stat_or_default_with_by_key(frame, module_name, player_key, T::default)
1812 }
1813
1814 fn frame_core_player_stat_or_default_by_key(
1815 &self,
1816 frame: &StatsSnapshotFrame,
1817 player_key: &str,
1818 ) -> SubtrActorResult<CorePlayerStats> {
1819 decode_core_player_stats_value(self.frame_player_stat_or_value_by_key(
1820 frame,
1821 "core",
1822 player_key,
1823 default_json_value::<CorePlayerStats>(),
1824 )?)
1825 }
1826
1827 fn frame_player_stat_or_default_with_by_key<T, F>(
1828 &self,
1829 frame: &StatsSnapshotFrame,
1830 module_name: &str,
1831 player_key: &str,
1832 default: F,
1833 ) -> SubtrActorResult<T>
1834 where
1835 T: DeserializeOwned + Serialize,
1836 F: FnOnce() -> T,
1837 {
1838 decode_json_value(self.frame_player_stat_or_value_by_key(
1839 frame,
1840 module_name,
1841 player_key,
1842 serialize_to_json_value(&default())?,
1843 )?)
1844 }
1845
1846 fn module_typed_array<T>(&self, module_name: &str, field: &str) -> SubtrActorResult<Vec<T>>
1847 where
1848 T: DeserializeOwned,
1849 {
1850 decode_json_value(Value::Array(self.module_array(module_name, field)))
1851 }
1852
1853 fn module_player_events<T, F>(
1854 &self,
1855 module_name: &str,
1856 field: &str,
1857 parse: F,
1858 ) -> SubtrActorResult<Vec<T>>
1859 where
1860 F: Fn(&Value) -> SubtrActorResult<T>,
1861 {
1862 self.module_array(module_name, field)
1863 .iter()
1864 .map(parse)
1865 .collect()
1866 }
1867
1868 fn module_array(&self, module_name: &str, field: &str) -> Vec<Value> {
1869 self.modules
1870 .get(module_name)
1871 .and_then(Value::as_object)
1872 .and_then(|module| module.get(field))
1873 .and_then(Value::as_array)
1874 .cloned()
1875 .unwrap_or_default()
1876 }
1877}
1878
1879impl CapturedStatsData<ReplayStatsFrame> {
1880 pub fn into_legacy_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
1881 let CapturedStatsData {
1882 replay_meta,
1883 config,
1884 modules,
1885 frames,
1886 } = self;
1887 CapturedStatsData::<StatsSnapshotFrame> {
1888 replay_meta,
1889 config,
1890 modules,
1891 frames: Vec::new(),
1892 }
1893 .into_replay_stats_timeline_with_frames(frames)
1894 }
1895
1896 #[deprecated(
1897 note = "use into_legacy_replay_stats_timeline for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
1898 )]
1899 pub fn into_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
1900 self.into_legacy_replay_stats_timeline()
1901 }
1902}
1903
1904fn player_stats_value_for_key<'a>(
1905 module: Option<&'a Value>,
1906 player_key: &str,
1907) -> SubtrActorResult<Option<&'a Value>> {
1908 let Some(entries) = module
1909 .and_then(Value::as_object)
1910 .and_then(|module| module.get("player_stats"))
1911 .and_then(Value::as_array)
1912 else {
1913 return Ok(None);
1914 };
1915
1916 for entry in entries {
1917 let Some(entry_object) = entry.as_object() else {
1918 continue;
1919 };
1920 let Some(player_id) = entry_object.get("player_id") else {
1921 continue;
1922 };
1923 let Some(player_stats) = entry_object.get("stats") else {
1924 continue;
1925 };
1926 if player_id_key(player_id)? == player_key {
1927 return Ok(Some(player_stats));
1928 }
1929 }
1930
1931 Ok(None)
1932}
1933
1934fn player_info_key(player: &PlayerInfo) -> SubtrActorResult<String> {
1935 player_id_key(&serialize_to_json_value(&player.remote_id)?)
1936}
1937
1938fn player_id_key(player_id: &Value) -> SubtrActorResult<String> {
1939 serde_json::to_string(player_id).map_err(|error| {
1940 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1941 error.to_string(),
1942 ))
1943 })
1944}
1945
1946fn default_json_value<T>() -> Value
1947where
1948 T: Default + Serialize,
1949{
1950 serde_json::to_value(T::default()).expect("default stats should serialize to json")
1951}
1952
1953fn decode_json_value<T>(value: Value) -> SubtrActorResult<T>
1954where
1955 T: DeserializeOwned,
1956{
1957 serde_json::from_value(value).map_err(|error| {
1958 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1959 error.to_string(),
1960 ))
1961 })
1962}
1963
1964fn decode_core_player_stats_value(mut value: Value) -> SubtrActorResult<CorePlayerStats> {
1965 normalize_core_player_stats_snapshot(&mut value)?;
1966 decode_json_value(value)
1967}
1968
1969fn normalize_core_player_stats_snapshot(value: &mut Value) -> SubtrActorResult<()> {
1970 let Some(object) = value.as_object_mut() else {
1971 return Ok(());
1972 };
1973
1974 insert_cumulative_from_average(
1975 object,
1976 "cumulative_boost_on_goals_against",
1977 "average_boost_on_goals_against",
1978 "goal_against_boost_sample_count",
1979 )?;
1980 insert_cumulative_from_average(
1981 object,
1982 "cumulative_average_boost_in_goal_against_leadup",
1983 "average_boost_in_goal_against_leadup",
1984 "goal_against_boost_leadup_sample_count",
1985 )?;
1986 insert_cumulative_from_average(
1987 object,
1988 "cumulative_min_boost_in_goal_against_leadup",
1989 "average_min_boost_in_goal_against_leadup",
1990 "goal_against_boost_leadup_sample_count",
1991 )?;
1992 insert_cumulative_from_average(
1993 object,
1994 "cumulative_goal_against_position_x",
1995 "average_goal_against_position_x",
1996 "goal_against_position_sample_count",
1997 )?;
1998 insert_cumulative_from_average(
1999 object,
2000 "cumulative_goal_against_position_y",
2001 "average_goal_against_position_y",
2002 "goal_against_position_sample_count",
2003 )?;
2004 insert_cumulative_from_average(
2005 object,
2006 "cumulative_goal_against_position_z",
2007 "average_goal_against_position_z",
2008 "goal_against_position_sample_count",
2009 )?;
2010 insert_cumulative_from_average(
2011 object,
2012 "cumulative_scoring_goal_last_touch_position_x",
2013 "average_scoring_goal_last_touch_position_x",
2014 "scoring_goal_last_touch_position_sample_count",
2015 )?;
2016 insert_cumulative_from_average(
2017 object,
2018 "cumulative_scoring_goal_last_touch_position_y",
2019 "average_scoring_goal_last_touch_position_y",
2020 "scoring_goal_last_touch_position_sample_count",
2021 )?;
2022 insert_cumulative_from_average(
2023 object,
2024 "cumulative_scoring_goal_last_touch_position_z",
2025 "average_scoring_goal_last_touch_position_z",
2026 "scoring_goal_last_touch_position_sample_count",
2027 )?;
2028 insert_cumulative_from_average(
2029 object,
2030 "cumulative_goal_ball_air_time",
2031 "average_goal_ball_air_time",
2032 "goal_ball_air_time_sample_count",
2033 )?;
2034
2035 if let Value::Object(defaults) = default_json_value::<CorePlayerStats>() {
2036 for (field, default_value) in defaults {
2037 object.entry(field).or_insert(default_value);
2038 }
2039 }
2040
2041 Ok(())
2042}
2043
2044fn insert_cumulative_from_average(
2045 object: &mut Map<String, Value>,
2046 cumulative_field: &str,
2047 average_field: &str,
2048 sample_count_field: &str,
2049) -> SubtrActorResult<()> {
2050 if object.contains_key(cumulative_field) {
2051 return Ok(());
2052 }
2053
2054 let average = object
2055 .get(average_field)
2056 .and_then(Value::as_f64)
2057 .unwrap_or(0.0) as f32;
2058 let sample_count = object
2059 .get(sample_count_field)
2060 .and_then(Value::as_u64)
2061 .unwrap_or(0) as f32;
2062 object.insert(
2063 cumulative_field.to_owned(),
2064 serialize_to_json_value(&(average * sample_count))?,
2065 );
2066
2067 Ok(())
2068}
2069
2070fn parse_timeline_event(value: &Value) -> SubtrActorResult<TimelineEvent> {
2071 let object = json_object(value, "timeline event")?;
2072 Ok(TimelineEvent {
2073 time: json_required_f32(object, "time")?,
2074 kind: decode_json_value(json_required_value(object, "kind")?.clone())?,
2075 player_id: json_optional_remote_id(object.get("player_id"))?,
2076 is_team_0: json_optional_bool(object.get("is_team_0")),
2077 })
2078}
2079
2080fn moment_mechanic_event(
2081 kind: &str,
2082 index: usize,
2083 frame: usize,
2084 time: f32,
2085 player_id: PlayerId,
2086 is_team_0: bool,
2087) -> MechanicEvent {
2088 MechanicEvent {
2089 id: format!("{kind}:{frame}:{index}"),
2090 kind: kind.to_owned(),
2091 player_id,
2092 is_team_0,
2093 timing: MechanicTiming::Moment { frame, time },
2094 properties: Vec::new(),
2095 }
2096}
2097
2098#[allow(clippy::too_many_arguments)]
2099fn span_mechanic_event(
2100 kind: &str,
2101 index: usize,
2102 start_frame: usize,
2103 end_frame: usize,
2104 start_time: f32,
2105 end_time: f32,
2106 player_id: PlayerId,
2107 is_team_0: bool,
2108) -> MechanicEvent {
2109 MechanicEvent {
2110 id: format!("{kind}:{start_frame}:{end_frame}:{index}"),
2111 kind: kind.to_owned(),
2112 player_id,
2113 is_team_0,
2114 timing: MechanicTiming::Span {
2115 start_frame,
2116 end_frame,
2117 start_time,
2118 end_time,
2119 },
2120 properties: Vec::new(),
2121 }
2122}
2123
2124fn mechanic_event_start_time(event: &MechanicEvent) -> f32 {
2125 match event.timing {
2126 MechanicTiming::Moment { time, .. } => time,
2127 MechanicTiming::Span { start_time, .. } => start_time,
2128 }
2129}
2130
2131fn mechanic_event_text_property(key: &str, value: &str) -> MechanicEventProperty {
2132 MechanicEventProperty {
2133 key: key.to_owned(),
2134 value: MechanicEventPropertyValue::Text(value.to_owned()),
2135 }
2136}
2137
2138fn mechanic_event_unsigned_property(key: &str, value: u32) -> MechanicEventProperty {
2139 MechanicEventProperty {
2140 key: key.to_owned(),
2141 value: MechanicEventPropertyValue::Unsigned(value),
2142 }
2143}
2144
2145fn ball_carry_mechanic_event_properties(
2146 object: &serde_json::Map<String, Value>,
2147) -> Vec<MechanicEventProperty> {
2148 let mut properties = Vec::new();
2149 if let Some(origin) = object.get("air_dribble_origin").and_then(Value::as_str) {
2150 properties.push(mechanic_event_text_property("origin", origin));
2151 }
2152 if let Some(touch_count) = object.get("touch_count").and_then(Value::as_u64) {
2153 properties.push(mechanic_event_unsigned_property(
2154 "touch_count",
2155 touch_count as u32,
2156 ));
2157 }
2158 properties
2159}
2160
2161fn parse_ball_carry_mechanic_event(value: &Value, index: usize) -> SubtrActorResult<MechanicEvent> {
2162 let object = json_object(value, "ball carry mechanic event")?;
2163 let serialized_kind = json_required_str(object, "kind")?;
2164 let kind = match serialized_kind {
2165 "carry" => "ball_carry",
2166 "air_dribble" => "air_dribble",
2167 other => other,
2168 };
2169 let mut mechanic_event = span_mechanic_event(
2170 kind,
2171 index,
2172 json_required_usize(object, "start_frame")?,
2173 json_required_usize(object, "end_frame")?,
2174 json_required_f32(object, "start_time")?,
2175 json_required_f32(object, "end_time")?,
2176 json_required_remote_id(object, "player_id")?,
2177 json_required_bool(object, "is_team_0")?,
2178 );
2179 if kind == "air_dribble" {
2180 mechanic_event.properties = ball_carry_mechanic_event_properties(object);
2181 }
2182 Ok(mechanic_event)
2183}
2184
2185fn parse_dodge_reset_mechanic_event(
2186 value: &Value,
2187 index: usize,
2188) -> SubtrActorResult<MechanicEvent> {
2189 let object = json_object(value, "dodge reset mechanic event")?;
2190 Ok(moment_mechanic_event(
2191 "flip_reset",
2192 index,
2193 json_required_usize(object, "frame")?,
2194 json_required_f32(object, "time")?,
2195 json_required_remote_id(object, "player")?,
2196 json_required_bool(object, "is_team_0")?,
2197 ))
2198}
2199
2200fn parse_dodge_reset_event(value: &Value) -> SubtrActorResult<DodgeResetEvent> {
2201 let object = json_object(value, "dodge reset event")?;
2202 Ok(DodgeResetEvent {
2203 time: json_required_f32(object, "time")?,
2204 frame: json_required_usize(object, "frame")?,
2205 player: json_required_remote_id(object, "player")?,
2206 is_team_0: json_required_bool(object, "is_team_0")?,
2207 counter_value: json_required_i32(object, "counter_value")?,
2208 on_ball: json_required_bool(object, "on_ball")?,
2209 })
2210}
2211
2212fn parse_powerslide_event(value: &Value) -> SubtrActorResult<PowerslideEvent> {
2213 let object = json_object(value, "powerslide event")?;
2214 Ok(PowerslideEvent {
2215 time: json_required_f32(object, "time")?,
2216 frame: json_required_usize(object, "frame")?,
2217 player: json_required_remote_id(object, "player")?,
2218 is_team_0: json_required_bool(object, "is_team_0")?,
2219 active: json_required_bool(object, "active")?,
2220 })
2221}
2222
2223fn parse_core_player_stats_event(value: &Value) -> SubtrActorResult<CorePlayerStatsEvent> {
2224 let object = json_object(value, "core player stats event")?;
2225 Ok(CorePlayerStatsEvent {
2226 time: json_required_f32(object, "time")?,
2227 frame: json_required_usize(object, "frame")?,
2228 player: json_required_remote_id(object, "player")?,
2229 is_team_0: json_required_bool(object, "is_team_0")?,
2230 delta: decode_json_value(json_required_value(object, "delta")?.clone())?,
2231 })
2232}
2233
2234fn parse_core_team_stats_event(value: &Value) -> SubtrActorResult<CoreTeamStatsEvent> {
2235 let object = json_object(value, "core team stats event")?;
2236 Ok(CoreTeamStatsEvent {
2237 time: json_required_f32(object, "time")?,
2238 frame: json_required_usize(object, "frame")?,
2239 is_team_0: json_required_bool(object, "is_team_0")?,
2240 delta: decode_json_value(json_required_value(object, "delta")?.clone())?,
2241 })
2242}
2243
2244fn parse_possession_event(value: &Value) -> SubtrActorResult<PossessionEvent> {
2245 let object = json_object(value, "possession event")?;
2246 Ok(PossessionEvent {
2247 time: json_required_f32(object, "time")?,
2248 frame: json_required_usize(object, "frame")?,
2249 active: json_required_bool(object, "active")?,
2250 possession_state: json_required_str(object, "possession_state")?.to_owned(),
2251 field_third: match object.get("field_third") {
2252 None | Some(Value::Null) => None,
2253 Some(_) => Some(json_required_str(object, "field_third")?.to_owned()),
2254 },
2255 })
2256}
2257
2258fn parse_pressure_event(value: &Value) -> SubtrActorResult<PressureEvent> {
2259 let object = json_object(value, "pressure event")?;
2260 Ok(PressureEvent {
2261 time: json_required_f32(object, "time")?,
2262 frame: json_required_usize(object, "frame")?,
2263 active: json_required_bool(object, "active")?,
2264 field_half: json_required_str(object, "field_half")?.to_owned(),
2265 })
2266}
2267
2268fn parse_movement_event(value: &Value) -> SubtrActorResult<MovementEvent> {
2269 let object = json_object(value, "movement event")?;
2270 Ok(MovementEvent {
2271 time: json_required_f32(object, "time")?,
2272 frame: json_required_usize(object, "frame")?,
2273 player: json_required_remote_id(object, "player")?,
2274 is_team_0: json_required_bool(object, "is_team_0")?,
2275 dt: json_required_f32(object, "dt")?,
2276 speed: json_required_f32(object, "speed")?,
2277 distance: json_required_f32(object, "distance")?,
2278 speed_band: json_required_str(object, "speed_band")?.to_owned(),
2279 height_band: json_required_str(object, "height_band")?.to_owned(),
2280 })
2281}
2282
2283fn parse_positioning_event(value: &Value) -> SubtrActorResult<PositioningEvent> {
2284 let object = json_object(value, "positioning event")?;
2285 Ok(PositioningEvent {
2286 time: json_required_f32(object, "time")?,
2287 frame: json_required_usize(object, "frame")?,
2288 player: json_required_remote_id(object, "player")?,
2289 is_team_0: json_required_bool(object, "is_team_0")?,
2290 active_game_time: json_required_f32(object, "active_game_time")?,
2291 tracked_time: json_required_f32(object, "tracked_time")?,
2292 sum_distance_to_teammates: json_required_f32(object, "sum_distance_to_teammates")?,
2293 sum_distance_to_ball: json_required_f32(object, "sum_distance_to_ball")?,
2294 sum_distance_to_ball_has_possession: json_required_f32(
2295 object,
2296 "sum_distance_to_ball_has_possession",
2297 )?,
2298 time_has_possession: json_required_f32(object, "time_has_possession")?,
2299 sum_distance_to_ball_no_possession: json_required_f32(
2300 object,
2301 "sum_distance_to_ball_no_possession",
2302 )?,
2303 time_no_possession: json_required_f32(object, "time_no_possession")?,
2304 time_demolished: json_required_f32(object, "time_demolished")?,
2305 time_no_teammates: json_required_f32(object, "time_no_teammates")?,
2306 time_most_back: json_required_f32(object, "time_most_back")?,
2307 time_most_forward: json_required_f32(object, "time_most_forward")?,
2308 time_mid_role: json_required_f32(object, "time_mid_role")?,
2309 time_other_role: json_required_f32(object, "time_other_role")?,
2310 time_defensive_zone: json_required_f32(object, "time_defensive_third")?,
2311 time_neutral_zone: json_required_f32(object, "time_neutral_third")?,
2312 time_offensive_zone: json_required_f32(object, "time_offensive_third")?,
2313 time_defensive_half: json_required_f32(object, "time_defensive_half")?,
2314 time_offensive_half: json_required_f32(object, "time_offensive_half")?,
2315 time_closest_to_ball: json_required_f32(object, "time_closest_to_ball")?,
2316 time_farthest_from_ball: json_required_f32(object, "time_farthest_from_ball")?,
2317 time_behind_ball: json_required_f32(object, "time_behind_ball")?,
2318 time_level_with_ball: json_required_f32(object, "time_level_with_ball")?,
2319 time_in_front_of_ball: json_required_f32(object, "time_in_front_of_ball")?,
2320 times_caught_ahead_of_play_on_conceded_goals: json_required_usize(
2321 object,
2322 "times_caught_ahead_of_play_on_conceded_goals",
2323 )? as u32,
2324 })
2325}
2326
2327fn parse_rotation_player_event(value: &Value) -> SubtrActorResult<RotationPlayerEvent> {
2328 let object = json_object(value, "rotation player event")?;
2329 Ok(RotationPlayerEvent {
2330 time: json_required_f32(object, "time")?,
2331 frame: json_required_usize(object, "frame")?,
2332 player: json_required_remote_id(object, "player")?,
2333 is_team_0: json_required_bool(object, "is_team_0")?,
2334 active: json_required_bool(object, "active")?,
2335 became_first_man_count: json_required_usize(object, "became_first_man_count")? as u32,
2336 lost_first_man_count: json_required_usize(object, "lost_first_man_count")? as u32,
2337 current_role_state: decode_json_value(
2338 json_required_value(object, "current_role_state")?.clone(),
2339 )?,
2340 current_depth_state: decode_json_value(
2341 json_required_value(object, "current_depth_state")?.clone(),
2342 )?,
2343 })
2344}
2345
2346fn parse_rotation_team_event(value: &Value) -> SubtrActorResult<RotationTeamEvent> {
2347 let object = json_object(value, "rotation team event")?;
2348 Ok(RotationTeamEvent {
2349 time: json_required_f32(object, "time")?,
2350 frame: json_required_usize(object, "frame")?,
2351 is_team_0: json_required_bool(object, "is_team_0")?,
2352 first_man_changes_for_team: json_required_usize(object, "first_man_changes_for_team")?
2353 as u32,
2354 rotation_count: json_required_usize(object, "rotation_count")? as u32,
2355 })
2356}
2357
2358fn parse_touch_stats_event(value: &Value) -> SubtrActorResult<TouchStatsEvent> {
2359 let object = json_object(value, "touch stats event")?;
2360 let time = json_required_f32(object, "time")?;
2361 let frame = json_required_usize(object, "frame")?;
2362 Ok(TouchStatsEvent {
2363 time,
2364 frame,
2365 sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
2366 sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
2367 player: json_required_remote_id(object, "player")?,
2368 is_team_0: json_required_bool(object, "is_team_0")?,
2369 kind: json_required_str(object, "kind")?.to_owned(),
2370 height_band: json_required_str(object, "height_band")?.to_owned(),
2371 surface: json_required_str(object, "surface")?.to_owned(),
2372 dodge_state: json_required_str(object, "dodge_state")?.to_owned(),
2373 ball_speed_change: json_required_f32(object, "ball_speed_change")?,
2374 })
2375}
2376
2377fn parse_touch_ball_movement_event(value: &Value) -> SubtrActorResult<TouchBallMovementEvent> {
2378 let object = json_object(value, "touch ball movement event")?;
2379 Ok(TouchBallMovementEvent {
2380 time: json_required_f32(object, "time")?,
2381 frame: json_required_usize(object, "frame")?,
2382 player: json_required_remote_id(object, "player")?,
2383 is_team_0: json_required_bool(object, "is_team_0")?,
2384 travel_distance: json_required_f32(object, "travel_distance")?,
2385 advance_distance: json_required_f32(object, "advance_distance")?,
2386 retreat_distance: json_required_f32(object, "retreat_distance")?,
2387 })
2388}
2389
2390fn parse_touch_last_touch_event(value: &Value) -> SubtrActorResult<TouchLastTouchEvent> {
2391 let object = json_object(value, "touch last-touch event")?;
2392 let time = json_required_f32(object, "time")?;
2393 let frame = json_required_usize(object, "frame")?;
2394 Ok(TouchLastTouchEvent {
2395 time,
2396 frame,
2397 sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
2398 sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
2399 is_team_0: json_required_bool(object, "is_team_0")?,
2400 player: json_optional_remote_id(object.get("player"))?,
2401 })
2402}
2403
2404fn parse_flick_mechanic_event(value: &Value, index: usize) -> SubtrActorResult<MechanicEvent> {
2405 let object = json_object(value, "flick mechanic event")?;
2406 Ok(span_mechanic_event(
2407 "flick",
2408 index,
2409 json_required_usize(object, "setup_start_frame")?,
2410 json_required_usize(object, "frame")?,
2411 json_required_f32(object, "setup_start_time")?,
2412 json_required_f32(object, "time")?,
2413 json_required_remote_id(object, "player")?,
2414 json_required_bool(object, "is_team_0")?,
2415 ))
2416}
2417
2418fn parse_flick_event(value: &Value) -> SubtrActorResult<FlickEvent> {
2419 let object = json_object(value, "flick event")?;
2420 let time = json_required_f32(object, "time")?;
2421 let frame = json_required_usize(object, "frame")?;
2422 Ok(FlickEvent {
2423 time,
2424 frame,
2425 sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
2426 sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
2427 player: json_required_remote_id(object, "player")?,
2428 is_team_0: json_required_bool(object, "is_team_0")?,
2429 dodge_time: json_required_f32(object, "dodge_time")?,
2430 dodge_frame: json_required_usize(object, "dodge_frame")?,
2431 time_since_dodge: json_required_f32(object, "time_since_dodge")?,
2432 setup_start_time: json_required_f32(object, "setup_start_time")?,
2433 setup_start_frame: json_required_usize(object, "setup_start_frame")?,
2434 setup_duration: json_required_f32(object, "setup_duration")?,
2435 setup_touch_count: json_required_usize(object, "setup_touch_count")? as u32,
2436 average_horizontal_gap: json_required_f32(object, "average_horizontal_gap")?,
2437 average_vertical_gap: json_required_f32(object, "average_vertical_gap")?,
2438 ball_speed_change: json_required_f32(object, "ball_speed_change")?,
2439 ball_impulse: json_required_vec3(object, "ball_impulse")?,
2440 impulse_away_alignment: json_required_f32(object, "impulse_away_alignment")?,
2441 vertical_impulse: json_required_f32(object, "vertical_impulse")?,
2442 confidence: json_required_f32(object, "confidence")?,
2443 })
2444}
2445
2446fn parse_musty_flick_mechanic_event(
2447 value: &Value,
2448 index: usize,
2449) -> SubtrActorResult<MechanicEvent> {
2450 let object = json_object(value, "musty flick mechanic event")?;
2451 Ok(span_mechanic_event(
2452 "musty_flick",
2453 index,
2454 json_required_usize(object, "dodge_frame")?,
2455 json_required_usize(object, "frame")?,
2456 json_required_f32(object, "dodge_time")?,
2457 json_required_f32(object, "time")?,
2458 json_required_remote_id(object, "player")?,
2459 json_required_bool(object, "is_team_0")?,
2460 ))
2461}
2462
2463fn parse_musty_flick_event(value: &Value) -> SubtrActorResult<MustyFlickEvent> {
2464 let object = json_object(value, "musty flick event")?;
2465 let time = json_required_f32(object, "time")?;
2466 let frame = json_required_usize(object, "frame")?;
2467 Ok(MustyFlickEvent {
2468 time,
2469 frame,
2470 sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
2471 sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
2472 player: json_required_remote_id(object, "player")?,
2473 is_team_0: json_required_bool(object, "is_team_0")?,
2474 aerial: json_required_bool(object, "aerial")?,
2475 dodge_time: json_required_f32(object, "dodge_time")?,
2476 dodge_frame: json_required_usize(object, "dodge_frame")?,
2477 time_since_dodge: json_required_f32(object, "time_since_dodge")?,
2478 confidence: json_required_f32(object, "confidence")?,
2479 local_ball_position: json_required_vec3(object, "local_ball_position")?,
2480 rear_alignment: json_required_f32(object, "rear_alignment")?,
2481 top_alignment: json_required_f32(object, "top_alignment")?,
2482 forward_approach_speed: json_required_f32(object, "forward_approach_speed")?,
2483 pitch_rate: json_required_f32(object, "pitch_rate")?,
2484 ball_speed_change: json_required_f32(object, "ball_speed_change")?,
2485 })
2486}
2487
2488fn parse_goal_context_event(value: &Value) -> SubtrActorResult<GoalContextEvent> {
2489 let object = json_object(value, "goal context event")?;
2490 Ok(GoalContextEvent {
2491 time: json_required_f32(object, "time")?,
2492 frame: json_required_usize(object, "frame")?,
2493 scoring_team_is_team_0: json_required_bool(object, "scoring_team_is_team_0")?,
2494 scorer: json_optional_remote_id(object.get("scorer"))?,
2495 scoring_team_most_back_player: json_optional_remote_id(
2496 object.get("scoring_team_most_back_player"),
2497 )?,
2498 defending_team_most_back_player: json_optional_remote_id(
2499 object.get("defending_team_most_back_player"),
2500 )?,
2501 ball_position: json_optional_goal_context_position(object.get("ball_position"))?,
2502 ball_air_time_before_goal: json_optional_f32(object.get("ball_air_time_before_goal"))?,
2503 goal_buildup: object
2504 .get("goal_buildup")
2505 .map(|value| decode_json_value(value.clone()))
2506 .transpose()?
2507 .unwrap_or_default(),
2508 scorer_last_touch: match object.get("scorer_last_touch") {
2509 None | Some(Value::Null) => None,
2510 Some(value) => Some(parse_goal_touch_context(value)?),
2511 },
2512 players: json_required_array(object, "players")?
2513 .iter()
2514 .map(parse_goal_player_context)
2515 .collect::<SubtrActorResult<Vec<_>>>()?,
2516 })
2517}
2518
2519fn parse_goal_player_context(value: &Value) -> SubtrActorResult<GoalPlayerContext> {
2520 let object = json_object(value, "goal player context")?;
2521 Ok(GoalPlayerContext {
2522 player: json_required_remote_id(object, "player")?,
2523 is_team_0: json_required_bool(object, "is_team_0")?,
2524 position: json_optional_goal_context_position(object.get("position"))?,
2525 boost_amount: json_optional_f32(object.get("boost_amount"))?,
2526 average_boost_in_leadup: json_optional_f32(object.get("average_boost_in_leadup"))?,
2527 min_boost_in_leadup: json_optional_f32(object.get("min_boost_in_leadup"))?,
2528 is_most_back: json_required_bool(object, "is_most_back")?,
2529 })
2530}
2531
2532fn parse_goal_touch_context(value: &Value) -> SubtrActorResult<GoalTouchContext> {
2533 let object = json_object(value, "goal touch context")?;
2534 Ok(GoalTouchContext {
2535 time: json_required_f32(object, "time")?,
2536 frame: json_required_usize(object, "frame")?,
2537 player: json_required_remote_id(object, "player")?,
2538 is_team_0: json_required_bool(object, "is_team_0")?,
2539 ball_position: json_optional_goal_context_position(object.get("ball_position"))?,
2540 player_position: json_optional_goal_context_position(object.get("player_position"))?,
2541 players: match object.get("players").and_then(Value::as_array) {
2542 Some(players) => players
2543 .iter()
2544 .map(parse_goal_player_context)
2545 .collect::<SubtrActorResult<Vec<_>>>()?,
2546 None => Vec::new(),
2547 },
2548 })
2549}
2550
2551fn parse_backboard_event(value: &Value) -> SubtrActorResult<BackboardBounceEvent> {
2552 let object = json_object(value, "backboard event")?;
2553 Ok(BackboardBounceEvent {
2554 time: json_required_f32(object, "time")?,
2555 frame: json_required_usize(object, "frame")?,
2556 player: json_required_remote_id(object, "player")?,
2557 is_team_0: json_required_bool(object, "is_team_0")?,
2558 })
2559}
2560
2561fn parse_ceiling_shot_event(value: &Value) -> SubtrActorResult<CeilingShotEvent> {
2562 let object = json_object(value, "ceiling shot event")?;
2563 Ok(CeilingShotEvent {
2564 time: json_required_f32(object, "time")?,
2565 frame: json_required_usize(object, "frame")?,
2566 player: json_required_remote_id(object, "player")?,
2567 is_team_0: json_required_bool(object, "is_team_0")?,
2568 ceiling_contact_time: json_required_f32(object, "ceiling_contact_time")?,
2569 ceiling_contact_frame: json_required_usize(object, "ceiling_contact_frame")?,
2570 time_since_ceiling_contact: json_required_f32(object, "time_since_ceiling_contact")?,
2571 ceiling_contact_position: json_required_vec3(object, "ceiling_contact_position")?,
2572 touch_position: json_required_vec3(object, "touch_position")?,
2573 local_ball_position: json_required_vec3(object, "local_ball_position")?,
2574 separation_from_ceiling: json_required_f32(object, "separation_from_ceiling")?,
2575 roof_alignment: json_required_f32(object, "roof_alignment")?,
2576 forward_alignment: json_required_f32(object, "forward_alignment")?,
2577 forward_approach_speed: json_required_f32(object, "forward_approach_speed")?,
2578 ball_speed_change: json_required_f32(object, "ball_speed_change")?,
2579 confidence: json_required_f32(object, "confidence")?,
2580 })
2581}
2582
2583fn parse_wall_aerial_event(value: &Value) -> SubtrActorResult<WallAerialEvent> {
2584 let object = json_object(value, "wall aerial event")?;
2585 let time = json_required_f32(object, "time")?;
2586 let frame = json_required_usize(object, "frame")?;
2587 Ok(WallAerialEvent {
2588 time,
2589 frame,
2590 sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
2591 sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
2592 player: json_required_remote_id(object, "player")?,
2593 is_team_0: json_required_bool(object, "is_team_0")?,
2594 wall: decode_json_value(json_required_value(object, "wall")?.clone())?,
2595 wall_contact_time: json_required_f32(object, "wall_contact_time")?,
2596 wall_contact_frame: json_required_usize(object, "wall_contact_frame")?,
2597 takeoff_time: json_required_f32(object, "takeoff_time")?,
2598 takeoff_frame: json_required_usize(object, "takeoff_frame")?,
2599 time_since_takeoff: json_required_f32(object, "time_since_takeoff")?,
2600 wall_contact_position: json_required_vec3(object, "wall_contact_position")?,
2601 takeoff_position: json_required_vec3(object, "takeoff_position")?,
2602 player_position: json_required_vec3(object, "player_position")?,
2603 ball_position: json_required_vec3(object, "ball_position")?,
2604 setup_start_time: json_required_f32(object, "setup_start_time")?,
2605 setup_start_frame: json_required_usize(object, "setup_start_frame")?,
2606 setup_duration: json_required_f32(object, "setup_duration")?,
2607 ball_speed: json_required_f32(object, "ball_speed")?,
2608 ball_speed_change: json_required_f32(object, "ball_speed_change")?,
2609 goal_alignment: json_required_f32(object, "goal_alignment")?,
2610 confidence: json_required_f32(object, "confidence")?,
2611 })
2612}
2613
2614fn parse_wall_aerial_shot_event(value: &Value) -> SubtrActorResult<WallAerialShotEvent> {
2615 let object = json_object(value, "wall aerial shot event")?;
2616 Ok(WallAerialShotEvent {
2617 time: json_required_f32(object, "time")?,
2618 frame: json_required_usize(object, "frame")?,
2619 player: json_required_remote_id(object, "player")?,
2620 is_team_0: json_required_bool(object, "is_team_0")?,
2621 wall: decode_json_value(json_required_value(object, "wall")?.clone())?,
2622 wall_contact_time: json_required_f32(object, "wall_contact_time")?,
2623 wall_contact_frame: json_required_usize(object, "wall_contact_frame")?,
2624 takeoff_time: json_required_f32(object, "takeoff_time")?,
2625 takeoff_frame: json_required_usize(object, "takeoff_frame")?,
2626 time_since_takeoff: json_required_f32(object, "time_since_takeoff")?,
2627 wall_contact_position: json_required_vec3(object, "wall_contact_position")?,
2628 takeoff_position: json_required_vec3(object, "takeoff_position")?,
2629 player_position: json_required_vec3(object, "player_position")?,
2630 ball_position: json_required_vec3(object, "ball_position")?,
2631 ball_speed: json_optional_f32(object.get("ball_speed"))?,
2632 goal_alignment: json_optional_f32(object.get("goal_alignment"))?,
2633 confidence: json_required_f32(object, "confidence")?,
2634 })
2635}
2636
2637fn parse_center_event(value: &Value) -> SubtrActorResult<CenterEvent> {
2638 let object = json_object(value, "center event")?;
2639 Ok(CenterEvent {
2640 time: json_required_f32(object, "time")?,
2641 frame: json_required_usize(object, "frame")?,
2642 player: json_required_remote_id(object, "player")?,
2643 is_team_0: json_required_bool(object, "is_team_0")?,
2644 start_time: json_required_f32(object, "start_time")?,
2645 start_frame: json_required_usize(object, "start_frame")?,
2646 duration: json_required_f32(object, "duration")?,
2647 start_ball_position: json_required_vec3(object, "start_ball_position")?,
2648 end_ball_position: json_required_vec3(object, "end_ball_position")?,
2649 ball_travel_distance: json_required_f32(object, "ball_travel_distance")?,
2650 ball_advance_distance: json_required_f32(object, "ball_advance_distance")?,
2651 lateral_centering_distance: json_required_f32(object, "lateral_centering_distance")?,
2652 })
2653}
2654
2655fn parse_double_tap_event(value: &Value) -> SubtrActorResult<DoubleTapEvent> {
2656 let object = json_object(value, "double tap event")?;
2657 Ok(DoubleTapEvent {
2658 time: json_required_f32(object, "time")?,
2659 frame: json_required_usize(object, "frame")?,
2660 player: json_required_remote_id(object, "player")?,
2661 is_team_0: json_required_bool(object, "is_team_0")?,
2662 backboard_time: json_required_f32(object, "backboard_time")?,
2663 backboard_frame: json_required_usize(object, "backboard_frame")?,
2664 })
2665}
2666
2667fn parse_pass_event(value: &Value) -> SubtrActorResult<PassEvent> {
2668 let object = json_object(value, "pass event")?;
2669 let time = json_required_f32(object, "time")?;
2670 let frame = json_required_usize(object, "frame")?;
2671 Ok(PassEvent {
2672 time,
2673 frame,
2674 sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
2675 sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
2676 passer: json_required_remote_id(object, "passer")?,
2677 receiver: json_required_remote_id(object, "receiver")?,
2678 is_team_0: json_required_bool(object, "is_team_0")?,
2679 start_time: json_required_f32(object, "start_time")?,
2680 start_frame: json_required_usize(object, "start_frame")?,
2681 duration: json_required_f32(object, "duration")?,
2682 ball_travel_distance: json_required_f32(object, "ball_travel_distance")?,
2683 ball_advance_distance: json_required_f32(object, "ball_advance_distance")?,
2684 pass_kind: parse_pass_kind(object.get("pass_kind"))?,
2685 })
2686}
2687
2688fn parse_pass_last_completed_event(value: &Value) -> SubtrActorResult<PassLastCompletedEvent> {
2689 let object = json_object(value, "pass last completed event")?;
2690 Ok(PassLastCompletedEvent {
2691 time: json_required_f32(object, "time")?,
2692 frame: json_required_usize(object, "frame")?,
2693 player: json_optional_remote_id(object.get("player"))?,
2694 })
2695}
2696
2697fn parse_pass_kind(value: Option<&Value>) -> SubtrActorResult<PassKind> {
2698 let Some(value) = value else {
2699 return Ok(PassKind::Direct);
2700 };
2701 let kind = value.as_str().ok_or_else(|| {
2702 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
2703 "Expected JSON field 'pass_kind' to be a string".to_owned(),
2704 ))
2705 })?;
2706 match kind {
2707 "direct" => Ok(PassKind::Direct),
2708 "backboard" => Ok(PassKind::Backboard),
2709 "fifty_fifty" => Ok(PassKind::FiftyFifty),
2710 "fifty_fifty_backboard" => Ok(PassKind::FiftyFiftyBackboard),
2711 other => Err(SubtrActorError::new(
2712 SubtrActorErrorVariant::StatsSerializationError(format!("Unknown pass kind '{other}'")),
2713 )),
2714 }
2715}
2716
2717fn parse_ball_carry_event(value: &Value) -> SubtrActorResult<BallCarryEvent> {
2718 let object = json_object(value, "ball carry event")?;
2719 Ok(BallCarryEvent {
2720 player_id: json_required_remote_id(object, "player_id")?,
2721 is_team_0: json_required_bool(object, "is_team_0")?,
2722 kind: parse_ball_carry_kind(json_required_str(object, "kind")?)?,
2723 start_frame: json_required_usize(object, "start_frame")?,
2724 end_frame: json_required_usize(object, "end_frame")?,
2725 start_time: json_required_f32(object, "start_time")?,
2726 end_time: json_required_f32(object, "end_time")?,
2727 duration: json_required_f32(object, "duration")?,
2728 straight_line_distance: json_required_f32(object, "straight_line_distance")?,
2729 path_distance: json_required_f32(object, "path_distance")?,
2730 average_horizontal_gap: json_required_f32(object, "average_horizontal_gap")?,
2731 average_vertical_gap: json_required_f32(object, "average_vertical_gap")?,
2732 average_speed: json_required_f32(object, "average_speed")?,
2733 touch_count: json_required_usize(object, "touch_count")? as u32,
2734 air_touch_count: json_required_usize(object, "air_touch_count")? as u32,
2735 air_dribble_origin: parse_air_dribble_origin(object.get("air_dribble_origin"))?,
2736 })
2737}
2738
2739fn parse_ball_carry_kind(kind: &str) -> SubtrActorResult<BallCarryKind> {
2740 match kind {
2741 "carry" => Ok(BallCarryKind::Carry),
2742 "air_dribble" => Ok(BallCarryKind::AirDribble),
2743 other => Err(SubtrActorError::new(
2744 SubtrActorErrorVariant::StatsSerializationError(format!(
2745 "Unknown ball carry kind '{other}'"
2746 )),
2747 )),
2748 }
2749}
2750
2751fn parse_air_dribble_origin(value: Option<&Value>) -> SubtrActorResult<Option<AirDribbleOrigin>> {
2752 let Some(value) = value else {
2753 return Ok(None);
2754 };
2755 if value.is_null() {
2756 return Ok(None);
2757 }
2758 let origin = value.as_str().ok_or_else(|| {
2759 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
2760 "Expected optional JSON field 'air_dribble_origin' to be a string".to_owned(),
2761 ))
2762 })?;
2763 match origin {
2764 "ground_to_air" => Ok(Some(AirDribbleOrigin::GroundToAir)),
2765 "wall_to_air" => Ok(Some(AirDribbleOrigin::WallToAir)),
2766 other => Err(SubtrActorError::new(
2767 SubtrActorErrorVariant::StatsSerializationError(format!(
2768 "Unknown air dribble origin '{other}'"
2769 )),
2770 )),
2771 }
2772}
2773
2774fn parse_one_timer_event(value: &Value) -> SubtrActorResult<OneTimerEvent> {
2775 let object = json_object(value, "one timer event")?;
2776 Ok(OneTimerEvent {
2777 time: json_required_f32(object, "time")?,
2778 frame: json_required_usize(object, "frame")?,
2779 player: json_required_remote_id(object, "player")?,
2780 passer: json_required_remote_id(object, "passer")?,
2781 is_team_0: json_required_bool(object, "is_team_0")?,
2782 pass_start_time: json_required_f32(object, "pass_start_time")?,
2783 pass_start_frame: json_required_usize(object, "pass_start_frame")?,
2784 pass_duration: json_required_f32(object, "pass_duration")?,
2785 pass_travel_distance: json_required_f32(object, "pass_travel_distance")?,
2786 pass_advance_distance: json_required_f32(object, "pass_advance_distance")?,
2787 ball_speed: json_required_f32(object, "ball_speed")?,
2788 goal_alignment: json_required_f32(object, "goal_alignment")?,
2789 })
2790}
2791
2792fn parse_half_volley_event(value: &Value) -> SubtrActorResult<HalfVolleyEvent> {
2793 let object = json_object(value, "half volley event")?;
2794 let time = json_required_f32(object, "time")?;
2795 let frame = json_required_usize(object, "frame")?;
2796 Ok(HalfVolleyEvent {
2797 time,
2798 frame,
2799 sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
2800 sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
2801 player: json_required_remote_id(object, "player")?,
2802 is_team_0: json_required_bool(object, "is_team_0")?,
2803 bounce_time: json_required_f32(object, "bounce_time")?,
2804 bounce_frame: json_required_usize(object, "bounce_frame")?,
2805 bounce_to_touch_seconds: json_required_f32(object, "bounce_to_touch_seconds")?,
2806 ball_speed: json_required_f32(object, "ball_speed")?,
2807 goal_alignment: json_required_f32(object, "goal_alignment")?,
2808 })
2809}
2810
2811fn parse_goal_tag_event(value: &Value) -> SubtrActorResult<GoalTagEvent> {
2812 let object = json_object(value, "goal tag event")?;
2813 Ok(GoalTagEvent {
2814 goal_index: json_required_usize(object, "goal_index")?,
2815 time: json_required_f32(object, "time")?,
2816 frame: json_required_usize(object, "frame")?,
2817 kind: decode_json_value(json_required_value(object, "kind")?.clone())?,
2818 scoring_team_is_team_0: json_required_bool(object, "scoring_team_is_team_0")?,
2819 scorer: json_optional_remote_id(object.get("scorer"))?,
2820 confidence: json_required_f32(object, "confidence")?,
2821 modifiers: json_optional_array(object.get("modifiers"))?
2822 .iter()
2823 .map(|modifier| decode_json_value(modifier.clone()))
2824 .collect::<SubtrActorResult<Vec<_>>>()?,
2825 evidence: json_required_array(object, "evidence")?
2826 .iter()
2827 .map(parse_goal_tag_evidence)
2828 .collect::<SubtrActorResult<Vec<_>>>()?,
2829 })
2830}
2831
2832fn parse_goal_tag_evidence(value: &Value) -> SubtrActorResult<GoalTagEvidence> {
2833 let object = json_object(value, "goal tag evidence")?;
2834 Ok(GoalTagEvidence {
2835 kind: decode_json_value(json_required_value(object, "kind")?.clone())?,
2836 time: json_required_f32(object, "time")?,
2837 frame: json_required_usize(object, "frame")?,
2838 player: json_optional_remote_id(object.get("player"))?,
2839 })
2840}
2841
2842fn parse_fifty_fifty_event(value: &Value) -> SubtrActorResult<FiftyFiftyEvent> {
2843 let object = json_object(value, "fifty fifty event")?;
2844 Ok(FiftyFiftyEvent {
2845 start_time: json_required_f32(object, "start_time")?,
2846 start_frame: json_required_usize(object, "start_frame")?,
2847 resolve_time: json_required_f32(object, "resolve_time")?,
2848 resolve_frame: json_required_usize(object, "resolve_frame")?,
2849 is_kickoff: json_required_bool(object, "is_kickoff")?,
2850 team_zero_player: json_optional_remote_id(object.get("team_zero_player"))?,
2851 team_one_player: json_optional_remote_id(object.get("team_one_player"))?,
2852 team_zero_position: json_required_vec3(object, "team_zero_position")?,
2853 team_one_position: json_required_vec3(object, "team_one_position")?,
2854 midpoint: json_required_vec3(object, "midpoint")?,
2855 plane_normal: json_required_vec3(object, "plane_normal")?,
2856 winning_team_is_team_0: json_optional_bool(object.get("winning_team_is_team_0")),
2857 possession_team_is_team_0: json_optional_bool(object.get("possession_team_is_team_0")),
2858 })
2859}
2860
2861fn parse_speed_flip_event(value: &Value) -> SubtrActorResult<SpeedFlipEvent> {
2862 let object = json_object(value, "speed flip event")?;
2863 let time = json_required_f32(object, "time")?;
2864 let frame = json_required_usize(object, "frame")?;
2865 Ok(SpeedFlipEvent {
2866 time,
2867 frame,
2868 resolved_time: json_optional_f32(object.get("resolved_time"))?.unwrap_or(time),
2869 resolved_frame: json_optional_usize(object.get("resolved_frame"))?.unwrap_or(frame),
2870 player: json_required_remote_id(object, "player")?,
2871 is_team_0: json_required_bool(object, "is_team_0")?,
2872 time_since_kickoff_start: json_required_f32(object, "time_since_kickoff_start")?,
2873 start_position: json_required_vec3(object, "start_position")?,
2874 end_position: json_required_vec3(object, "end_position")?,
2875 start_speed: json_required_f32(object, "start_speed")?,
2876 max_speed: json_required_f32(object, "max_speed")?,
2877 best_alignment: json_required_f32(object, "best_alignment")?,
2878 diagonal_score: json_required_f32(object, "diagonal_score")?,
2879 cancel_score: json_required_f32(object, "cancel_score")?,
2880 speed_score: json_required_f32(object, "speed_score")?,
2881 confidence: json_required_f32(object, "confidence")?,
2882 })
2883}
2884
2885fn parse_half_flip_event(value: &Value) -> SubtrActorResult<HalfFlipEvent> {
2886 let object = json_object(value, "half flip event")?;
2887 Ok(HalfFlipEvent {
2888 time: json_required_f32(object, "time")?,
2889 frame: json_required_usize(object, "frame")?,
2890 player: json_required_remote_id(object, "player")?,
2891 is_team_0: json_required_bool(object, "is_team_0")?,
2892 start_position: json_required_vec3(object, "start_position")?,
2893 end_position: json_required_vec3(object, "end_position")?,
2894 start_speed: json_required_f32(object, "start_speed")?,
2895 end_speed: json_required_f32(object, "end_speed")?,
2896 start_backward_alignment: json_required_f32(object, "start_backward_alignment")?,
2897 best_reorientation_alignment: json_required_f32(object, "best_reorientation_alignment")?,
2898 best_forward_reversal: json_required_f32(object, "best_forward_reversal")?,
2899 max_forward_vertical: json_required_f32(object, "max_forward_vertical")?,
2900 confidence: json_required_f32(object, "confidence")?,
2901 })
2902}
2903
2904fn parse_wavedash_event(value: &Value) -> SubtrActorResult<WavedashEvent> {
2905 let object = json_object(value, "wavedash event")?;
2906 Ok(WavedashEvent {
2907 time: json_required_f32(object, "time")?,
2908 frame: json_required_usize(object, "frame")?,
2909 player: json_required_remote_id(object, "player")?,
2910 is_team_0: json_required_bool(object, "is_team_0")?,
2911 dodge_time: json_required_f32(object, "dodge_time")?,
2912 dodge_frame: json_required_usize(object, "dodge_frame")?,
2913 time_since_dodge: json_required_f32(object, "time_since_dodge")?,
2914 dodge_position: json_required_vec3(object, "dodge_position")?,
2915 landing_position: json_required_vec3(object, "landing_position")?,
2916 start_speed: json_required_f32(object, "start_speed")?,
2917 landing_speed: json_required_f32(object, "landing_speed")?,
2918 horizontal_speed_gain: json_required_f32(object, "horizontal_speed_gain")?,
2919 landing_uprightness: json_required_f32(object, "landing_uprightness")?,
2920 confidence: json_required_f32(object, "confidence")?,
2921 })
2922}
2923
2924fn parse_whiff_event(value: &Value) -> SubtrActorResult<WhiffEvent> {
2925 let object = json_object(value, "whiff event")?;
2926 let time = json_required_f32(object, "time")?;
2927 let frame = json_required_usize(object, "frame")?;
2928 Ok(WhiffEvent {
2929 kind: match object.get("kind").and_then(Value::as_str) {
2930 None | Some("whiff") => WhiffEventKind::Whiff,
2931 Some("beaten_to_ball") => WhiffEventKind::BeatenToBall,
2932 Some(kind) => {
2933 return SubtrActorError::new_result(
2934 SubtrActorErrorVariant::StatsSerializationError(format!(
2935 "Unknown whiff event kind '{kind}'"
2936 )),
2937 );
2938 }
2939 },
2940 time,
2941 frame,
2942 resolved_time: json_optional_f32(object.get("resolved_time"))?.unwrap_or(time),
2943 resolved_frame: json_optional_usize(object.get("resolved_frame"))?.unwrap_or(frame),
2944 player: json_required_remote_id(object, "player")?,
2945 is_team_0: json_required_bool(object, "is_team_0")?,
2946 closest_approach_distance: json_required_f32(object, "closest_approach_distance")?,
2947 forward_alignment: json_required_f32(object, "forward_alignment")?,
2948 approach_speed: json_required_f32(object, "approach_speed")?,
2949 dodge_active: json_required_bool(object, "dodge_active")?,
2950 aerial: json_required_bool(object, "aerial")?,
2951 })
2952}
2953
2954fn parse_bump_event(value: &Value) -> SubtrActorResult<BumpEvent> {
2955 let object = json_object(value, "bump event")?;
2956 Ok(BumpEvent {
2957 time: json_required_f32(object, "time")?,
2958 frame: json_required_usize(object, "frame")?,
2959 initiator: json_required_remote_id(object, "initiator")?,
2960 victim: json_required_remote_id(object, "victim")?,
2961 initiator_is_team_0: json_required_bool(object, "initiator_is_team_0")?,
2962 victim_is_team_0: json_required_bool(object, "victim_is_team_0")?,
2963 is_team_bump: json_required_bool(object, "is_team_bump")?,
2964 strength: json_required_f32(object, "strength")?,
2965 confidence: json_required_f32(object, "confidence")?,
2966 contact_distance: json_required_f32(object, "contact_distance")?,
2967 closing_speed: json_required_f32(object, "closing_speed")?,
2968 victim_impulse: json_required_f32(object, "victim_impulse")?,
2969 initiator_position: json_required_vec3(object, "initiator_position")?,
2970 victim_position: json_required_vec3(object, "victim_position")?,
2971 })
2972}
2973
2974fn parse_boost_pickup_comparison_event(
2975 value: &Value,
2976) -> SubtrActorResult<BoostPickupComparisonEvent> {
2977 let object = json_object(value, "boost pickup comparison event")?;
2978 Ok(BoostPickupComparisonEvent {
2979 comparison: decode_json_value(json_required_value(object, "comparison")?.clone())?,
2980 frame: json_required_usize(object, "frame")?,
2981 time: json_required_f32(object, "time")?,
2982 player_id: json_required_remote_id(object, "player_id")?,
2983 is_team_0: json_required_bool(object, "is_team_0")?,
2984 pad_type: decode_json_value(json_required_value(object, "pad_type")?.clone())?,
2985 field_half: decode_json_value(json_required_value(object, "field_half")?.clone())?,
2986 activity: decode_json_value(json_required_value(object, "activity")?.clone())?,
2987 reported_frame: json_optional_usize(object.get("reported_frame"))?,
2988 reported_time: json_optional_f32(object.get("reported_time"))?,
2989 inferred_frame: json_optional_usize(object.get("inferred_frame"))?,
2990 inferred_time: json_optional_f32(object.get("inferred_time"))?,
2991 boost_before: json_optional_f32(object.get("boost_before"))?,
2992 boost_after: json_optional_f32(object.get("boost_after"))?,
2993 })
2994}
2995
2996fn parse_boost_ledger_event(value: &Value) -> SubtrActorResult<BoostLedgerEvent> {
2997 let object = json_object(value, "boost ledger event")?;
2998 Ok(BoostLedgerEvent {
2999 frame: json_required_usize(object, "frame")?,
3000 time: json_required_f32(object, "time")?,
3001 player_id: json_required_remote_id(object, "player_id")?,
3002 is_team_0: json_required_bool(object, "is_team_0")?,
3003 transaction: decode_json_value(json_required_value(object, "transaction")?.clone())?,
3004 amount: json_required_f32(object, "amount")?,
3005 count: json_required_usize(object, "count")? as u32,
3006 labels: decode_json_value(
3007 object
3008 .get("labels")
3009 .cloned()
3010 .unwrap_or_else(|| Value::Array(Vec::new())),
3011 )?,
3012 boost_before: json_optional_f32(object.get("boost_before"))?,
3013 boost_after: json_optional_f32(object.get("boost_after"))?,
3014 })
3015}
3016
3017fn parse_boost_state_event(value: &Value) -> SubtrActorResult<BoostStateEvent> {
3018 let object = json_object(value, "boost state event")?;
3019 Ok(BoostStateEvent {
3020 frame: json_required_usize(object, "frame")?,
3021 time: json_required_f32(object, "time")?,
3022 player_id: json_required_remote_id(object, "player_id")?,
3023 is_team_0: json_required_bool(object, "is_team_0")?,
3024 boost_amount: json_required_f32(object, "boost_amount")?,
3025 boost_before: json_optional_f32(object.get("boost_before"))?,
3026 })
3027}
3028
3029fn json_object<'a>(
3030 value: &'a Value,
3031 context: &str,
3032) -> SubtrActorResult<&'a serde_json::Map<String, Value>> {
3033 value.as_object().ok_or_else(|| {
3034 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3035 "Expected {context} to be a JSON object"
3036 )))
3037 })
3038}
3039
3040fn json_required_value<'a>(
3041 object: &'a serde_json::Map<String, Value>,
3042 field: &str,
3043) -> SubtrActorResult<&'a Value> {
3044 object.get(field).ok_or_else(|| {
3045 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3046 "Missing JSON field '{field}'"
3047 )))
3048 })
3049}
3050
3051fn json_required_array<'a>(
3052 object: &'a serde_json::Map<String, Value>,
3053 field: &str,
3054) -> SubtrActorResult<&'a Vec<Value>> {
3055 json_required_value(object, field)?
3056 .as_array()
3057 .ok_or_else(|| {
3058 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3059 "Expected JSON field '{field}' to be an array"
3060 )))
3061 })
3062}
3063
3064fn json_optional_array(value: Option<&Value>) -> SubtrActorResult<&[Value]> {
3065 match value {
3066 Some(Value::Array(values)) => Ok(values),
3067 Some(_) => SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
3068 "Expected optional JSON value to be an array".to_owned(),
3069 )),
3070 None => Ok(&[]),
3071 }
3072}
3073
3074fn json_f32(value: &Value) -> Option<f32> {
3075 value.as_f64().map(|number| number as f32)
3076}
3077
3078fn json_config_f32(
3079 config: Option<&Map<String, Value>>,
3080 key: &str,
3081 legacy_key: &str,
3082) -> Option<f32> {
3083 config.and_then(|config| {
3084 config
3085 .get(key)
3086 .or_else(|| config.get(legacy_key))
3087 .and_then(json_f32)
3088 })
3089}
3090
3091fn json_required_f32(
3092 object: &serde_json::Map<String, Value>,
3093 field: &str,
3094) -> SubtrActorResult<f32> {
3095 json_f32(json_required_value(object, field)?).ok_or_else(|| {
3096 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3097 "Expected JSON field '{field}' to be a float"
3098 )))
3099 })
3100}
3101
3102fn json_required_usize(
3103 object: &serde_json::Map<String, Value>,
3104 field: &str,
3105) -> SubtrActorResult<usize> {
3106 json_required_value(object, field)?
3107 .as_u64()
3108 .map(|number| number as usize)
3109 .ok_or_else(|| {
3110 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3111 "Expected JSON field '{field}' to be an unsigned integer"
3112 )))
3113 })
3114}
3115
3116fn json_required_i32(
3117 object: &serde_json::Map<String, Value>,
3118 field: &str,
3119) -> SubtrActorResult<i32> {
3120 json_required_value(object, field)?
3121 .as_i64()
3122 .map(|number| number as i32)
3123 .ok_or_else(|| {
3124 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3125 "Expected JSON field '{field}' to be a signed integer"
3126 )))
3127 })
3128}
3129
3130fn json_required_bool(
3131 object: &serde_json::Map<String, Value>,
3132 field: &str,
3133) -> SubtrActorResult<bool> {
3134 json_required_value(object, field)?
3135 .as_bool()
3136 .ok_or_else(|| {
3137 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3138 "Expected JSON field '{field}' to be a bool"
3139 )))
3140 })
3141}
3142
3143fn json_required_str<'a>(
3144 object: &'a serde_json::Map<String, Value>,
3145 field: &str,
3146) -> SubtrActorResult<&'a str> {
3147 json_required_value(object, field)?.as_str().ok_or_else(|| {
3148 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3149 "Expected JSON field '{field}' to be a string"
3150 )))
3151 })
3152}
3153
3154fn json_optional_bool(value: Option<&Value>) -> Option<bool> {
3155 value.and_then(Value::as_bool)
3156}
3157
3158fn json_optional_f32(value: Option<&Value>) -> SubtrActorResult<Option<f32>> {
3159 match value {
3160 None | Some(Value::Null) => Ok(None),
3161 Some(value) => json_f32(value).map(Some).ok_or_else(|| {
3162 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
3163 "Expected optional JSON value to be a float".to_owned(),
3164 ))
3165 }),
3166 }
3167}
3168
3169fn json_optional_usize(value: Option<&Value>) -> SubtrActorResult<Option<usize>> {
3170 match value {
3171 None | Some(Value::Null) => Ok(None),
3172 Some(value) => value
3173 .as_u64()
3174 .map(|number| Some(number as usize))
3175 .ok_or_else(|| {
3176 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
3177 "Expected optional JSON value to be an unsigned integer".to_owned(),
3178 ))
3179 }),
3180 }
3181}
3182
3183fn json_goal_context_position(value: &Value) -> SubtrActorResult<GoalContextPosition> {
3184 let object = json_object(value, "goal context position")?;
3185 Ok(GoalContextPosition {
3186 x: json_required_f32(object, "x")?,
3187 y: json_required_f32(object, "y")?,
3188 z: json_required_f32(object, "z")?,
3189 })
3190}
3191
3192fn json_optional_goal_context_position(
3193 value: Option<&Value>,
3194) -> SubtrActorResult<Option<GoalContextPosition>> {
3195 match value {
3196 None | Some(Value::Null) => Ok(None),
3197 Some(value) => json_goal_context_position(value).map(Some),
3198 }
3199}
3200
3201fn json_required_vec3(
3202 object: &serde_json::Map<String, Value>,
3203 field: &str,
3204) -> SubtrActorResult<[f32; 3]> {
3205 let array = json_required_value(object, field)?
3206 .as_array()
3207 .ok_or_else(|| {
3208 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3209 "Expected JSON field '{field}' to be a 3-element array"
3210 )))
3211 })?;
3212 if array.len() != 3 {
3213 return SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
3214 format!("Expected JSON field '{field}' to contain exactly 3 elements"),
3215 ));
3216 }
3217 Ok([
3218 json_f32(&array[0]).ok_or_else(|| {
3219 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3220 "Expected JSON field '{field}[0]' to be a float"
3221 )))
3222 })?,
3223 json_f32(&array[1]).ok_or_else(|| {
3224 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3225 "Expected JSON field '{field}[1]' to be a float"
3226 )))
3227 })?,
3228 json_f32(&array[2]).ok_or_else(|| {
3229 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
3230 "Expected JSON field '{field}[2]' to be a float"
3231 )))
3232 })?,
3233 ])
3234}
3235
3236fn json_required_remote_id(
3237 object: &serde_json::Map<String, Value>,
3238 field: &str,
3239) -> SubtrActorResult<PlayerId> {
3240 json_remote_id(json_required_value(object, field)?)
3241}
3242
3243fn json_optional_remote_id(value: Option<&Value>) -> SubtrActorResult<Option<PlayerId>> {
3244 match value {
3245 None | Some(Value::Null) => Ok(None),
3246 Some(value) => Ok(Some(json_remote_id(value)?)),
3247 }
3248}
3249
3250fn json_remote_id(value: &Value) -> SubtrActorResult<PlayerId> {
3251 let object = json_object(value, "remote id")?;
3252 if object.len() != 1 {
3253 return SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
3254 "Expected remote id to contain exactly one variant".to_owned(),
3255 ));
3256 }
3257
3258 let (variant, payload) = object.iter().next().expect("validated single variant");
3259 match variant.as_str() {
3260 "PlayStation" => {
3261 let payload = json_object(payload, "playstation remote id")?;
3262 Ok(RemoteId::PlayStation(Ps4Id {
3263 online_id: json_u64(json_required_value(payload, "online_id")?)?,
3264 name: json_required_value(payload, "name")?
3265 .as_str()
3266 .ok_or_else(|| {
3267 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
3268 "Expected PlayStation name to be a string".to_owned(),
3269 ))
3270 })?
3271 .to_owned(),
3272 unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
3273 }))
3274 }
3275 "PsyNet" => {
3276 let payload = json_object(payload, "psynet remote id")?;
3277 Ok(RemoteId::PsyNet(PsyNetId {
3278 online_id: json_u64(json_required_value(payload, "online_id")?)?,
3279 unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
3280 }))
3281 }
3282 "SplitScreen" => Ok(RemoteId::SplitScreen(json_u64(payload)? as u32)),
3283 "Steam" => Ok(RemoteId::Steam(json_u64(payload)?)),
3284 "Switch" => {
3285 let payload = json_object(payload, "switch remote id")?;
3286 Ok(RemoteId::Switch(SwitchId {
3287 online_id: json_u64(json_required_value(payload, "online_id")?)?,
3288 unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
3289 }))
3290 }
3291 "Xbox" => Ok(RemoteId::Xbox(json_u64(payload)?)),
3292 "QQ" => Ok(RemoteId::QQ(json_u64(payload)?)),
3293 "Epic" => Ok(RemoteId::Epic(
3294 payload
3295 .as_str()
3296 .ok_or_else(|| {
3297 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
3298 "Expected Epic remote id payload to be a string".to_owned(),
3299 ))
3300 })?
3301 .to_owned(),
3302 )),
3303 variant => SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
3304 format!("Unknown remote id variant '{variant}'"),
3305 )),
3306 }
3307}
3308
3309fn json_u64(value: &Value) -> SubtrActorResult<u64> {
3310 value
3311 .as_u64()
3312 .or_else(|| value.as_str().and_then(|text| text.parse().ok()))
3313 .ok_or_else(|| {
3314 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
3315 "Expected JSON value to be a u64".to_owned(),
3316 ))
3317 })
3318}
3319
3320fn json_u8_vec(value: &Value) -> SubtrActorResult<Vec<u8>> {
3321 value
3322 .as_array()
3323 .ok_or_else(|| {
3324 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
3325 "Expected JSON value to be an array of bytes".to_owned(),
3326 ))
3327 })?
3328 .iter()
3329 .map(|entry| {
3330 entry
3331 .as_u64()
3332 .and_then(|number| u8::try_from(number).ok())
3333 .ok_or_else(|| {
3334 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
3335 "Expected JSON array entry to be a byte".to_owned(),
3336 ))
3337 })
3338 })
3339 .collect()
3340}