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        StatsTimelineConfig {
47            most_back_forward_threshold_y: PositioningCalculatorConfig::default()
48                .most_back_forward_threshold_y,
49            level_ball_depth_margin: PositioningCalculatorConfig::default().level_ball_depth_margin,
50            pressure_neutral_zone_half_width_y: PressureCalculatorConfig::default()
51                .neutral_zone_half_width_y,
52            rush_max_start_y: RushCalculatorConfig::default().max_start_y,
53            rush_attack_support_distance_y: RushCalculatorConfig::default()
54                .attack_support_distance_y,
55            rush_defender_distance_y: RushCalculatorConfig::default().defender_distance_y,
56            rush_min_possession_retained_seconds: RushCalculatorConfig::default()
57                .min_possession_retained_seconds,
58        }
59    }
60
61    fn snapshot_frame(&self) -> SubtrActorResult<ReplayStatsFrame> {
62        self.graph
63            .state::<StatsTimelineFrameState>()
64            .and_then(|state| state.frame.clone())
65            .ok_or_else(|| {
66                SubtrActorError::new(SubtrActorErrorVariant::CallbackError(
67                    "missing StatsTimelineFrame state while building timeline frame".to_owned(),
68                ))
69            })
70    }
71
72    pub fn into_replay_stats_timeline(self) -> SubtrActorResult<ReplayStatsTimeline> {
73        let replay_meta = self
74            .replay_meta
75            .clone()
76            .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::CouldNotBuildReplayMeta))?;
77        let mut events = self
78            .graph
79            .state::<StatsTimelineEventsState>()
80            .map(|state| state.events.clone())
81            .unwrap_or_default();
82        if let Some(boost) = self.graph.state::<BoostCalculator>() {
83            events.boost_pickups = boost.pickup_comparison_events().to_vec();
84        }
85        Ok(ReplayStatsTimeline {
86            config: self.timeline_config(),
87            replay_meta,
88            events,
89            frames: self.frames,
90        })
91    }
92
93    pub fn with_frame_resolution(mut self, resolution: StatsFrameResolution) -> Self {
94        self.frame_persistence = StatsFramePersistenceController::new(resolution);
95        self
96    }
97
98    pub fn get_replay_data(
99        mut self,
100        replay: &boxcars::Replay,
101    ) -> SubtrActorResult<ReplayStatsTimeline> {
102        let mut processor = ReplayProcessor::new(replay)?;
103        processor.process(&mut self)?;
104        self.into_replay_stats_timeline()
105    }
106
107    pub fn into_timeline(self) -> ReplayStatsTimeline {
108        self.into_replay_stats_timeline()
109            .expect("analysis-node timeline collector should build typed stats frames")
110    }
111}
112
113impl Collector for StatsTimelineCollector {
114    fn process_frame(
115        &mut self,
116        processor: &ReplayProcessor,
117        _frame: &boxcars::Frame,
118        frame_number: usize,
119        current_time: f32,
120    ) -> SubtrActorResult<TimeAdvance> {
121        let player_count = processor.player_count();
122        if self.last_replay_meta_player_count != Some(player_count) {
123            let replay_meta = processor.get_replay_meta()?;
124            self.graph.on_replay_meta(&replay_meta)?;
125            self.replay_meta = Some(replay_meta);
126            self.last_replay_meta_player_count = Some(player_count);
127        }
128
129        let dt = self
130            .last_sample_time
131            .map(|last_time| (current_time - last_time).max(0.0))
132            .unwrap_or(0.0);
133        let frame_input = FrameInput::timeline(processor, frame_number, current_time, dt);
134        self.graph.evaluate_with_state(&frame_input)?;
135        self.last_sample_time = Some(current_time);
136
137        if let Some(emitted_dt) = self.frame_persistence.on_frame(frame_number, current_time) {
138            let mut frame = self.snapshot_frame()?;
139            frame.dt = emitted_dt;
140            self.frames.push(frame);
141        }
142
143        Ok(TimeAdvance::NextFrame)
144    }
145
146    fn finish_replay(&mut self, _processor: &ReplayProcessor) -> SubtrActorResult<()> {
147        self.graph.finish()?;
148        let Some(_) = self.replay_meta.as_ref() else {
149            return Ok(());
150        };
151        let Some(_) = self.graph.state::<StatsTimelineFrameState>() else {
152            return Ok(());
153        };
154        let mut final_snapshot = self.snapshot_frame()?;
155        match self
156            .frame_persistence
157            .final_frame_action(final_snapshot.frame_number, final_snapshot.time)
158        {
159            Some(FinalStatsFrameAction::Append { dt }) => {
160                final_snapshot.dt = dt;
161                self.frames.push(final_snapshot);
162            }
163            Some(FinalStatsFrameAction::ReplaceLast { dt }) => {
164                final_snapshot.dt = dt;
165                if let Some(last_frame) = self.frames.last_mut() {
166                    *last_frame = final_snapshot;
167                }
168            }
169            None => {}
170        }
171        Ok(())
172    }
173}