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