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