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