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 timeline_event_sets_typed(&self) -> SubtrActorResult<ReplayStatsTimelineEvents> {
160 Ok(ReplayStatsTimelineEvents {
161 timeline: self.timeline_events_typed()?,
162 backboard: self.module_player_events("backboard", "events", parse_backboard_event)?,
163 ceiling_shot: self.module_player_events(
164 "ceiling_shot",
165 "events",
166 parse_ceiling_shot_event,
167 )?,
168 double_tap: self.module_player_events(
169 "double_tap",
170 "events",
171 parse_double_tap_event,
172 )?,
173 fifty_fifty: self.module_player_events(
174 "fifty_fifty",
175 "events",
176 parse_fifty_fifty_event,
177 )?,
178 rush: self.module_typed_array("rush", "events")?,
179 speed_flip: self.module_player_events(
180 "speed_flip",
181 "events",
182 parse_speed_flip_event,
183 )?,
184 whiff: self.module_player_events("whiff", "events", parse_whiff_event)?,
185 boost_pickups: self.module_player_events(
186 "boost",
187 "events",
188 parse_boost_pickup_comparison_event,
189 )?,
190 })
191 }
192
193 fn timeline_event_sets_value(&self) -> Value {
194 let mut events = Map::new();
195 events.insert("timeline".to_owned(), Value::Array(self.timeline_events()));
196 events.insert(
197 "backboard".to_owned(),
198 Value::Array(self.module_array("backboard", "events")),
199 );
200 events.insert(
201 "ceiling_shot".to_owned(),
202 Value::Array(self.module_array("ceiling_shot", "events")),
203 );
204 events.insert(
205 "double_tap".to_owned(),
206 Value::Array(self.module_array("double_tap", "events")),
207 );
208 events.insert(
209 "fifty_fifty".to_owned(),
210 Value::Array(self.module_array("fifty_fifty", "events")),
211 );
212 events.insert(
213 "rush".to_owned(),
214 Value::Array(self.module_array("rush", "events")),
215 );
216 events.insert(
217 "speed_flip".to_owned(),
218 Value::Array(self.module_array("speed_flip", "events")),
219 );
220 events.insert(
221 "whiff".to_owned(),
222 Value::Array(self.module_array("whiff", "events")),
223 );
224 events.insert(
225 "boost_pickups".to_owned(),
226 Value::Array(self.module_array("boost", "events")),
227 );
228 Value::Object(events)
229 }
230
231 fn timeline_config(&self) -> StatsTimelineConfig {
232 let positioning_config = self.config.get("positioning").and_then(Value::as_object);
233 let pressure_config = self.config.get("pressure").and_then(Value::as_object);
234 let rush_config = self.config.get("rush").and_then(Value::as_object);
235 let rush_defaults = RushCalculatorConfig::default();
236
237 StatsTimelineConfig {
238 most_back_forward_threshold_y: positioning_config
239 .and_then(|config| config.get("most_back_forward_threshold_y"))
240 .and_then(json_f32)
241 .unwrap_or(PositioningCalculatorConfig::default().most_back_forward_threshold_y),
242 level_ball_depth_margin: positioning_config
243 .and_then(|config| config.get("level_ball_depth_margin"))
244 .and_then(json_f32)
245 .unwrap_or(PositioningCalculatorConfig::default().level_ball_depth_margin),
246 pressure_neutral_zone_half_width_y: pressure_config
247 .and_then(|config| config.get("pressure_neutral_zone_half_width_y"))
248 .and_then(json_f32)
249 .unwrap_or(PressureCalculatorConfig::default().neutral_zone_half_width_y),
250 rush_max_start_y: rush_config
251 .and_then(|config| config.get("rush_max_start_y"))
252 .and_then(json_f32)
253 .unwrap_or(rush_defaults.max_start_y),
254 rush_attack_support_distance_y: rush_config
255 .and_then(|config| config.get("rush_attack_support_distance_y"))
256 .and_then(json_f32)
257 .unwrap_or(rush_defaults.attack_support_distance_y),
258 rush_defender_distance_y: rush_config
259 .and_then(|config| config.get("rush_defender_distance_y"))
260 .and_then(json_f32)
261 .unwrap_or(rush_defaults.defender_distance_y),
262 rush_min_possession_retained_seconds: rush_config
263 .and_then(|config| config.get("rush_min_possession_retained_seconds"))
264 .and_then(json_f32)
265 .unwrap_or(rush_defaults.min_possession_retained_seconds),
266 }
267 }
268
269 fn timeline_config_value(&self) -> SubtrActorResult<Value> {
270 let positioning_config = self.config.get("positioning").and_then(Value::as_object);
271 let pressure_config = self.config.get("pressure").and_then(Value::as_object);
272 let rush_config = self.config.get("rush").and_then(Value::as_object);
273
274 let mut config = Map::new();
275 config.insert(
276 "most_back_forward_threshold_y".to_owned(),
277 serialize_to_json_value(
278 &positioning_config
279 .and_then(|config| config.get("most_back_forward_threshold_y"))
280 .and_then(Value::as_f64)
281 .unwrap_or(
282 PositioningCalculatorConfig::default().most_back_forward_threshold_y as f64,
283 ),
284 )?,
285 );
286 config.insert(
287 "level_ball_depth_margin".to_owned(),
288 serialize_to_json_value(
289 &positioning_config
290 .and_then(|config| config.get("level_ball_depth_margin"))
291 .and_then(Value::as_f64)
292 .unwrap_or(
293 PositioningCalculatorConfig::default().level_ball_depth_margin as f64,
294 ),
295 )?,
296 );
297 config.insert(
298 "pressure_neutral_zone_half_width_y".to_owned(),
299 serialize_to_json_value(
300 &pressure_config
301 .and_then(|config| config.get("pressure_neutral_zone_half_width_y"))
302 .and_then(Value::as_f64)
303 .unwrap_or(
304 PressureCalculatorConfig::default().neutral_zone_half_width_y as f64,
305 ),
306 )?,
307 );
308 let rush_defaults = RushCalculatorConfig::default();
309 config.insert(
310 "rush_max_start_y".to_owned(),
311 serialize_to_json_value(
312 &rush_config
313 .and_then(|config| config.get("rush_max_start_y"))
314 .and_then(Value::as_f64)
315 .unwrap_or(rush_defaults.max_start_y as f64),
316 )?,
317 );
318 config.insert(
319 "rush_attack_support_distance_y".to_owned(),
320 serialize_to_json_value(
321 &rush_config
322 .and_then(|config| config.get("rush_attack_support_distance_y"))
323 .and_then(Value::as_f64)
324 .unwrap_or(rush_defaults.attack_support_distance_y as f64),
325 )?,
326 );
327 config.insert(
328 "rush_defender_distance_y".to_owned(),
329 serialize_to_json_value(
330 &rush_config
331 .and_then(|config| config.get("rush_defender_distance_y"))
332 .and_then(Value::as_f64)
333 .unwrap_or(rush_defaults.defender_distance_y as f64),
334 )?,
335 );
336 config.insert(
337 "rush_min_possession_retained_seconds".to_owned(),
338 serialize_to_json_value(
339 &rush_config
340 .and_then(|config| config.get("rush_min_possession_retained_seconds"))
341 .and_then(Value::as_f64)
342 .unwrap_or(rush_defaults.min_possession_retained_seconds as f64),
343 )?,
344 );
345 Ok(Value::Object(config))
346 }
347
348 fn timeline_frame_value(&self, frame: &StatsSnapshotFrame) -> SubtrActorResult<Value> {
349 let mut timeline = Map::new();
350 timeline.insert(
351 "frame_number".to_owned(),
352 serialize_to_json_value(&frame.frame_number)?,
353 );
354 timeline.insert("time".to_owned(), serialize_to_json_value(&frame.time)?);
355 timeline.insert("dt".to_owned(), serialize_to_json_value(&frame.dt)?);
356 timeline.insert(
357 "seconds_remaining".to_owned(),
358 serialize_to_json_value(&frame.seconds_remaining)?,
359 );
360 timeline.insert(
361 "game_state".to_owned(),
362 serialize_to_json_value(&frame.game_state)?,
363 );
364 timeline.insert(
365 "gameplay_phase".to_owned(),
366 serialize_to_json_value(&frame.gameplay_phase)?,
367 );
368 timeline.insert(
369 "is_live_play".to_owned(),
370 serialize_to_json_value(&frame.is_live_play)?,
371 );
372 timeline.insert(
373 "fifty_fifty".to_owned(),
374 self.frame_stats_or_default::<FiftyFiftyStats>(frame, "fifty_fifty"),
375 );
376 timeline.insert(
377 "possession".to_owned(),
378 self.frame_stats_or_default::<PossessionStats>(frame, "possession"),
379 );
380 timeline.insert(
381 "pressure".to_owned(),
382 self.frame_stats_or_default::<PressureStats>(frame, "pressure"),
383 );
384 timeline.insert(
385 "rush".to_owned(),
386 self.frame_stats_or_default::<RushStats>(frame, "rush"),
387 );
388 timeline.insert(
389 "team_zero".to_owned(),
390 self.timeline_team_value(frame, "team_zero")?,
391 );
392 timeline.insert(
393 "team_one".to_owned(),
394 self.timeline_team_value(frame, "team_one")?,
395 );
396 timeline.insert(
397 "players".to_owned(),
398 Value::Array(
399 self.replay_meta
400 .player_order()
401 .map(|player| self.timeline_player_value(frame, player))
402 .collect::<SubtrActorResult<Vec<_>>>()?,
403 ),
404 );
405 Ok(Value::Object(timeline))
406 }
407
408 pub(crate) fn replay_stats_frame(
409 &self,
410 frame: &StatsSnapshotFrame,
411 ) -> SubtrActorResult<ReplayStatsFrame> {
412 Ok(ReplayStatsFrame {
413 frame_number: frame.frame_number,
414 time: frame.time,
415 dt: frame.dt,
416 seconds_remaining: frame.seconds_remaining,
417 game_state: frame.game_state,
418 gameplay_phase: frame.gameplay_phase,
419 is_live_play: frame.is_live_play,
420 team_zero: self.replay_team_stats(frame, "team_zero")?,
421 team_one: self.replay_team_stats(frame, "team_one")?,
422 players: self
423 .replay_meta
424 .player_order()
425 .map(|player| self.replay_player_stats(frame, player))
426 .collect::<SubtrActorResult<Vec<_>>>()?,
427 })
428 }
429
430 fn replay_team_stats(
431 &self,
432 frame: &StatsSnapshotFrame,
433 team_key: &str,
434 ) -> SubtrActorResult<TeamStatsSnapshot> {
435 let is_team_zero = team_key == "team_zero";
436 Ok(TeamStatsSnapshot {
437 fifty_fifty: self
438 .frame_stats_or_default_typed::<FiftyFiftyStats>(frame, "fifty_fifty")?
439 .for_team(is_team_zero),
440 possession: self
441 .frame_stats_or_default_typed::<PossessionStats>(frame, "possession")?
442 .for_team(is_team_zero),
443 pressure: self
444 .frame_stats_or_default_typed::<PressureStats>(frame, "pressure")?
445 .for_team(is_team_zero),
446 rush: self
447 .frame_stats_or_default_typed::<RushStats>(frame, "rush")?
448 .for_team(is_team_zero),
449 core: self.frame_team_stat_or_default_typed(frame, "core", team_key)?,
450 backboard: self.frame_team_stat_or_default_typed(frame, "backboard", team_key)?,
451 double_tap: self.frame_team_stat_or_default_typed(frame, "double_tap", team_key)?,
452 ball_carry: self.frame_team_stat_or_default_typed(frame, "ball_carry", team_key)?,
453 boost: self.frame_team_stat_or_default_typed(frame, "boost", team_key)?,
454 movement: self.frame_team_stat_or_default_typed(frame, "movement", team_key)?,
455 powerslide: self.frame_team_stat_or_default_typed(frame, "powerslide", team_key)?,
456 demo: self.frame_team_stat_or_default_typed(frame, "demo", team_key)?,
457 })
458 }
459
460 fn replay_player_stats(
461 &self,
462 frame: &StatsSnapshotFrame,
463 player: &PlayerInfo,
464 ) -> SubtrActorResult<PlayerStatsSnapshot> {
465 let player_key = player_info_key(player)?;
466 Ok(PlayerStatsSnapshot {
467 player_id: player.remote_id.clone(),
468 name: player.name.clone(),
469 is_team_0: self.is_team_zero_player(player),
470 core: self.frame_player_stat_or_default_typed_by_key(frame, "core", &player_key)?,
471 backboard: self.frame_player_stat_or_default_typed_by_key(
472 frame,
473 "backboard",
474 &player_key,
475 )?,
476 ceiling_shot: self.frame_player_stat_or_default_typed_by_key(
477 frame,
478 "ceiling_shot",
479 &player_key,
480 )?,
481 double_tap: self.frame_player_stat_or_default_typed_by_key(
482 frame,
483 "double_tap",
484 &player_key,
485 )?,
486 fifty_fifty: self.frame_player_stat_or_default_typed_by_key(
487 frame,
488 "fifty_fifty",
489 &player_key,
490 )?,
491 speed_flip: self.frame_player_stat_or_default_typed_by_key(
492 frame,
493 "speed_flip",
494 &player_key,
495 )?,
496 touch: if frame.modules.contains_key("touch") {
497 self.frame_player_stat_or_default_with_by_key(frame, "touch", &player_key, || {
498 TouchStats::default().with_complete_labeled_touch_counts()
499 })?
500 } else {
501 self.frame_player_stat_or_default_typed_by_key(frame, "touch", &player_key)?
502 },
503 whiff: self.frame_player_stat_or_default_typed_by_key(frame, "whiff", &player_key)?,
504 musty_flick: self.frame_player_stat_or_default_typed_by_key(
505 frame,
506 "musty_flick",
507 &player_key,
508 )?,
509 dodge_reset: self.frame_player_stat_or_default_typed_by_key(
510 frame,
511 "dodge_reset",
512 &player_key,
513 )?,
514 ball_carry: self.frame_player_stat_or_default_typed_by_key(
515 frame,
516 "ball_carry",
517 &player_key,
518 )?,
519 boost: self.frame_player_stat_or_default_typed_by_key(frame, "boost", &player_key)?,
520 movement: self.frame_player_stat_or_default_with_by_key(
521 frame,
522 "movement",
523 &player_key,
524 || MovementStats::default().with_complete_labeled_tracked_time(),
525 )?,
526 positioning: self.frame_player_stat_or_default_typed_by_key(
527 frame,
528 "positioning",
529 &player_key,
530 )?,
531 powerslide: self.frame_player_stat_or_default_typed_by_key(
532 frame,
533 "powerslide",
534 &player_key,
535 )?,
536 demo: self.frame_player_stat_or_default_typed_by_key(frame, "demo", &player_key)?,
537 })
538 }
539
540 fn is_team_zero_player(&self, player: &PlayerInfo) -> bool {
541 self.replay_meta
542 .team_zero
543 .iter()
544 .any(|team_player| team_player.remote_id == player.remote_id)
545 }
546
547 fn timeline_team_value(
548 &self,
549 frame: &StatsSnapshotFrame,
550 team_key: &str,
551 ) -> SubtrActorResult<Value> {
552 let is_team_zero = team_key == "team_zero";
553 let mut team = Map::new();
554 team.insert(
555 "fifty_fifty".to_owned(),
556 serialize_to_json_value(
557 &self
558 .frame_stats_or_default_typed::<FiftyFiftyStats>(frame, "fifty_fifty")?
559 .for_team(is_team_zero),
560 )?,
561 );
562 team.insert(
563 "possession".to_owned(),
564 serialize_to_json_value(
565 &self
566 .frame_stats_or_default_typed::<PossessionStats>(frame, "possession")?
567 .for_team(is_team_zero),
568 )?,
569 );
570 team.insert(
571 "pressure".to_owned(),
572 serialize_to_json_value(
573 &self
574 .frame_stats_or_default_typed::<PressureStats>(frame, "pressure")?
575 .for_team(is_team_zero),
576 )?,
577 );
578 team.insert(
579 "rush".to_owned(),
580 serialize_to_json_value(
581 &self
582 .frame_stats_or_default_typed::<RushStats>(frame, "rush")?
583 .for_team(is_team_zero),
584 )?,
585 );
586 team.insert(
587 "core".to_owned(),
588 self.frame_team_stat_or_default::<CoreTeamStats>(frame, "core", team_key),
589 );
590 team.insert(
591 "backboard".to_owned(),
592 self.frame_team_stat_or_default::<BackboardTeamStats>(frame, "backboard", team_key),
593 );
594 team.insert(
595 "double_tap".to_owned(),
596 self.frame_team_stat_or_default::<DoubleTapTeamStats>(frame, "double_tap", team_key),
597 );
598 team.insert(
599 "ball_carry".to_owned(),
600 self.frame_team_stat_or_default::<BallCarryStats>(frame, "ball_carry", team_key),
601 );
602 team.insert(
603 "boost".to_owned(),
604 self.frame_team_stat_or_default::<BoostStats>(frame, "boost", team_key),
605 );
606 team.insert(
607 "movement".to_owned(),
608 self.frame_team_stat_or_default::<MovementStats>(frame, "movement", team_key),
609 );
610 team.insert(
611 "powerslide".to_owned(),
612 self.frame_team_stat_or_default::<PowerslideStats>(frame, "powerslide", team_key),
613 );
614 team.insert(
615 "demo".to_owned(),
616 self.frame_team_stat_or_default::<DemoTeamStats>(frame, "demo", team_key),
617 );
618 Ok(Value::Object(team))
619 }
620
621 fn timeline_player_value(
622 &self,
623 frame: &StatsSnapshotFrame,
624 player: &PlayerInfo,
625 ) -> SubtrActorResult<Value> {
626 let player_key = player_info_key(player)?;
627 let mut player_value = Map::new();
628 player_value.insert(
629 "player_id".to_owned(),
630 serialize_to_json_value(&player.remote_id)?,
631 );
632 player_value.insert("name".to_owned(), serialize_to_json_value(&player.name)?);
633 player_value.insert(
634 "is_team_0".to_owned(),
635 serialize_to_json_value(
636 &self
637 .replay_meta
638 .team_zero
639 .iter()
640 .any(|team_player| team_player.remote_id == player.remote_id),
641 )?,
642 );
643 player_value.insert(
644 "core".to_owned(),
645 self.frame_player_stat_or_default_by_key::<CorePlayerStats>(
646 frame,
647 "core",
648 &player_key,
649 )?,
650 );
651 player_value.insert(
652 "backboard".to_owned(),
653 self.frame_player_stat_or_default_by_key::<BackboardPlayerStats>(
654 frame,
655 "backboard",
656 &player_key,
657 )?,
658 );
659 player_value.insert(
660 "ceiling_shot".to_owned(),
661 self.frame_player_stat_or_default_by_key::<CeilingShotStats>(
662 frame,
663 "ceiling_shot",
664 &player_key,
665 )?,
666 );
667 player_value.insert(
668 "double_tap".to_owned(),
669 self.frame_player_stat_or_default_by_key::<DoubleTapPlayerStats>(
670 frame,
671 "double_tap",
672 &player_key,
673 )?,
674 );
675 player_value.insert(
676 "fifty_fifty".to_owned(),
677 self.frame_player_stat_or_default_by_key::<FiftyFiftyPlayerStats>(
678 frame,
679 "fifty_fifty",
680 &player_key,
681 )?,
682 );
683 player_value.insert(
684 "speed_flip".to_owned(),
685 self.frame_player_stat_or_default_by_key::<SpeedFlipStats>(
686 frame,
687 "speed_flip",
688 &player_key,
689 )?,
690 );
691 player_value.insert(
692 "touch".to_owned(),
693 self.frame_player_stat_or_value_by_key(
694 frame,
695 "touch",
696 &player_key,
697 if frame.modules.contains_key("touch") {
698 serialize_to_json_value(
699 &TouchStats::default().with_complete_labeled_touch_counts(),
700 )?
701 } else {
702 default_json_value::<TouchStats>()
703 },
704 )?,
705 );
706 player_value.insert(
707 "whiff".to_owned(),
708 self.frame_player_stat_or_default_by_key::<WhiffStats>(frame, "whiff", &player_key)?,
709 );
710 player_value.insert(
711 "musty_flick".to_owned(),
712 self.frame_player_stat_or_default_by_key::<MustyFlickStats>(
713 frame,
714 "musty_flick",
715 &player_key,
716 )?,
717 );
718 player_value.insert(
719 "dodge_reset".to_owned(),
720 self.frame_player_stat_or_default_by_key::<DodgeResetStats>(
721 frame,
722 "dodge_reset",
723 &player_key,
724 )?,
725 );
726 player_value.insert(
727 "ball_carry".to_owned(),
728 self.frame_player_stat_or_default_by_key::<BallCarryStats>(
729 frame,
730 "ball_carry",
731 &player_key,
732 )?,
733 );
734 player_value.insert(
735 "boost".to_owned(),
736 self.frame_player_stat_or_default_by_key::<BoostStats>(frame, "boost", &player_key)?,
737 );
738 player_value.insert(
739 "movement".to_owned(),
740 self.frame_player_stat_or_value_by_key(
741 frame,
742 "movement",
743 &player_key,
744 if frame.modules.contains_key("movement") {
745 serialize_to_json_value(
746 &MovementStats::default().with_complete_labeled_tracked_time(),
747 )?
748 } else {
749 default_json_value::<MovementStats>()
750 },
751 )?,
752 );
753 player_value.insert(
754 "positioning".to_owned(),
755 self.frame_player_stat_or_default_by_key::<PositioningStats>(
756 frame,
757 "positioning",
758 &player_key,
759 )?,
760 );
761 player_value.insert(
762 "powerslide".to_owned(),
763 self.frame_player_stat_or_default_by_key::<PowerslideStats>(
764 frame,
765 "powerslide",
766 &player_key,
767 )?,
768 );
769 player_value.insert(
770 "demo".to_owned(),
771 self.frame_player_stat_or_default_by_key::<DemoPlayerStats>(
772 frame,
773 "demo",
774 &player_key,
775 )?,
776 );
777 Ok(Value::Object(player_value))
778 }
779
780 fn frame_stats_or_default<T>(&self, frame: &StatsSnapshotFrame, module_name: &str) -> Value
781 where
782 T: Default + Serialize,
783 {
784 frame
785 .modules
786 .get(module_name)
787 .and_then(Value::as_object)
788 .and_then(|module| module.get("stats"))
789 .cloned()
790 .unwrap_or_else(|| default_json_value::<T>())
791 }
792
793 fn frame_team_stat_or_default<T>(
794 &self,
795 frame: &StatsSnapshotFrame,
796 module_name: &str,
797 team_key: &str,
798 ) -> Value
799 where
800 T: Default + Serialize,
801 {
802 frame
803 .modules
804 .get(module_name)
805 .and_then(Value::as_object)
806 .and_then(|module| module.get(team_key))
807 .cloned()
808 .unwrap_or_else(|| default_json_value::<T>())
809 }
810
811 fn frame_player_stat_or_default_by_key<T>(
812 &self,
813 frame: &StatsSnapshotFrame,
814 module_name: &str,
815 player_key: &str,
816 ) -> SubtrActorResult<Value>
817 where
818 T: Default + Serialize,
819 {
820 self.frame_player_stat_or_value_by_key(
821 frame,
822 module_name,
823 player_key,
824 default_json_value::<T>(),
825 )
826 }
827
828 fn frame_player_stat_or_value_by_key(
829 &self,
830 frame: &StatsSnapshotFrame,
831 module_name: &str,
832 player_key: &str,
833 default_value: Value,
834 ) -> SubtrActorResult<Value> {
835 Ok(
836 player_stats_value_for_key(frame.modules.get(module_name), player_key)?
837 .cloned()
838 .unwrap_or(default_value),
839 )
840 }
841
842 fn frame_stats_or_default_typed<T>(
843 &self,
844 frame: &StatsSnapshotFrame,
845 module_name: &str,
846 ) -> SubtrActorResult<T>
847 where
848 T: Default + DeserializeOwned + Serialize,
849 {
850 decode_json_value(self.frame_stats_or_default::<T>(frame, module_name))
851 }
852
853 fn frame_team_stat_or_default_typed<T>(
854 &self,
855 frame: &StatsSnapshotFrame,
856 module_name: &str,
857 team_key: &str,
858 ) -> SubtrActorResult<T>
859 where
860 T: Default + DeserializeOwned + Serialize,
861 {
862 decode_json_value(self.frame_team_stat_or_default::<T>(frame, module_name, team_key))
863 }
864
865 fn frame_player_stat_or_default_typed_by_key<T>(
866 &self,
867 frame: &StatsSnapshotFrame,
868 module_name: &str,
869 player_key: &str,
870 ) -> SubtrActorResult<T>
871 where
872 T: Default + DeserializeOwned + Serialize,
873 {
874 self.frame_player_stat_or_default_with_by_key(frame, module_name, player_key, T::default)
875 }
876
877 fn frame_player_stat_or_default_with_by_key<T, F>(
878 &self,
879 frame: &StatsSnapshotFrame,
880 module_name: &str,
881 player_key: &str,
882 default: F,
883 ) -> SubtrActorResult<T>
884 where
885 T: DeserializeOwned + Serialize,
886 F: FnOnce() -> T,
887 {
888 decode_json_value(self.frame_player_stat_or_value_by_key(
889 frame,
890 module_name,
891 player_key,
892 serialize_to_json_value(&default())?,
893 )?)
894 }
895
896 fn module_typed_array<T>(&self, module_name: &str, field: &str) -> SubtrActorResult<Vec<T>>
897 where
898 T: DeserializeOwned,
899 {
900 decode_json_value(Value::Array(self.module_array(module_name, field)))
901 }
902
903 fn module_player_events<T, F>(
904 &self,
905 module_name: &str,
906 field: &str,
907 parse: F,
908 ) -> SubtrActorResult<Vec<T>>
909 where
910 F: Fn(&Value) -> SubtrActorResult<T>,
911 {
912 self.module_array(module_name, field)
913 .iter()
914 .map(parse)
915 .collect()
916 }
917
918 fn module_array(&self, module_name: &str, field: &str) -> Vec<Value> {
919 self.modules
920 .get(module_name)
921 .and_then(Value::as_object)
922 .and_then(|module| module.get(field))
923 .and_then(Value::as_array)
924 .cloned()
925 .unwrap_or_default()
926 }
927}
928
929impl CapturedStatsData<ReplayStatsFrame> {
930 pub fn into_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
931 let CapturedStatsData {
932 replay_meta,
933 config,
934 modules,
935 frames,
936 } = self;
937 CapturedStatsData::<StatsSnapshotFrame> {
938 replay_meta,
939 config,
940 modules,
941 frames: Vec::new(),
942 }
943 .into_replay_stats_timeline_with_frames(frames)
944 }
945}
946
947fn player_stats_value_for_key<'a>(
948 module: Option<&'a Value>,
949 player_key: &str,
950) -> SubtrActorResult<Option<&'a Value>> {
951 let Some(entries) = module
952 .and_then(Value::as_object)
953 .and_then(|module| module.get("player_stats"))
954 .and_then(Value::as_array)
955 else {
956 return Ok(None);
957 };
958
959 for entry in entries {
960 let Some(entry_object) = entry.as_object() else {
961 continue;
962 };
963 let Some(player_id) = entry_object.get("player_id") else {
964 continue;
965 };
966 let Some(player_stats) = entry_object.get("stats") else {
967 continue;
968 };
969 if player_id_key(player_id)? == player_key {
970 return Ok(Some(player_stats));
971 }
972 }
973
974 Ok(None)
975}
976
977fn player_info_key(player: &PlayerInfo) -> SubtrActorResult<String> {
978 player_id_key(&serialize_to_json_value(&player.remote_id)?)
979}
980
981fn player_id_key(player_id: &Value) -> SubtrActorResult<String> {
982 serde_json::to_string(player_id).map_err(|error| {
983 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
984 error.to_string(),
985 ))
986 })
987}
988
989fn default_json_value<T>() -> Value
990where
991 T: Default + Serialize,
992{
993 serde_json::to_value(T::default()).expect("default stats should serialize to json")
994}
995
996fn decode_json_value<T>(value: Value) -> SubtrActorResult<T>
997where
998 T: DeserializeOwned,
999{
1000 serde_json::from_value(value).map_err(|error| {
1001 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1002 error.to_string(),
1003 ))
1004 })
1005}
1006
1007fn parse_timeline_event(value: &Value) -> SubtrActorResult<TimelineEvent> {
1008 let object = json_object(value, "timeline event")?;
1009 Ok(TimelineEvent {
1010 time: json_required_f32(object, "time")?,
1011 kind: decode_json_value(json_required_value(object, "kind")?.clone())?,
1012 player_id: json_optional_remote_id(object.get("player_id"))?,
1013 is_team_0: json_optional_bool(object.get("is_team_0")),
1014 })
1015}
1016
1017fn parse_backboard_event(value: &Value) -> SubtrActorResult<BackboardBounceEvent> {
1018 let object = json_object(value, "backboard event")?;
1019 Ok(BackboardBounceEvent {
1020 time: json_required_f32(object, "time")?,
1021 frame: json_required_usize(object, "frame")?,
1022 player: json_required_remote_id(object, "player")?,
1023 is_team_0: json_required_bool(object, "is_team_0")?,
1024 })
1025}
1026
1027fn parse_ceiling_shot_event(value: &Value) -> SubtrActorResult<CeilingShotEvent> {
1028 let object = json_object(value, "ceiling shot event")?;
1029 Ok(CeilingShotEvent {
1030 time: json_required_f32(object, "time")?,
1031 frame: json_required_usize(object, "frame")?,
1032 player: json_required_remote_id(object, "player")?,
1033 is_team_0: json_required_bool(object, "is_team_0")?,
1034 ceiling_contact_time: json_required_f32(object, "ceiling_contact_time")?,
1035 ceiling_contact_frame: json_required_usize(object, "ceiling_contact_frame")?,
1036 time_since_ceiling_contact: json_required_f32(object, "time_since_ceiling_contact")?,
1037 ceiling_contact_position: json_required_vec3(object, "ceiling_contact_position")?,
1038 touch_position: json_required_vec3(object, "touch_position")?,
1039 local_ball_position: json_required_vec3(object, "local_ball_position")?,
1040 separation_from_ceiling: json_required_f32(object, "separation_from_ceiling")?,
1041 roof_alignment: json_required_f32(object, "roof_alignment")?,
1042 forward_alignment: json_required_f32(object, "forward_alignment")?,
1043 forward_approach_speed: json_required_f32(object, "forward_approach_speed")?,
1044 ball_speed_change: json_required_f32(object, "ball_speed_change")?,
1045 confidence: json_required_f32(object, "confidence")?,
1046 })
1047}
1048
1049fn parse_double_tap_event(value: &Value) -> SubtrActorResult<DoubleTapEvent> {
1050 let object = json_object(value, "double tap event")?;
1051 Ok(DoubleTapEvent {
1052 time: json_required_f32(object, "time")?,
1053 frame: json_required_usize(object, "frame")?,
1054 player: json_required_remote_id(object, "player")?,
1055 is_team_0: json_required_bool(object, "is_team_0")?,
1056 backboard_time: json_required_f32(object, "backboard_time")?,
1057 backboard_frame: json_required_usize(object, "backboard_frame")?,
1058 })
1059}
1060
1061fn parse_fifty_fifty_event(value: &Value) -> SubtrActorResult<FiftyFiftyEvent> {
1062 let object = json_object(value, "fifty fifty event")?;
1063 Ok(FiftyFiftyEvent {
1064 start_time: json_required_f32(object, "start_time")?,
1065 start_frame: json_required_usize(object, "start_frame")?,
1066 resolve_time: json_required_f32(object, "resolve_time")?,
1067 resolve_frame: json_required_usize(object, "resolve_frame")?,
1068 is_kickoff: json_required_bool(object, "is_kickoff")?,
1069 team_zero_player: json_optional_remote_id(object.get("team_zero_player"))?,
1070 team_one_player: json_optional_remote_id(object.get("team_one_player"))?,
1071 team_zero_position: json_required_vec3(object, "team_zero_position")?,
1072 team_one_position: json_required_vec3(object, "team_one_position")?,
1073 midpoint: json_required_vec3(object, "midpoint")?,
1074 plane_normal: json_required_vec3(object, "plane_normal")?,
1075 winning_team_is_team_0: json_optional_bool(object.get("winning_team_is_team_0")),
1076 possession_team_is_team_0: json_optional_bool(object.get("possession_team_is_team_0")),
1077 })
1078}
1079
1080fn parse_speed_flip_event(value: &Value) -> SubtrActorResult<SpeedFlipEvent> {
1081 let object = json_object(value, "speed flip event")?;
1082 Ok(SpeedFlipEvent {
1083 time: json_required_f32(object, "time")?,
1084 frame: json_required_usize(object, "frame")?,
1085 player: json_required_remote_id(object, "player")?,
1086 is_team_0: json_required_bool(object, "is_team_0")?,
1087 time_since_kickoff_start: json_required_f32(object, "time_since_kickoff_start")?,
1088 start_position: json_required_vec3(object, "start_position")?,
1089 end_position: json_required_vec3(object, "end_position")?,
1090 start_speed: json_required_f32(object, "start_speed")?,
1091 max_speed: json_required_f32(object, "max_speed")?,
1092 best_alignment: json_required_f32(object, "best_alignment")?,
1093 diagonal_score: json_required_f32(object, "diagonal_score")?,
1094 cancel_score: json_required_f32(object, "cancel_score")?,
1095 speed_score: json_required_f32(object, "speed_score")?,
1096 confidence: json_required_f32(object, "confidence")?,
1097 })
1098}
1099
1100fn parse_whiff_event(value: &Value) -> SubtrActorResult<WhiffEvent> {
1101 let object = json_object(value, "whiff event")?;
1102 Ok(WhiffEvent {
1103 time: json_required_f32(object, "time")?,
1104 frame: json_required_usize(object, "frame")?,
1105 player: json_required_remote_id(object, "player")?,
1106 is_team_0: json_required_bool(object, "is_team_0")?,
1107 closest_approach_distance: json_required_f32(object, "closest_approach_distance")?,
1108 forward_alignment: json_required_f32(object, "forward_alignment")?,
1109 approach_speed: json_required_f32(object, "approach_speed")?,
1110 dodge_active: json_required_bool(object, "dodge_active")?,
1111 aerial: json_required_bool(object, "aerial")?,
1112 })
1113}
1114
1115fn parse_boost_pickup_comparison_event(
1116 value: &Value,
1117) -> SubtrActorResult<BoostPickupComparisonEvent> {
1118 let object = json_object(value, "boost pickup comparison event")?;
1119 Ok(BoostPickupComparisonEvent {
1120 comparison: decode_json_value(json_required_value(object, "comparison")?.clone())?,
1121 frame: json_required_usize(object, "frame")?,
1122 time: json_required_f32(object, "time")?,
1123 player_id: json_required_remote_id(object, "player_id")?,
1124 is_team_0: json_required_bool(object, "is_team_0")?,
1125 pad_type: decode_json_value(json_required_value(object, "pad_type")?.clone())?,
1126 field_half: decode_json_value(json_required_value(object, "field_half")?.clone())?,
1127 activity: decode_json_value(json_required_value(object, "activity")?.clone())?,
1128 reported_frame: json_optional_usize(object.get("reported_frame"))?,
1129 reported_time: json_optional_f32(object.get("reported_time"))?,
1130 inferred_frame: json_optional_usize(object.get("inferred_frame"))?,
1131 inferred_time: json_optional_f32(object.get("inferred_time"))?,
1132 boost_before: json_optional_f32(object.get("boost_before"))?,
1133 boost_after: json_optional_f32(object.get("boost_after"))?,
1134 })
1135}
1136
1137fn json_object<'a>(
1138 value: &'a Value,
1139 context: &str,
1140) -> SubtrActorResult<&'a serde_json::Map<String, Value>> {
1141 value.as_object().ok_or_else(|| {
1142 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
1143 "Expected {context} to be a JSON object"
1144 )))
1145 })
1146}
1147
1148fn json_required_value<'a>(
1149 object: &'a serde_json::Map<String, Value>,
1150 field: &str,
1151) -> SubtrActorResult<&'a Value> {
1152 object.get(field).ok_or_else(|| {
1153 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
1154 "Missing JSON field '{field}'"
1155 )))
1156 })
1157}
1158
1159fn json_f32(value: &Value) -> Option<f32> {
1160 value.as_f64().map(|number| number as f32)
1161}
1162
1163fn json_required_f32(
1164 object: &serde_json::Map<String, Value>,
1165 field: &str,
1166) -> SubtrActorResult<f32> {
1167 json_f32(json_required_value(object, field)?).ok_or_else(|| {
1168 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
1169 "Expected JSON field '{field}' to be a float"
1170 )))
1171 })
1172}
1173
1174fn json_required_usize(
1175 object: &serde_json::Map<String, Value>,
1176 field: &str,
1177) -> SubtrActorResult<usize> {
1178 json_required_value(object, field)?
1179 .as_u64()
1180 .map(|number| number as usize)
1181 .ok_or_else(|| {
1182 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
1183 "Expected JSON field '{field}' to be an unsigned integer"
1184 )))
1185 })
1186}
1187
1188fn json_required_bool(
1189 object: &serde_json::Map<String, Value>,
1190 field: &str,
1191) -> SubtrActorResult<bool> {
1192 json_required_value(object, field)?
1193 .as_bool()
1194 .ok_or_else(|| {
1195 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
1196 "Expected JSON field '{field}' to be a bool"
1197 )))
1198 })
1199}
1200
1201fn json_optional_bool(value: Option<&Value>) -> Option<bool> {
1202 value.and_then(Value::as_bool)
1203}
1204
1205fn json_optional_f32(value: Option<&Value>) -> SubtrActorResult<Option<f32>> {
1206 match value {
1207 None | Some(Value::Null) => Ok(None),
1208 Some(value) => json_f32(value).map(Some).ok_or_else(|| {
1209 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1210 "Expected optional JSON value to be a float".to_owned(),
1211 ))
1212 }),
1213 }
1214}
1215
1216fn json_optional_usize(value: Option<&Value>) -> SubtrActorResult<Option<usize>> {
1217 match value {
1218 None | Some(Value::Null) => Ok(None),
1219 Some(value) => value
1220 .as_u64()
1221 .map(|number| Some(number as usize))
1222 .ok_or_else(|| {
1223 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1224 "Expected optional JSON value to be an unsigned integer".to_owned(),
1225 ))
1226 }),
1227 }
1228}
1229
1230fn json_required_vec3(
1231 object: &serde_json::Map<String, Value>,
1232 field: &str,
1233) -> SubtrActorResult<[f32; 3]> {
1234 let array = json_required_value(object, field)?
1235 .as_array()
1236 .ok_or_else(|| {
1237 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
1238 "Expected JSON field '{field}' to be a 3-element array"
1239 )))
1240 })?;
1241 if array.len() != 3 {
1242 return SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
1243 format!("Expected JSON field '{field}' to contain exactly 3 elements"),
1244 ));
1245 }
1246 Ok([
1247 json_f32(&array[0]).ok_or_else(|| {
1248 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
1249 "Expected JSON field '{field}[0]' to be a float"
1250 )))
1251 })?,
1252 json_f32(&array[1]).ok_or_else(|| {
1253 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
1254 "Expected JSON field '{field}[1]' to be a float"
1255 )))
1256 })?,
1257 json_f32(&array[2]).ok_or_else(|| {
1258 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
1259 "Expected JSON field '{field}[2]' to be a float"
1260 )))
1261 })?,
1262 ])
1263}
1264
1265fn json_required_remote_id(
1266 object: &serde_json::Map<String, Value>,
1267 field: &str,
1268) -> SubtrActorResult<PlayerId> {
1269 json_remote_id(json_required_value(object, field)?)
1270}
1271
1272fn json_optional_remote_id(value: Option<&Value>) -> SubtrActorResult<Option<PlayerId>> {
1273 match value {
1274 None | Some(Value::Null) => Ok(None),
1275 Some(value) => Ok(Some(json_remote_id(value)?)),
1276 }
1277}
1278
1279fn json_remote_id(value: &Value) -> SubtrActorResult<PlayerId> {
1280 let object = json_object(value, "remote id")?;
1281 if object.len() != 1 {
1282 return SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
1283 "Expected remote id to contain exactly one variant".to_owned(),
1284 ));
1285 }
1286
1287 let (variant, payload) = object.iter().next().expect("validated single variant");
1288 match variant.as_str() {
1289 "PlayStation" => {
1290 let payload = json_object(payload, "playstation remote id")?;
1291 Ok(RemoteId::PlayStation(Ps4Id {
1292 online_id: json_u64(json_required_value(payload, "online_id")?)?,
1293 name: json_required_value(payload, "name")?
1294 .as_str()
1295 .ok_or_else(|| {
1296 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1297 "Expected PlayStation name to be a string".to_owned(),
1298 ))
1299 })?
1300 .to_owned(),
1301 unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
1302 }))
1303 }
1304 "PsyNet" => {
1305 let payload = json_object(payload, "psynet remote id")?;
1306 Ok(RemoteId::PsyNet(PsyNetId {
1307 online_id: json_u64(json_required_value(payload, "online_id")?)?,
1308 unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
1309 }))
1310 }
1311 "SplitScreen" => Ok(RemoteId::SplitScreen(json_u64(payload)? as u32)),
1312 "Steam" => Ok(RemoteId::Steam(json_u64(payload)?)),
1313 "Switch" => {
1314 let payload = json_object(payload, "switch remote id")?;
1315 Ok(RemoteId::Switch(SwitchId {
1316 online_id: json_u64(json_required_value(payload, "online_id")?)?,
1317 unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
1318 }))
1319 }
1320 "Xbox" => Ok(RemoteId::Xbox(json_u64(payload)?)),
1321 "QQ" => Ok(RemoteId::QQ(json_u64(payload)?)),
1322 "Epic" => Ok(RemoteId::Epic(
1323 payload
1324 .as_str()
1325 .ok_or_else(|| {
1326 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1327 "Expected Epic remote id payload to be a string".to_owned(),
1328 ))
1329 })?
1330 .to_owned(),
1331 )),
1332 variant => SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
1333 format!("Unknown remote id variant '{variant}'"),
1334 )),
1335 }
1336}
1337
1338fn json_u64(value: &Value) -> SubtrActorResult<u64> {
1339 value
1340 .as_u64()
1341 .or_else(|| value.as_str().and_then(|text| text.parse().ok()))
1342 .ok_or_else(|| {
1343 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1344 "Expected JSON value to be a u64".to_owned(),
1345 ))
1346 })
1347}
1348
1349fn json_u8_vec(value: &Value) -> SubtrActorResult<Vec<u8>> {
1350 value
1351 .as_array()
1352 .ok_or_else(|| {
1353 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1354 "Expected JSON value to be an array of bytes".to_owned(),
1355 ))
1356 })?
1357 .iter()
1358 .map(|entry| {
1359 entry
1360 .as_u64()
1361 .and_then(|number| u8::try_from(number).ok())
1362 .ok_or_else(|| {
1363 SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
1364 "Expected JSON array entry to be a byte".to_owned(),
1365 ))
1366 })
1367 })
1368 .collect()
1369}