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;