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