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 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}