Skip to main content

subtr_actor/stats/timeline/
collector.rs

1use 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            double_tap_goal_max_event_to_goal_seconds: DoubleTapGoalCalculatorConfig::default()
76                .max_event_to_goal_seconds,
77            one_timer_goal_max_event_to_goal_seconds: OneTimerGoalCalculatorConfig::default()
78                .max_event_to_goal_seconds,
79            air_dribble_goal_max_end_to_goal_seconds: AirDribbleGoalCalculatorConfig::default()
80                .max_end_to_goal_seconds,
81            flip_reset_goal_max_event_to_goal_seconds: FlipResetGoalCalculatorConfig::default()
82                .max_event_to_goal_seconds,
83            half_volley_max_bounce_to_touch_seconds: HalfVolleyCalculatorConfig::default()
84                .max_bounce_to_touch_seconds,
85            half_volley_min_ball_speed: HalfVolleyCalculatorConfig::default().min_ball_speed,
86            half_volley_goal_max_touch_to_goal_seconds: HalfVolleyGoalCalculatorConfig::default()
87                .max_touch_to_goal_seconds,
88            half_volley_goal_min_goal_alignment: HalfVolleyGoalCalculatorConfig::default()
89                .min_goal_alignment,
90        }
91    }
92
93    fn snapshot_frame(&self) -> SubtrActorResult<ReplayStatsFrame> {
94        self.graph
95            .state::<StatsTimelineFrameState>()
96            .and_then(|state| state.frame.clone())
97            .ok_or_else(|| {
98                SubtrActorError::new(SubtrActorErrorVariant::CallbackError(
99                    "missing StatsTimelineFrame state while building timeline frame".to_owned(),
100                ))
101            })
102    }
103
104    pub fn into_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
105        let replay_meta = self
106            .replay_meta
107            .clone()
108            .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::CouldNotBuildReplayMeta))?;
109        let mut events = self
110            .graph
111            .state::<StatsTimelineEventsState>()
112            .map(|state| state.events.clone())
113            .unwrap_or_default();
114        if let Some(boost) = self.graph.state::<BoostCalculator>() {
115            events.boost_pickups = boost.pickup_comparison_events().to_vec();
116        }
117        Ok(ReplayStatsTimeline {
118            config: self.timeline_config(),
119            replay_meta,
120            events,
121            frames: self.frames,
122        })
123    }
124
125    pub fn with_frame_resolution(mut self, resolution: StatsFrameResolution) -> Self {
126        self.frame_persistence = StatsFramePersistenceController::new(resolution);
127        self
128    }
129
130    pub fn get_replay_data(
131        mut self,
132        replay: &boxcars::Replay,
133    ) -> SubtrActorResult<ReplayStatsTimeline> {
134        let mut processor = ReplayProcessor::new(replay)?;
135        processor.process(&mut self)?;
136        self.into_replay_stats_timeline()
137    }
138
139    pub fn into_timeline(self) -> ReplayStatsTimeline {
140        self.into_replay_stats_timeline()
141            .expect("analysis-node timeline collector should build typed stats frames")
142    }
143}
144
145impl Collector for StatsTimelineCollector {
146    fn process_frame(
147        &mut self,
148        processor: &ReplayProcessor,
149        _frame: &boxcars::Frame,
150        frame_number: usize,
151        current_time: f32,
152    ) -> SubtrActorResult<TimeAdvance> {
153        let player_count = processor.player_count();
154        if self.last_replay_meta_player_count != Some(player_count) {
155            let replay_meta = processor.get_replay_meta()?;
156            self.graph.on_replay_meta(&replay_meta)?;
157            self.replay_meta = Some(replay_meta);
158            self.last_replay_meta_player_count = Some(player_count);
159        }
160
161        let dt = self
162            .last_sample_time
163            .map(|last_time| (current_time - last_time).max(0.0))
164            .unwrap_or(0.0);
165        let frame_input = FrameInput::timeline(processor, frame_number, current_time, dt);
166        self.graph.evaluate_with_state(&frame_input)?;
167        self.last_sample_time = Some(current_time);
168
169        if let Some(emitted_dt) = self.frame_persistence.on_frame(frame_number, current_time) {
170            let mut frame = self.snapshot_frame()?;
171            frame.dt = emitted_dt;
172            self.frames.push(frame);
173        }
174
175        Ok(TimeAdvance::NextFrame)
176    }
177
178    fn finish_replay(&mut self, _processor: &ReplayProcessor) -> SubtrActorResult<()> {
179        self.graph.finish()?;
180        let Some(_) = self.replay_meta.as_ref() else {
181            return Ok(());
182        };
183        let Some(_) = self.graph.state::<StatsTimelineFrameState>() else {
184            return Ok(());
185        };
186        let mut final_snapshot = self.snapshot_frame()?;
187        match self
188            .frame_persistence
189            .final_frame_action(final_snapshot.frame_number, final_snapshot.time)
190        {
191            Some(FinalStatsFrameAction::Append { dt }) => {
192                final_snapshot.dt = dt;
193                self.frames.push(final_snapshot);
194            }
195            Some(FinalStatsFrameAction::ReplaceLast { dt }) => {
196                final_snapshot.dt = dt;
197                if let Some(last_frame) = self.frames.last_mut() {
198                    *last_frame = final_snapshot;
199                }
200            }
201            None => {}
202        }
203        Ok(())
204    }
205}