subtr-actor 0.12.0

Rocket League replay transformer
Documentation
#![allow(dead_code)]

use std::collections::HashSet;

use serde::Serialize;

use crate::Collector;
use crate::{SubtrActorError, SubtrActorErrorVariant, SubtrActorResult};

pub mod graph;
pub use graph::{
    AnalysisDependency, AnalysisGraph, AnalysisNode, AnalysisNodeDyn, AnalysisStateContext,
    AnalysisStateRef,
};

#[macro_use]
mod node_macros;

mod collector;
mod nodes;

use crate::stats::calculators::FrameInput;

#[allow(unused_imports)]
pub use collector::AnalysisNodeCollector;
#[allow(unused_imports)]
pub use nodes::*;

pub const BUILTIN_ANALYSIS_NODE_NAMES: &[&str] = &[
    "core",
    "frame_info",
    "gameplay_state",
    "ball_frame_state",
    "player_frame_state",
    "frame_events_state",
    "live_play",
    "match_stats",
    "backboard",
    "backboard_bounce_state",
    "ceiling_shot",
    "center",
    "continuous_ball_control",
    "double_tap",
    "fifty_fifty",
    "fifty_fifty_state",
    "possession",
    "possession_state",
    "pressure",
    "territorial_pressure",
    "rotation",
    "rush",
    "touch",
    "touch_state",
    "wall_aerial",
    "wall_aerial_shot",
    "whiff",
    "wavedash",
    "speed_flip",
    "half_flip",
    "half_volley",
    "flick",
    "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",
    "musty_flick",
    "one_timer",
    "pass",
    "dodge_reset",
    "ball_carry",
    "air_dribble",
    "boost",
    "bump",
    "movement",
    "positioning",
    "powerslide",
    "player_vertical_state",
    "demo",
    "settings",
    "stats_timeline_frame",
    "stats_timeline_events",
];

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct BuiltinAnalysisNodeAlias {
    pub alias: &'static str,
    pub node_name: &'static str,
}

pub const BUILTIN_ANALYSIS_NODE_ALIASES: &[BuiltinAnalysisNodeAlias] = &[
    BuiltinAnalysisNodeAlias {
        alias: "core",
        node_name: "match_stats",
    },
    BuiltinAnalysisNodeAlias {
        alias: "air_dribble",
        node_name: "ball_carry",
    },
];

pub fn builtin_analysis_node_names() -> &'static [&'static str] {
    BUILTIN_ANALYSIS_NODE_NAMES
}

pub fn builtin_analysis_node_aliases() -> &'static [BuiltinAnalysisNodeAlias] {
    BUILTIN_ANALYSIS_NODE_ALIASES
}

fn canonical_builtin_analysis_node_name(name: &str) -> Option<&'static str> {
    builtin_analysis_node_aliases()
        .iter()
        .find_map(|alias| (alias.alias == name).then_some(alias.node_name))
        .or_else(|| {
            builtin_analysis_node_names()
                .iter()
                .copied()
                .find(|candidate| *candidate == name)
        })
}

