Skip to main content

subtr_actor/stats/calculators/
frame_input.rs

1use crate::*;
2
3use super::{
4    BallFrameState, BallSample, DemoEventSample, FrameEventsState, FrameInfo, GameplayState,
5    LivePlayState, PlayerFrameState, PlayerSample,
6};
7
8/// Builds the per-frame input fed into the analysis graph from replay state.
9#[derive(Debug, Clone, Default)]
10pub struct ReplayFrameInputBuilder {
11    aggregate_events: FrameEventsSampler,
12}
13
14#[derive(Debug, Clone, Default)]
15struct FrameEventsSampler {
16    cursors: ProcessorEventCursors,
17}
18
19impl ReplayFrameInputBuilder {
20    pub fn timeline(
21        &mut self,
22        processor: &dyn ProcessorView,
23        frame_number: usize,
24        current_time: f32,
25        dt: f32,
26    ) -> FrameInput {
27        FrameInput::timeline(processor, frame_number, current_time, dt)
28    }
29
30    pub fn aggregate(
31        &mut self,
32        processor: &dyn ProcessorView,
33        frame_number: usize,
34        current_time: f32,
35        dt: f32,
36    ) -> FrameInput {
37        let frame_events_state = self.aggregate_events.events_since_last_sample(processor);
38        FrameInput::from_processor_and_events(
39            processor,
40            frame_number,
41            current_time,
42            dt,
43            frame_events_state,
44        )
45    }
46}
47
48impl FrameEventsSampler {
49    fn events_since_last_sample(&mut self, processor: &dyn ProcessorView) -> FrameEventsState {
50        let events = FrameInput::build_events_since_last_sample(processor, &self.cursors);
51        self.cursors = ProcessorEventCursors::from_processor(processor);
52        events
53    }
54}
55
56#[derive(Debug, Clone, Default)]
57struct ProcessorEventCursors {
58    demolish_count: usize,
59    boost_pad_event_count: usize,
60    touch_event_count: usize,
61    dodge_refreshed_event_count: usize,
62    player_stat_event_count: usize,
63    goal_event_count: usize,
64}
65
66impl ProcessorEventCursors {
67    fn from_processor(processor: &dyn ProcessorView) -> Self {
68        Self {
69            demolish_count: processor.demolishes().len(),
70            boost_pad_event_count: processor.boost_pad_events().len(),
71            touch_event_count: processor.touch_events().len(),
72            dodge_refreshed_event_count: processor.dodge_refreshed_events().len(),
73            player_stat_event_count: processor.player_stat_events().len(),
74            goal_event_count: processor.goal_events().len(),
75        }
76    }
77}
78
79/// The bundled per-frame input consumed by analysis-graph source nodes.
80#[derive(Debug, Clone)]
81pub struct FrameInput {
82    frame_info: FrameInfo,
83    gameplay_state: GameplayState,
84    ball_frame_state: BallFrameState,
85    player_frame_state: PlayerFrameState,
86    frame_events_state: FrameEventsState,
87    live_play_state: Option<LivePlayState>,
88}
89
90impl FrameInput {
91    /// Builds a frame input from already-materialized frame component states.
92    ///
93    /// Replay callers should usually use [`ReplayFrameInputBuilder`]. Live
94    /// callers can construct these same component states directly from their
95    /// sampled game state.
96    pub fn from_parts(
97        frame_info: FrameInfo,
98        gameplay_state: GameplayState,
99        ball_frame_state: BallFrameState,
100        player_frame_state: PlayerFrameState,
101        frame_events_state: FrameEventsState,
102    ) -> Self {
103        Self {
104            frame_info,
105            gameplay_state,
106            ball_frame_state,
107            player_frame_state,
108            frame_events_state,
109            live_play_state: None,
110        }
111    }
112
113    /// Builds a frame input with an explicitly sampled live-play state.
114    ///
115    /// Replay processing should let the graph derive live play from replicated
116    /// gameplay fields. Live callers can use this when the host integration has
117    /// a stronger source of truth for whether analysis should run on a frame.
118    pub fn from_parts_with_live_play_state(
119        frame_info: FrameInfo,
120        gameplay_state: GameplayState,
121        ball_frame_state: BallFrameState,
122        player_frame_state: PlayerFrameState,
123        frame_events_state: FrameEventsState,
124        live_play_state: LivePlayState,
125    ) -> Self {
126        Self {
127            frame_info,
128            gameplay_state,
129            ball_frame_state,
130            player_frame_state,
131            frame_events_state,
132            live_play_state: Some(live_play_state),
133        }
134    }
135
136    pub fn timeline(
137        processor: &dyn ProcessorView,
138        frame_number: usize,
139        current_time: f32,
140        dt: f32,
141    ) -> Self {
142        let frame_events_state = Self::build_current_frame_events_state(processor);
143        Self::from_processor_and_events(
144            processor,
145            frame_number,
146            current_time,
147            dt,
148            frame_events_state,
149        )
150    }
151
152    fn from_processor_and_events(
153        processor: &dyn ProcessorView,
154        frame_number: usize,
155        current_time: f32,
156        dt: f32,
157        frame_events_state: FrameEventsState,
158    ) -> Self {
159        Self {
160            frame_info: Self::build_frame_info(processor, frame_number, current_time, dt),
161            gameplay_state: Self::build_gameplay_state(processor),
162            ball_frame_state: Self::build_ball_frame_state(processor, current_time),
163            player_frame_state: Self::build_player_frame_state(processor, current_time),
164            frame_events_state,
165            live_play_state: None,
166        }
167    }
168
169    pub fn timeline_with_live_play_state(
170        processor: &dyn ProcessorView,
171        frame_number: usize,
172        current_time: f32,
173        dt: f32,
174        live_play_state: LivePlayState,
175    ) -> Self {
176        let mut input = Self::timeline(processor, frame_number, current_time, dt);
177        input.live_play_state = Some(live_play_state);
178        input
179    }
180
181    fn build_frame_info(
182        processor: &dyn ProcessorView,
183        frame_number: usize,
184        current_time: f32,
185        dt: f32,
186    ) -> FrameInfo {
187        FrameInfo {
188            frame_number,
189            time: current_time,
190            dt,
191            seconds_remaining: processor.get_seconds_remaining().ok(),
192        }
193    }
194
195    fn build_gameplay_state(processor: &dyn ProcessorView) -> GameplayState {
196        let team_scores = processor.get_team_scores().ok();
197        let possession_team_is_team_0 =
198            processor
199                .get_ball_hit_team_num()
200                .ok()
201                .and_then(|team_num| match team_num {
202                    0 => Some(true),
203                    1 => Some(false),
204                    _ => None,
205                });
206        let scored_on_team_is_team_0 =
207            processor
208                .get_scored_on_team_num()
209                .ok()
210                .and_then(|team_num| match team_num {
211                    0 => Some(true),
212                    1 => Some(false),
213                    _ => None,
214                });
215        GameplayState {
216            game_state: processor.get_replicated_state_name().ok(),
217            ball_has_been_hit: processor.get_ball_has_been_hit().ok(),
218            kickoff_countdown_time: processor.get_replicated_game_state_time_remaining().ok(),
219            team_zero_score: team_scores.map(|scores| scores.0),
220            team_one_score: team_scores.map(|scores| scores.1),
221            possession_team_is_team_0,
222            scored_on_team_is_team_0,
223            current_in_game_team_player_counts: processor.current_in_game_team_player_counts(),
224        }
225    }
226
227    fn build_ball_frame_state(processor: &dyn ProcessorView, current_time: f32) -> BallFrameState {
228        processor
229            .get_interpolated_ball_rigid_body(current_time, 0.0)
230            .ok()
231            .map(|rigid_body| BallSample { rigid_body })
232            .into()
233    }
234
235    fn build_player_frame_state(
236        processor: &dyn ProcessorView,
237        current_time: f32,
238    ) -> PlayerFrameState {
239        let mut players = Vec::new();
240        for player_id in processor.iter_player_ids_in_order() {
241            let Ok(is_team_0) = processor.get_player_is_team_0(player_id) else {
242                continue;
243            };
244            players.push(PlayerSample {
245                player_id: player_id.clone(),
246                is_team_0,
247                hitbox: processor.get_player_car_hitbox(player_id),
248                rigid_body: processor
249                    .get_interpolated_player_rigid_body(player_id, current_time, 0.0)
250                    .ok()
251                    .filter(|rigid_body| !rigid_body.sleeping),
252                boost_amount: processor.get_player_boost_level(player_id).ok(),
253                last_boost_amount: processor.get_player_last_boost_level(player_id).ok(),
254                boost_active: processor.get_boost_active(player_id).unwrap_or(0) % 2 == 1,
255                dodge_active: processor.get_dodge_active(player_id).unwrap_or(0) % 2 == 1,
256                dodge_torque: processor
257                    .get_dodge_torque(player_id)
258                    .ok()
259                    .map(|(x, y, z)| glam::Vec3::new(x, y, z)),
260                powerslide_active: processor.get_powerslide_active(player_id).unwrap_or(false),
261                match_goals: processor.get_player_match_goals(player_id).ok(),
262                match_assists: processor.get_player_match_assists(player_id).ok(),
263                match_saves: processor.get_player_match_saves(player_id).ok(),
264                match_shots: processor.get_player_match_shots(player_id).ok(),
265                match_score: processor.get_player_match_score(player_id).ok(),
266            });
267        }
268        PlayerFrameState { players }
269    }
270
271    fn build_active_demo_events(processor: &dyn ProcessorView) -> Vec<DemoEventSample> {
272        let active_demo_events = processor.current_frame_active_demo_events();
273        if !active_demo_events.is_empty() {
274            active_demo_events.to_vec()
275        } else {
276            match processor.get_active_demos() {
277                Ok(demos) => demos
278                    .into_iter()
279                    .filter_map(|demo| {
280                        let attacker = processor
281                            .get_player_id_from_car_id(&demo.attacker_actor_id())
282                            .ok()?;
283                        let victim = processor
284                            .get_player_id_from_car_id(&demo.victim_actor_id())
285                            .ok()?;
286                        Some(DemoEventSample { attacker, victim })
287                    })
288                    .collect(),
289                _ => Vec::new(),
290            }
291        }
292    }
293
294    fn build_current_frame_events_state(processor: &dyn ProcessorView) -> FrameEventsState {
295        FrameEventsState {
296            active_demos: Self::build_active_demo_events(processor),
297            demo_events: processor.current_frame_demolish_events().to_vec(),
298            boost_pad_events: processor.current_frame_boost_pad_events().to_vec(),
299            touch_events: processor.current_frame_touch_events().to_vec(),
300            dodge_refreshed_counter_available: processor.dodge_refreshed_counter_available(),
301            dodge_refreshed_events: processor.current_frame_dodge_refreshed_events().to_vec(),
302            player_stat_events: processor.current_frame_player_stat_events().to_vec(),
303            goal_events: processor.current_frame_goal_events().to_vec(),
304        }
305    }
306
307    fn build_events_since_last_sample(
308        processor: &dyn ProcessorView,
309        event_cursors: &ProcessorEventCursors,
310    ) -> FrameEventsState {
311        FrameEventsState {
312            active_demos: Self::build_active_demo_events(processor),
313            demo_events: processor.demolishes()[event_cursors.demolish_count..].to_vec(),
314            boost_pad_events: processor.boost_pad_events()[event_cursors.boost_pad_event_count..]
315                .to_vec(),
316            touch_events: processor.touch_events()[event_cursors.touch_event_count..].to_vec(),
317            dodge_refreshed_counter_available: processor.dodge_refreshed_counter_available(),
318            dodge_refreshed_events: processor.dodge_refreshed_events()
319                [event_cursors.dodge_refreshed_event_count..]
320                .to_vec(),
321            player_stat_events: processor.player_stat_events()
322                [event_cursors.player_stat_event_count..]
323                .to_vec(),
324            goal_events: processor.goal_events()[event_cursors.goal_event_count..].to_vec(),
325        }
326    }
327
328    pub fn frame_info(&self) -> FrameInfo {
329        self.frame_info.clone()
330    }
331
332    pub fn gameplay_state(&self) -> GameplayState {
333        self.gameplay_state.clone()
334    }
335
336    pub fn ball_frame_state(&self) -> BallFrameState {
337        self.ball_frame_state.clone()
338    }
339
340    pub fn player_frame_state(&self) -> PlayerFrameState {
341        self.player_frame_state.clone()
342    }
343
344    pub fn frame_events_state(&self) -> FrameEventsState {
345        self.frame_events_state.clone()
346    }
347
348    pub fn live_play_state(&self) -> Option<LivePlayState> {
349        self.live_play_state.clone()
350    }
351}