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