subtr_actor/stats/timeline/
collector.rs1use crate::collector::frame_resolution::{
2 FinalStatsFrameAction, StatsFramePersistenceController, StatsFrameResolution,
3};
4use crate::stats::analysis_graph::{
5 AnalysisGraph, StatsTimelineEventsNode, StatsTimelineEventsState, StatsTimelineFrameNode,
6 StatsTimelineFrameState,
7};
8use crate::*;
9
10pub fn build_timeline_graph() -> AnalysisGraph {
11 let mut graph = AnalysisGraph::new().with_input_state_type::<FrameInput>();
12 graph.push_boxed_node(Box::new(StatsTimelineFrameNode::new()));
13 graph.push_boxed_node(Box::new(StatsTimelineEventsNode::new()));
14 graph
15}
16
17pub struct StatsTimelineCollector {
18 graph: AnalysisGraph,
19 replay_meta: Option<ReplayMeta>,
20 last_replay_meta_player_count: Option<usize>,
21 frames: Vec<ReplayStatsFrame>,
22 last_sample_time: Option<f32>,
23 frame_persistence: StatsFramePersistenceController,
24}
25
26impl Default for StatsTimelineCollector {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32impl StatsTimelineCollector {
33 pub fn new() -> Self {
34 let graph = build_timeline_graph();
35 Self {
36 graph,
37 replay_meta: None,
38 last_replay_meta_player_count: None,
39 frames: Vec::new(),
40 last_sample_time: None,
41 frame_persistence: StatsFramePersistenceController::new(StatsFrameResolution::default()),
42 }
43 }
44
45 fn timeline_config(&self) -> StatsTimelineConfig {
46 let rotation_defaults = RotationCalculatorConfig::default();
47 StatsTimelineConfig {
48 most_back_forward_threshold_y: PositioningCalculatorConfig::default()
49 .most_back_forward_threshold_y,
50 level_ball_depth_margin: PositioningCalculatorConfig::default().level_ball_depth_margin,
51 pressure_neutral_zone_half_width_y: PressureCalculatorConfig::default()
52 .neutral_zone_half_width_y,
53 rotation_role_depth_margin: rotation_defaults.role_depth_margin,
54 rotation_first_man_ambiguity_margin: rotation_defaults.first_man_ambiguity_margin,
55 rotation_first_man_debounce_seconds: rotation_defaults.first_man_debounce_seconds,
56 rush_max_start_y: RushCalculatorConfig::default().max_start_y,
57 rush_attack_support_distance_y: RushCalculatorConfig::default()
58 .attack_support_distance_y,
59 rush_defender_distance_y: RushCalculatorConfig::default().defender_distance_y,
60 rush_min_possession_retained_seconds: RushCalculatorConfig::default()
61 .min_possession_retained_seconds,
62 aerial_goal_min_ball_z: AerialGoalCalculatorConfig::default().min_ball_z,
63 high_aerial_goal_min_ball_z: HighAerialGoalCalculatorConfig::default().min_ball_z,
64 long_distance_goal_max_attacking_y: LongDistanceGoalCalculatorConfig::default()
65 .max_attacking_y,
66 own_half_goal_max_attacking_y: OwnHalfGoalCalculatorConfig::default().max_attacking_y,
67 empty_net_min_defender_y_margin: EmptyNetGoalCalculatorConfig::default()
68 .min_defender_y_margin,
69 empty_net_min_defender_distance: EmptyNetGoalCalculatorConfig::default()
70 .min_defender_distance,
71 empty_net_max_touch_attacking_y: EmptyNetGoalCalculatorConfig::default()
72 .max_touch_attacking_y,
73 flick_goal_max_event_to_goal_seconds: FlickGoalCalculatorConfig::default()
74 .max_event_to_goal_seconds,
75 one_timer_goal_max_event_to_goal_seconds: OneTimerGoalCalculatorConfig::default()
76 .max_event_to_goal_seconds,
77 air_dribble_goal_max_end_to_goal_seconds: AirDribbleGoalCalculatorConfig::default()
78 .max_end_to_goal_seconds,
79 flip_reset_goal_max_event_to_goal_seconds: FlipResetGoalCalculatorConfig::default()
80 .max_event_to_goal_seconds,
81 half_volley_max_bounce_to_touch_seconds: HalfVolleyCalculatorConfig::default()
82 .max_bounce_to_touch_seconds,
83 half_volley_min_ball_speed: HalfVolleyCalculatorConfig::default().min_ball_speed,
84 half_volley_goal_max_touch_to_goal_seconds: HalfVolleyGoalCalculatorConfig::default()
85 .max_touch_to_goal_seconds,
86 half_volley_goal_min_goal_alignment: HalfVolleyGoalCalculatorConfig::default()
87 .min_goal_alignment,
88 }
89 }
90
91 fn snapshot_frame(&self) -> SubtrActorResult<ReplayStatsFrame> {
92 self.graph
93 .state::<StatsTimelineFrameState>()
94 .and_then(|state| state.frame.clone())
95 .ok_or_else(|| {
96 SubtrActorError::new(SubtrActorErrorVariant::CallbackError(
97 "missing StatsTimelineFrame state while building timeline frame".to_owned(),
98 ))
99 })
100 }
101
102 pub fn into_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
103 let replay_meta = self
104 .replay_meta
105 .clone()
106 .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::CouldNotBuildReplayMeta))?;
107 let mut events = self
108 .graph
109 .state::<StatsTimelineEventsState>()
110 .map(|state| state.events.clone())
111 .unwrap_or_default();
112 if let Some(boost) = self.graph.state::<BoostCalculator>() {
113 events.boost_pickups = boost.pickup_comparison_events().to_vec();
114 }
115 Ok(ReplayStatsTimeline {
116 config: self.timeline_config(),
117 replay_meta,
118 events,
119 frames: self.frames,
120 })
121 }
122
123 pub fn with_frame_resolution(mut self, resolution: StatsFrameResolution) -> Self {
124 self.frame_persistence = StatsFramePersistenceController::new(resolution);
125 self
126 }
127
128 pub fn get_replay_data(
129 mut self,
130 replay: &boxcars::Replay,
131 ) -> SubtrActorResult<ReplayStatsTimeline> {
132 let mut processor = ReplayProcessor::new(replay)?;
133 processor.process(&mut self)?;
134 self.into_replay_stats_timeline()
135 }
136
137 pub fn into_timeline(self) -> ReplayStatsTimeline {
138 self.into_replay_stats_timeline()
139 .expect("analysis-node timeline collector should build typed stats frames")
140 }
141}
142
143impl Collector for StatsTimelineCollector {
144 fn process_frame(
145 &mut self,
146 processor: &ReplayProcessor,
147 _frame: &boxcars::Frame,
148 frame_number: usize,
149 current_time: f32,
150 ) -> SubtrActorResult<TimeAdvance> {
151 let player_count = processor.player_count();
152 if self.last_replay_meta_player_count != Some(player_count) {
153 let replay_meta = processor.get_replay_meta()?;
154 self.graph.on_replay_meta(&replay_meta)?;
155 self.replay_meta = Some(replay_meta);
156 self.last_replay_meta_player_count = Some(player_count);
157 }
158
159 let dt = self
160 .last_sample_time
161 .map(|last_time| (current_time - last_time).max(0.0))
162 .unwrap_or(0.0);
163 let frame_input = FrameInput::timeline(processor, frame_number, current_time, dt);
164 self.graph.evaluate_with_state(&frame_input)?;
165 self.last_sample_time = Some(current_time);
166
167 if let Some(emitted_dt) = self.frame_persistence.on_frame(frame_number, current_time) {
168 let mut frame = self.snapshot_frame()?;
169 frame.dt = emitted_dt;
170 self.frames.push(frame);
171 }
172
173 Ok(TimeAdvance::NextFrame)
174 }
175
176 fn finish_replay(&mut self, _processor: &ReplayProcessor) -> SubtrActorResult<()> {
177 self.graph.finish()?;
178 let Some(_) = self.replay_meta.as_ref() else {
179 return Ok(());
180 };
181 let Some(_) = self.graph.state::<StatsTimelineFrameState>() else {
182 return Ok(());
183 };
184 let mut final_snapshot = self.snapshot_frame()?;
185 match self
186 .frame_persistence
187 .final_frame_action(final_snapshot.frame_number, final_snapshot.time)
188 {
189 Some(FinalStatsFrameAction::Append { dt }) => {
190 final_snapshot.dt = dt;
191 self.frames.push(final_snapshot);
192 }
193 Some(FinalStatsFrameAction::ReplaceLast { dt }) => {
194 final_snapshot.dt = dt;
195 if let Some(last_frame) = self.frames.last_mut() {
196 *last_frame = final_snapshot;
197 }
198 }
199 None => {}
200 }
201 Ok(())
202 }
203}