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;