pub(crate) fn boxed_analysis_node_by_name(name: &str) -> Option<Box<dyn AnalysisNodeDyn>> {
    match name {
        "core" => Some(nodes::match_stats::boxed_default()),
        "frame_info" => Some(nodes::frame_info::boxed_default()),
        "gameplay_state" => Some(nodes::gameplay_state::boxed_default()),
        "ball_frame_state" => Some(nodes::ball_frame_state::boxed_default()),
        "player_frame_state" => Some(nodes::player_frame_state::boxed_default()),
        "frame_events_state" => Some(nodes::frame_events_state::boxed_default()),
        "live_play" => Some(nodes::live_play::boxed_default()),
        "match_stats" => Some(nodes::match_stats::boxed_default()),
        "backboard" => Some(nodes::backboard::boxed_default()),
        "backboard_bounce_state" => Some(nodes::backboard_bounce::boxed_default()),
        "ceiling_shot" => Some(nodes::ceiling_shot::boxed_default()),
        "center" => Some(nodes::center::boxed_default()),
        "continuous_ball_control" => Some(nodes::continuous_ball_control::boxed_default()),
        "double_tap" => Some(nodes::double_tap::boxed_default()),
        "fifty_fifty" => Some(nodes::fifty_fifty::boxed_default()),
        "fifty_fifty_state" => Some(nodes::fifty_fifty_state::boxed_default()),
        "possession" => Some(nodes::possession::boxed_default()),
        "possession_state" => Some(nodes::possession_state::boxed_default()),
        "pressure" => Some(nodes::pressure::boxed_default()),
        "territorial_pressure" => Some(nodes::territorial_pressure::boxed_default()),
        "rotation" => Some(nodes::rotation::boxed_default()),
        "rush" => Some(nodes::rush::boxed_default()),
        "touch" => Some(nodes::touch::boxed_default()),
        "touch_state" => Some(nodes::touch_state::boxed_default()),
        "wall_aerial" => Some(nodes::wall_aerial::boxed_default()),
        "wall_aerial_shot" => Some(nodes::wall_aerial_shot::boxed_default()),
        "whiff" => Some(nodes::whiff::boxed_default()),
        "wavedash" => Some(nodes::wavedash::boxed_default()),
        "speed_flip" => Some(nodes::speed_flip::boxed_default()),
        "half_flip" => Some(nodes::half_flip::boxed_default()),
        "half_volley" => Some(nodes::half_volley::boxed_default()),
        "flick" => Some(nodes::flick::boxed_default()),
        "aerial_goal" => Some(nodes::goal_tags::boxed_aerial_goal()),
        "high_aerial_goal" => Some(nodes::goal_tags::boxed_high_aerial_goal()),
        "long_distance_goal" => Some(nodes::goal_tags::boxed_long_distance_goal()),
        "own_half_goal" => Some(nodes::goal_tags::boxed_own_half_goal()),
        "empty_net_goal" => Some(nodes::goal_tags::boxed_empty_net_goal()),
        "counter_attack_goal" => Some(nodes::goal_tags::boxed_counter_attack_goal()),
        "flick_goal" => Some(nodes::goal_tags::boxed_flick_goal()),
        "double_tap_goal" => Some(nodes::goal_tags::boxed_double_tap_goal()),
        "one_timer_goal" => Some(nodes::goal_tags::boxed_one_timer_goal()),
        "passing_goal" => Some(nodes::goal_tags::boxed_passing_goal()),
        "air_dribble_goal" => Some(nodes::goal_tags::boxed_air_dribble_goal()),
        "flip_reset_goal" => Some(nodes::goal_tags::boxed_flip_reset_goal()),
        "half_volley_goal" => Some(nodes::goal_tags::boxed_half_volley_goal()),
        "musty_flick" => Some(nodes::musty_flick::boxed_default()),
        "one_timer" => Some(nodes::one_timer::boxed_default()),
        "pass" => Some(nodes::pass::boxed_default()),
        "dodge_reset" => Some(nodes::dodge_reset::boxed_default()),
        "ball_carry" => Some(nodes::ball_carry::boxed_default()),
        "boost" => Some(nodes::boost::boxed_default()),
        "bump" => Some(nodes::bump::boxed_default()),
        "movement" => Some(nodes::movement::boxed_default()),
        "positioning" => Some(nodes::positioning::boxed_default()),
        "powerslide" => Some(nodes::powerslide::boxed_default()),
        "player_vertical_state" => Some(nodes::player_vertical_state::boxed_default()),
        "demo" => Some(nodes::demo::boxed_default()),
        "settings" => Some(nodes::settings::boxed_default()),
        "stats_timeline_frame" => Some(nodes::stats_timeline_frame::boxed_default()),
        "stats_timeline_events" => Some(nodes::stats_timeline_events::boxed_default()),
        _ => None,
    }
}

