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