Skip to main content

subtr_actor/stats/analysis_graph/
mod.rs

1use std::collections::HashSet;
2
3use serde::Serialize;
4
5use crate::Collector;
6use crate::{SubtrActorError, SubtrActorErrorVariant, SubtrActorResult};
7
8pub mod graph;
9pub use graph::{
10    AnalysisDependency, AnalysisGraph, AnalysisNode, AnalysisNodeDyn, AnalysisStateContext,
11    AnalysisStateRef,
12};
13
14#[macro_use]
15mod node_macros;
16
17mod collector;
18mod nodes;
19
20use crate::stats::calculators::FrameInput;
21#[allow(unused_imports)]
22pub use collector::AnalysisNodeCollector;
23#[allow(unused_imports)]
24pub use nodes::*;
25
26pub const BUILTIN_ANALYSIS_NODE_NAMES: &[&str] = &[
27    "core",
28    "frame_info",
29    "gameplay_state",
30    "ball_frame_state",
31    "player_frame_state",
32    "frame_events_state",
33    "live_play",
34    "match_stats",
35    "backboard",
36    "backboard_bounce_state",
37    "ceiling_shot",
38    "center",
39    "controlled_play",
40    "continuous_ball_control",
41    "double_tap",
42    "fifty_fifty",
43    "fifty_fifty_state",
44    "kickoff",
45    "player_possession",
46    "possession",
47    "possession_state",
48    "ball_half",
49    "territorial_pressure",
50    "rotation",
51    "rush",
52    "touch",
53    "touch_state",
54    "wall_aerial",
55    "wall_aerial_shot",
56    "whiff",
57    "wavedash",
58    "dodge",
59    "speed_flip",
60    "half_flip",
61    "half_volley",
62    "flick",
63    "aerial_goal",
64    "high_aerial_goal",
65    "long_distance_goal",
66    "own_half_goal",
67    "empty_net_goal",
68    "counter_attack_goal",
69    "sustained_pressure_goal",
70    "kickoff_goal",
71    "flick_goal",
72    "ceiling_shot_goal",
73    "double_tap_goal",
74    "one_timer_goal",
75    "passing_goal",
76    "air_dribble_goal",
77    "flip_reset_goal",
78    "flip_into_ball_goal",
79    "bump_goal",
80    "demo_goal",
81    "half_volley_goal",
82    "musty_flick",
83    "one_timer",
84    "pass",
85    "dodge_reset",
86    "ball_carry",
87    "air_dribble",
88    "boost",
89    "bump",
90    "movement",
91    "positioning",
92    "powerslide",
93    "player_vertical_state",
94    "demo",
95    "settings",
96    "stats_projection",
97    "stats_timeline_frame",
98    "stats_timeline_events",
99];
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
102pub struct BuiltinAnalysisNodeAlias {
103    pub alias: &'static str,
104    pub node_name: &'static str,
105}
106
107pub const BUILTIN_ANALYSIS_NODE_ALIASES: &[BuiltinAnalysisNodeAlias] = &[
108    BuiltinAnalysisNodeAlias {
109        alias: "core",
110        node_name: "match_stats",
111    },
112    BuiltinAnalysisNodeAlias {
113        alias: "air_dribble",
114        node_name: "ball_carry",
115    },
116    BuiltinAnalysisNodeAlias {
117        alias: "flip_impulse",
118        node_name: "dodge",
119    },
120];
121
122pub fn builtin_analysis_node_names() -> &'static [&'static str] {
123    BUILTIN_ANALYSIS_NODE_NAMES
124}
125
126pub fn builtin_analysis_node_aliases() -> &'static [BuiltinAnalysisNodeAlias] {
127    BUILTIN_ANALYSIS_NODE_ALIASES
128}
129
130pub(crate) fn canonical_builtin_analysis_node_name(name: &str) -> Option<&'static str> {
131    builtin_analysis_node_aliases()
132        .iter()
133        .find_map(|alias| (alias.alias == name).then_some(alias.node_name))
134        .or_else(|| {
135            builtin_analysis_node_names()
136                .iter()
137                .copied()
138                .find(|candidate| *candidate == name)
139        })
140}
141
142pub(crate) fn boxed_analysis_node_by_name(name: &str) -> Option<Box<dyn AnalysisNodeDyn>> {
143    match name {
144        "core" => Some(nodes::match_stats::boxed_default()),
145        "frame_info" => Some(nodes::frame_info::boxed_default()),
146        "gameplay_state" => Some(nodes::gameplay_state::boxed_default()),
147        "ball_frame_state" => Some(nodes::ball_frame_state::boxed_default()),
148        "player_frame_state" => Some(nodes::player_frame_state::boxed_default()),
149        "frame_events_state" => Some(nodes::frame_events_state::boxed_default()),
150        "live_play" => Some(nodes::live_play::boxed_default()),
151        "match_stats" => Some(nodes::match_stats::boxed_default()),
152        "backboard" => Some(nodes::backboard::boxed_default()),
153        "backboard_bounce_state" => Some(nodes::backboard_bounce::boxed_default()),
154        "ceiling_shot" => Some(nodes::ceiling_shot::boxed_default()),
155        "center" => Some(nodes::center::boxed_default()),
156        "controlled_play" => Some(nodes::controlled_play::boxed_default()),
157        "continuous_ball_control" => Some(nodes::continuous_ball_control::boxed_default()),
158        "double_tap" => Some(nodes::double_tap::boxed_default()),
159        "fifty_fifty" => Some(nodes::fifty_fifty::boxed_default()),
160        "fifty_fifty_state" => Some(nodes::fifty_fifty_state::boxed_default()),
161        "kickoff" => Some(nodes::kickoff::boxed_default()),
162        "player_possession" => Some(nodes::player_possession::boxed_default()),
163        "possession" => Some(nodes::possession::boxed_default()),
164        "possession_state" => Some(nodes::possession_state::boxed_default()),
165        "ball_half" => Some(nodes::ball_half::boxed_default()),
166        "territorial_pressure" => Some(nodes::territorial_pressure::boxed_default()),
167        "rotation" => Some(nodes::rotation::boxed_default()),
168        "rush" => Some(nodes::rush::boxed_default()),
169        "touch" => Some(nodes::touch::boxed_default()),
170        "touch_state" => Some(nodes::touch_state::boxed_default()),
171        "wall_aerial" => Some(nodes::wall_aerial::boxed_default()),
172        "wall_aerial_shot" => Some(nodes::wall_aerial_shot::boxed_default()),
173        "whiff" => Some(nodes::whiff::boxed_default()),
174        "wavedash" => Some(nodes::wavedash::boxed_default()),
175        "speed_flip" => Some(nodes::speed_flip::boxed_default()),
176        "dodge" => Some(nodes::flip_impulse::boxed_default()),
177        "half_flip" => Some(nodes::half_flip::boxed_default()),
178        "half_volley" => Some(nodes::half_volley::boxed_default()),
179        "flick" => Some(nodes::flick::boxed_default()),
180        "aerial_goal" => Some(nodes::goal_tags::boxed_aerial_goal()),
181        "high_aerial_goal" => Some(nodes::goal_tags::boxed_high_aerial_goal()),
182        "long_distance_goal" => Some(nodes::goal_tags::boxed_long_distance_goal()),
183        "own_half_goal" => Some(nodes::goal_tags::boxed_own_half_goal()),
184        "empty_net_goal" => Some(nodes::goal_tags::boxed_empty_net_goal()),
185        "counter_attack_goal" => Some(nodes::goal_tags::boxed_counter_attack_goal()),
186        "sustained_pressure_goal" => Some(nodes::goal_tags::boxed_sustained_pressure_goal()),
187        "kickoff_goal" => Some(nodes::goal_tags::boxed_kickoff_goal()),
188        "flick_goal" => Some(nodes::goal_tags::boxed_flick_goal()),
189        "ceiling_shot_goal" => Some(nodes::goal_tags::boxed_ceiling_shot_goal()),
190        "double_tap_goal" => Some(nodes::goal_tags::boxed_double_tap_goal()),
191        "one_timer_goal" => Some(nodes::goal_tags::boxed_one_timer_goal()),
192        "passing_goal" => Some(nodes::goal_tags::boxed_passing_goal()),
193        "air_dribble_goal" => Some(nodes::goal_tags::boxed_air_dribble_goal()),
194        "flip_reset_goal" => Some(nodes::goal_tags::boxed_flip_reset_goal()),
195        "flip_into_ball_goal" => Some(nodes::goal_tags::boxed_flip_into_ball_goal()),
196        "bump_goal" => Some(nodes::goal_tags::boxed_bump_goal()),
197        "demo_goal" => Some(nodes::goal_tags::boxed_demo_goal()),
198        "half_volley_goal" => Some(nodes::goal_tags::boxed_half_volley_goal()),
199        "musty_flick" => Some(nodes::musty_flick::boxed_default()),
200        "one_timer" => Some(nodes::one_timer::boxed_default()),
201        "pass" => Some(nodes::pass::boxed_default()),
202        "dodge_reset" => Some(nodes::dodge_reset::boxed_default()),
203        "ball_carry" => Some(nodes::ball_carry::boxed_default()),
204        "boost" => Some(nodes::boost::boxed_default()),
205        "bump" => Some(nodes::bump::boxed_default()),
206        "movement" => Some(nodes::movement::boxed_default()),
207        "positioning" => Some(nodes::positioning::boxed_default()),
208        "powerslide" => Some(nodes::powerslide::boxed_default()),
209        "player_vertical_state" => Some(nodes::player_vertical_state::boxed_default()),
210        "demo" => Some(nodes::demo::boxed_default()),
211        "settings" => Some(nodes::settings::boxed_default()),
212        "stats_projection" => Some(nodes::stats_projection::boxed_default()),
213        "stats_timeline_frame" => Some(nodes::stats_timeline_frame::boxed_default()),
214        "stats_timeline_events" => Some(nodes::stats_timeline_events::boxed_default()),
215        _ => None,
216    }
217}
218
219pub fn graph_with_builtin_analysis_nodes<I, S>(names: I) -> SubtrActorResult<AnalysisGraph>
220where
221    I: IntoIterator<Item = S>,
222    S: AsRef<str>,
223{
224    let mut graph = AnalysisGraph::new().with_input_state_type::<FrameInput>();
225    let mut seen = HashSet::new();
226    for name in names {
227        let name = name.as_ref();
228        let canonical_name = canonical_builtin_analysis_node_name(name).ok_or_else(|| {
229            SubtrActorError::new(SubtrActorErrorVariant::UnknownStatsModuleName(
230                name.to_owned(),
231            ))
232        })?;
233        if !seen.insert(canonical_name) {
234            continue;
235        }
236        graph.push_boxed_node(boxed_analysis_node_by_name(canonical_name).ok_or_else(|| {
237            SubtrActorError::new(SubtrActorErrorVariant::UnknownStatsModuleName(
238                name.to_owned(),
239            ))
240        })?);
241    }
242    Ok(graph)
243}
244
245pub fn collect_analysis_graph_for_replay(
246    replay: &boxcars::Replay,
247    graph: AnalysisGraph,
248) -> SubtrActorResult<AnalysisGraph> {
249    let collector = collector::AnalysisNodeCollector::new(graph).process_replay(replay)?;
250    Ok(collector.into_graph())
251}
252
253pub fn collect_builtin_analysis_graph_for_replay<I, S>(
254    replay: &boxcars::Replay,
255    names: I,
256) -> SubtrActorResult<AnalysisGraph>
257where
258    I: IntoIterator<Item = S>,
259    S: AsRef<str>,
260{
261    collect_analysis_graph_for_replay(replay, graph_with_builtin_analysis_nodes(names)?)
262}
263
264pub fn all_analysis_nodes() -> Vec<Box<dyn AnalysisNodeDyn>> {
265    let mut seen = HashSet::new();
266    builtin_analysis_node_names()
267        .iter()
268        .filter_map(|name| canonical_builtin_analysis_node_name(name))
269        .filter(|name| seen.insert(*name))
270        .map(|name| {
271            boxed_analysis_node_by_name(name)
272                .unwrap_or_else(|| panic!("builtin analysis node should be registered: {name}"))
273        })
274        .collect()
275}
276
277pub fn graph_with_all_analysis_nodes() -> AnalysisGraph {
278    let mut graph = AnalysisGraph::new().with_input_state_type::<FrameInput>();
279    for node in all_analysis_nodes() {
280        graph.push_boxed_node(node);
281    }
282    graph
283}
284
285#[cfg(test)]
286#[path = "module_tests.rs"]
287mod tests;