Skip to main content

subtr_actor/stats/analysis_graph/
mod.rs

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