use boxcars::{Ps4Id, PsyNetId, RemoteId, SwitchId};
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json::{Map, Value};
use crate::*;
use super::types::serialize_to_json_value;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct CapturedStatsFrame<Modules> {
pub frame_number: usize,
pub time: f32,
pub dt: f32,
pub seconds_remaining: Option<i32>,
pub game_state: Option<i32>,
pub ball_has_been_hit: Option<bool>,
pub kickoff_countdown_time: Option<i32>,
pub gameplay_phase: GameplayPhase,
pub is_live_play: bool,
pub modules: Modules,
}
pub type StatsSnapshotFrame = CapturedStatsFrame<Map<String, Value>>;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct CapturedStatsData<Frame> {
pub replay_meta: ReplayMeta,
pub config: Map<String, Value>,
pub modules: Map<String, Value>,
pub frames: Vec<Frame>,
}
pub type StatsSnapshotData = CapturedStatsData<StatsSnapshotFrame>;
impl<Modules> CapturedStatsFrame<Modules> {
pub fn map_modules<Mapped, F>(
self,
transform: F,
) -> SubtrActorResult<CapturedStatsFrame<Mapped>>
where
F: FnOnce(Modules) -> SubtrActorResult<Mapped>,
{
Ok(CapturedStatsFrame {
frame_number: self.frame_number,
time: self.time,
dt: self.dt,
seconds_remaining: self.seconds_remaining,
game_state: self.game_state,
ball_has_been_hit: self.ball_has_been_hit,
kickoff_countdown_time: self.kickoff_countdown_time,
gameplay_phase: self.gameplay_phase,
is_live_play: self.is_live_play,
modules: transform(self.modules)?,
})
}
}
impl CapturedStatsData<StatsSnapshotFrame> {
pub fn into_legacy_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
self.to_legacy_replay_stats_timeline()
}
#[deprecated(
note = "use into_legacy_replay_stats_timeline for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
)]
pub fn into_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
self.into_legacy_replay_stats_timeline()
}
pub fn into_legacy_replay_stats_timeline_with_progress<F>(
self,
frame_interval: usize,
mut on_progress: F,
) -> SubtrActorResult<ReplayStatsTimeline>
where
F: FnMut(usize, usize) -> SubtrActorResult<()>,
{
let frame_interval = frame_interval.max(1);
let total_frames = self.frames.len();
on_progress(0, total_frames)?;
let frames = self
.frames
.iter()
.enumerate()
.map(|(frame_index, frame)| {
let replay_frame = self.replay_stats_frame(frame)?;
let processed_frames = frame_index + 1;
if processed_frames == total_frames
|| processed_frames.is_multiple_of(frame_interval)
{
on_progress(processed_frames, total_frames)?;
}
Ok(replay_frame)
})
.collect::<SubtrActorResult<Vec<_>>>()?;
self.to_replay_stats_timeline_with_frames(frames)
}
#[deprecated(
note = "use into_legacy_replay_stats_timeline_with_progress for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
)]
pub fn into_stats_timeline_with_progress<F>(
self,
frame_interval: usize,
on_progress: F,
) -> SubtrActorResult<ReplayStatsTimeline>
where
F: FnMut(usize, usize) -> SubtrActorResult<()>,
{
self.into_legacy_replay_stats_timeline_with_progress(frame_interval, on_progress)
}
pub fn to_legacy_replay_stats_timeline(&self) -> SubtrActorResult<ReplayStatsTimeline> {
self.to_replay_stats_timeline_with_frames(
self.frames
.iter()
.map(|frame| self.replay_stats_frame(frame))
.collect::<SubtrActorResult<Vec<_>>>()?,
)
}
#[deprecated(
note = "use to_legacy_replay_stats_timeline for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
)]
pub fn to_stats_timeline(&self) -> SubtrActorResult<ReplayStatsTimeline> {
self.to_legacy_replay_stats_timeline()
}
pub(crate) fn into_replay_stats_timeline_with_frames(
self,
frames: Vec<ReplayStatsFrame>,
) -> SubtrActorResult<ReplayStatsTimeline> {
self.to_replay_stats_timeline_with_frames(frames)
}
fn to_replay_stats_timeline_with_frames(
&self,
frames: Vec<ReplayStatsFrame>,
) -> SubtrActorResult<ReplayStatsTimeline> {
Ok(ReplayStatsTimeline {
config: self.timeline_config(),
replay_meta: self.replay_meta.clone(),
events: self.timeline_event_sets_typed()?,
frames,
})
}
pub fn into_legacy_stats_timeline_value(self) -> SubtrActorResult<Value> {
self.to_legacy_stats_timeline_value()
}
#[deprecated(
note = "use into_legacy_stats_timeline_value for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
)]
pub fn into_stats_timeline_value(self) -> SubtrActorResult<Value> {
self.into_legacy_stats_timeline_value()
}
pub fn to_legacy_stats_timeline_value(&self) -> SubtrActorResult<Value> {
let mut timeline = Map::new();
timeline.insert("config".to_owned(), self.timeline_config_value()?);
timeline.insert(
"replay_meta".to_owned(),
serialize_to_json_value(&self.replay_meta)?,
);
timeline.insert("events".to_owned(), self.timeline_event_sets_value());
timeline.insert(
"frames".to_owned(),
Value::Array(
self.frames
.iter()
.map(|frame| self.timeline_frame_value(frame))
.collect::<SubtrActorResult<Vec<_>>>()?,
),
);
Ok(Value::Object(timeline))
}
#[deprecated(
note = "use to_legacy_stats_timeline_value for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
)]
pub fn to_stats_timeline_value(&self) -> SubtrActorResult<Value> {
self.to_legacy_stats_timeline_value()
}
fn timeline_events(&self) -> Vec<Value> {
let mut events = self.module_array("core", "timeline");
events.extend(self.module_array("demo", "timeline"));
events.sort_by(|left, right| {
let left_time = left.get("time").and_then(Value::as_f64).unwrap_or(0.0);
let right_time = right.get("time").and_then(Value::as_f64).unwrap_or(0.0);
left_time.total_cmp(&right_time)
});
events
}
fn timeline_events_typed(&self) -> SubtrActorResult<Vec<TimelineEvent>> {
self.timeline_events()
.iter()
.map(parse_timeline_event)
.collect()
}
fn goal_tag_events_typed(&self) -> SubtrActorResult<Vec<GoalTagEvent>> {
let mut events = Vec::new();
for module_name in [
"aerial_goal",
"high_aerial_goal",
"long_distance_goal",
"own_half_goal",
"empty_net_goal",
"counter_attack_goal",
"flick_goal",
"double_tap_goal",
"one_timer_goal",
"passing_goal",
"air_dribble_goal",
"flip_reset_goal",
"half_volley_goal",
] {
events.extend(self.module_player_events(
module_name,
"events",
parse_goal_tag_event,
)?);
}
events.sort_by(|left, right| {
left.time
.total_cmp(&right.time)
.then_with(|| left.frame.cmp(&right.frame))
.then_with(|| left.goal_index.cmp(&right.goal_index))
.then_with(|| format!("{:?}", left.kind).cmp(&format!("{:?}", right.kind)))
});
Ok(events)
}
fn mechanic_events_typed(&self) -> SubtrActorResult<Vec<MechanicEvent>> {
let mut events = Vec::new();
for (index, value) in self.module_array("ball_carry", "events").iter().enumerate() {
events.push(parse_ball_carry_mechanic_event(value, index)?);
}
for (index, value) in self
.module_array("ceiling_shot", "events")
.iter()
.enumerate()
{
let event = parse_ceiling_shot_event(value)?;
events.push(span_mechanic_event(
"ceiling_shot",
index,
event.ceiling_contact_frame,
event.frame,
event.ceiling_contact_time,
event.time,
event.player,
event.is_team_0,
));
}
for (index, value) in self
.module_array("wall_aerial", "events")
.iter()
.enumerate()
{
let event = parse_wall_aerial_event(value)?;
let mut mechanic_event = span_mechanic_event(
"wall_aerial",
index,
event.wall_contact_frame,
event.frame,
event.wall_contact_time,
event.time,
event.player,
event.is_team_0,
);
mechanic_event.properties = vec![mechanic_event_text_property(
"wall",
event.wall.as_label_value(),
)];
events.push(mechanic_event);
}
for (index, value) in self
.module_array("wall_aerial_shot", "events")
.iter()
.enumerate()
{
let event = parse_wall_aerial_shot_event(value)?;
let mut mechanic_event = span_mechanic_event(
"wall_aerial_shot",
index,
event.wall_contact_frame,
event.frame,
event.wall_contact_time,
event.time,
event.player,
event.is_team_0,
);
mechanic_event.properties = vec![mechanic_event_text_property(
"wall",
event.wall.as_label_value(),
)];
events.push(mechanic_event);
}
for (index, value) in self.module_array("center", "events").iter().enumerate() {
let event = parse_center_event(value)?;
events.push(span_mechanic_event(
"center",
index,
event.start_frame,
event.frame,
event.start_time,
event.time,
event.player,
event.is_team_0,
));
}
for (index, value) in self
.module_array("dodge_reset", "on_ball_events")
.iter()
.enumerate()
{
events.push(parse_dodge_reset_mechanic_event(value, index)?);
}
for (index, value) in self.module_array("double_tap", "events").iter().enumerate() {
let event = parse_double_tap_event(value)?;
events.push(span_mechanic_event(
"double_tap",
index,
event.backboard_frame,
event.frame,
event.backboard_time,
event.time,
event.player,
event.is_team_0,
));
}
for (index, value) in self.module_array("flick", "events").iter().enumerate() {
events.push(parse_flick_mechanic_event(value, index)?);
}
for (index, value) in self
.module_array("musty_flick", "events")
.iter()
.enumerate()
{
events.push(parse_musty_flick_mechanic_event(value, index)?);
}
for (index, value) in self.module_array("one_timer", "events").iter().enumerate() {
let event = parse_one_timer_event(value)?;
events.push(span_mechanic_event(
"one_timer",
index,
event.pass_start_frame,
event.frame,
event.pass_start_time,
event.time,
event.player,
event.is_team_0,
));
}
for (index, value) in self.module_array("pass", "events").iter().enumerate() {
let event = parse_pass_event(value)?;
events.push(span_mechanic_event(
"pass",
index,
event.start_frame,
event.frame,
event.start_time,
event.time,
event.passer,
event.is_team_0,
));
}
for (index, value) in self.module_array("speed_flip", "events").iter().enumerate() {
let event = parse_speed_flip_event(value)?;
events.push(moment_mechanic_event(
"speed_flip",
index,
event.frame,
event.time,
event.player,
event.is_team_0,
));
}
for (index, value) in self.module_array("half_flip", "events").iter().enumerate() {
let event = parse_half_flip_event(value)?;
events.push(moment_mechanic_event(
"half_flip",
index,
event.frame,
event.time,
event.player,
event.is_team_0,
));
}
for (index, value) in self
.module_array("half_volley", "events")
.iter()
.enumerate()
{
let event = parse_half_volley_event(value)?;
events.push(moment_mechanic_event(
"half_volley",
index,
event.frame,
event.time,
event.player,
event.is_team_0,
));
}
for (index, value) in self.module_array("wavedash", "events").iter().enumerate() {
let event = parse_wavedash_event(value)?;
events.push(span_mechanic_event(
"wavedash",
index,
event.dodge_frame,
event.frame,
event.dodge_time,
event.time,
event.player,
event.is_team_0,
));
}
events.sort_by(|left, right| {
let left_time = mechanic_event_start_time(left);
let right_time = mechanic_event_start_time(right);
left_time
.total_cmp(&right_time)
.then_with(|| left.kind.cmp(&right.kind))
.then_with(|| left.id.cmp(&right.id))
});
Ok(events)
}
fn goal_tag_events_value(&self) -> Vec<Value> {
let mut events = Vec::new();
for module_name in [
"aerial_goal",
"high_aerial_goal",
"long_distance_goal",
"own_half_goal",
"empty_net_goal",
"counter_attack_goal",
"flick_goal",
"double_tap_goal",
"one_timer_goal",
"passing_goal",
"air_dribble_goal",
"flip_reset_goal",
"half_volley_goal",
] {
events.extend(self.module_array(module_name, "events"));
}
events.sort_by(|left, right| {
let left_time = left.get("time").and_then(Value::as_f64).unwrap_or(0.0);
let right_time = right.get("time").and_then(Value::as_f64).unwrap_or(0.0);
left_time.total_cmp(&right_time)
});
events
}
fn timeline_event_sets_typed(&self) -> SubtrActorResult<ReplayStatsTimelineEvents> {
Ok(ReplayStatsTimelineEvents {
timeline: self.timeline_events_typed()?,
core_player: self.module_player_events(
"core",
"player_events",
parse_core_player_stats_event,
)?,
core_team: self.module_player_events(
"core",
"team_events",
parse_core_team_stats_event,
)?,
possession: self.module_player_events(
"possession",
"events",
parse_possession_event,
)?,
pressure: self.module_player_events("pressure", "events", parse_pressure_event)?,
movement: self.module_player_events("movement", "events", parse_movement_event)?,
positioning: self.module_player_events(
"positioning",
"events",
parse_positioning_event,
)?,
rotation_player: self.module_player_events(
"rotation",
"player_events",
parse_rotation_player_event,
)?,
rotation_team: self.module_player_events(
"rotation",
"team_events",
parse_rotation_team_event,
)?,
mechanics: self.mechanic_events_typed()?,
goal_context: self.module_player_events(
"core",
"goal_context",
parse_goal_context_event,
)?,
backboard: self.module_player_events("backboard", "events", parse_backboard_event)?,
ceiling_shot: self.module_player_events(
"ceiling_shot",
"events",
parse_ceiling_shot_event,
)?,
wall_aerial: self.module_player_events(
"wall_aerial",
"events",
parse_wall_aerial_event,
)?,
wall_aerial_shot: self.module_player_events(
"wall_aerial_shot",
"events",
parse_wall_aerial_shot_event,
)?,
center: self.module_player_events("center", "events", parse_center_event)?,
flick: self.module_player_events("flick", "events", parse_flick_event)?,
musty_flick: self.module_player_events(
"musty_flick",
"events",
parse_musty_flick_event,
)?,
dodge_reset: self.module_player_events(
"dodge_reset",
"events",
parse_dodge_reset_event,
)?,
double_tap: self.module_player_events(
"double_tap",
"events",
parse_double_tap_event,
)?,
one_timer: self.module_player_events("one_timer", "events", parse_one_timer_event)?,
fifty_fifty: self.module_player_events(
"fifty_fifty",
"events",
parse_fifty_fifty_event,
)?,
pass: self.module_player_events("pass", "events", parse_pass_event)?,
pass_last_completed: self.module_player_events(
"pass",
"last_completed_events",
parse_pass_last_completed_event,
)?,
ball_carry: self.module_player_events(
"ball_carry",
"events",
parse_ball_carry_event,
)?,
goal_tags: self.goal_tag_events_typed()?,
rush: self.module_typed_array("rush", "events")?,
speed_flip: self.module_player_events(
"speed_flip",
"events",
parse_speed_flip_event,
)?,
half_flip: self.module_player_events("half_flip", "events", parse_half_flip_event)?,
half_volley: self.module_player_events(
"half_volley",
"events",
parse_half_volley_event,
)?,
wavedash: self.module_player_events("wavedash", "events", parse_wavedash_event)?,
whiff: self.module_player_events("whiff", "events", parse_whiff_event)?,
powerslide: self.module_player_events(
"powerslide",
"events",
parse_powerslide_event,
)?,
touch: self.module_player_events("touch", "events", parse_touch_stats_event)?,
touch_ball_movement: self.module_player_events(
"touch",
"ball_movement_events",
parse_touch_ball_movement_event,
)?,
touch_last_touch: self.module_player_events(
"touch",
"last_touch_events",
parse_touch_last_touch_event,
)?,
boost_pickups: self.module_player_events(
"boost",
"events",
parse_boost_pickup_comparison_event,
)?,
boost_ledger: self.module_player_events(
"boost",
"ledger_events",
parse_boost_ledger_event,
)?,
boost_state: self.module_player_events(
"boost",
"state_events",
parse_boost_state_event,
)?,
bump: self.module_player_events("bump", "events", parse_bump_event)?,
})
}
fn timeline_event_sets_value(&self) -> Value {
let mut events = Map::new();
events.insert("timeline".to_owned(), Value::Array(self.timeline_events()));
events.insert(
"core_player".to_owned(),
Value::Array(self.module_array("core", "player_events")),
);
events.insert(
"core_team".to_owned(),
Value::Array(self.module_array("core", "team_events")),
);
events.insert(
"possession".to_owned(),
Value::Array(self.module_array("possession", "events")),
);
events.insert(
"pressure".to_owned(),
Value::Array(self.module_array("pressure", "events")),
);
events.insert(
"movement".to_owned(),
Value::Array(self.module_array("movement", "events")),
);
events.insert(
"positioning".to_owned(),
Value::Array(self.module_array("positioning", "events")),
);
events.insert(
"rotation_player".to_owned(),
Value::Array(self.module_array("rotation", "player_events")),
);
events.insert(
"rotation_team".to_owned(),
Value::Array(self.module_array("rotation", "team_events")),
);
events.insert("mechanics".to_owned(), Value::Array(Vec::new()));
events.insert(
"backboard".to_owned(),
Value::Array(self.module_array("backboard", "events")),
);
events.insert(
"ceiling_shot".to_owned(),
Value::Array(self.module_array("ceiling_shot", "events")),
);
events.insert(
"wall_aerial".to_owned(),
Value::Array(self.module_array("wall_aerial", "events")),
);
events.insert(
"wall_aerial_shot".to_owned(),
Value::Array(self.module_array("wall_aerial_shot", "events")),
);
events.insert(
"center".to_owned(),
Value::Array(self.module_array("center", "events")),
);
events.insert(
"double_tap".to_owned(),
Value::Array(self.module_array("double_tap", "events")),
);
events.insert(
"one_timer".to_owned(),
Value::Array(self.module_array("one_timer", "events")),
);
events.insert(
"pass".to_owned(),
Value::Array(self.module_array("pass", "events")),
);
events.insert(
"goal_tags".to_owned(),
Value::Array(self.goal_tag_events_value()),
);
events.insert(
"fifty_fifty".to_owned(),
Value::Array(self.module_array("fifty_fifty", "events")),
);
events.insert(
"rush".to_owned(),
Value::Array(self.module_array("rush", "events")),
);
events.insert(
"speed_flip".to_owned(),
Value::Array(self.module_array("speed_flip", "events")),
);
events.insert(
"half_flip".to_owned(),
Value::Array(self.module_array("half_flip", "events")),
);
events.insert(
"half_volley".to_owned(),
Value::Array(self.module_array("half_volley", "events")),
);
events.insert(
"wavedash".to_owned(),
Value::Array(self.module_array("wavedash", "events")),
);
events.insert(
"whiff".to_owned(),
Value::Array(self.module_array("whiff", "events")),
);
events.insert(
"touch".to_owned(),
Value::Array(self.module_array("touch", "events")),
);
events.insert(
"touch_ball_movement".to_owned(),
Value::Array(self.module_array("touch", "ball_movement_events")),
);
events.insert(
"touch_last_touch".to_owned(),
Value::Array(self.module_array("touch", "last_touch_events")),
);
events.insert(
"boost_pickups".to_owned(),
Value::Array(self.module_array("boost", "events")),
);
events.insert(
"boost_ledger".to_owned(),
Value::Array(self.module_array("boost", "ledger_events")),
);
events.insert(
"boost_state".to_owned(),
Value::Array(self.module_array("boost", "state_events")),
);
events.insert(
"bump".to_owned(),
Value::Array(self.module_array("bump", "events")),
);
Value::Object(events)
}
fn timeline_config(&self) -> StatsTimelineConfig {
let positioning_config = self.config.get("positioning").and_then(Value::as_object);
let pressure_config = self.config.get("pressure").and_then(Value::as_object);
let rotation_config = self.config.get("rotation").and_then(Value::as_object);
let rotation_defaults = RotationCalculatorConfig::default();
let rush_config = self.config.get("rush").and_then(Value::as_object);
let rush_defaults = RushCalculatorConfig::default();
let aerial_goal_config = self.config.get("aerial_goal").and_then(Value::as_object);
let high_aerial_goal_config = self
.config
.get("high_aerial_goal")
.and_then(Value::as_object);
let long_distance_goal_config = self
.config
.get("long_distance_goal")
.and_then(Value::as_object);
let own_half_goal_config = self.config.get("own_half_goal").and_then(Value::as_object);
let empty_net_goal_config = self.config.get("empty_net_goal").and_then(Value::as_object);
let flick_goal_config = self.config.get("flick_goal").and_then(Value::as_object);
let double_tap_goal_config = self
.config
.get("double_tap_goal")
.and_then(Value::as_object);
let one_timer_goal_config = self.config.get("one_timer_goal").and_then(Value::as_object);
let air_dribble_goal_config = self
.config
.get("air_dribble_goal")
.and_then(Value::as_object);
let flip_reset_goal_config = self
.config
.get("flip_reset_goal")
.and_then(Value::as_object);
let half_volley_config = self.config.get("half_volley").and_then(Value::as_object);
let half_volley_goal_config = self
.config
.get("half_volley_goal")
.and_then(Value::as_object);
StatsTimelineConfig {
most_back_forward_threshold_y: positioning_config
.and_then(|config| config.get("most_back_forward_threshold_y"))
.and_then(json_f32)
.unwrap_or(PositioningCalculatorConfig::default().most_back_forward_threshold_y),
level_ball_depth_margin: positioning_config
.and_then(|config| config.get("level_ball_depth_margin"))
.and_then(json_f32)
.unwrap_or(PositioningCalculatorConfig::default().level_ball_depth_margin),
pressure_neutral_zone_half_width_y: pressure_config
.and_then(|config| config.get("pressure_neutral_zone_half_width_y"))
.and_then(json_f32)
.unwrap_or(PressureCalculatorConfig::default().neutral_zone_half_width_y),
rotation_role_depth_margin: rotation_config
.and_then(|config| config.get("role_depth_margin"))
.and_then(json_f32)
.unwrap_or(rotation_defaults.role_depth_margin),
rotation_first_man_ambiguity_margin: rotation_config
.and_then(|config| config.get("first_man_ambiguity_margin"))
.and_then(json_f32)
.unwrap_or(rotation_defaults.first_man_ambiguity_margin),
rotation_first_man_debounce_seconds: rotation_config
.and_then(|config| config.get("first_man_debounce_seconds"))
.and_then(json_f32)
.unwrap_or(rotation_defaults.first_man_debounce_seconds),
rush_max_start_y: rush_config
.and_then(|config| config.get("rush_max_start_y"))
.and_then(json_f32)
.unwrap_or(rush_defaults.max_start_y),
rush_attack_support_distance_y: rush_config
.and_then(|config| config.get("rush_attack_support_distance_y"))
.and_then(json_f32)
.unwrap_or(rush_defaults.attack_support_distance_y),
rush_defender_distance_y: rush_config
.and_then(|config| config.get("rush_defender_distance_y"))
.and_then(json_f32)
.unwrap_or(rush_defaults.defender_distance_y),
rush_min_possession_retained_seconds: rush_config
.and_then(|config| config.get("rush_min_possession_retained_seconds"))
.and_then(json_f32)
.unwrap_or(rush_defaults.min_possession_retained_seconds),
aerial_goal_min_ball_z: aerial_goal_config
.and_then(|config| config.get("aerial_goal_min_ball_z"))
.and_then(json_f32)
.unwrap_or(AerialGoalCalculatorConfig::default().min_ball_z),
high_aerial_goal_min_ball_z: high_aerial_goal_config
.and_then(|config| config.get("high_aerial_goal_min_ball_z"))
.and_then(json_f32)
.unwrap_or(HighAerialGoalCalculatorConfig::default().min_ball_z),
long_distance_goal_max_attacking_y: long_distance_goal_config
.and_then(|config| config.get("long_distance_goal_max_attacking_y"))
.and_then(json_f32)
.unwrap_or(LongDistanceGoalCalculatorConfig::default().max_attacking_y),
own_half_goal_max_attacking_y: own_half_goal_config
.and_then(|config| config.get("own_half_goal_max_attacking_y"))
.and_then(json_f32)
.unwrap_or(OwnHalfGoalCalculatorConfig::default().max_attacking_y),
empty_net_min_defender_y_margin: empty_net_goal_config
.and_then(|config| config.get("empty_net_min_defender_y_margin"))
.and_then(json_f32)
.unwrap_or(EmptyNetGoalCalculatorConfig::default().min_defender_y_margin),
empty_net_min_defender_distance: empty_net_goal_config
.and_then(|config| config.get("empty_net_min_defender_distance"))
.and_then(json_f32)
.unwrap_or(EmptyNetGoalCalculatorConfig::default().min_defender_distance),
empty_net_max_touch_attacking_y: empty_net_goal_config
.and_then(|config| config.get("empty_net_max_touch_attacking_y"))
.and_then(json_f32)
.unwrap_or(EmptyNetGoalCalculatorConfig::default().max_touch_attacking_y),
flick_goal_max_event_to_goal_seconds: json_config_f32(
flick_goal_config,
"flick_goal_max_event_to_goal_seconds",
"flick_goal_max_event_to_touch_seconds",
)
.unwrap_or(FlickGoalCalculatorConfig::default().max_event_to_goal_seconds),
double_tap_goal_max_event_to_goal_seconds: json_config_f32(
double_tap_goal_config,
"double_tap_goal_max_event_to_goal_seconds",
"double_tap_goal_max_event_to_touch_seconds",
)
.unwrap_or(DoubleTapGoalCalculatorConfig::default().max_event_to_goal_seconds),
one_timer_goal_max_event_to_goal_seconds: json_config_f32(
one_timer_goal_config,
"one_timer_goal_max_event_to_goal_seconds",
"one_timer_goal_max_event_to_touch_seconds",
)
.unwrap_or(OneTimerGoalCalculatorConfig::default().max_event_to_goal_seconds),
air_dribble_goal_max_end_to_goal_seconds: json_config_f32(
air_dribble_goal_config,
"air_dribble_goal_max_end_to_goal_seconds",
"air_dribble_goal_max_end_to_touch_seconds",
)
.unwrap_or(AirDribbleGoalCalculatorConfig::default().max_end_to_goal_seconds),
flip_reset_goal_max_event_to_goal_seconds: json_config_f32(
flip_reset_goal_config,
"flip_reset_goal_max_event_to_goal_seconds",
"flip_reset_goal_max_event_to_touch_seconds",
)
.unwrap_or(FlipResetGoalCalculatorConfig::default().max_event_to_goal_seconds),
half_volley_max_bounce_to_touch_seconds: half_volley_config
.and_then(|config| config.get("half_volley_max_bounce_to_touch_seconds"))
.and_then(json_f32)
.unwrap_or(HalfVolleyCalculatorConfig::default().max_bounce_to_touch_seconds),
half_volley_min_ball_speed: half_volley_config
.and_then(|config| config.get("half_volley_min_ball_speed"))
.and_then(json_f32)
.unwrap_or(HalfVolleyCalculatorConfig::default().min_ball_speed),
half_volley_goal_max_touch_to_goal_seconds: half_volley_goal_config
.and_then(|config| config.get("half_volley_goal_max_touch_to_goal_seconds"))
.and_then(json_f32)
.unwrap_or(HalfVolleyGoalCalculatorConfig::default().max_touch_to_goal_seconds),
half_volley_goal_min_goal_alignment: half_volley_goal_config
.and_then(|config| config.get("half_volley_goal_min_goal_alignment"))
.and_then(json_f32)
.unwrap_or(HalfVolleyGoalCalculatorConfig::default().min_goal_alignment),
}
}
fn timeline_config_value(&self) -> SubtrActorResult<Value> {
let positioning_config = self.config.get("positioning").and_then(Value::as_object);
let pressure_config = self.config.get("pressure").and_then(Value::as_object);
let rotation_config = self.config.get("rotation").and_then(Value::as_object);
let rush_config = self.config.get("rush").and_then(Value::as_object);
let aerial_goal_config = self.config.get("aerial_goal").and_then(Value::as_object);
let high_aerial_goal_config = self
.config
.get("high_aerial_goal")
.and_then(Value::as_object);
let long_distance_goal_config = self
.config
.get("long_distance_goal")
.and_then(Value::as_object);
let own_half_goal_config = self.config.get("own_half_goal").and_then(Value::as_object);
let empty_net_goal_config = self.config.get("empty_net_goal").and_then(Value::as_object);
let flick_goal_config = self.config.get("flick_goal").and_then(Value::as_object);
let double_tap_goal_config = self
.config
.get("double_tap_goal")
.and_then(Value::as_object);
let one_timer_goal_config = self.config.get("one_timer_goal").and_then(Value::as_object);
let air_dribble_goal_config = self
.config
.get("air_dribble_goal")
.and_then(Value::as_object);
let flip_reset_goal_config = self
.config
.get("flip_reset_goal")
.and_then(Value::as_object);
let half_volley_config = self.config.get("half_volley").and_then(Value::as_object);
let half_volley_goal_config = self
.config
.get("half_volley_goal")
.and_then(Value::as_object);
let mut config = Map::new();
config.insert(
"most_back_forward_threshold_y".to_owned(),
serialize_to_json_value(
&positioning_config
.and_then(|config| config.get("most_back_forward_threshold_y"))
.and_then(Value::as_f64)
.unwrap_or(
PositioningCalculatorConfig::default().most_back_forward_threshold_y as f64,
),
)?,
);
config.insert(
"level_ball_depth_margin".to_owned(),
serialize_to_json_value(
&positioning_config
.and_then(|config| config.get("level_ball_depth_margin"))
.and_then(Value::as_f64)
.unwrap_or(
PositioningCalculatorConfig::default().level_ball_depth_margin as f64,
),
)?,
);
config.insert(
"pressure_neutral_zone_half_width_y".to_owned(),
serialize_to_json_value(
&pressure_config
.and_then(|config| config.get("pressure_neutral_zone_half_width_y"))
.and_then(Value::as_f64)
.unwrap_or(
PressureCalculatorConfig::default().neutral_zone_half_width_y as f64,
),
)?,
);
let rotation_defaults = RotationCalculatorConfig::default();
for (key, default_value) in [
(
"rotation_role_depth_margin",
rotation_defaults.role_depth_margin,
),
(
"rotation_first_man_ambiguity_margin",
rotation_defaults.first_man_ambiguity_margin,
),
(
"rotation_first_man_debounce_seconds",
rotation_defaults.first_man_debounce_seconds,
),
] {
let source_key = key.strip_prefix("rotation_").unwrap_or(key);
config.insert(
key.to_owned(),
serialize_to_json_value(
&rotation_config
.and_then(|config| config.get(source_key))
.and_then(Value::as_f64)
.unwrap_or(default_value as f64),
)?,
);
}
let rush_defaults = RushCalculatorConfig::default();
config.insert(
"rush_max_start_y".to_owned(),
serialize_to_json_value(
&rush_config
.and_then(|config| config.get("rush_max_start_y"))
.and_then(Value::as_f64)
.unwrap_or(rush_defaults.max_start_y as f64),
)?,
);
config.insert(
"rush_attack_support_distance_y".to_owned(),
serialize_to_json_value(
&rush_config
.and_then(|config| config.get("rush_attack_support_distance_y"))
.and_then(Value::as_f64)
.unwrap_or(rush_defaults.attack_support_distance_y as f64),
)?,
);
config.insert(
"rush_defender_distance_y".to_owned(),
serialize_to_json_value(
&rush_config
.and_then(|config| config.get("rush_defender_distance_y"))
.and_then(Value::as_f64)
.unwrap_or(rush_defaults.defender_distance_y as f64),
)?,
);
config.insert(
"rush_min_possession_retained_seconds".to_owned(),
serialize_to_json_value(
&rush_config
.and_then(|config| config.get("rush_min_possession_retained_seconds"))
.and_then(Value::as_f64)
.unwrap_or(rush_defaults.min_possession_retained_seconds as f64),
)?,
);
for (module_config, key, default_value) in [
(
aerial_goal_config,
"aerial_goal_min_ball_z",
AerialGoalCalculatorConfig::default().min_ball_z,
),
(
high_aerial_goal_config,
"high_aerial_goal_min_ball_z",
HighAerialGoalCalculatorConfig::default().min_ball_z,
),
(
long_distance_goal_config,
"long_distance_goal_max_attacking_y",
LongDistanceGoalCalculatorConfig::default().max_attacking_y,
),
(
own_half_goal_config,
"own_half_goal_max_attacking_y",
OwnHalfGoalCalculatorConfig::default().max_attacking_y,
),
(
empty_net_goal_config,
"empty_net_min_defender_y_margin",
EmptyNetGoalCalculatorConfig::default().min_defender_y_margin,
),
(
empty_net_goal_config,
"empty_net_min_defender_distance",
EmptyNetGoalCalculatorConfig::default().min_defender_distance,
),
(
empty_net_goal_config,
"empty_net_max_touch_attacking_y",
EmptyNetGoalCalculatorConfig::default().max_touch_attacking_y,
),
(
flick_goal_config,
"flick_goal_max_event_to_goal_seconds",
FlickGoalCalculatorConfig::default().max_event_to_goal_seconds,
),
(
double_tap_goal_config,
"double_tap_goal_max_event_to_goal_seconds",
DoubleTapGoalCalculatorConfig::default().max_event_to_goal_seconds,
),
(
one_timer_goal_config,
"one_timer_goal_max_event_to_goal_seconds",
OneTimerGoalCalculatorConfig::default().max_event_to_goal_seconds,
),
(
air_dribble_goal_config,
"air_dribble_goal_max_end_to_goal_seconds",
AirDribbleGoalCalculatorConfig::default().max_end_to_goal_seconds,
),
(
flip_reset_goal_config,
"flip_reset_goal_max_event_to_goal_seconds",
FlipResetGoalCalculatorConfig::default().max_event_to_goal_seconds,
),
(
half_volley_config,
"half_volley_max_bounce_to_touch_seconds",
HalfVolleyCalculatorConfig::default().max_bounce_to_touch_seconds,
),
(
half_volley_config,
"half_volley_min_ball_speed",
HalfVolleyCalculatorConfig::default().min_ball_speed,
),
(
half_volley_goal_config,
"half_volley_goal_max_touch_to_goal_seconds",
HalfVolleyGoalCalculatorConfig::default().max_touch_to_goal_seconds,
),
(
half_volley_goal_config,
"half_volley_goal_min_goal_alignment",
HalfVolleyGoalCalculatorConfig::default().min_goal_alignment,
),
] {
config.insert(
key.to_owned(),
serialize_to_json_value(
&module_config
.and_then(|config| config.get(key))
.and_then(Value::as_f64)
.unwrap_or(default_value as f64),
)?,
);
}
Ok(Value::Object(config))
}
fn timeline_frame_value(&self, frame: &StatsSnapshotFrame) -> SubtrActorResult<Value> {
let mut timeline = Map::new();
timeline.insert(
"frame_number".to_owned(),
serialize_to_json_value(&frame.frame_number)?,
);
timeline.insert("time".to_owned(), serialize_to_json_value(&frame.time)?);
timeline.insert("dt".to_owned(), serialize_to_json_value(&frame.dt)?);
timeline.insert(
"seconds_remaining".to_owned(),
serialize_to_json_value(&frame.seconds_remaining)?,
);
timeline.insert(
"game_state".to_owned(),
serialize_to_json_value(&frame.game_state)?,
);
timeline.insert(
"ball_has_been_hit".to_owned(),
serialize_to_json_value(&frame.ball_has_been_hit)?,
);
timeline.insert(
"kickoff_countdown_time".to_owned(),
serialize_to_json_value(&frame.kickoff_countdown_time)?,
);
timeline.insert(
"gameplay_phase".to_owned(),
serialize_to_json_value(&frame.gameplay_phase)?,
);
timeline.insert(
"is_live_play".to_owned(),
serialize_to_json_value(&frame.is_live_play)?,
);
timeline.insert(
"fifty_fifty".to_owned(),
self.frame_stats_or_default::<FiftyFiftyStats>(frame, "fifty_fifty"),
);
timeline.insert(
"possession".to_owned(),
self.frame_stats_or_default::<PossessionStats>(frame, "possession"),
);
timeline.insert(
"pressure".to_owned(),
self.frame_stats_or_default::<PressureStats>(frame, "pressure"),
);
timeline.insert(
"rush".to_owned(),
self.frame_stats_or_default::<RushStats>(frame, "rush"),
);
timeline.insert(
"team_zero".to_owned(),
self.timeline_team_value(frame, "team_zero")?,
);
timeline.insert(
"team_one".to_owned(),
self.timeline_team_value(frame, "team_one")?,
);
timeline.insert(
"players".to_owned(),
Value::Array(
self.replay_meta
.player_order()
.map(|player| self.timeline_player_value(frame, player))
.collect::<SubtrActorResult<Vec<_>>>()?,
),
);
Ok(Value::Object(timeline))
}
pub(crate) fn replay_stats_frame(
&self,
frame: &StatsSnapshotFrame,
) -> SubtrActorResult<ReplayStatsFrame> {
Ok(ReplayStatsFrame {
frame_number: frame.frame_number,
time: frame.time,
dt: frame.dt,
seconds_remaining: frame.seconds_remaining,
game_state: frame.game_state,
ball_has_been_hit: frame.ball_has_been_hit,
kickoff_countdown_time: frame.kickoff_countdown_time,
gameplay_phase: frame.gameplay_phase,
is_live_play: frame.is_live_play,
team_zero: self.replay_team_stats(frame, "team_zero")?,
team_one: self.replay_team_stats(frame, "team_one")?,
players: self
.replay_meta
.player_order()
.map(|player| self.replay_player_stats(frame, player))
.collect::<SubtrActorResult<Vec<_>>>()?,
})
}
fn replay_team_stats(
&self,
frame: &StatsSnapshotFrame,
team_key: &str,
) -> SubtrActorResult<TeamStatsSnapshot> {
let is_team_zero = team_key == "team_zero";
Ok(TeamStatsSnapshot {
fifty_fifty: self
.frame_stats_or_default_typed::<FiftyFiftyStats>(frame, "fifty_fifty")?
.for_team(is_team_zero),
possession: self
.frame_stats_or_default_typed::<PossessionStats>(frame, "possession")?
.for_team(is_team_zero),
pressure: self
.frame_stats_or_default_typed::<PressureStats>(frame, "pressure")?
.for_team(is_team_zero),
rotation: self.frame_team_stat_or_default_typed(frame, "rotation", team_key)?,
rush: self
.frame_stats_or_default_typed::<RushStats>(frame, "rush")?
.for_team(is_team_zero),
core: self.frame_team_stat_or_default_typed(frame, "core", team_key)?,
backboard: self.frame_team_stat_or_default_typed(frame, "backboard", team_key)?,
double_tap: self.frame_team_stat_or_default_typed(frame, "double_tap", team_key)?,
one_timer: self.frame_team_stat_or_default_typed(frame, "one_timer", team_key)?,
pass: self.frame_team_stat_or_default_typed(frame, "pass", team_key)?,
ball_carry: self.frame_team_stat_or_default_typed(frame, "ball_carry", team_key)?,
air_dribble: self.frame_team_stat_or_default_typed(frame, "air_dribble", team_key)?,
boost: self.frame_team_stat_or_default_typed(frame, "boost", team_key)?,
bump: self.frame_team_stat_or_default_typed(frame, "bump", team_key)?,
half_volley: self.frame_team_stat_or_default_typed(frame, "half_volley", team_key)?,
movement: self.frame_team_stat_or_default_typed(frame, "movement", team_key)?,
powerslide: self.frame_team_stat_or_default_typed(frame, "powerslide", team_key)?,
demo: self.frame_team_stat_or_default_typed(frame, "demo", team_key)?,
})
}
fn replay_player_stats(
&self,
frame: &StatsSnapshotFrame,
player: &PlayerInfo,
) -> SubtrActorResult<PlayerStatsSnapshot> {
let player_key = player_info_key(player)?;
Ok(PlayerStatsSnapshot {
player_id: player.remote_id.clone(),
name: player.name.clone(),
is_team_0: self.is_team_zero_player(player),
core: self.frame_core_player_stat_or_default_by_key(frame, &player_key)?,
backboard: self.frame_player_stat_or_default_typed_by_key(
frame,
"backboard",
&player_key,
)?,
ceiling_shot: self.frame_player_stat_or_default_typed_by_key(
frame,
"ceiling_shot",
&player_key,
)?,
wall_aerial: self.frame_player_stat_or_default_typed_by_key(
frame,
"wall_aerial",
&player_key,
)?,
wall_aerial_shot: self.frame_player_stat_or_default_typed_by_key(
frame,
"wall_aerial_shot",
&player_key,
)?,
double_tap: self.frame_player_stat_or_default_typed_by_key(
frame,
"double_tap",
&player_key,
)?,
one_timer: self.frame_player_stat_or_default_typed_by_key(
frame,
"one_timer",
&player_key,
)?,
pass: self.frame_player_stat_or_default_typed_by_key(frame, "pass", &player_key)?,
fifty_fifty: self.frame_player_stat_or_default_typed_by_key(
frame,
"fifty_fifty",
&player_key,
)?,
speed_flip: self.frame_player_stat_or_default_typed_by_key(
frame,
"speed_flip",
&player_key,
)?,
half_flip: self.frame_player_stat_or_default_typed_by_key(
frame,
"half_flip",
&player_key,
)?,
wavedash: self.frame_player_stat_or_default_typed_by_key(
frame,
"wavedash",
&player_key,
)?,
touch: if frame.modules.contains_key("touch") {
self.frame_player_stat_or_default_with_by_key(frame, "touch", &player_key, || {
TouchStats::default().with_complete_labeled_touch_counts()
})?
} else {
self.frame_player_stat_or_default_typed_by_key(frame, "touch", &player_key)?
},
whiff: self.frame_player_stat_or_default_typed_by_key(frame, "whiff", &player_key)?,
flick: self.frame_player_stat_or_default_typed_by_key(frame, "flick", &player_key)?,
musty_flick: self.frame_player_stat_or_default_typed_by_key(
frame,
"musty_flick",
&player_key,
)?,
dodge_reset: self.frame_player_stat_or_default_typed_by_key(
frame,
"dodge_reset",
&player_key,
)?,
ball_carry: self.frame_player_stat_or_default_typed_by_key(
frame,
"ball_carry",
&player_key,
)?,
air_dribble: self.frame_player_stat_or_default_typed_by_key(
frame,
"air_dribble",
&player_key,
)?,
boost: self.frame_player_stat_or_default_typed_by_key(frame, "boost", &player_key)?,
bump: self.frame_player_stat_or_default_typed_by_key(frame, "bump", &player_key)?,
half_volley: self.frame_player_stat_or_default_typed_by_key(
frame,
"half_volley",
&player_key,
)?,
movement: self.frame_player_stat_or_default_with_by_key(
frame,
"movement",
&player_key,
|| MovementStats::default().with_complete_labeled_tracked_time(),
)?,
positioning: self.frame_player_stat_or_default_typed_by_key(
frame,
"positioning",
&player_key,
)?,
rotation: self.frame_player_stat_or_default_typed_by_key(
frame,
"rotation",
&player_key,
)?,
powerslide: self.frame_player_stat_or_default_typed_by_key(
frame,
"powerslide",
&player_key,
)?,
demo: self.frame_player_stat_or_default_typed_by_key(frame, "demo", &player_key)?,
})
}
fn is_team_zero_player(&self, player: &PlayerInfo) -> bool {
self.replay_meta
.team_zero
.iter()
.any(|team_player| team_player.remote_id == player.remote_id)
}
fn timeline_team_value(
&self,
frame: &StatsSnapshotFrame,
team_key: &str,
) -> SubtrActorResult<Value> {
let is_team_zero = team_key == "team_zero";
let mut team = Map::new();
team.insert(
"fifty_fifty".to_owned(),
serialize_to_json_value(
&self
.frame_stats_or_default_typed::<FiftyFiftyStats>(frame, "fifty_fifty")?
.for_team(is_team_zero),
)?,
);
team.insert(
"possession".to_owned(),
serialize_to_json_value(
&self
.frame_stats_or_default_typed::<PossessionStats>(frame, "possession")?
.for_team(is_team_zero),
)?,
);
team.insert(
"pressure".to_owned(),
serialize_to_json_value(
&self
.frame_stats_or_default_typed::<PressureStats>(frame, "pressure")?
.for_team(is_team_zero),
)?,
);
team.insert(
"rotation".to_owned(),
self.frame_team_stat_or_default::<RotationTeamStats>(frame, "rotation", team_key),
);
team.insert(
"rush".to_owned(),
serialize_to_json_value(
&self
.frame_stats_or_default_typed::<RushStats>(frame, "rush")?
.for_team(is_team_zero),
)?,
);
team.insert(
"core".to_owned(),
self.frame_team_stat_or_default::<CoreTeamStats>(frame, "core", team_key),
);
team.insert(
"backboard".to_owned(),
self.frame_team_stat_or_default::<BackboardTeamStats>(frame, "backboard", team_key),
);
team.insert(
"double_tap".to_owned(),
self.frame_team_stat_or_default::<DoubleTapTeamStats>(frame, "double_tap", team_key),
);
team.insert(
"one_timer".to_owned(),
self.frame_team_stat_or_default::<OneTimerTeamStats>(frame, "one_timer", team_key),
);
team.insert(
"pass".to_owned(),
self.frame_team_stat_or_default::<PassTeamStats>(frame, "pass", team_key),
);
team.insert(
"ball_carry".to_owned(),
self.frame_team_stat_or_default::<BallCarryStats>(frame, "ball_carry", team_key),
);
team.insert(
"air_dribble".to_owned(),
self.frame_team_stat_or_default::<AirDribbleStats>(frame, "air_dribble", team_key),
);
team.insert(
"boost".to_owned(),
self.frame_team_stat_or_default::<BoostStats>(frame, "boost", team_key),
);
team.insert(
"bump".to_owned(),
self.frame_team_stat_or_default::<BumpTeamStats>(frame, "bump", team_key),
);
team.insert(
"half_volley".to_owned(),
self.frame_team_stat_or_default::<HalfVolleyTeamStats>(frame, "half_volley", team_key),
);
team.insert(
"movement".to_owned(),
self.frame_team_stat_or_default::<MovementStats>(frame, "movement", team_key),
);
team.insert(
"powerslide".to_owned(),
self.frame_team_stat_or_default::<PowerslideStats>(frame, "powerslide", team_key),
);
team.insert(
"demo".to_owned(),
self.frame_team_stat_or_default::<DemoTeamStats>(frame, "demo", team_key),
);
Ok(Value::Object(team))
}
fn timeline_player_value(
&self,
frame: &StatsSnapshotFrame,
player: &PlayerInfo,
) -> SubtrActorResult<Value> {
let player_key = player_info_key(player)?;
let mut player_value = Map::new();
player_value.insert(
"player_id".to_owned(),
serialize_to_json_value(&player.remote_id)?,
);
player_value.insert("name".to_owned(), serialize_to_json_value(&player.name)?);
player_value.insert(
"is_team_0".to_owned(),
serialize_to_json_value(
&self
.replay_meta
.team_zero
.iter()
.any(|team_player| team_player.remote_id == player.remote_id),
)?,
);
player_value.insert(
"core".to_owned(),
self.frame_player_stat_or_default_by_key::<CorePlayerStats>(
frame,
"core",
&player_key,
)?,
);
player_value.insert(
"backboard".to_owned(),
self.frame_player_stat_or_default_by_key::<BackboardPlayerStats>(
frame,
"backboard",
&player_key,
)?,
);
player_value.insert(
"ceiling_shot".to_owned(),
self.frame_player_stat_or_default_by_key::<CeilingShotStats>(
frame,
"ceiling_shot",
&player_key,
)?,
);
player_value.insert(
"wall_aerial".to_owned(),
self.frame_player_stat_or_default_by_key::<WallAerialStats>(
frame,
"wall_aerial",
&player_key,
)?,
);
player_value.insert(
"wall_aerial_shot".to_owned(),
self.frame_player_stat_or_default_by_key::<WallAerialShotStats>(
frame,
"wall_aerial_shot",
&player_key,
)?,
);
player_value.insert(
"double_tap".to_owned(),
self.frame_player_stat_or_default_by_key::<DoubleTapPlayerStats>(
frame,
"double_tap",
&player_key,
)?,
);
player_value.insert(
"one_timer".to_owned(),
self.frame_player_stat_or_default_by_key::<OneTimerPlayerStats>(
frame,
"one_timer",
&player_key,
)?,
);
player_value.insert(
"pass".to_owned(),
self.frame_player_stat_or_default_by_key::<PassPlayerStats>(
frame,
"pass",
&player_key,
)?,
);
player_value.insert(
"fifty_fifty".to_owned(),
self.frame_player_stat_or_default_by_key::<FiftyFiftyPlayerStats>(
frame,
"fifty_fifty",
&player_key,
)?,
);
player_value.insert(
"speed_flip".to_owned(),
self.frame_player_stat_or_default_by_key::<SpeedFlipStats>(
frame,
"speed_flip",
&player_key,
)?,
);
player_value.insert(
"half_flip".to_owned(),
self.frame_player_stat_or_default_by_key::<HalfFlipStats>(
frame,
"half_flip",
&player_key,
)?,
);
player_value.insert(
"half_volley".to_owned(),
self.frame_player_stat_or_default_by_key::<HalfVolleyPlayerStats>(
frame,
"half_volley",
&player_key,
)?,
);
player_value.insert(
"wavedash".to_owned(),
self.frame_player_stat_or_default_by_key::<WavedashStats>(
frame,
"wavedash",
&player_key,
)?,
);
player_value.insert(
"touch".to_owned(),
self.frame_player_stat_or_value_by_key(
frame,
"touch",
&player_key,
if frame.modules.contains_key("touch") {
serialize_to_json_value(
&TouchStats::default().with_complete_labeled_touch_counts(),
)?
} else {
default_json_value::<TouchStats>()
},
)?,
);
player_value.insert(
"whiff".to_owned(),
self.frame_player_stat_or_default_by_key::<WhiffStats>(frame, "whiff", &player_key)?,
);
player_value.insert(
"flick".to_owned(),
self.frame_player_stat_or_default_by_key::<FlickStats>(frame, "flick", &player_key)?,
);
player_value.insert(
"musty_flick".to_owned(),
self.frame_player_stat_or_default_by_key::<MustyFlickStats>(
frame,
"musty_flick",
&player_key,
)?,
);
player_value.insert(
"dodge_reset".to_owned(),
self.frame_player_stat_or_default_by_key::<DodgeResetStats>(
frame,
"dodge_reset",
&player_key,
)?,
);
player_value.insert(
"ball_carry".to_owned(),
self.frame_player_stat_or_default_by_key::<BallCarryStats>(
frame,
"ball_carry",
&player_key,
)?,
);
player_value.insert(
"air_dribble".to_owned(),
self.frame_player_stat_or_default_by_key::<AirDribbleStats>(
frame,
"air_dribble",
&player_key,
)?,
);
player_value.insert(
"boost".to_owned(),
self.frame_player_stat_or_default_by_key::<BoostStats>(frame, "boost", &player_key)?,
);
player_value.insert(
"bump".to_owned(),
self.frame_player_stat_or_default_by_key::<BumpPlayerStats>(
frame,
"bump",
&player_key,
)?,
);
player_value.insert(
"movement".to_owned(),
self.frame_player_stat_or_value_by_key(
frame,
"movement",
&player_key,
if frame.modules.contains_key("movement") {
serialize_to_json_value(
&MovementStats::default().with_complete_labeled_tracked_time(),
)?
} else {
default_json_value::<MovementStats>()
},
)?,
);
player_value.insert(
"positioning".to_owned(),
self.frame_player_stat_or_default_by_key::<PositioningStats>(
frame,
"positioning",
&player_key,
)?,
);
player_value.insert(
"rotation".to_owned(),
self.frame_player_stat_or_default_by_key::<RotationPlayerStats>(
frame,
"rotation",
&player_key,
)?,
);
player_value.insert(
"powerslide".to_owned(),
self.frame_player_stat_or_default_by_key::<PowerslideStats>(
frame,
"powerslide",
&player_key,
)?,
);
player_value.insert(
"demo".to_owned(),
self.frame_player_stat_or_default_by_key::<DemoPlayerStats>(
frame,
"demo",
&player_key,
)?,
);
Ok(Value::Object(player_value))
}
fn frame_stats_or_default<T>(&self, frame: &StatsSnapshotFrame, module_name: &str) -> Value
where
T: Default + Serialize,
{
frame
.modules
.get(module_name)
.and_then(Value::as_object)
.and_then(|module| module.get("stats"))
.cloned()
.unwrap_or_else(|| default_json_value::<T>())
}
fn frame_team_stat_or_default<T>(
&self,
frame: &StatsSnapshotFrame,
module_name: &str,
team_key: &str,
) -> Value
where
T: Default + Serialize,
{
frame
.modules
.get(module_name)
.and_then(Value::as_object)
.and_then(|module| module.get(team_key))
.cloned()
.unwrap_or_else(|| default_json_value::<T>())
}
fn frame_player_stat_or_default_by_key<T>(
&self,
frame: &StatsSnapshotFrame,
module_name: &str,
player_key: &str,
) -> SubtrActorResult<Value>
where
T: Default + Serialize,
{
self.frame_player_stat_or_value_by_key(
frame,
module_name,
player_key,
default_json_value::<T>(),
)
}
fn frame_player_stat_or_value_by_key(
&self,
frame: &StatsSnapshotFrame,
module_name: &str,
player_key: &str,
default_value: Value,
) -> SubtrActorResult<Value> {
Ok(
player_stats_value_for_key(frame.modules.get(module_name), player_key)?
.cloned()
.unwrap_or(default_value),
)
}
fn frame_stats_or_default_typed<T>(
&self,
frame: &StatsSnapshotFrame,
module_name: &str,
) -> SubtrActorResult<T>
where
T: Default + DeserializeOwned + Serialize,
{
decode_json_value(self.frame_stats_or_default::<T>(frame, module_name))
}
fn frame_team_stat_or_default_typed<T>(
&self,
frame: &StatsSnapshotFrame,
module_name: &str,
team_key: &str,
) -> SubtrActorResult<T>
where
T: Default + DeserializeOwned + Serialize,
{
decode_json_value(self.frame_team_stat_or_default::<T>(frame, module_name, team_key))
}
fn frame_player_stat_or_default_typed_by_key<T>(
&self,
frame: &StatsSnapshotFrame,
module_name: &str,
player_key: &str,
) -> SubtrActorResult<T>
where
T: Default + DeserializeOwned + Serialize,
{
self.frame_player_stat_or_default_with_by_key(frame, module_name, player_key, T::default)
}
fn frame_core_player_stat_or_default_by_key(
&self,
frame: &StatsSnapshotFrame,
player_key: &str,
) -> SubtrActorResult<CorePlayerStats> {
decode_core_player_stats_value(self.frame_player_stat_or_value_by_key(
frame,
"core",
player_key,
default_json_value::<CorePlayerStats>(),
)?)
}
fn frame_player_stat_or_default_with_by_key<T, F>(
&self,
frame: &StatsSnapshotFrame,
module_name: &str,
player_key: &str,
default: F,
) -> SubtrActorResult<T>
where
T: DeserializeOwned + Serialize,
F: FnOnce() -> T,
{
decode_json_value(self.frame_player_stat_or_value_by_key(
frame,
module_name,
player_key,
serialize_to_json_value(&default())?,
)?)
}
fn module_typed_array<T>(&self, module_name: &str, field: &str) -> SubtrActorResult<Vec<T>>
where
T: DeserializeOwned,
{
decode_json_value(Value::Array(self.module_array(module_name, field)))
}
fn module_player_events<T, F>(
&self,
module_name: &str,
field: &str,
parse: F,
) -> SubtrActorResult<Vec<T>>
where
F: Fn(&Value) -> SubtrActorResult<T>,
{
self.module_array(module_name, field)
.iter()
.map(parse)
.collect()
}
fn module_array(&self, module_name: &str, field: &str) -> Vec<Value> {
self.modules
.get(module_name)
.and_then(Value::as_object)
.and_then(|module| module.get(field))
.and_then(Value::as_array)
.cloned()
.unwrap_or_default()
}
}
impl CapturedStatsData<ReplayStatsFrame> {
pub fn into_legacy_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
let CapturedStatsData {
replay_meta,
config,
modules,
frames,
} = self;
CapturedStatsData::<StatsSnapshotFrame> {
replay_meta,
config,
modules,
frames: Vec::new(),
}
.into_replay_stats_timeline_with_frames(frames)
}
#[deprecated(
note = "use into_legacy_replay_stats_timeline for full partial-sum snapshots, or StatsTimelineEventCollector for compact event-backed timelines"
)]
pub fn into_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
self.into_legacy_replay_stats_timeline()
}
}
fn player_stats_value_for_key<'a>(
module: Option<&'a Value>,
player_key: &str,
) -> SubtrActorResult<Option<&'a Value>> {
let Some(entries) = module
.and_then(Value::as_object)
.and_then(|module| module.get("player_stats"))
.and_then(Value::as_array)
else {
return Ok(None);
};
for entry in entries {
let Some(entry_object) = entry.as_object() else {
continue;
};
let Some(player_id) = entry_object.get("player_id") else {
continue;
};
let Some(player_stats) = entry_object.get("stats") else {
continue;
};
if player_id_key(player_id)? == player_key {
return Ok(Some(player_stats));
}
}
Ok(None)
}
fn player_info_key(player: &PlayerInfo) -> SubtrActorResult<String> {
player_id_key(&serialize_to_json_value(&player.remote_id)?)
}
fn player_id_key(player_id: &Value) -> SubtrActorResult<String> {
serde_json::to_string(player_id).map_err(|error| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
error.to_string(),
))
})
}
fn default_json_value<T>() -> Value
where
T: Default + Serialize,
{
serde_json::to_value(T::default()).expect("default stats should serialize to json")
}
fn decode_json_value<T>(value: Value) -> SubtrActorResult<T>
where
T: DeserializeOwned,
{
serde_json::from_value(value).map_err(|error| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
error.to_string(),
))
})
}
fn decode_core_player_stats_value(mut value: Value) -> SubtrActorResult<CorePlayerStats> {
normalize_core_player_stats_snapshot(&mut value)?;
decode_json_value(value)
}
fn normalize_core_player_stats_snapshot(value: &mut Value) -> SubtrActorResult<()> {
let Some(object) = value.as_object_mut() else {
return Ok(());
};
insert_cumulative_from_average(
object,
"cumulative_boost_on_goals_against",
"average_boost_on_goals_against",
"goal_against_boost_sample_count",
)?;
insert_cumulative_from_average(
object,
"cumulative_average_boost_in_goal_against_leadup",
"average_boost_in_goal_against_leadup",
"goal_against_boost_leadup_sample_count",
)?;
insert_cumulative_from_average(
object,
"cumulative_min_boost_in_goal_against_leadup",
"average_min_boost_in_goal_against_leadup",
"goal_against_boost_leadup_sample_count",
)?;
insert_cumulative_from_average(
object,
"cumulative_goal_against_position_x",
"average_goal_against_position_x",
"goal_against_position_sample_count",
)?;
insert_cumulative_from_average(
object,
"cumulative_goal_against_position_y",
"average_goal_against_position_y",
"goal_against_position_sample_count",
)?;
insert_cumulative_from_average(
object,
"cumulative_goal_against_position_z",
"average_goal_against_position_z",
"goal_against_position_sample_count",
)?;
insert_cumulative_from_average(
object,
"cumulative_scoring_goal_last_touch_position_x",
"average_scoring_goal_last_touch_position_x",
"scoring_goal_last_touch_position_sample_count",
)?;
insert_cumulative_from_average(
object,
"cumulative_scoring_goal_last_touch_position_y",
"average_scoring_goal_last_touch_position_y",
"scoring_goal_last_touch_position_sample_count",
)?;
insert_cumulative_from_average(
object,
"cumulative_scoring_goal_last_touch_position_z",
"average_scoring_goal_last_touch_position_z",
"scoring_goal_last_touch_position_sample_count",
)?;
insert_cumulative_from_average(
object,
"cumulative_goal_ball_air_time",
"average_goal_ball_air_time",
"goal_ball_air_time_sample_count",
)?;
if let Value::Object(defaults) = default_json_value::<CorePlayerStats>() {
for (field, default_value) in defaults {
object.entry(field).or_insert(default_value);
}
}
Ok(())
}
fn insert_cumulative_from_average(
object: &mut Map<String, Value>,
cumulative_field: &str,
average_field: &str,
sample_count_field: &str,
) -> SubtrActorResult<()> {
if object.contains_key(cumulative_field) {
return Ok(());
}
let average = object
.get(average_field)
.and_then(Value::as_f64)
.unwrap_or(0.0) as f32;
let sample_count = object
.get(sample_count_field)
.and_then(Value::as_u64)
.unwrap_or(0) as f32;
object.insert(
cumulative_field.to_owned(),
serialize_to_json_value(&(average * sample_count))?,
);
Ok(())
}
fn parse_timeline_event(value: &Value) -> SubtrActorResult<TimelineEvent> {
let object = json_object(value, "timeline event")?;
Ok(TimelineEvent {
time: json_required_f32(object, "time")?,
kind: decode_json_value(json_required_value(object, "kind")?.clone())?,
player_id: json_optional_remote_id(object.get("player_id"))?,
is_team_0: json_optional_bool(object.get("is_team_0")),
})
}
fn moment_mechanic_event(
kind: &str,
index: usize,
frame: usize,
time: f32,
player_id: PlayerId,
is_team_0: bool,
) -> MechanicEvent {
MechanicEvent {
id: format!("{kind}:{frame}:{index}"),
kind: kind.to_owned(),
player_id,
is_team_0,
timing: MechanicTiming::Moment { frame, time },
properties: Vec::new(),
}
}
#[allow(clippy::too_many_arguments)]
fn span_mechanic_event(
kind: &str,
index: usize,
start_frame: usize,
end_frame: usize,
start_time: f32,
end_time: f32,
player_id: PlayerId,
is_team_0: bool,
) -> MechanicEvent {
MechanicEvent {
id: format!("{kind}:{start_frame}:{end_frame}:{index}"),
kind: kind.to_owned(),
player_id,
is_team_0,
timing: MechanicTiming::Span {
start_frame,
end_frame,
start_time,
end_time,
},
properties: Vec::new(),
}
}
fn mechanic_event_start_time(event: &MechanicEvent) -> f32 {
match event.timing {
MechanicTiming::Moment { time, .. } => time,
MechanicTiming::Span { start_time, .. } => start_time,
}
}
fn mechanic_event_text_property(key: &str, value: &str) -> MechanicEventProperty {
MechanicEventProperty {
key: key.to_owned(),
value: MechanicEventPropertyValue::Text(value.to_owned()),
}
}
fn mechanic_event_unsigned_property(key: &str, value: u32) -> MechanicEventProperty {
MechanicEventProperty {
key: key.to_owned(),
value: MechanicEventPropertyValue::Unsigned(value),
}
}
fn ball_carry_mechanic_event_properties(
object: &serde_json::Map<String, Value>,
) -> Vec<MechanicEventProperty> {
let mut properties = Vec::new();
if let Some(origin) = object.get("air_dribble_origin").and_then(Value::as_str) {
properties.push(mechanic_event_text_property("origin", origin));
}
if let Some(touch_count) = object.get("touch_count").and_then(Value::as_u64) {
properties.push(mechanic_event_unsigned_property(
"touch_count",
touch_count as u32,
));
}
properties
}
fn parse_ball_carry_mechanic_event(value: &Value, index: usize) -> SubtrActorResult<MechanicEvent> {
let object = json_object(value, "ball carry mechanic event")?;
let serialized_kind = json_required_str(object, "kind")?;
let kind = match serialized_kind {
"carry" => "ball_carry",
"air_dribble" => "air_dribble",
other => other,
};
let mut mechanic_event = span_mechanic_event(
kind,
index,
json_required_usize(object, "start_frame")?,
json_required_usize(object, "end_frame")?,
json_required_f32(object, "start_time")?,
json_required_f32(object, "end_time")?,
json_required_remote_id(object, "player_id")?,
json_required_bool(object, "is_team_0")?,
);
if kind == "air_dribble" {
mechanic_event.properties = ball_carry_mechanic_event_properties(object);
}
Ok(mechanic_event)
}
fn parse_dodge_reset_mechanic_event(
value: &Value,
index: usize,
) -> SubtrActorResult<MechanicEvent> {
let object = json_object(value, "dodge reset mechanic event")?;
Ok(moment_mechanic_event(
"flip_reset",
index,
json_required_usize(object, "frame")?,
json_required_f32(object, "time")?,
json_required_remote_id(object, "player")?,
json_required_bool(object, "is_team_0")?,
))
}
fn parse_dodge_reset_event(value: &Value) -> SubtrActorResult<DodgeResetEvent> {
let object = json_object(value, "dodge reset event")?;
Ok(DodgeResetEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
counter_value: json_required_i32(object, "counter_value")?,
on_ball: json_required_bool(object, "on_ball")?,
})
}
fn parse_powerslide_event(value: &Value) -> SubtrActorResult<PowerslideEvent> {
let object = json_object(value, "powerslide event")?;
Ok(PowerslideEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
active: json_required_bool(object, "active")?,
})
}
fn parse_core_player_stats_event(value: &Value) -> SubtrActorResult<CorePlayerStatsEvent> {
let object = json_object(value, "core player stats event")?;
Ok(CorePlayerStatsEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
delta: decode_json_value(json_required_value(object, "delta")?.clone())?,
})
}
fn parse_core_team_stats_event(value: &Value) -> SubtrActorResult<CoreTeamStatsEvent> {
let object = json_object(value, "core team stats event")?;
Ok(CoreTeamStatsEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
is_team_0: json_required_bool(object, "is_team_0")?,
delta: decode_json_value(json_required_value(object, "delta")?.clone())?,
})
}
fn parse_possession_event(value: &Value) -> SubtrActorResult<PossessionEvent> {
let object = json_object(value, "possession event")?;
Ok(PossessionEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
active: json_required_bool(object, "active")?,
possession_state: json_required_str(object, "possession_state")?.to_owned(),
field_third: match object.get("field_third") {
None | Some(Value::Null) => None,
Some(_) => Some(json_required_str(object, "field_third")?.to_owned()),
},
})
}
fn parse_pressure_event(value: &Value) -> SubtrActorResult<PressureEvent> {
let object = json_object(value, "pressure event")?;
Ok(PressureEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
active: json_required_bool(object, "active")?,
field_half: json_required_str(object, "field_half")?.to_owned(),
})
}
fn parse_movement_event(value: &Value) -> SubtrActorResult<MovementEvent> {
let object = json_object(value, "movement event")?;
Ok(MovementEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
dt: json_required_f32(object, "dt")?,
speed: json_required_f32(object, "speed")?,
distance: json_required_f32(object, "distance")?,
speed_band: json_required_str(object, "speed_band")?.to_owned(),
height_band: json_required_str(object, "height_band")?.to_owned(),
})
}
fn parse_positioning_event(value: &Value) -> SubtrActorResult<PositioningEvent> {
let object = json_object(value, "positioning event")?;
Ok(PositioningEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
active_game_time: json_required_f32(object, "active_game_time")?,
tracked_time: json_required_f32(object, "tracked_time")?,
sum_distance_to_teammates: json_required_f32(object, "sum_distance_to_teammates")?,
sum_distance_to_ball: json_required_f32(object, "sum_distance_to_ball")?,
sum_distance_to_ball_has_possession: json_required_f32(
object,
"sum_distance_to_ball_has_possession",
)?,
time_has_possession: json_required_f32(object, "time_has_possession")?,
sum_distance_to_ball_no_possession: json_required_f32(
object,
"sum_distance_to_ball_no_possession",
)?,
time_no_possession: json_required_f32(object, "time_no_possession")?,
time_demolished: json_required_f32(object, "time_demolished")?,
time_no_teammates: json_required_f32(object, "time_no_teammates")?,
time_most_back: json_required_f32(object, "time_most_back")?,
time_most_forward: json_required_f32(object, "time_most_forward")?,
time_mid_role: json_required_f32(object, "time_mid_role")?,
time_other_role: json_required_f32(object, "time_other_role")?,
time_defensive_zone: json_required_f32(object, "time_defensive_third")?,
time_neutral_zone: json_required_f32(object, "time_neutral_third")?,
time_offensive_zone: json_required_f32(object, "time_offensive_third")?,
time_defensive_half: json_required_f32(object, "time_defensive_half")?,
time_offensive_half: json_required_f32(object, "time_offensive_half")?,
time_closest_to_ball: json_required_f32(object, "time_closest_to_ball")?,
time_farthest_from_ball: json_required_f32(object, "time_farthest_from_ball")?,
time_behind_ball: json_required_f32(object, "time_behind_ball")?,
time_level_with_ball: json_required_f32(object, "time_level_with_ball")?,
time_in_front_of_ball: json_required_f32(object, "time_in_front_of_ball")?,
times_caught_ahead_of_play_on_conceded_goals: json_required_usize(
object,
"times_caught_ahead_of_play_on_conceded_goals",
)? as u32,
})
}
fn parse_rotation_player_event(value: &Value) -> SubtrActorResult<RotationPlayerEvent> {
let object = json_object(value, "rotation player event")?;
Ok(RotationPlayerEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
active: json_required_bool(object, "active")?,
became_first_man_count: json_required_usize(object, "became_first_man_count")? as u32,
lost_first_man_count: json_required_usize(object, "lost_first_man_count")? as u32,
current_role_state: decode_json_value(
json_required_value(object, "current_role_state")?.clone(),
)?,
current_depth_state: decode_json_value(
json_required_value(object, "current_depth_state")?.clone(),
)?,
})
}
fn parse_rotation_team_event(value: &Value) -> SubtrActorResult<RotationTeamEvent> {
let object = json_object(value, "rotation team event")?;
Ok(RotationTeamEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
is_team_0: json_required_bool(object, "is_team_0")?,
first_man_changes_for_team: json_required_usize(object, "first_man_changes_for_team")?
as u32,
rotation_count: json_required_usize(object, "rotation_count")? as u32,
})
}
fn parse_touch_stats_event(value: &Value) -> SubtrActorResult<TouchStatsEvent> {
let object = json_object(value, "touch stats event")?;
let time = json_required_f32(object, "time")?;
let frame = json_required_usize(object, "frame")?;
Ok(TouchStatsEvent {
time,
frame,
sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
kind: json_required_str(object, "kind")?.to_owned(),
height_band: json_required_str(object, "height_band")?.to_owned(),
surface: json_required_str(object, "surface")?.to_owned(),
dodge_state: json_required_str(object, "dodge_state")?.to_owned(),
ball_speed_change: json_required_f32(object, "ball_speed_change")?,
})
}
fn parse_touch_ball_movement_event(value: &Value) -> SubtrActorResult<TouchBallMovementEvent> {
let object = json_object(value, "touch ball movement event")?;
Ok(TouchBallMovementEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
travel_distance: json_required_f32(object, "travel_distance")?,
advance_distance: json_required_f32(object, "advance_distance")?,
retreat_distance: json_required_f32(object, "retreat_distance")?,
})
}
fn parse_touch_last_touch_event(value: &Value) -> SubtrActorResult<TouchLastTouchEvent> {
let object = json_object(value, "touch last-touch event")?;
let time = json_required_f32(object, "time")?;
let frame = json_required_usize(object, "frame")?;
Ok(TouchLastTouchEvent {
time,
frame,
sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
is_team_0: json_required_bool(object, "is_team_0")?,
player: json_optional_remote_id(object.get("player"))?,
})
}
fn parse_flick_mechanic_event(value: &Value, index: usize) -> SubtrActorResult<MechanicEvent> {
let object = json_object(value, "flick mechanic event")?;
Ok(span_mechanic_event(
"flick",
index,
json_required_usize(object, "setup_start_frame")?,
json_required_usize(object, "frame")?,
json_required_f32(object, "setup_start_time")?,
json_required_f32(object, "time")?,
json_required_remote_id(object, "player")?,
json_required_bool(object, "is_team_0")?,
))
}
fn parse_flick_event(value: &Value) -> SubtrActorResult<FlickEvent> {
let object = json_object(value, "flick event")?;
let time = json_required_f32(object, "time")?;
let frame = json_required_usize(object, "frame")?;
Ok(FlickEvent {
time,
frame,
sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
dodge_time: json_required_f32(object, "dodge_time")?,
dodge_frame: json_required_usize(object, "dodge_frame")?,
time_since_dodge: json_required_f32(object, "time_since_dodge")?,
setup_start_time: json_required_f32(object, "setup_start_time")?,
setup_start_frame: json_required_usize(object, "setup_start_frame")?,
setup_duration: json_required_f32(object, "setup_duration")?,
setup_touch_count: json_required_usize(object, "setup_touch_count")? as u32,
average_horizontal_gap: json_required_f32(object, "average_horizontal_gap")?,
average_vertical_gap: json_required_f32(object, "average_vertical_gap")?,
ball_speed_change: json_required_f32(object, "ball_speed_change")?,
ball_impulse: json_required_vec3(object, "ball_impulse")?,
impulse_away_alignment: json_required_f32(object, "impulse_away_alignment")?,
vertical_impulse: json_required_f32(object, "vertical_impulse")?,
confidence: json_required_f32(object, "confidence")?,
})
}
fn parse_musty_flick_mechanic_event(
value: &Value,
index: usize,
) -> SubtrActorResult<MechanicEvent> {
let object = json_object(value, "musty flick mechanic event")?;
Ok(span_mechanic_event(
"musty_flick",
index,
json_required_usize(object, "dodge_frame")?,
json_required_usize(object, "frame")?,
json_required_f32(object, "dodge_time")?,
json_required_f32(object, "time")?,
json_required_remote_id(object, "player")?,
json_required_bool(object, "is_team_0")?,
))
}
fn parse_musty_flick_event(value: &Value) -> SubtrActorResult<MustyFlickEvent> {
let object = json_object(value, "musty flick event")?;
let time = json_required_f32(object, "time")?;
let frame = json_required_usize(object, "frame")?;
Ok(MustyFlickEvent {
time,
frame,
sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
aerial: json_required_bool(object, "aerial")?,
dodge_time: json_required_f32(object, "dodge_time")?,
dodge_frame: json_required_usize(object, "dodge_frame")?,
time_since_dodge: json_required_f32(object, "time_since_dodge")?,
confidence: json_required_f32(object, "confidence")?,
local_ball_position: json_required_vec3(object, "local_ball_position")?,
rear_alignment: json_required_f32(object, "rear_alignment")?,
top_alignment: json_required_f32(object, "top_alignment")?,
forward_approach_speed: json_required_f32(object, "forward_approach_speed")?,
pitch_rate: json_required_f32(object, "pitch_rate")?,
ball_speed_change: json_required_f32(object, "ball_speed_change")?,
})
}
fn parse_goal_context_event(value: &Value) -> SubtrActorResult<GoalContextEvent> {
let object = json_object(value, "goal context event")?;
Ok(GoalContextEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
scoring_team_is_team_0: json_required_bool(object, "scoring_team_is_team_0")?,
scorer: json_optional_remote_id(object.get("scorer"))?,
scoring_team_most_back_player: json_optional_remote_id(
object.get("scoring_team_most_back_player"),
)?,
defending_team_most_back_player: json_optional_remote_id(
object.get("defending_team_most_back_player"),
)?,
ball_position: json_optional_goal_context_position(object.get("ball_position"))?,
ball_air_time_before_goal: json_optional_f32(object.get("ball_air_time_before_goal"))?,
goal_buildup: object
.get("goal_buildup")
.map(|value| decode_json_value(value.clone()))
.transpose()?
.unwrap_or_default(),
scorer_last_touch: match object.get("scorer_last_touch") {
None | Some(Value::Null) => None,
Some(value) => Some(parse_goal_touch_context(value)?),
},
players: json_required_array(object, "players")?
.iter()
.map(parse_goal_player_context)
.collect::<SubtrActorResult<Vec<_>>>()?,
})
}
fn parse_goal_player_context(value: &Value) -> SubtrActorResult<GoalPlayerContext> {
let object = json_object(value, "goal player context")?;
Ok(GoalPlayerContext {
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
position: json_optional_goal_context_position(object.get("position"))?,
boost_amount: json_optional_f32(object.get("boost_amount"))?,
average_boost_in_leadup: json_optional_f32(object.get("average_boost_in_leadup"))?,
min_boost_in_leadup: json_optional_f32(object.get("min_boost_in_leadup"))?,
is_most_back: json_required_bool(object, "is_most_back")?,
})
}
fn parse_goal_touch_context(value: &Value) -> SubtrActorResult<GoalTouchContext> {
let object = json_object(value, "goal touch context")?;
Ok(GoalTouchContext {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
ball_position: json_optional_goal_context_position(object.get("ball_position"))?,
player_position: json_optional_goal_context_position(object.get("player_position"))?,
players: match object.get("players").and_then(Value::as_array) {
Some(players) => players
.iter()
.map(parse_goal_player_context)
.collect::<SubtrActorResult<Vec<_>>>()?,
None => Vec::new(),
},
})
}
fn parse_backboard_event(value: &Value) -> SubtrActorResult<BackboardBounceEvent> {
let object = json_object(value, "backboard event")?;
Ok(BackboardBounceEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
})
}
fn parse_ceiling_shot_event(value: &Value) -> SubtrActorResult<CeilingShotEvent> {
let object = json_object(value, "ceiling shot event")?;
Ok(CeilingShotEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
ceiling_contact_time: json_required_f32(object, "ceiling_contact_time")?,
ceiling_contact_frame: json_required_usize(object, "ceiling_contact_frame")?,
time_since_ceiling_contact: json_required_f32(object, "time_since_ceiling_contact")?,
ceiling_contact_position: json_required_vec3(object, "ceiling_contact_position")?,
touch_position: json_required_vec3(object, "touch_position")?,
local_ball_position: json_required_vec3(object, "local_ball_position")?,
separation_from_ceiling: json_required_f32(object, "separation_from_ceiling")?,
roof_alignment: json_required_f32(object, "roof_alignment")?,
forward_alignment: json_required_f32(object, "forward_alignment")?,
forward_approach_speed: json_required_f32(object, "forward_approach_speed")?,
ball_speed_change: json_required_f32(object, "ball_speed_change")?,
confidence: json_required_f32(object, "confidence")?,
})
}
fn parse_wall_aerial_event(value: &Value) -> SubtrActorResult<WallAerialEvent> {
let object = json_object(value, "wall aerial event")?;
let time = json_required_f32(object, "time")?;
let frame = json_required_usize(object, "frame")?;
Ok(WallAerialEvent {
time,
frame,
sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
wall: decode_json_value(json_required_value(object, "wall")?.clone())?,
wall_contact_time: json_required_f32(object, "wall_contact_time")?,
wall_contact_frame: json_required_usize(object, "wall_contact_frame")?,
takeoff_time: json_required_f32(object, "takeoff_time")?,
takeoff_frame: json_required_usize(object, "takeoff_frame")?,
time_since_takeoff: json_required_f32(object, "time_since_takeoff")?,
wall_contact_position: json_required_vec3(object, "wall_contact_position")?,
takeoff_position: json_required_vec3(object, "takeoff_position")?,
player_position: json_required_vec3(object, "player_position")?,
ball_position: json_required_vec3(object, "ball_position")?,
setup_start_time: json_required_f32(object, "setup_start_time")?,
setup_start_frame: json_required_usize(object, "setup_start_frame")?,
setup_duration: json_required_f32(object, "setup_duration")?,
ball_speed: json_required_f32(object, "ball_speed")?,
ball_speed_change: json_required_f32(object, "ball_speed_change")?,
goal_alignment: json_required_f32(object, "goal_alignment")?,
confidence: json_required_f32(object, "confidence")?,
})
}
fn parse_wall_aerial_shot_event(value: &Value) -> SubtrActorResult<WallAerialShotEvent> {
let object = json_object(value, "wall aerial shot event")?;
Ok(WallAerialShotEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
wall: decode_json_value(json_required_value(object, "wall")?.clone())?,
wall_contact_time: json_required_f32(object, "wall_contact_time")?,
wall_contact_frame: json_required_usize(object, "wall_contact_frame")?,
takeoff_time: json_required_f32(object, "takeoff_time")?,
takeoff_frame: json_required_usize(object, "takeoff_frame")?,
time_since_takeoff: json_required_f32(object, "time_since_takeoff")?,
wall_contact_position: json_required_vec3(object, "wall_contact_position")?,
takeoff_position: json_required_vec3(object, "takeoff_position")?,
player_position: json_required_vec3(object, "player_position")?,
ball_position: json_required_vec3(object, "ball_position")?,
ball_speed: json_optional_f32(object.get("ball_speed"))?,
goal_alignment: json_optional_f32(object.get("goal_alignment"))?,
confidence: json_required_f32(object, "confidence")?,
})
}
fn parse_center_event(value: &Value) -> SubtrActorResult<CenterEvent> {
let object = json_object(value, "center event")?;
Ok(CenterEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
start_time: json_required_f32(object, "start_time")?,
start_frame: json_required_usize(object, "start_frame")?,
duration: json_required_f32(object, "duration")?,
start_ball_position: json_required_vec3(object, "start_ball_position")?,
end_ball_position: json_required_vec3(object, "end_ball_position")?,
ball_travel_distance: json_required_f32(object, "ball_travel_distance")?,
ball_advance_distance: json_required_f32(object, "ball_advance_distance")?,
lateral_centering_distance: json_required_f32(object, "lateral_centering_distance")?,
})
}
fn parse_double_tap_event(value: &Value) -> SubtrActorResult<DoubleTapEvent> {
let object = json_object(value, "double tap event")?;
Ok(DoubleTapEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
backboard_time: json_required_f32(object, "backboard_time")?,
backboard_frame: json_required_usize(object, "backboard_frame")?,
})
}
fn parse_pass_event(value: &Value) -> SubtrActorResult<PassEvent> {
let object = json_object(value, "pass event")?;
let time = json_required_f32(object, "time")?;
let frame = json_required_usize(object, "frame")?;
Ok(PassEvent {
time,
frame,
sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
passer: json_required_remote_id(object, "passer")?,
receiver: json_required_remote_id(object, "receiver")?,
is_team_0: json_required_bool(object, "is_team_0")?,
start_time: json_required_f32(object, "start_time")?,
start_frame: json_required_usize(object, "start_frame")?,
duration: json_required_f32(object, "duration")?,
ball_travel_distance: json_required_f32(object, "ball_travel_distance")?,
ball_advance_distance: json_required_f32(object, "ball_advance_distance")?,
pass_kind: parse_pass_kind(object.get("pass_kind"))?,
})
}
fn parse_pass_last_completed_event(value: &Value) -> SubtrActorResult<PassLastCompletedEvent> {
let object = json_object(value, "pass last completed event")?;
Ok(PassLastCompletedEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
player: json_optional_remote_id(object.get("player"))?,
})
}
fn parse_pass_kind(value: Option<&Value>) -> SubtrActorResult<PassKind> {
let Some(value) = value else {
return Ok(PassKind::Direct);
};
let kind = value.as_str().ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
"Expected JSON field 'pass_kind' to be a string".to_owned(),
))
})?;
match kind {
"direct" => Ok(PassKind::Direct),
"backboard" => Ok(PassKind::Backboard),
"fifty_fifty" => Ok(PassKind::FiftyFifty),
"fifty_fifty_backboard" => Ok(PassKind::FiftyFiftyBackboard),
other => Err(SubtrActorError::new(
SubtrActorErrorVariant::StatsSerializationError(format!("Unknown pass kind '{other}'")),
)),
}
}
fn parse_ball_carry_event(value: &Value) -> SubtrActorResult<BallCarryEvent> {
let object = json_object(value, "ball carry event")?;
Ok(BallCarryEvent {
player_id: json_required_remote_id(object, "player_id")?,
is_team_0: json_required_bool(object, "is_team_0")?,
kind: parse_ball_carry_kind(json_required_str(object, "kind")?)?,
start_frame: json_required_usize(object, "start_frame")?,
end_frame: json_required_usize(object, "end_frame")?,
start_time: json_required_f32(object, "start_time")?,
end_time: json_required_f32(object, "end_time")?,
duration: json_required_f32(object, "duration")?,
straight_line_distance: json_required_f32(object, "straight_line_distance")?,
path_distance: json_required_f32(object, "path_distance")?,
average_horizontal_gap: json_required_f32(object, "average_horizontal_gap")?,
average_vertical_gap: json_required_f32(object, "average_vertical_gap")?,
average_speed: json_required_f32(object, "average_speed")?,
touch_count: json_required_usize(object, "touch_count")? as u32,
air_touch_count: json_required_usize(object, "air_touch_count")? as u32,
air_dribble_origin: parse_air_dribble_origin(object.get("air_dribble_origin"))?,
})
}
fn parse_ball_carry_kind(kind: &str) -> SubtrActorResult<BallCarryKind> {
match kind {
"carry" => Ok(BallCarryKind::Carry),
"air_dribble" => Ok(BallCarryKind::AirDribble),
other => Err(SubtrActorError::new(
SubtrActorErrorVariant::StatsSerializationError(format!(
"Unknown ball carry kind '{other}'"
)),
)),
}
}
fn parse_air_dribble_origin(value: Option<&Value>) -> SubtrActorResult<Option<AirDribbleOrigin>> {
let Some(value) = value else {
return Ok(None);
};
if value.is_null() {
return Ok(None);
}
let origin = value.as_str().ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
"Expected optional JSON field 'air_dribble_origin' to be a string".to_owned(),
))
})?;
match origin {
"ground_to_air" => Ok(Some(AirDribbleOrigin::GroundToAir)),
"wall_to_air" => Ok(Some(AirDribbleOrigin::WallToAir)),
other => Err(SubtrActorError::new(
SubtrActorErrorVariant::StatsSerializationError(format!(
"Unknown air dribble origin '{other}'"
)),
)),
}
}
fn parse_one_timer_event(value: &Value) -> SubtrActorResult<OneTimerEvent> {
let object = json_object(value, "one timer event")?;
Ok(OneTimerEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
player: json_required_remote_id(object, "player")?,
passer: json_required_remote_id(object, "passer")?,
is_team_0: json_required_bool(object, "is_team_0")?,
pass_start_time: json_required_f32(object, "pass_start_time")?,
pass_start_frame: json_required_usize(object, "pass_start_frame")?,
pass_duration: json_required_f32(object, "pass_duration")?,
pass_travel_distance: json_required_f32(object, "pass_travel_distance")?,
pass_advance_distance: json_required_f32(object, "pass_advance_distance")?,
ball_speed: json_required_f32(object, "ball_speed")?,
goal_alignment: json_required_f32(object, "goal_alignment")?,
})
}
fn parse_half_volley_event(value: &Value) -> SubtrActorResult<HalfVolleyEvent> {
let object = json_object(value, "half volley event")?;
let time = json_required_f32(object, "time")?;
let frame = json_required_usize(object, "frame")?;
Ok(HalfVolleyEvent {
time,
frame,
sample_time: json_optional_f32(object.get("sample_time"))?.unwrap_or(time),
sample_frame: json_optional_usize(object.get("sample_frame"))?.unwrap_or(frame),
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
bounce_time: json_required_f32(object, "bounce_time")?,
bounce_frame: json_required_usize(object, "bounce_frame")?,
bounce_to_touch_seconds: json_required_f32(object, "bounce_to_touch_seconds")?,
ball_speed: json_required_f32(object, "ball_speed")?,
goal_alignment: json_required_f32(object, "goal_alignment")?,
})
}
fn parse_goal_tag_event(value: &Value) -> SubtrActorResult<GoalTagEvent> {
let object = json_object(value, "goal tag event")?;
Ok(GoalTagEvent {
goal_index: json_required_usize(object, "goal_index")?,
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
kind: decode_json_value(json_required_value(object, "kind")?.clone())?,
scoring_team_is_team_0: json_required_bool(object, "scoring_team_is_team_0")?,
scorer: json_optional_remote_id(object.get("scorer"))?,
confidence: json_required_f32(object, "confidence")?,
modifiers: json_optional_array(object.get("modifiers"))?
.iter()
.map(|modifier| decode_json_value(modifier.clone()))
.collect::<SubtrActorResult<Vec<_>>>()?,
evidence: json_required_array(object, "evidence")?
.iter()
.map(parse_goal_tag_evidence)
.collect::<SubtrActorResult<Vec<_>>>()?,
})
}
fn parse_goal_tag_evidence(value: &Value) -> SubtrActorResult<GoalTagEvidence> {
let object = json_object(value, "goal tag evidence")?;
Ok(GoalTagEvidence {
kind: decode_json_value(json_required_value(object, "kind")?.clone())?,
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
player: json_optional_remote_id(object.get("player"))?,
})
}
fn parse_fifty_fifty_event(value: &Value) -> SubtrActorResult<FiftyFiftyEvent> {
let object = json_object(value, "fifty fifty event")?;
Ok(FiftyFiftyEvent {
start_time: json_required_f32(object, "start_time")?,
start_frame: json_required_usize(object, "start_frame")?,
resolve_time: json_required_f32(object, "resolve_time")?,
resolve_frame: json_required_usize(object, "resolve_frame")?,
is_kickoff: json_required_bool(object, "is_kickoff")?,
team_zero_player: json_optional_remote_id(object.get("team_zero_player"))?,
team_one_player: json_optional_remote_id(object.get("team_one_player"))?,
team_zero_position: json_required_vec3(object, "team_zero_position")?,
team_one_position: json_required_vec3(object, "team_one_position")?,
midpoint: json_required_vec3(object, "midpoint")?,
plane_normal: json_required_vec3(object, "plane_normal")?,
winning_team_is_team_0: json_optional_bool(object.get("winning_team_is_team_0")),
possession_team_is_team_0: json_optional_bool(object.get("possession_team_is_team_0")),
})
}
fn parse_speed_flip_event(value: &Value) -> SubtrActorResult<SpeedFlipEvent> {
let object = json_object(value, "speed flip event")?;
let time = json_required_f32(object, "time")?;
let frame = json_required_usize(object, "frame")?;
Ok(SpeedFlipEvent {
time,
frame,
resolved_time: json_optional_f32(object.get("resolved_time"))?.unwrap_or(time),
resolved_frame: json_optional_usize(object.get("resolved_frame"))?.unwrap_or(frame),
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
time_since_kickoff_start: json_required_f32(object, "time_since_kickoff_start")?,
start_position: json_required_vec3(object, "start_position")?,
end_position: json_required_vec3(object, "end_position")?,
start_speed: json_required_f32(object, "start_speed")?,
max_speed: json_required_f32(object, "max_speed")?,
best_alignment: json_required_f32(object, "best_alignment")?,
diagonal_score: json_required_f32(object, "diagonal_score")?,
cancel_score: json_required_f32(object, "cancel_score")?,
speed_score: json_required_f32(object, "speed_score")?,
confidence: json_required_f32(object, "confidence")?,
})
}
fn parse_half_flip_event(value: &Value) -> SubtrActorResult<HalfFlipEvent> {
let object = json_object(value, "half flip event")?;
Ok(HalfFlipEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
start_position: json_required_vec3(object, "start_position")?,
end_position: json_required_vec3(object, "end_position")?,
start_speed: json_required_f32(object, "start_speed")?,
end_speed: json_required_f32(object, "end_speed")?,
start_backward_alignment: json_required_f32(object, "start_backward_alignment")?,
best_reorientation_alignment: json_required_f32(object, "best_reorientation_alignment")?,
best_forward_reversal: json_required_f32(object, "best_forward_reversal")?,
max_forward_vertical: json_required_f32(object, "max_forward_vertical")?,
confidence: json_required_f32(object, "confidence")?,
})
}
fn parse_wavedash_event(value: &Value) -> SubtrActorResult<WavedashEvent> {
let object = json_object(value, "wavedash event")?;
Ok(WavedashEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
dodge_time: json_required_f32(object, "dodge_time")?,
dodge_frame: json_required_usize(object, "dodge_frame")?,
time_since_dodge: json_required_f32(object, "time_since_dodge")?,
dodge_position: json_required_vec3(object, "dodge_position")?,
landing_position: json_required_vec3(object, "landing_position")?,
start_speed: json_required_f32(object, "start_speed")?,
landing_speed: json_required_f32(object, "landing_speed")?,
horizontal_speed_gain: json_required_f32(object, "horizontal_speed_gain")?,
landing_uprightness: json_required_f32(object, "landing_uprightness")?,
confidence: json_required_f32(object, "confidence")?,
})
}
fn parse_whiff_event(value: &Value) -> SubtrActorResult<WhiffEvent> {
let object = json_object(value, "whiff event")?;
let time = json_required_f32(object, "time")?;
let frame = json_required_usize(object, "frame")?;
Ok(WhiffEvent {
kind: match object.get("kind").and_then(Value::as_str) {
None | Some("whiff") => WhiffEventKind::Whiff,
Some("beaten_to_ball") => WhiffEventKind::BeatenToBall,
Some(kind) => {
return SubtrActorError::new_result(
SubtrActorErrorVariant::StatsSerializationError(format!(
"Unknown whiff event kind '{kind}'"
)),
);
}
},
time,
frame,
resolved_time: json_optional_f32(object.get("resolved_time"))?.unwrap_or(time),
resolved_frame: json_optional_usize(object.get("resolved_frame"))?.unwrap_or(frame),
player: json_required_remote_id(object, "player")?,
is_team_0: json_required_bool(object, "is_team_0")?,
closest_approach_distance: json_required_f32(object, "closest_approach_distance")?,
forward_alignment: json_required_f32(object, "forward_alignment")?,
approach_speed: json_required_f32(object, "approach_speed")?,
dodge_active: json_required_bool(object, "dodge_active")?,
aerial: json_required_bool(object, "aerial")?,
})
}
fn parse_bump_event(value: &Value) -> SubtrActorResult<BumpEvent> {
let object = json_object(value, "bump event")?;
Ok(BumpEvent {
time: json_required_f32(object, "time")?,
frame: json_required_usize(object, "frame")?,
initiator: json_required_remote_id(object, "initiator")?,
victim: json_required_remote_id(object, "victim")?,
initiator_is_team_0: json_required_bool(object, "initiator_is_team_0")?,
victim_is_team_0: json_required_bool(object, "victim_is_team_0")?,
is_team_bump: json_required_bool(object, "is_team_bump")?,
strength: json_required_f32(object, "strength")?,
confidence: json_required_f32(object, "confidence")?,
contact_distance: json_required_f32(object, "contact_distance")?,
closing_speed: json_required_f32(object, "closing_speed")?,
victim_impulse: json_required_f32(object, "victim_impulse")?,
initiator_position: json_required_vec3(object, "initiator_position")?,
victim_position: json_required_vec3(object, "victim_position")?,
})
}
fn parse_boost_pickup_comparison_event(
value: &Value,
) -> SubtrActorResult<BoostPickupComparisonEvent> {
let object = json_object(value, "boost pickup comparison event")?;
Ok(BoostPickupComparisonEvent {
comparison: decode_json_value(json_required_value(object, "comparison")?.clone())?,
frame: json_required_usize(object, "frame")?,
time: json_required_f32(object, "time")?,
player_id: json_required_remote_id(object, "player_id")?,
is_team_0: json_required_bool(object, "is_team_0")?,
pad_type: decode_json_value(json_required_value(object, "pad_type")?.clone())?,
field_half: decode_json_value(json_required_value(object, "field_half")?.clone())?,
activity: decode_json_value(json_required_value(object, "activity")?.clone())?,
reported_frame: json_optional_usize(object.get("reported_frame"))?,
reported_time: json_optional_f32(object.get("reported_time"))?,
inferred_frame: json_optional_usize(object.get("inferred_frame"))?,
inferred_time: json_optional_f32(object.get("inferred_time"))?,
boost_before: json_optional_f32(object.get("boost_before"))?,
boost_after: json_optional_f32(object.get("boost_after"))?,
})
}
fn parse_boost_ledger_event(value: &Value) -> SubtrActorResult<BoostLedgerEvent> {
let object = json_object(value, "boost ledger event")?;
Ok(BoostLedgerEvent {
frame: json_required_usize(object, "frame")?,
time: json_required_f32(object, "time")?,
player_id: json_required_remote_id(object, "player_id")?,
is_team_0: json_required_bool(object, "is_team_0")?,
transaction: decode_json_value(json_required_value(object, "transaction")?.clone())?,
amount: json_required_f32(object, "amount")?,
count: json_required_usize(object, "count")? as u32,
labels: decode_json_value(
object
.get("labels")
.cloned()
.unwrap_or_else(|| Value::Array(Vec::new())),
)?,
boost_before: json_optional_f32(object.get("boost_before"))?,
boost_after: json_optional_f32(object.get("boost_after"))?,
})
}
fn parse_boost_state_event(value: &Value) -> SubtrActorResult<BoostStateEvent> {
let object = json_object(value, "boost state event")?;
Ok(BoostStateEvent {
frame: json_required_usize(object, "frame")?,
time: json_required_f32(object, "time")?,
player_id: json_required_remote_id(object, "player_id")?,
is_team_0: json_required_bool(object, "is_team_0")?,
boost_amount: json_required_f32(object, "boost_amount")?,
boost_before: json_optional_f32(object.get("boost_before"))?,
})
}
fn json_object<'a>(
value: &'a Value,
context: &str,
) -> SubtrActorResult<&'a serde_json::Map<String, Value>> {
value.as_object().ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
"Expected {context} to be a JSON object"
)))
})
}
fn json_required_value<'a>(
object: &'a serde_json::Map<String, Value>,
field: &str,
) -> SubtrActorResult<&'a Value> {
object.get(field).ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
"Missing JSON field '{field}'"
)))
})
}
fn json_required_array<'a>(
object: &'a serde_json::Map<String, Value>,
field: &str,
) -> SubtrActorResult<&'a Vec<Value>> {
json_required_value(object, field)?
.as_array()
.ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
"Expected JSON field '{field}' to be an array"
)))
})
}
fn json_optional_array(value: Option<&Value>) -> SubtrActorResult<&[Value]> {
match value {
Some(Value::Array(values)) => Ok(values),
Some(_) => SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
"Expected optional JSON value to be an array".to_owned(),
)),
None => Ok(&[]),
}
}
fn json_f32(value: &Value) -> Option<f32> {
value.as_f64().map(|number| number as f32)
}
fn json_config_f32(
config: Option<&Map<String, Value>>,
key: &str,
legacy_key: &str,
) -> Option<f32> {
config.and_then(|config| {
config
.get(key)
.or_else(|| config.get(legacy_key))
.and_then(json_f32)
})
}
fn json_required_f32(
object: &serde_json::Map<String, Value>,
field: &str,
) -> SubtrActorResult<f32> {
json_f32(json_required_value(object, field)?).ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
"Expected JSON field '{field}' to be a float"
)))
})
}
fn json_required_usize(
object: &serde_json::Map<String, Value>,
field: &str,
) -> SubtrActorResult<usize> {
json_required_value(object, field)?
.as_u64()
.map(|number| number as usize)
.ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
"Expected JSON field '{field}' to be an unsigned integer"
)))
})
}
fn json_required_i32(
object: &serde_json::Map<String, Value>,
field: &str,
) -> SubtrActorResult<i32> {
json_required_value(object, field)?
.as_i64()
.map(|number| number as i32)
.ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
"Expected JSON field '{field}' to be a signed integer"
)))
})
}
fn json_required_bool(
object: &serde_json::Map<String, Value>,
field: &str,
) -> SubtrActorResult<bool> {
json_required_value(object, field)?
.as_bool()
.ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
"Expected JSON field '{field}' to be a bool"
)))
})
}
fn json_required_str<'a>(
object: &'a serde_json::Map<String, Value>,
field: &str,
) -> SubtrActorResult<&'a str> {
json_required_value(object, field)?.as_str().ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
"Expected JSON field '{field}' to be a string"
)))
})
}
fn json_optional_bool(value: Option<&Value>) -> Option<bool> {
value.and_then(Value::as_bool)
}
fn json_optional_f32(value: Option<&Value>) -> SubtrActorResult<Option<f32>> {
match value {
None | Some(Value::Null) => Ok(None),
Some(value) => json_f32(value).map(Some).ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
"Expected optional JSON value to be a float".to_owned(),
))
}),
}
}
fn json_optional_usize(value: Option<&Value>) -> SubtrActorResult<Option<usize>> {
match value {
None | Some(Value::Null) => Ok(None),
Some(value) => value
.as_u64()
.map(|number| Some(number as usize))
.ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
"Expected optional JSON value to be an unsigned integer".to_owned(),
))
}),
}
}
fn json_goal_context_position(value: &Value) -> SubtrActorResult<GoalContextPosition> {
let object = json_object(value, "goal context position")?;
Ok(GoalContextPosition {
x: json_required_f32(object, "x")?,
y: json_required_f32(object, "y")?,
z: json_required_f32(object, "z")?,
})
}
fn json_optional_goal_context_position(
value: Option<&Value>,
) -> SubtrActorResult<Option<GoalContextPosition>> {
match value {
None | Some(Value::Null) => Ok(None),
Some(value) => json_goal_context_position(value).map(Some),
}
}
fn json_required_vec3(
object: &serde_json::Map<String, Value>,
field: &str,
) -> SubtrActorResult<[f32; 3]> {
let array = json_required_value(object, field)?
.as_array()
.ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
"Expected JSON field '{field}' to be a 3-element array"
)))
})?;
if array.len() != 3 {
return SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
format!("Expected JSON field '{field}' to contain exactly 3 elements"),
));
}
Ok([
json_f32(&array[0]).ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
"Expected JSON field '{field}[0]' to be a float"
)))
})?,
json_f32(&array[1]).ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
"Expected JSON field '{field}[1]' to be a float"
)))
})?,
json_f32(&array[2]).ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(format!(
"Expected JSON field '{field}[2]' to be a float"
)))
})?,
])
}
fn json_required_remote_id(
object: &serde_json::Map<String, Value>,
field: &str,
) -> SubtrActorResult<PlayerId> {
json_remote_id(json_required_value(object, field)?)
}
fn json_optional_remote_id(value: Option<&Value>) -> SubtrActorResult<Option<PlayerId>> {
match value {
None | Some(Value::Null) => Ok(None),
Some(value) => Ok(Some(json_remote_id(value)?)),
}
}
fn json_remote_id(value: &Value) -> SubtrActorResult<PlayerId> {
let object = json_object(value, "remote id")?;
if object.len() != 1 {
return SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
"Expected remote id to contain exactly one variant".to_owned(),
));
}
let (variant, payload) = object.iter().next().expect("validated single variant");
match variant.as_str() {
"PlayStation" => {
let payload = json_object(payload, "playstation remote id")?;
Ok(RemoteId::PlayStation(Ps4Id {
online_id: json_u64(json_required_value(payload, "online_id")?)?,
name: json_required_value(payload, "name")?
.as_str()
.ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
"Expected PlayStation name to be a string".to_owned(),
))
})?
.to_owned(),
unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
}))
}
"PsyNet" => {
let payload = json_object(payload, "psynet remote id")?;
Ok(RemoteId::PsyNet(PsyNetId {
online_id: json_u64(json_required_value(payload, "online_id")?)?,
unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
}))
}
"SplitScreen" => Ok(RemoteId::SplitScreen(json_u64(payload)? as u32)),
"Steam" => Ok(RemoteId::Steam(json_u64(payload)?)),
"Switch" => {
let payload = json_object(payload, "switch remote id")?;
Ok(RemoteId::Switch(SwitchId {
online_id: json_u64(json_required_value(payload, "online_id")?)?,
unknown1: json_u8_vec(json_required_value(payload, "unknown1")?)?,
}))
}
"Xbox" => Ok(RemoteId::Xbox(json_u64(payload)?)),
"QQ" => Ok(RemoteId::QQ(json_u64(payload)?)),
"Epic" => Ok(RemoteId::Epic(
payload
.as_str()
.ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
"Expected Epic remote id payload to be a string".to_owned(),
))
})?
.to_owned(),
)),
variant => SubtrActorError::new_result(SubtrActorErrorVariant::StatsSerializationError(
format!("Unknown remote id variant '{variant}'"),
)),
}
}
fn json_u64(value: &Value) -> SubtrActorResult<u64> {
value
.as_u64()
.or_else(|| value.as_str().and_then(|text| text.parse().ok()))
.ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
"Expected JSON value to be a u64".to_owned(),
))
})
}
fn json_u8_vec(value: &Value) -> SubtrActorResult<Vec<u8>> {
value
.as_array()
.ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
"Expected JSON value to be an array of bytes".to_owned(),
))
})?
.iter()
.map(|entry| {
entry
.as_u64()
.and_then(|number| u8::try_from(number).ok())
.ok_or_else(|| {
SubtrActorError::new(SubtrActorErrorVariant::StatsSerializationError(
"Expected JSON array entry to be a byte".to_owned(),
))
})
})
.collect()
}