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    pub(crate) fn get_player_car_hitbox(&self, player_id: &PlayerId) -> CarHitbox {
184        car_hitbox_for_body_id_or_name(
185            self.get_player_loadout_body_id(player_id),
186            self.get_player_loadout_body_name(player_id).as_deref(),
187        )
188        .unwrap_or_else(default_car_hitbox)
189    }
190
191    fn get_player_int_stat(
192        &self,
193        player_id: &PlayerId,
194        key: &'static str,
195    ) -> SubtrActorResult<i32> {
196        get_actor_attribute_matching!(
197            self,
198            &self.get_player_actor_id(player_id)?,
199            key,
200            boxcars::Attribute::Int
201        )
202        .cloned()
203    }
204
205    /// Returns the replay object-name key for the player's team actor.
206    pub fn get_player_team_key(&self, player_id: &PlayerId) -> SubtrActorResult<String> {
207        let team_actor_id = self
208            .player_to_team
209            .get(&self.get_player_actor_id(player_id)?)
210            .ok_or_else(|| {
211                SubtrActorError::new(SubtrActorErrorVariant::UnknownPlayerTeam {
212                    player_id: player_id.clone(),
213                })
214            })?;
215        let state = self.get_actor_state(team_actor_id)?;
216        self.object_id_to_name
217            .get(&state.object_id)
218            .ok_or_else(|| {
219                SubtrActorError::new(SubtrActorErrorVariant::UnknownPlayerTeam {
220                    player_id: player_id.clone(),
221                })
222            })
223            .cloned()
224    }
225
226    /// Returns whether the player belongs to team 0.
227    pub fn get_player_is_team_0(&self, player_id: &PlayerId) -> SubtrActorResult<bool> {
228        Ok(self
229            .get_player_team_key(player_id)?
230            .chars()
231            .last()
232            .ok_or_else(|| {
233                SubtrActorError::new(SubtrActorErrorVariant::EmptyTeamName {
234                    player_id: player_id.clone(),
235                })
236            })?
237            == '0')
238    }
239
240    /// Returns the team actor id for the requested side.
241    pub(crate) fn get_team_actor_id_for_side(
242        &self,
243        is_team_0: bool,
244    ) -> SubtrActorResult<boxcars::ActorId> {
245        let player_id = if is_team_0 {
246            self.team_zero.first()
247        } else {
248            self.team_one.first()
249        }
250        .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::NoGameActor))?;
251
252        self.player_to_team
253            .get(&self.get_player_actor_id(player_id)?)
254            .copied()
255            .ok_or_else(|| {
256                SubtrActorError::new(SubtrActorErrorVariant::ActorNotFound {
257                    name: "Team",
258                    player_id: player_id.clone(),
259                })
260            })
261    }
262
263    /// Returns the score for the requested team side.
264    pub fn get_team_score(&self, is_team_0: bool) -> SubtrActorResult<i32> {
265        let team_actor_id = self.get_team_actor_id_for_side(is_team_0)?;
266        get_actor_attribute_matching!(
267            self,
268            &team_actor_id,
269            TEAM_GAME_SCORE_KEY,
270            boxcars::Attribute::Int
271        )
272        .cloned()
273    }
274
275    /// Returns `(team_zero_score, team_one_score)`.
276    pub fn get_team_scores(&self) -> SubtrActorResult<(i32, i32)> {
277        Ok((self.get_team_score(true)?, self.get_team_score(false)?))
278    }
279
280    /// Returns the player's current car rigid body.
281    pub fn get_player_rigid_body(
282        &self,
283        player_id: &PlayerId,
284    ) -> SubtrActorResult<&boxcars::RigidBody> {
285        self.get_car_actor_id(player_id)
286            .and_then(|actor_id| self.get_actor_rigid_body(&actor_id).map(|v| v.0))
287    }
288
289    /// Returns the player's current car rigid body after spatial normalization.
290    pub fn get_normalized_player_rigid_body(
291        &self,
292        player_id: &PlayerId,
293    ) -> SubtrActorResult<boxcars::RigidBody> {
294        self.get_player_rigid_body(player_id)
295            .map(|rigid_body| self.normalize_rigid_body(rigid_body))
296    }
297
298    /// Returns the player's current normalized car position.
299    pub(crate) fn get_normalized_player_position(
300        &self,
301        player_id: &PlayerId,
302    ) -> Option<boxcars::Vector3f> {
303        self.get_normalized_player_rigid_body(player_id)
304            .ok()
305            .map(|rigid_body| rigid_body.location)
306    }
307
308    /// Returns the player's rigid body and the frame where it was last updated.
309    pub fn get_player_rigid_body_and_updated(
310        &self,
311        player_id: &PlayerId,
312    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
313        self.get_car_actor_id(player_id).and_then(|actor_id| {
314            get_attribute_and_updated!(
315                self,
316                &self.get_actor_state(&actor_id)?.attributes,
317                RIGID_BODY_STATE_KEY,
318                boxcars::Attribute::RigidBody
319            )
320        })
321    }
322
323    /// Like [`Self::get_player_rigid_body_and_updated`], but can use recently deleted state.
324    pub fn get_player_rigid_body_and_updated_or_recently_deleted(
325        &self,
326        player_id: &PlayerId,
327    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
328        self.get_car_actor_id(player_id)
329            .and_then(|actor_id| self.get_actor_rigid_body_or_recently_deleted(&actor_id))
330    }
331
332    /// Applies stored player velocity forward to the requested time.
333    pub fn get_velocity_applied_player_rigid_body(
334        &self,
335        player_id: &PlayerId,
336        target_time: f32,
337    ) -> SubtrActorResult<boxcars::RigidBody> {
338        let (current_rigid_body, frame_index) =
339            self.get_player_rigid_body_and_updated(player_id)?;
340        self.velocities_applied_rigid_body(current_rigid_body, *frame_index, target_time)
341    }
342
343    /// Interpolates the player's car rigid body to the requested time.
344    pub fn get_interpolated_player_rigid_body(
345        &self,
346        player_id: &PlayerId,
347        time: f32,
348        close_enough: f32,
349    ) -> SubtrActorResult<boxcars::RigidBody> {
350        self.get_car_actor_id(player_id).and_then(|car_actor_id| {
351            self.get_interpolated_actor_rigid_body(&car_actor_id, time, close_enough)
352        })
353    }
354
355    /// Returns the player's current boost amount in raw replay units.
356    pub fn get_player_boost_level(&self, player_id: &PlayerId) -> SubtrActorResult<f32> {
357        self.get_boost_actor_id(player_id).and_then(|actor_id| {
358            let boost_state = self.get_actor_state(&actor_id)?;
359            get_derived_attribute!(
360                boost_state.derived_attributes,
361                BOOST_AMOUNT_KEY,
362                boxcars::Attribute::Float
363            )
364            .cloned()
365        })
366    }
367
368    /// Returns the previous boost amount recorded for the player in raw replay units.
369    pub fn get_player_last_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                LAST_BOOST_AMOUNT_KEY,
375                boxcars::Attribute::Byte
376            )
377            .map(|value| *value as f32)
378        })
379    }
380
381    /// Returns the player's boost level scaled to the conventional 0.0-100.0 range.
382    pub fn get_player_boost_percentage(&self, player_id: &PlayerId) -> SubtrActorResult<f32> {
383        self.get_player_boost_level(player_id)
384            .map(boost_amount_to_percent)
385    }
386
387    /// Returns the player's match assists counter.
388    pub fn get_player_match_assists(&self, player_id: &PlayerId) -> SubtrActorResult<i32> {
389        self.get_player_int_stat(player_id, MATCH_ASSISTS_KEY)
390    }
391
392    /// Returns the player's match goals counter.
393    pub fn get_player_match_goals(&self, player_id: &PlayerId) -> SubtrActorResult<i32> {
394        self.get_player_int_stat(player_id, MATCH_GOALS_KEY)
395    }
396
397    /// Returns the player's match saves counter.
398    pub fn get_player_match_saves(&self, player_id: &PlayerId) -> SubtrActorResult<i32> {
399        self.get_player_int_stat(player_id, MATCH_SAVES_KEY)
400    }
401
402    /// Returns the player's match score counter.
403    pub fn get_player_match_score(&self, player_id: &PlayerId) -> SubtrActorResult<i32> {
404        self.get_player_int_stat(player_id, MATCH_SCORE_KEY)
405    }
406
407    /// Returns the player's match shots counter.
408    pub fn get_player_match_shots(&self, player_id: &PlayerId) -> SubtrActorResult<i32> {
409        self.get_player_int_stat(player_id, MATCH_SHOTS_KEY)
410    }
411
412    /// Returns the team number recorded as the last ball-touching side.
413    pub fn get_ball_hit_team_num(&self) -> SubtrActorResult<u8> {
414        let ball_actor_id = self.get_ball_actor_id()?;
415        get_actor_attribute_matching!(
416            self,
417            &ball_actor_id,
418            BALL_HIT_TEAM_NUM_KEY,
419            boxcars::Attribute::Byte
420        )
421        .cloned()
422    }
423
424    /// Returns the team number currently marked as having been scored on.
425    pub fn get_scored_on_team_num(&self) -> SubtrActorResult<u8> {
426        get_actor_attribute_matching!(
427            self,
428            &self.get_metadata_actor_id()?,
429            REPLICATED_SCORED_ON_TEAM_KEY,
430            boxcars::Attribute::Byte
431        )
432        .cloned()
433    }
434
435    /// Returns a component actor's active byte.
436    pub fn get_component_active(&self, actor_id: &boxcars::ActorId) -> SubtrActorResult<u8> {
437        get_actor_attribute_matching!(
438            self,
439            &actor_id,
440            COMPONENT_ACTIVE_KEY,
441            boxcars::Attribute::Byte
442        )
443        .cloned()
444    }
445
446    /// Returns the active byte for the player's boost component.
447    pub fn get_boost_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
448        self.get_boost_actor_id(player_id)
449            .and_then(|actor_id| self.get_component_active(&actor_id))
450    }
451
452    /// Returns the active byte for the player's jump component.
453    pub fn get_jump_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
454        self.get_jump_actor_id(player_id)
455            .and_then(|actor_id| self.get_component_active(&actor_id))
456    }
457
458    /// Returns the active byte for the player's double-jump component.
459    pub fn get_double_jump_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
460        self.get_double_jump_actor_id(player_id)
461            .and_then(|actor_id| self.get_component_active(&actor_id))
462    }
463
464    /// Returns the active byte for the player's dodge component.
465    pub fn get_dodge_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
466        self.get_dodge_actor_id(player_id)
467            .and_then(|actor_id| self.get_component_active(&actor_id))
468    }
469
470    /// Returns whether the player's handbrake / powerslide flag is active.
471    pub fn get_powerslide_active(&self, player_id: &PlayerId) -> SubtrActorResult<bool> {
472        get_actor_attribute_matching!(
473            self,
474            &self.get_car_actor_id(player_id)?,
475            HANDBRAKE_KEY,
476            boxcars::Attribute::Boolean
477        )
478        .cloned()
479    }
480}