Skip to main content

subtr_actor/processor/
player_queries.rs

1use super::*;
2
3impl<'a> ReplayProcessor<'a> {
4    /// Returns the replicated match clock in whole seconds.
5    pub fn get_seconds_remaining(&self) -> SubtrActorResult<i32> {
6        let seconds_remaining_object_id =
7            self.cached_object_ids.seconds_remaining.ok_or_else(|| {
8                SubtrActorError::new(SubtrActorErrorVariant::ObjectIdNotFound {
9                    name: SECONDS_REMAINING_KEY,
10                })
11            })?;
12        let metadata_actor_id = self.get_metadata_actor_id()?;
13        let metadata_state = self.get_actor_state(&metadata_actor_id)?;
14        metadata_state
15            .attributes
16            .get(&seconds_remaining_object_id)
17            .ok_or_else(|| {
18                SubtrActorError::new(SubtrActorErrorVariant::PropertyNotFoundInState {
19                    property: SECONDS_REMAINING_KEY,
20                })
21            })
22            .and_then(|(attribute, _)| attribute_match!(attribute, boxcars::Attribute::Int))
23            .copied()
24    }
25
26    /// Returns the replicated game-state enum value from the metadata actor.
27    pub fn get_replicated_state_name(&self) -> SubtrActorResult<i32> {
28        get_actor_attribute_matching!(
29            self,
30            &self.get_metadata_actor_id()?,
31            REPLICATED_STATE_NAME_KEY,
32            boxcars::Attribute::Int
33        )
34        .cloned()
35    }
36
37    /// Returns the replicated kickoff countdown / time-remaining field.
38    pub fn get_replicated_game_state_time_remaining(&self) -> SubtrActorResult<i32> {
39        get_actor_attribute_matching!(
40            self,
41            &self.get_metadata_actor_id()?,
42            REPLICATED_GAME_STATE_TIME_REMAINING_KEY,
43            boxcars::Attribute::Int
44        )
45        .cloned()
46    }
47
48    /// Returns whether the replay currently reports that the ball has been hit.
49    pub fn get_ball_has_been_hit(&self) -> SubtrActorResult<bool> {
50        get_actor_attribute_matching!(
51            self,
52            &self.get_metadata_actor_id()?,
53            BALL_HAS_BEEN_HIT_KEY,
54            boxcars::Attribute::Boolean
55        )
56        .cloned()
57    }
58
59    /// Returns the ball actor's ignore-syncing flag.
60    pub fn get_ignore_ball_syncing(&self) -> SubtrActorResult<bool> {
61        let actor_id = self.get_ball_actor_id()?;
62        get_actor_attribute_matching!(
63            self,
64            &actor_id,
65            IGNORE_SYNCING_KEY,
66            boxcars::Attribute::Boolean
67        )
68        .cloned()
69    }
70
71    /// Returns the current ball rigid body from live actor state.
72    pub fn get_ball_rigid_body(&self) -> SubtrActorResult<&boxcars::RigidBody> {
73        self.ball_actor_id
74            .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::BallActorNotFound))
75            .and_then(|actor_id| self.get_actor_rigid_body(&actor_id).map(|v| v.0))
76    }
77
78    /// Returns the current ball rigid body after spatial normalization.
79    pub fn get_normalized_ball_rigid_body(&self) -> SubtrActorResult<boxcars::RigidBody> {
80        self.get_ball_rigid_body()
81            .map(|rigid_body| self.normalize_rigid_body(rigid_body))
82    }
83
84    /// Returns whether a non-sleeping ball rigid body is currently available.
85    pub fn ball_rigid_body_exists(&self) -> SubtrActorResult<bool> {
86        Ok(self
87            .get_ball_rigid_body()
88            .map(|rb| !rb.sleeping)
89            .unwrap_or(false))
90    }
91
92    /// Returns the current ball rigid body and the frame where it was last updated.
93    pub fn get_ball_rigid_body_and_updated(
94        &self,
95    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
96        self.ball_actor_id
97            .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::BallActorNotFound))
98            .and_then(|actor_id| {
99                get_attribute_and_updated!(
100                    self,
101                    &self.get_actor_state(&actor_id)?.attributes,
102                    RIGID_BODY_STATE_KEY,
103                    boxcars::Attribute::RigidBody
104                )
105            })
106    }
107
108    /// Applies stored ball velocity forward to the requested time.
109    pub fn get_velocity_applied_ball_rigid_body(
110        &self,
111        target_time: f32,
112    ) -> SubtrActorResult<boxcars::RigidBody> {
113        let (current_rigid_body, frame_index) = self.get_ball_rigid_body_and_updated()?;
114        self.velocities_applied_rigid_body(current_rigid_body, *frame_index, target_time)
115    }
116
117    /// Interpolates the ball rigid body to the requested time.
118    pub fn get_interpolated_ball_rigid_body(
119        &self,
120        time: f32,
121        close_enough: f32,
122    ) -> SubtrActorResult<boxcars::RigidBody> {
123        self.get_interpolated_actor_rigid_body(&self.get_ball_actor_id()?, time, close_enough)
124    }
125
126    /// Returns the player's replicated display name.
127    pub fn get_player_name(&self, player_id: &PlayerId) -> SubtrActorResult<String> {
128        get_actor_attribute_matching!(
129            self,
130            &self.get_player_actor_id(player_id)?,
131            PLAYER_NAME_KEY,
132            boxcars::Attribute::String
133        )
134        .cloned()
135    }
136
137    fn player_stats_headers(&self) -> Option<&Vec<Vec<(String, boxcars::HeaderProp)>>> {
138        self.replay
139            .properties
140            .iter()
141            .find_map(|(key, value)| match (key.as_str(), value) {
142                ("PlayerStats", boxcars::HeaderProp::Array(player_stats)) => Some(player_stats),
143                _ => None,
144            })
145    }
146
147    fn player_header_stats(
148        &self,
149        player_id: &PlayerId,
150    ) -> Option<std::collections::HashMap<String, boxcars::HeaderProp>> {
151        let player_stats = self.player_stats_headers()?;
152        let fallback_name = String::new();
153        self.get_player_name(player_id)
154            .ok()
155            .and_then(|name| {
156                crate::replay_meta::find_player_stats(player_id, &name, player_stats).ok()
157            })
158            .or_else(|| {
159                crate::replay_meta::find_player_stats(player_id, &fallback_name, player_stats).ok()
160            })
161    }
162
163    pub(crate) fn get_player_loadout_body_name(&self, player_id: &PlayerId) -> Option<String> {
164        self.player_header_stats(player_id)?
165            .get("LoadoutBody")
166            .and_then(|property| match property {
167                boxcars::HeaderProp::Str(body_name) => Some(body_name.clone()),
168                _ => None,
169            })
170    }
171
172    pub(crate) fn get_player_loadout_body_id(&self, player_id: &PlayerId) -> Option<u32> {
173        let player_actor_id = self.get_player_actor_id(player_id).ok()?;
174        let loadout = self.player_actor_to_loadout.get(&player_actor_id)?;
175        match self.get_player_is_team_0(player_id).ok() {
176            Some(true) => Some(loadout.blue.body),
177            Some(false) => Some(loadout.orange.body),
178            None if loadout.blue.body == loadout.orange.body => Some(loadout.blue.body),
179            None => Some(loadout.blue.body),
180        }
181    }
182
183    /// Returns the player's replicated Rocket League camera preset, when one
184    /// was captured from a `TAGame.CameraSettingsActor_TA` actor while
185    /// processing frames.
186    pub(crate) fn get_player_camera_settings(
187        &self,
188        player_id: &PlayerId,
189    ) -> Option<PlayerCameraSettings> {
190        let player_actor_id = self.get_player_actor_id(player_id).ok()?;
191        self.player_actor_to_camera_settings
192            .get(&player_actor_id)
193            .copied()
194    }
195
196    pub(crate) fn get_player_car_hitbox(&self, player_id: &PlayerId) -> CarHitbox {
197        car_hitbox_for_body_id_or_name(
198            self.get_player_loadout_body_id(player_id),
199            self.get_player_loadout_body_name(player_id).as_deref(),
200        )
201        .unwrap_or_else(default_car_hitbox)
202    }
203
204    fn get_player_int_stat(
205        &self,
206        player_id: &PlayerId,
207        key: &'static str,
208    ) -> SubtrActorResult<i32> {
209        get_actor_attribute_matching!(
210            self,
211            &self.get_player_actor_id(player_id)?,
212            key,
213            boxcars::Attribute::Int
214        )
215        .cloned()
216    }
217
218    /// Returns the replay object-name key for the player's team actor.
219    pub fn get_player_team_key(&self, player_id: &PlayerId) -> SubtrActorResult<String> {
220        let team_actor_id = self
221            .player_to_team
222            .get(&self.get_player_actor_id(player_id)?)
223            .ok_or_else(|| {
224                SubtrActorError::new(SubtrActorErrorVariant::UnknownPlayerTeam {
225                    player_id: player_id.clone(),
226                })
227            })?;
228        let state = self.get_actor_state(team_actor_id)?;
229        self.object_id_to_name
230            .get(&state.object_id)
231            .ok_or_else(|| {
232                SubtrActorError::new(SubtrActorErrorVariant::UnknownPlayerTeam {
233                    player_id: player_id.clone(),
234                })
235            })
236            .cloned()
237    }
238
239    /// Returns whether the player belongs to team 0.
240    pub fn get_player_is_team_0(&self, player_id: &PlayerId) -> SubtrActorResult<bool> {
241        Ok(self
242            .get_player_team_key(player_id)?
243            .chars()
244            .last()
245            .ok_or_else(|| {
246                SubtrActorError::new(SubtrActorErrorVariant::EmptyTeamName {
247                    player_id: player_id.clone(),
248                })
249            })?
250            == '0')
251    }
252
253    /// Returns the team actor id for the requested side.
254    pub(crate) fn get_team_actor_id_for_side(
255        &self,
256        is_team_0: bool,
257    ) -> SubtrActorResult<boxcars::ActorId> {
258        let player_id = if is_team_0 {
259            self.team_zero.first()
260        } else {
261            self.team_one.first()
262        }
263        .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::NoGameActor))?;
264
265        self.player_to_team
266            .get(&self.get_player_actor_id(player_id)?)
267            .copied()
268            .ok_or_else(|| {
269                SubtrActorError::new(SubtrActorErrorVariant::ActorNotFound {
270                    name: "Team",
271                    player_id: player_id.clone(),
272                })
273            })
274    }
275
276    /// Returns the score for the requested team side.
277    pub fn get_team_score(&self, is_team_0: bool) -> SubtrActorResult<i32> {
278        let team_actor_id = self.get_team_actor_id_for_side(is_team_0)?;
279        get_actor_attribute_matching!(
280            self,
281            &team_actor_id,
282            TEAM_GAME_SCORE_KEY,
283            boxcars::Attribute::Int
284        )
285        .cloned()
286    }
287
288    /// Returns `(team_zero_score, team_one_score)`.
289    pub fn get_team_scores(&self) -> SubtrActorResult<(i32, i32)> {
290        Ok((self.get_team_score(true)?, self.get_team_score(false)?))
291    }
292
293    /// Returns the player's current car rigid body.
294    pub fn get_player_rigid_body(
295        &self,
296        player_id: &PlayerId,
297    ) -> SubtrActorResult<&boxcars::RigidBody> {
298        self.get_car_actor_id(player_id)
299            .and_then(|actor_id| self.get_actor_rigid_body(&actor_id).map(|v| v.0))
300    }
301
302    /// Returns the player's current car rigid body after spatial normalization.
303    pub fn get_normalized_player_rigid_body(
304        &self,
305        player_id: &PlayerId,
306    ) -> SubtrActorResult<boxcars::RigidBody> {
307        self.get_player_rigid_body(player_id)
308            .map(|rigid_body| self.normalize_rigid_body(rigid_body))
309    }
310
311    /// Returns the player's current normalized car position.
312    pub(crate) fn get_normalized_player_position(
313        &self,
314        player_id: &PlayerId,
315    ) -> Option<boxcars::Vector3f> {
316        self.get_normalized_player_rigid_body(player_id)
317            .ok()
318            .map(|rigid_body| rigid_body.location)
319    }
320
321    /// Returns the player's rigid body and the frame where it was last updated.
322    pub fn get_player_rigid_body_and_updated(
323        &self,
324        player_id: &PlayerId,
325    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
326        self.get_car_actor_id(player_id).and_then(|actor_id| {
327            get_attribute_and_updated!(
328                self,
329                &self.get_actor_state(&actor_id)?.attributes,
330                RIGID_BODY_STATE_KEY,
331                boxcars::Attribute::RigidBody
332            )
333        })
334    }
335
336    /// Like [`Self::get_player_rigid_body_and_updated`], but can use recently deleted state.
337    pub fn get_player_rigid_body_and_updated_or_recently_deleted(
338        &self,
339        player_id: &PlayerId,
340    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
341        self.get_car_actor_id(player_id)
342            .and_then(|actor_id| self.get_actor_rigid_body_or_recently_deleted(&actor_id))
343    }
344
345    /// Applies stored player velocity forward to the requested time.
346    pub fn get_velocity_applied_player_rigid_body(
347        &self,
348        player_id: &PlayerId,
349        target_time: f32,
350    ) -> SubtrActorResult<boxcars::RigidBody> {
351        let (current_rigid_body, frame_index) =
352            self.get_player_rigid_body_and_updated(player_id)?;
353        self.velocities_applied_rigid_body(current_rigid_body, *frame_index, target_time)
354    }
355
356    /// Interpolates the player's car rigid body to the requested time.
357    pub fn get_interpolated_player_rigid_body(
358        &self,
359        player_id: &PlayerId,
360        time: f32,
361        close_enough: f32,
362    ) -> SubtrActorResult<boxcars::RigidBody> {
363        self.get_car_actor_id(player_id).and_then(|car_actor_id| {
364            self.get_interpolated_actor_rigid_body(&car_actor_id, time, close_enough)
365        })
366    }
367
368    /// Returns the player's current boost amount in raw replay units.
369    pub fn get_player_boost_level(&self, player_id: &PlayerId) -> SubtrActorResult<f32> {
370        self.get_boost_actor_id(player_id).and_then(|actor_id| {
371            let boost_state = self.get_actor_state(&actor_id)?;
372            get_derived_attribute!(
373                boost_state.derived_attributes,
374                BOOST_AMOUNT_KEY,
375                boxcars::Attribute::Float
376            )
377            .cloned()
378        })
379    }
380
381    /// Returns the previous boost amount recorded for the player in raw replay units.
382    pub fn get_player_last_boost_level(&self, player_id: &PlayerId) -> SubtrActorResult<f32> {
383        self.get_boost_actor_id(player_id).and_then(|actor_id| {
384            let boost_state = self.get_actor_state(&actor_id)?;
385            get_derived_attribute!(
386                boost_state.derived_attributes,
387                LAST_BOOST_AMOUNT_KEY,
388                boxcars::Attribute::Byte
389            )
390            .map(|value| *value as f32)
391        })
392    }
393
394    /// Returns the player's boost level scaled to the conventional 0.0-100.0 range.
395    pub fn get_player_boost_percentage(&self, player_id: &PlayerId) -> SubtrActorResult<f32> {
396        self.get_player_boost_level(player_id)
397            .map(boost_amount_to_percent)
398    }
399
400    /// Returns the player's match assists counter.
401    pub fn get_player_match_assists(&self, player_id: &PlayerId) -> SubtrActorResult<i32> {
402        self.get_player_int_stat(player_id, MATCH_ASSISTS_KEY)
403    }
404
405    /// Returns the player's match goals counter.
406    pub fn get_player_match_goals(&self, player_id: &PlayerId) -> SubtrActorResult<i32> {
407        self.get_player_int_stat(player_id, MATCH_GOALS_KEY)
408    }
409
410    /// Returns the player's match saves counter.
411    pub fn get_player_match_saves(&self, player_id: &PlayerId) -> SubtrActorResult<i32> {
412        self.get_player_int_stat(player_id, MATCH_SAVES_KEY)
413    }
414
415    /// Returns the player's match score counter.
416    pub fn get_player_match_score(&self, player_id: &PlayerId) -> SubtrActorResult<i32> {
417        self.get_player_int_stat(player_id, MATCH_SCORE_KEY)
418    }
419
420    /// Returns the player's match shots counter.
421    pub fn get_player_match_shots(&self, player_id: &PlayerId) -> SubtrActorResult<i32> {
422        self.get_player_int_stat(player_id, MATCH_SHOTS_KEY)
423    }
424
425    /// Returns the team number recorded as the last ball-touching side.
426    pub fn get_ball_hit_team_num(&self) -> SubtrActorResult<u8> {
427        let ball_actor_id = self.get_ball_actor_id()?;
428        get_actor_attribute_matching!(
429            self,
430            &ball_actor_id,
431            BALL_HIT_TEAM_NUM_KEY,
432            boxcars::Attribute::Byte
433        )
434        .cloned()
435    }
436
437    /// Returns the team number currently marked as having been scored on.
438    pub fn get_scored_on_team_num(&self) -> SubtrActorResult<u8> {
439        get_actor_attribute_matching!(
440            self,
441            &self.get_metadata_actor_id()?,
442            REPLICATED_SCORED_ON_TEAM_KEY,
443            boxcars::Attribute::Byte
444        )
445        .cloned()
446    }
447
448    /// Returns a component actor's active byte.
449    pub fn get_component_active(&self, actor_id: &boxcars::ActorId) -> SubtrActorResult<u8> {
450        get_actor_attribute_matching!(
451            self,
452            &actor_id,
453            COMPONENT_ACTIVE_KEY,
454            boxcars::Attribute::Byte
455        )
456        .cloned()
457    }
458
459    /// Returns the active byte for the player's boost component.
460    pub fn get_boost_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
461        self.get_boost_actor_id(player_id)
462            .and_then(|actor_id| self.get_component_active(&actor_id))
463    }
464
465    /// Returns the active byte for the player's jump component.
466    pub fn get_jump_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
467        self.get_jump_actor_id(player_id)
468            .and_then(|actor_id| self.get_component_active(&actor_id))
469    }
470
471    /// Returns the active byte for the player's double-jump component.
472    pub fn get_double_jump_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
473        self.get_double_jump_actor_id(player_id)
474            .and_then(|actor_id| self.get_component_active(&actor_id))
475    }
476
477    /// Returns the active byte for the player's dodge component.
478    pub fn get_dodge_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
479        self.get_dodge_actor_id(player_id)
480            .and_then(|actor_id| self.get_component_active(&actor_id))
481    }
482
483    /// Returns whether the player's handbrake / powerslide flag is active.
484    pub fn get_powerslide_active(&self, player_id: &PlayerId) -> SubtrActorResult<bool> {
485        get_actor_attribute_matching!(
486            self,
487            &self.get_car_actor_id(player_id)?,
488            HANDBRAKE_KEY,
489            boxcars::Attribute::Boolean
490        )
491        .cloned()
492    }
493}