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;