Skip to main content

subtr_actor/processor/
queries.rs

1use super::*;
2
3impl<'a> ReplayProcessor<'a> {
4    /// Searches forward or backward for the next update of a specific actor property.
5    pub fn find_update_in_direction(
6        &self,
7        current_index: usize,
8        actor_id: &boxcars::ActorId,
9        object_id: &boxcars::ObjectId,
10        direction: SearchDirection,
11    ) -> SubtrActorResult<(boxcars::Attribute, usize)> {
12        let frames = self
13            .replay
14            .network_frames
15            .as_ref()
16            .ok_or(SubtrActorError::new(
17                SubtrActorErrorVariant::NoNetworkFrames,
18            ))?;
19        match direction {
20            SearchDirection::Forward => {
21                for index in (current_index + 1)..frames.frames.len() {
22                    if let Some(attribute) = frames.frames[index]
23                        .updated_actors
24                        .iter()
25                        .find(|update| {
26                            &update.actor_id == actor_id && &update.object_id == object_id
27                        })
28                        .map(|update| update.attribute.clone())
29                    {
30                        return Ok((attribute, index));
31                    }
32                }
33            }
34            SearchDirection::Backward => {
35                for index in (0..current_index).rev() {
36                    if let Some(attribute) = frames.frames[index]
37                        .updated_actors
38                        .iter()
39                        .find(|update| {
40                            &update.actor_id == actor_id && &update.object_id == object_id
41                        })
42                        .map(|update| update.attribute.clone())
43                    {
44                        return Ok((attribute, index));
45                    }
46                }
47            }
48        }
49
50        SubtrActorError::new_result(SubtrActorErrorVariant::NoUpdateAfterFrame {
51            actor_id: *actor_id,
52            object_id: *object_id,
53            frame_index: current_index,
54        })
55    }
56
57    /// Resolves a car actor id back to the owning player id.
58    pub fn get_player_id_from_car_id(
59        &self,
60        actor_id: &boxcars::ActorId,
61    ) -> SubtrActorResult<PlayerId> {
62        self.get_player_id_from_actor_id(&self.get_player_actor_id_from_car_actor_id(actor_id)?)
63    }
64
65    /// Resolves a player-controller actor id back to the owning player id.
66    pub(crate) fn get_player_id_from_actor_id(
67        &self,
68        actor_id: &boxcars::ActorId,
69    ) -> SubtrActorResult<PlayerId> {
70        for (player_id, player_actor_id) in self.player_to_actor_id.iter() {
71            if actor_id == player_actor_id {
72                return Ok(player_id.clone());
73            }
74        }
75        SubtrActorError::new_result(SubtrActorErrorVariant::NoMatchingPlayerId {
76            actor_id: *actor_id,
77        })
78    }
79
80    fn get_player_actor_id_from_car_actor_id(
81        &self,
82        actor_id: &boxcars::ActorId,
83    ) -> SubtrActorResult<boxcars::ActorId> {
84        self.car_to_player.get(actor_id).copied().ok_or_else(|| {
85            SubtrActorError::new(SubtrActorErrorVariant::NoMatchingPlayerId {
86                actor_id: *actor_id,
87            })
88        })
89    }
90
91    /// Returns whether a demolish has already been recorded within the dedupe window.
92    pub(crate) fn demolish_is_known(&self, demo: &DemolishAttribute, frame_index: usize) -> bool {
93        self.known_demolishes
94            .iter()
95            .any(|(existing, existing_frame_index)| {
96                existing == demo
97                    && frame_index
98                        .checked_sub(*existing_frame_index)
99                        .or_else(|| existing_frame_index.checked_sub(frame_index))
100                        .unwrap()
101                        < MAX_DEMOLISH_KNOWN_FRAMES_PASSED
102            })
103    }
104
105    /// Returns the demolish attribute encoding currently used by the replay, if known.
106    pub fn get_demolish_format(&self) -> Option<DemolishFormat> {
107        self.demolish_format
108    }
109
110    /// Returns the boost-pad events detected while processing the current frame.
111    pub fn current_frame_boost_pad_events(&self) -> &[BoostPadEvent] {
112        &self.current_frame_boost_pad_events
113    }
114
115    /// Returns the touch events detected while processing the current frame.
116    pub fn current_frame_touch_events(&self) -> &[TouchEvent] {
117        &self.current_frame_touch_events
118    }
119
120    /// Returns the dodge-refresh events detected while processing the current frame.
121    pub fn current_frame_dodge_refreshed_events(&self) -> &[DodgeRefreshedEvent] {
122        &self.current_frame_dodge_refreshed_events
123    }
124
125    /// Returns the goal events detected while processing the current frame.
126    pub fn current_frame_goal_events(&self) -> &[GoalEvent] {
127        &self.current_frame_goal_events
128    }
129
130    /// Returns the player stat events detected while processing the current frame.
131    pub fn current_frame_player_stat_events(&self) -> &[PlayerStatEvent] {
132        &self.current_frame_player_stat_events
133    }
134
135    /// Inspects current actor state to infer which demolish attribute format is present.
136    pub fn detect_demolish_format(&self) -> Option<DemolishFormat> {
137        let actors = self.iter_actors_by_type_err(CAR_TYPE).ok()?;
138        for (_actor_id, state) in actors {
139            if get_attribute_errors_expected!(
140                self,
141                &state.attributes,
142                DEMOLISH_EXTENDED_KEY,
143                boxcars::Attribute::DemolishExtended
144            )
145            .is_ok()
146            {
147                return Some(DemolishFormat::Extended);
148            }
149            if get_attribute_errors_expected!(
150                self,
151                &state.attributes,
152                DEMOLISH_GOAL_EXPLOSION_KEY,
153                boxcars::Attribute::DemolishFx
154            )
155            .is_ok()
156            {
157                return Some(DemolishFormat::Fx);
158            }
159        }
160        None
161    }
162
163    /// Returns an iterator over currently active demolish attributes in actor state.
164    pub fn get_active_demos(
165        &self,
166    ) -> SubtrActorResult<impl Iterator<Item = DemolishAttribute> + '_> {
167        let format = self.demolish_format;
168        let actors: Vec<_> = self.iter_actors_by_type_err(CAR_TYPE)?.collect();
169        Ok(actors
170            .into_iter()
171            .filter_map(move |(_actor_id, state)| match format {
172                Some(DemolishFormat::Extended) => get_attribute_errors_expected!(
173                    self,
174                    &state.attributes,
175                    DEMOLISH_EXTENDED_KEY,
176                    boxcars::Attribute::DemolishExtended
177                )
178                .ok()
179                .map(|demo| DemolishAttribute::Extended(**demo)),
180                Some(DemolishFormat::Fx) => get_attribute_errors_expected!(
181                    self,
182                    &state.attributes,
183                    DEMOLISH_GOAL_EXPLOSION_KEY,
184                    boxcars::Attribute::DemolishFx
185                )
186                .ok()
187                .map(|demo| DemolishAttribute::Fx(**demo)),
188                None => None,
189            }))
190    }
191
192    fn get_frame(&self, frame_index: usize) -> SubtrActorResult<&boxcars::Frame> {
193        self.replay
194            .network_frames
195            .as_ref()
196            .ok_or(SubtrActorError::new(
197                SubtrActorErrorVariant::NoNetworkFrames,
198            ))?
199            .frames
200            .get(frame_index)
201            .ok_or(SubtrActorError::new(
202                SubtrActorErrorVariant::FrameIndexOutOfBounds,
203            ))
204    }
205
206    fn velocities_applied_rigid_body(
207        &self,
208        rigid_body: &boxcars::RigidBody,
209        rb_frame_index: usize,
210        target_time: f32,
211    ) -> SubtrActorResult<boxcars::RigidBody> {
212        let rb_frame = self.get_frame(rb_frame_index)?;
213        let interpolation_amount = target_time - rb_frame.time;
214        Ok(self.normalize_rigid_body(&apply_velocities_to_rigid_body(
215            rigid_body,
216            interpolation_amount,
217        )))
218    }
219
220    /// Interpolates an arbitrary actor rigid body to the requested replay time.
221    pub fn get_interpolated_actor_rigid_body(
222        &self,
223        actor_id: &boxcars::ActorId,
224        time: f32,
225        close_enough: f32,
226    ) -> SubtrActorResult<boxcars::RigidBody> {
227        let (frame_body, frame_index) = self.get_actor_rigid_body(actor_id)?;
228        let frame_time = self.get_frame(*frame_index)?.time;
229        let time_and_frame_difference = time - frame_time;
230
231        if time_and_frame_difference.abs() <= close_enough.abs() {
232            return Ok(self.normalize_rigid_body(frame_body));
233        }
234
235        let search_direction = if time_and_frame_difference > 0.0 {
236            util::SearchDirection::Forward
237        } else {
238            util::SearchDirection::Backward
239        };
240
241        let object_id = self.get_object_id_for_key(RIGID_BODY_STATE_KEY)?;
242
243        let (attribute, found_frame) =
244            self.find_update_in_direction(*frame_index, actor_id, object_id, search_direction)?;
245        let found_time = self.get_frame(found_frame)?.time;
246
247        let found_body = attribute_match!(attribute, boxcars::Attribute::RigidBody)?;
248
249        if (found_time - time).abs() <= close_enough {
250            return Ok(self.normalize_rigid_body(&found_body));
251        }
252
253        let (start_body, start_time, end_body, end_time) = match search_direction {
254            util::SearchDirection::Forward => (frame_body, frame_time, &found_body, found_time),
255            util::SearchDirection::Backward => (&found_body, found_time, frame_body, frame_time),
256        };
257
258        util::get_interpolated_rigid_body(start_body, start_time, end_body, end_time, time)
259            .map(|rigid_body| self.normalize_rigid_body(&rigid_body))
260    }
261
262    /// Looks up the object id associated with a replay property name.
263    pub fn get_object_id_for_key(
264        &self,
265        name: &'static str,
266    ) -> SubtrActorResult<&boxcars::ObjectId> {
267        self.name_to_object_id
268            .get(name)
269            .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::ObjectIdNotFound { name }))
270    }
271
272    /// Returns the actor ids currently associated with a named object type.
273    pub fn get_actor_ids_by_type(
274        &self,
275        name: &'static str,
276    ) -> SubtrActorResult<&[boxcars::ActorId]> {
277        self.get_object_id_for_key(name)
278            .map(|object_id| self.get_actor_ids_by_object_id(object_id))
279    }
280
281    fn get_actor_ids_by_object_id(&self, object_id: &boxcars::ObjectId) -> &[boxcars::ActorId] {
282        self.actor_state
283            .actor_ids_by_type
284            .get(object_id)
285            .map(|v| &v[..])
286            .unwrap_or_else(|| &EMPTY_ACTOR_IDS)
287    }
288
289    /// Returns the current modeled state for an actor id.
290    pub(crate) fn get_actor_state(
291        &self,
292        actor_id: &boxcars::ActorId,
293    ) -> SubtrActorResult<&ActorState> {
294        self.actor_state.actor_states.get(actor_id).ok_or_else(|| {
295            SubtrActorError::new(SubtrActorErrorVariant::NoStateForActorId {
296                actor_id: *actor_id,
297            })
298        })
299    }
300
301    /// Returns current or recently deleted modeled state for an actor id.
302    pub(crate) fn get_actor_state_or_recently_deleted(
303        &self,
304        actor_id: &boxcars::ActorId,
305    ) -> SubtrActorResult<&ActorState> {
306        self.actor_state
307            .actor_states
308            .get(actor_id)
309            .or_else(|| self.actor_state.recently_deleted_actor_states.get(actor_id))
310            .ok_or_else(|| {
311                SubtrActorError::new(SubtrActorErrorVariant::NoStateForActorId {
312                    actor_id: *actor_id,
313                })
314            })
315    }
316
317    fn get_actor_attribute<'b>(
318        &'b self,
319        actor_id: &boxcars::ActorId,
320        property: &'static str,
321    ) -> SubtrActorResult<&'b boxcars::Attribute> {
322        self.get_attribute(&self.get_actor_state(actor_id)?.attributes, property)
323    }
324
325    /// Reads a property from an actor or derived-attribute map by property name.
326    pub fn get_attribute<'b>(
327        &'b self,
328        map: &'b HashMap<boxcars::ObjectId, (boxcars::Attribute, usize)>,
329        property: &'static str,
330    ) -> SubtrActorResult<&'b boxcars::Attribute> {
331        self.get_attribute_and_updated(map, property).map(|v| &v.0)
332    }
333
334    /// Reads a property and the frame index when it was last updated.
335    pub fn get_attribute_and_updated<'b>(
336        &'b self,
337        map: &'b HashMap<boxcars::ObjectId, (boxcars::Attribute, usize)>,
338        property: &'static str,
339    ) -> SubtrActorResult<&'b (boxcars::Attribute, usize)> {
340        let attribute_object_id = self.get_object_id_for_key(property)?;
341        map.get(attribute_object_id).ok_or_else(|| {
342            SubtrActorError::new(SubtrActorErrorVariant::PropertyNotFoundInState { property })
343        })
344    }
345
346    /// Scans the actor graph for the first actor that matches a known ball type.
347    pub(crate) fn find_ball_actor(&self) -> Option<boxcars::ActorId> {
348        BALL_TYPES
349            .iter()
350            .filter_map(|ball_type| self.iter_actors_by_type(ball_type))
351            .flatten()
352            .map(|(actor_id, _)| *actor_id)
353            .next()
354    }
355
356    /// Returns the tracked actor id for the replay ball.
357    pub fn get_ball_actor_id(&self) -> SubtrActorResult<boxcars::ActorId> {
358        self.ball_actor_id.ok_or(SubtrActorError::new(
359            SubtrActorErrorVariant::BallActorNotFound,
360        ))
361    }
362
363    /// Returns the main game metadata actor id.
364    pub fn get_metadata_actor_id(&self) -> SubtrActorResult<&boxcars::ActorId> {
365        self.get_actor_ids_by_type(GAME_TYPE)?
366            .iter()
367            .next()
368            .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::NoGameActor))
369    }
370
371    /// Returns the actor id associated with a player id.
372    pub fn get_player_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
373        self.player_to_actor_id
374            .get(player_id)
375            .ok_or_else(|| {
376                SubtrActorError::new(SubtrActorErrorVariant::ActorNotFound {
377                    name: "ActorId",
378                    player_id: player_id.clone(),
379                })
380            })
381            .cloned()
382    }
383
384    /// Returns the car actor id currently associated with a player.
385    pub fn get_car_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
386        self.player_to_car
387            .get(&self.get_player_actor_id(player_id)?)
388            .ok_or_else(|| {
389                SubtrActorError::new(SubtrActorErrorVariant::ActorNotFound {
390                    name: "Car",
391                    player_id: player_id.clone(),
392                })
393            })
394            .cloned()
395    }
396
397    /// Resolves a player to a connected component actor through the supplied mapping.
398    pub fn get_car_connected_actor_id(
399        &self,
400        player_id: &PlayerId,
401        map: &HashMap<boxcars::ActorId, boxcars::ActorId>,
402        name: &'static str,
403    ) -> SubtrActorResult<boxcars::ActorId> {
404        map.get(&self.get_car_actor_id(player_id)?)
405            .ok_or_else(|| {
406                SubtrActorError::new(SubtrActorErrorVariant::ActorNotFound {
407                    name,
408                    player_id: player_id.clone(),
409                })
410            })
411            .cloned()
412    }
413
414    /// Returns the player's boost component actor id.
415    pub fn get_boost_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
416        self.get_car_connected_actor_id(player_id, &self.car_to_boost, "Boost")
417    }
418
419    /// Returns the player's jump component actor id.
420    pub fn get_jump_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
421        self.get_car_connected_actor_id(player_id, &self.car_to_jump, "Jump")
422    }
423
424    /// Returns the player's double-jump component actor id.
425    pub fn get_double_jump_actor_id(
426        &self,
427        player_id: &PlayerId,
428    ) -> SubtrActorResult<boxcars::ActorId> {
429        self.get_car_connected_actor_id(player_id, &self.car_to_double_jump, "Double Jump")
430    }
431
432    /// Returns the player's dodge component actor id.
433    pub fn get_dodge_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
434        self.get_car_connected_actor_id(player_id, &self.car_to_dodge, "Dodge")
435    }
436
437    /// Returns an actor's rigid body together with the frame index of its last update.
438    pub fn get_actor_rigid_body(
439        &self,
440        actor_id: &boxcars::ActorId,
441    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
442        get_attribute_and_updated!(
443            self,
444            &self.get_actor_state(actor_id)?.attributes,
445            RIGID_BODY_STATE_KEY,
446            boxcars::Attribute::RigidBody
447        )
448    }
449
450    /// Like [`Self::get_actor_rigid_body`], but falls back to recently deleted actor state.
451    pub fn get_actor_rigid_body_or_recently_deleted(
452        &self,
453        actor_id: &boxcars::ActorId,
454    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
455        get_attribute_and_updated!(
456            self,
457            &self
458                .get_actor_state_or_recently_deleted(actor_id)?
459                .attributes,
460            RIGID_BODY_STATE_KEY,
461            boxcars::Attribute::RigidBody
462        )
463    }
464
465    /// Iterates over players in the stable team-zero, then team-one ordering.
466    pub fn iter_player_ids_in_order(&self) -> impl Iterator<Item = &PlayerId> {
467        self.team_zero.iter().chain(self.team_one.iter())
468    }
469
470    /// Counts currently in-game players per team from live actor state.
471    pub fn current_in_game_team_player_counts(&self) -> [usize; 2] {
472        let mut counts = [0, 0];
473        let Ok(player_actor_ids) = self.get_actor_ids_by_type(PLAYER_TYPE) else {
474            return counts;
475        };
476        let mut seen_players = std::collections::HashSet::new();
477
478        for actor_id in player_actor_ids {
479            let Ok(player_id) = self.get_player_id_from_actor_id(actor_id) else {
480                continue;
481            };
482            if !seen_players.insert(player_id) {
483                continue;
484            }
485
486            let Some(team_actor_id) = self.player_to_team.get(actor_id) else {
487                continue;
488            };
489            let Ok(team_state) = self.get_actor_state(team_actor_id) else {
490                continue;
491            };
492            let Some(team_name) = self.object_id_to_name.get(&team_state.object_id) else {
493                continue;
494            };
495
496            match team_name.chars().last() {
497                Some('0') => counts[0] += 1,
498                Some('1') => counts[1] += 1,
499                _ => {}
500            }
501        }
502
503        counts
504    }
505
506    /// Returns the number of players in the stored replay ordering.
507    pub fn player_count(&self) -> usize {
508        self.iter_player_ids_in_order().count()
509    }
510
511    /// Returns a map from player ids to their resolved display names.
512    pub fn get_player_names(&self) -> HashMap<PlayerId, String> {
513        self.iter_player_ids_in_order()
514            .filter_map(|player_id| {
515                self.get_player_name(player_id)
516                    .ok()
517                    .map(|name| (player_id.clone(), name))
518            })
519            .collect()
520    }
521
522    /// Iterates over actors of a named object type, returning an error if the type is unknown.
523    pub(crate) fn iter_actors_by_type_err(
524        &self,
525        name: &'static str,
526    ) -> SubtrActorResult<impl Iterator<Item = (&boxcars::ActorId, &ActorState)>> {
527        Ok(self.iter_actors_by_object_id(self.get_object_id_for_key(name)?))
528    }
529
530    /// Iterates over actors of a named object type, if that type exists in the replay.
531    pub fn iter_actors_by_type(
532        &self,
533        name: &'static str,
534    ) -> Option<impl Iterator<Item = (&boxcars::ActorId, &ActorState)>> {
535        self.iter_actors_by_type_err(name).ok()
536    }
537
538    /// Iterates over actors for a concrete object id.
539    pub fn iter_actors_by_object_id<'b>(
540        &'b self,
541        object_id: &'b boxcars::ObjectId,
542    ) -> impl Iterator<Item = (&'b boxcars::ActorId, &'b ActorState)> + 'b {
543        let actor_ids = self
544            .actor_state
545            .actor_ids_by_type
546            .get(object_id)
547            .map(|v| &v[..])
548            .unwrap_or_else(|| &EMPTY_ACTOR_IDS);
549
550        actor_ids
551            .iter()
552            .map(move |id| (id, self.actor_state.actor_states.get(id).unwrap()))
553    }
554
555    /// Returns the replicated match clock in whole seconds.
556    pub fn get_seconds_remaining(&self) -> SubtrActorResult<i32> {
557        get_actor_attribute_matching!(
558            self,
559            self.get_metadata_actor_id()?,
560            SECONDS_REMAINING_KEY,
561            boxcars::Attribute::Int
562        )
563        .cloned()
564    }
565
566    /// Returns the replicated game-state enum value from the metadata actor.
567    pub fn get_replicated_state_name(&self) -> SubtrActorResult<i32> {
568        get_actor_attribute_matching!(
569            self,
570            self.get_metadata_actor_id()?,
571            REPLICATED_STATE_NAME_KEY,
572            boxcars::Attribute::Int
573        )
574        .cloned()
575    }
576
577    /// Returns the replicated kickoff countdown / time-remaining field.
578    pub fn get_replicated_game_state_time_remaining(&self) -> SubtrActorResult<i32> {
579        get_actor_attribute_matching!(
580            self,
581            self.get_metadata_actor_id()?,
582            REPLICATED_GAME_STATE_TIME_REMAINING_KEY,
583            boxcars::Attribute::Int
584        )
585        .cloned()
586    }
587
588    /// Returns whether the replay currently reports that the ball has been hit.
589    pub fn get_ball_has_been_hit(&self) -> SubtrActorResult<bool> {
590        get_actor_attribute_matching!(
591            self,
592            self.get_metadata_actor_id()?,
593            BALL_HAS_BEEN_HIT_KEY,
594            boxcars::Attribute::Boolean
595        )
596        .cloned()
597    }
598
599    /// Returns the ball actor's ignore-syncing flag.
600    pub fn get_ignore_ball_syncing(&self) -> SubtrActorResult<bool> {
601        let actor_id = self.get_ball_actor_id()?;
602        get_actor_attribute_matching!(
603            self,
604            &actor_id,
605            IGNORE_SYNCING_KEY,
606            boxcars::Attribute::Boolean
607        )
608        .cloned()
609    }
610
611    /// Returns the current ball rigid body from live actor state.
612    pub fn get_ball_rigid_body(&self) -> SubtrActorResult<&boxcars::RigidBody> {
613        self.ball_actor_id
614            .ok_or(SubtrActorError::new(
615                SubtrActorErrorVariant::BallActorNotFound,
616            ))
617            .and_then(|actor_id| self.get_actor_rigid_body(&actor_id).map(|v| v.0))
618    }
619
620    /// Returns the current ball rigid body after spatial normalization.
621    pub fn get_normalized_ball_rigid_body(&self) -> SubtrActorResult<boxcars::RigidBody> {
622        self.get_ball_rigid_body()
623            .map(|rigid_body| self.normalize_rigid_body(rigid_body))
624    }
625
626    /// Returns whether a non-sleeping ball rigid body is currently available.
627    pub fn ball_rigid_body_exists(&self) -> SubtrActorResult<bool> {
628        Ok(self
629            .get_ball_rigid_body()
630            .map(|rb| !rb.sleeping)
631            .unwrap_or(false))
632    }
633
634    /// Returns the current ball rigid body and the frame where it was last updated.
635    pub fn get_ball_rigid_body_and_updated(
636        &self,
637    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
638        self.ball_actor_id
639            .ok_or(SubtrActorError::new(
640                SubtrActorErrorVariant::BallActorNotFound,
641            ))
642            .and_then(|actor_id| {
643                get_attribute_and_updated!(
644                    self,
645                    &self.get_actor_state(&actor_id)?.attributes,
646                    RIGID_BODY_STATE_KEY,
647                    boxcars::Attribute::RigidBody
648                )
649            })
650    }
651
652    /// Applies stored ball velocity forward to the requested time.
653    pub fn get_velocity_applied_ball_rigid_body(
654        &self,
655        target_time: f32,
656    ) -> SubtrActorResult<boxcars::RigidBody> {
657        let (current_rigid_body, frame_index) = self.get_ball_rigid_body_and_updated()?;
658        self.velocities_applied_rigid_body(current_rigid_body, *frame_index, target_time)
659    }
660
661    /// Interpolates the ball rigid body to the requested time.
662    pub fn get_interpolated_ball_rigid_body(
663        &self,
664        time: f32,
665        close_enough: f32,
666    ) -> SubtrActorResult<boxcars::RigidBody> {
667        self.get_interpolated_actor_rigid_body(&self.get_ball_actor_id()?, time, close_enough)
668    }
669
670    /// Returns the player's replicated display name.
671    pub fn get_player_name(&self, player_id: &PlayerId) -> SubtrActorResult<String> {
672        get_actor_attribute_matching!(
673            self,
674            &self.get_player_actor_id(player_id)?,
675            PLAYER_NAME_KEY,
676            boxcars::Attribute::String
677        )
678        .cloned()
679    }
680
681    fn get_player_int_stat(
682        &self,
683        player_id: &PlayerId,
684        key: &'static str,
685    ) -> SubtrActorResult<i32> {
686        get_actor_attribute_matching!(
687            self,
688            &self.get_player_actor_id(player_id)?,
689            key,
690            boxcars::Attribute::Int
691        )
692        .cloned()
693    }
694
695    /// Returns the replay object-name key for the player's team actor.
696    pub fn get_player_team_key(&self, player_id: &PlayerId) -> SubtrActorResult<String> {
697        let team_actor_id = self
698            .player_to_team
699            .get(&self.get_player_actor_id(player_id)?)
700            .ok_or_else(|| {
701                SubtrActorError::new(SubtrActorErrorVariant::UnknownPlayerTeam {
702                    player_id: player_id.clone(),
703                })
704            })?;
705        let state = self.get_actor_state(team_actor_id)?;
706        self.object_id_to_name
707            .get(&state.object_id)
708            .ok_or_else(|| {
709                SubtrActorError::new(SubtrActorErrorVariant::UnknownPlayerTeam {
710                    player_id: player_id.clone(),
711                })
712            })
713            .cloned()
714    }
715
716    /// Returns whether the player belongs to team 0.
717    pub fn get_player_is_team_0(&self, player_id: &PlayerId) -> SubtrActorResult<bool> {
718        Ok(self
719            .get_player_team_key(player_id)?
720            .chars()
721            .last()
722            .ok_or_else(|| {
723                SubtrActorError::new(SubtrActorErrorVariant::EmptyTeamName {
724                    player_id: player_id.clone(),
725                })
726            })?
727            == '0')
728    }
729
730    /// Returns the team actor id for the requested side.
731    pub(crate) fn get_team_actor_id_for_side(
732        &self,
733        is_team_0: bool,
734    ) -> SubtrActorResult<boxcars::ActorId> {
735        let player_id = if is_team_0 {
736            self.team_zero.first()
737        } else {
738            self.team_one.first()
739        }
740        .ok_or(SubtrActorError::new(SubtrActorErrorVariant::NoGameActor))?;
741
742        self.player_to_team
743            .get(&self.get_player_actor_id(player_id)?)
744            .copied()
745            .ok_or_else(|| {
746                SubtrActorError::new(SubtrActorErrorVariant::ActorNotFound {
747                    name: "Team",
748                    player_id: player_id.clone(),
749                })
750            })
751    }
752
753    /// Returns the score for the requested team side.
754    pub fn get_team_score(&self, is_team_0: bool) -> SubtrActorResult<i32> {
755        let team_actor_id = self.get_team_actor_id_for_side(is_team_0)?;
756        get_actor_attribute_matching!(
757            self,
758            &team_actor_id,
759            TEAM_GAME_SCORE_KEY,
760            boxcars::Attribute::Int
761        )
762        .cloned()
763    }
764
765    /// Returns `(team_zero_score, team_one_score)`.
766    pub fn get_team_scores(&self) -> SubtrActorResult<(i32, i32)> {
767        Ok((self.get_team_score(true)?, self.get_team_score(false)?))
768    }
769
770    /// Returns the player's current car rigid body.
771    pub fn get_player_rigid_body(
772        &self,
773        player_id: &PlayerId,
774    ) -> SubtrActorResult<&boxcars::RigidBody> {
775        self.get_car_actor_id(player_id)
776            .and_then(|actor_id| self.get_actor_rigid_body(&actor_id).map(|v| v.0))
777    }
778
779    /// Returns the player's current car rigid body after spatial normalization.
780    pub fn get_normalized_player_rigid_body(
781        &self,
782        player_id: &PlayerId,
783    ) -> SubtrActorResult<boxcars::RigidBody> {
784        self.get_player_rigid_body(player_id)
785            .map(|rigid_body| self.normalize_rigid_body(rigid_body))
786    }
787
788    /// Returns the player's rigid body and the frame where it was last updated.
789    pub fn get_player_rigid_body_and_updated(
790        &self,
791        player_id: &PlayerId,
792    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
793        self.get_car_actor_id(player_id).and_then(|actor_id| {
794            get_attribute_and_updated!(
795                self,
796                &self.get_actor_state(&actor_id)?.attributes,
797                RIGID_BODY_STATE_KEY,
798                boxcars::Attribute::RigidBody
799            )
800        })
801    }
802
803    /// Like [`Self::get_player_rigid_body_and_updated`], but can use recently deleted state.
804    pub fn get_player_rigid_body_and_updated_or_recently_deleted(
805        &self,
806        player_id: &PlayerId,
807    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
808        self.get_car_actor_id(player_id)
809            .and_then(|actor_id| self.get_actor_rigid_body_or_recently_deleted(&actor_id))
810    }
811
812    /// Applies stored player velocity forward to the requested time.
813    pub fn get_velocity_applied_player_rigid_body(
814        &self,
815        player_id: &PlayerId,
816        target_time: f32,
817    ) -> SubtrActorResult<boxcars::RigidBody> {
818        let (current_rigid_body, frame_index) =
819            self.get_player_rigid_body_and_updated(player_id)?;
820        self.velocities_applied_rigid_body(current_rigid_body, *frame_index, target_time)
821    }
822
823    /// Interpolates the player's car rigid body to the requested time.
824    pub fn get_interpolated_player_rigid_body(
825        &self,
826        player_id: &PlayerId,
827        time: f32,
828        close_enough: f32,
829    ) -> SubtrActorResult<boxcars::RigidBody> {
830        self.get_car_actor_id(player_id).and_then(|car_actor_id| {
831            self.get_interpolated_actor_rigid_body(&car_actor_id, time, close_enough)
832        })
833    }
834
835    /// Returns the player's current boost amount in raw replay units.
836    pub fn get_player_boost_level(&self, player_id: &PlayerId) -> SubtrActorResult<f32> {
837        self.get_boost_actor_id(player_id).and_then(|actor_id| {
838            let boost_state = self.get_actor_state(&actor_id)?;
839            get_derived_attribute!(
840                boost_state.derived_attributes,
841                BOOST_AMOUNT_KEY,
842                boxcars::Attribute::Float
843            )
844            .cloned()
845        })
846    }
847
848    /// Returns the previous boost amount recorded for the player in raw replay units.
849    pub fn get_player_last_boost_level(&self, player_id: &PlayerId) -> SubtrActorResult<f32> {
850        self.get_boost_actor_id(player_id).and_then(|actor_id| {
851            let boost_state = self.get_actor_state(&actor_id)?;
852            get_derived_attribute!(
853                boost_state.derived_attributes,
854                LAST_BOOST_AMOUNT_KEY,
855                boxcars::Attribute::Byte
856            )
857            .map(|value| *value as f32)
858        })
859    }
860
861    /// Returns the player's boost level scaled to the conventional 0.0-100.0 range.
862    pub fn get_player_boost_percentage(&self, player_id: &PlayerId) -> SubtrActorResult<f32> {
863        self.get_player_boost_level(player_id)
864            .map(boost_amount_to_percent)
865    }
866
867    /// Returns the player's match assists counter.
868    pub fn get_player_match_assists(&self, player_id: &PlayerId) -> SubtrActorResult<i32> {
869        self.get_player_int_stat(player_id, MATCH_ASSISTS_KEY)
870    }
871
872    /// Returns the player's match goals counter.
873    pub fn get_player_match_goals(&self, player_id: &PlayerId) -> SubtrActorResult<i32> {
874        self.get_player_int_stat(player_id, MATCH_GOALS_KEY)
875    }
876
877    /// Returns the player's match saves counter.
878    pub fn get_player_match_saves(&self, player_id: &PlayerId) -> SubtrActorResult<i32> {
879        self.get_player_int_stat(player_id, MATCH_SAVES_KEY)
880    }
881
882    /// Returns the player's match score counter.
883    pub fn get_player_match_score(&self, player_id: &PlayerId) -> SubtrActorResult<i32> {
884        self.get_player_int_stat(player_id, MATCH_SCORE_KEY)
885    }
886
887    /// Returns the player's match shots counter.
888    pub fn get_player_match_shots(&self, player_id: &PlayerId) -> SubtrActorResult<i32> {
889        self.get_player_int_stat(player_id, MATCH_SHOTS_KEY)
890    }
891
892    /// Returns the team number recorded as the last ball-touching side.
893    pub fn get_ball_hit_team_num(&self) -> SubtrActorResult<u8> {
894        let ball_actor_id = self.get_ball_actor_id()?;
895        get_actor_attribute_matching!(
896            self,
897            &ball_actor_id,
898            BALL_HIT_TEAM_NUM_KEY,
899            boxcars::Attribute::Byte
900        )
901        .cloned()
902    }
903
904    /// Returns the team number currently marked as having been scored on.
905    pub fn get_scored_on_team_num(&self) -> SubtrActorResult<u8> {
906        get_actor_attribute_matching!(
907            self,
908            self.get_metadata_actor_id()?,
909            REPLICATED_SCORED_ON_TEAM_KEY,
910            boxcars::Attribute::Byte
911        )
912        .cloned()
913    }
914
915    /// Returns a component actor's active byte.
916    pub fn get_component_active(&self, actor_id: &boxcars::ActorId) -> SubtrActorResult<u8> {
917        get_actor_attribute_matching!(
918            self,
919            &actor_id,
920            COMPONENT_ACTIVE_KEY,
921            boxcars::Attribute::Byte
922        )
923        .cloned()
924    }
925
926    /// Returns the active byte for the player's boost component.
927    pub fn get_boost_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
928        self.get_boost_actor_id(player_id)
929            .and_then(|actor_id| self.get_component_active(&actor_id))
930    }
931
932    /// Returns the active byte for the player's jump component.
933    pub fn get_jump_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
934        self.get_jump_actor_id(player_id)
935            .and_then(|actor_id| self.get_component_active(&actor_id))
936    }
937
938    /// Returns the active byte for the player's double-jump component.
939    pub fn get_double_jump_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
940        self.get_double_jump_actor_id(player_id)
941            .and_then(|actor_id| self.get_component_active(&actor_id))
942    }
943
944    /// Returns the active byte for the player's dodge component.
945    pub fn get_dodge_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
946        self.get_dodge_actor_id(player_id)
947            .and_then(|actor_id| self.get_component_active(&actor_id))
948    }
949
950    /// Returns whether the player's handbrake / powerslide flag is active.
951    pub fn get_powerslide_active(&self, player_id: &PlayerId) -> SubtrActorResult<bool> {
952        get_actor_attribute_matching!(
953            self,
954            &self.get_car_actor_id(player_id)?,
955            HANDBRAKE_KEY,
956            boxcars::Attribute::Boolean
957        )
958        .cloned()
959    }
960}