pub fn graph_with_builtin_analysis_nodes<I, S>(names: I) -> SubtrActorResult<AnalysisGraph>
where
    I: IntoIterator<Item = S>,
    S: AsRef<str>,
{
    let mut graph = AnalysisGraph::new().with_input_state_type::<FrameInput>();
    let mut seen = HashSet::new();
    for name in names {
        let name = name.as_ref();
        let canonical_name = canonical_builtin_analysis_node_name(name).ok_or_else(|| {
            SubtrActorError::new(SubtrActorErrorVariant::UnknownStatsModuleName(
                name.to_owned(),
            ))
        })?;
        if !seen.insert(canonical_name) {
            continue;
        }
        graph.push_boxed_node(boxed_analysis_node_by_name(canonical_name).ok_or_else(|| {
            SubtrActorError::new(SubtrActorErrorVariant::UnknownStatsModuleName(
                name.to_owned(),
            ))
        })?);
    }
    Ok(graph)
}

pub fn collect_analysis_graph_for_replay(
    replay: &boxcars::Replay,
    graph: AnalysisGraph,
) -> SubtrActorResult<AnalysisGraph> {
    let collector = collector::AnalysisNodeCollector::new(graph).process_replay(replay)?;
    Ok(collector.into_graph())
}

pub fn collect_builtin_analysis_graph_for_replay<I, S>(
    replay: &boxcars::Replay,
    names: I,
) -> SubtrActorResult<AnalysisGraph>
where
    I: IntoIterator<Item = S>,
    S: AsRef<str>,
{
    collect_analysis_graph_for_replay(replay, graph_with_builtin_analysis_nodes(names)?)
}

pub fn all_analysis_nodes() -> Vec<Box<dyn AnalysisNodeDyn>> {
    vec![
        nodes::backboard::boxed_default(),
        nodes::ball_carry::boxed_default(),
        nodes::boost::boxed_default(),
        nodes::bump::boxed_default(),
        nodes::ceiling_shot::boxed_default(),
        nodes::center::boxed_default(),
        nodes::continuous_ball_control::boxed_default(),
        nodes::demo::boxed_default(),
        nodes::dodge_reset::boxed_default(),
        nodes::double_tap::boxed_default(),
        nodes::fifty_fifty::boxed_default(),
        nodes::match_stats::boxed_default(),
        nodes::movement::boxed_default(),
        nodes::flick::boxed_default(),
        nodes::goal_tags::boxed_aerial_goal(),
        nodes::goal_tags::boxed_high_aerial_goal(),
        nodes::goal_tags::boxed_long_distance_goal(),
        nodes::goal_tags::boxed_own_half_goal(),
        nodes::goal_tags::boxed_empty_net_goal(),
        nodes::goal_tags::boxed_counter_attack_goal(),
        nodes::goal_tags::boxed_flick_goal(),
        nodes::goal_tags::boxed_double_tap_goal(),
        nodes::goal_tags::boxed_one_timer_goal(),
        nodes::goal_tags::boxed_passing_goal(),
        nodes::goal_tags::boxed_air_dribble_goal(),
        nodes::goal_tags::boxed_flip_reset_goal(),
        nodes::goal_tags::boxed_half_volley_goal(),
        nodes::musty_flick::boxed_default(),
        nodes::one_timer::boxed_default(),
        nodes::pass::boxed_default(),
        nodes::positioning::boxed_default(),
        nodes::possession::boxed_default(),
        nodes::powerslide::boxed_default(),
        nodes::pressure::boxed_default(),
        nodes::territorial_pressure::boxed_default(),
        nodes::rotation::boxed_default(),
        nodes::rush::boxed_default(),
        nodes::settings::boxed_default(),
        nodes::speed_flip::boxed_default(),
        nodes::half_flip::boxed_default(),
        nodes::half_volley::boxed_default(),
        nodes::wavedash::boxed_default(),
        nodes::touch::boxed_default(),
        nodes::wall_aerial::boxed_default(),
        nodes::wall_aerial_shot::boxed_default(),
        nodes::whiff::boxed_default(),
        nodes::stats_timeline_frame::boxed_default(),
        nodes::stats_timeline_events::boxed_default(),
    ]
}

pub fn graph_with_all_analysis_nodes() -> AnalysisGraph {
    let mut graph = AnalysisGraph::new().with_input_state_type::<FrameInput>();
    for node in all_analysis_nodes() {
        graph.push_boxed_node(node);
    }
    graph
}

#[cfg(test)]
#[path = "module_tests.rs"]
mod tests;