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