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        let normalized_rigid_body = self.normalize_rigid_body(rigid_body);
215        Ok(apply_velocities_to_rigid_body(
216            &normalized_rigid_body,
217            interpolation_amount,
218        ))
219    }
220
221    /// Interpolates an arbitrary actor rigid body to the requested replay time.
222    pub fn get_interpolated_actor_rigid_body(
223        &self,
224        actor_id: &boxcars::ActorId,
225        time: f32,
226        close_enough: f32,
227    ) -> SubtrActorResult<boxcars::RigidBody> {
228        let (frame_body, frame_index) = self.get_actor_rigid_body(actor_id)?;
229        let frame_time = self.get_frame(*frame_index)?.time;
230        let time_and_frame_difference = time - frame_time;
231
232        if time_and_frame_difference.abs() <= close_enough.abs() {
233            return Ok(self.normalize_rigid_body(frame_body));
234        }
235
236        let search_direction = if time_and_frame_difference > 0.0 {
237            SearchDirection::Forward
238        } else {
239            SearchDirection::Backward
240        };
241
242        let object_id = self.get_object_id_for_key(RIGID_BODY_STATE_KEY)?;
243
244        let (attribute, found_frame) =
245            self.find_update_in_direction(*frame_index, actor_id, object_id, search_direction)?;
246        let found_time = self.get_frame(found_frame)?.time;
247
248        let found_body = attribute_match!(attribute, boxcars::Attribute::RigidBody)?;
249
250        if (found_time - time).abs() <= close_enough {
251            return Ok(self.normalize_rigid_body(&found_body));
252        }
253
254        let (start_body, start_time, end_body, end_time) = match search_direction {
255            SearchDirection::Forward => (frame_body, frame_time, &found_body, found_time),
256            SearchDirection::Backward => (&found_body, found_time, frame_body, frame_time),
257        };
258        let start_body = self.normalize_rigid_body(start_body);
259        let end_body = self.normalize_rigid_body(end_body);
260
261        get_interpolated_rigid_body(&start_body, start_time, &end_body, end_time, time)
262    }
263
264    /// Looks up the object id associated with a replay property name.
265    pub fn get_object_id_for_key(
266        &self,
267        name: &'static str,
268    ) -> SubtrActorResult<&boxcars::ObjectId> {
269        self.name_to_object_id
270            .get(name)
271            .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::ObjectIdNotFound { name }))
272    }
273
274    /// Returns the actor ids currently associated with a named object type.
275    pub fn get_actor_ids_by_type(
276        &self,
277        name: &'static str,
278    ) -> SubtrActorResult<&[boxcars::ActorId]> {
279        self.get_object_id_for_key(name)
280            .map(|object_id| self.get_actor_ids_by_object_id(object_id))
281    }
282
283    pub(crate) fn get_actor_ids_by_object_id(
284        &self,
285        object_id: &boxcars::ObjectId,
286    ) -> &[boxcars::ActorId] {
287        self.actor_state
288            .actor_ids_by_type
289            .get(object_id)
290            .map(|v| &v[..])
291            .unwrap_or_else(|| &EMPTY_ACTOR_IDS)
292    }
293
294    /// Returns the current modeled state for an actor id.
295    pub(crate) fn get_actor_state(
296        &self,
297        actor_id: &boxcars::ActorId,
298    ) -> SubtrActorResult<&ActorState> {
299        self.actor_state.actor_states.get(actor_id).ok_or_else(|| {
300            SubtrActorError::new(SubtrActorErrorVariant::NoStateForActorId {
301                actor_id: *actor_id,
302            })
303        })
304    }
305
306    /// Returns current or recently deleted modeled state for an actor id.
307    pub(crate) fn get_actor_state_or_recently_deleted(
308        &self,
309        actor_id: &boxcars::ActorId,
310    ) -> SubtrActorResult<&ActorState> {
311        self.actor_state
312            .actor_states
313            .get(actor_id)
314            .or_else(|| self.actor_state.recently_deleted_actor_states.get(actor_id))
315            .ok_or_else(|| {
316                SubtrActorError::new(SubtrActorErrorVariant::NoStateForActorId {
317                    actor_id: *actor_id,
318                })
319            })
320    }
321
322    fn get_actor_attribute<'b>(
323        &'b self,
324        actor_id: &boxcars::ActorId,
325        property: &'static str,
326    ) -> SubtrActorResult<&'b boxcars::Attribute> {
327        self.get_attribute(&self.get_actor_state(actor_id)?.attributes, property)
328    }
329
330    /// Reads a property from an actor or derived-attribute map by property name.
331    pub fn get_attribute<'b>(
332        &'b self,
333        map: &'b HashMap<boxcars::ObjectId, (boxcars::Attribute, usize)>,
334        property: &'static str,
335    ) -> SubtrActorResult<&'b boxcars::Attribute> {
336        self.get_attribute_and_updated(map, property).map(|v| &v.0)
337    }
338
339    /// Reads a property and the frame index when it was last updated.
340    pub fn get_attribute_and_updated<'b>(
341        &'b self,
342        map: &'b HashMap<boxcars::ObjectId, (boxcars::Attribute, usize)>,
343        property: &'static str,
344    ) -> SubtrActorResult<&'b (boxcars::Attribute, usize)> {
345        let attribute_object_id = self.get_object_id_for_key(property)?;
346        map.get(attribute_object_id).ok_or_else(|| {
347            SubtrActorError::new(SubtrActorErrorVariant::PropertyNotFoundInState { property })
348        })
349    }
350
351    /// Scans the actor graph for the first actor that matches a known ball type.
352    pub(crate) fn find_ball_actor(&self) -> Option<boxcars::ActorId> {
353        BALL_TYPES
354            .iter()
355            .filter_map(|ball_type| self.iter_actors_by_type(ball_type))
356            .flatten()
357            .map(|(actor_id, _)| *actor_id)
358            .next()
359    }
360
361    /// Returns the tracked actor id for the replay ball.
362    pub fn get_ball_actor_id(&self) -> SubtrActorResult<boxcars::ActorId> {
363        self.ball_actor_id.ok_or(SubtrActorError::new(
364            SubtrActorErrorVariant::BallActorNotFound,
365        ))
366    }
367
368    /// Returns the main game metadata actor id.
369    pub fn get_metadata_actor_id(&self) -> SubtrActorResult<boxcars::ActorId> {
370        if let Ok(actor_ids) = self.get_actor_ids_by_type(GAME_TYPE) {
371            if let Some(actor_id) = actor_ids.first() {
372                return Ok(*actor_id);
373            }
374        }
375
376        let metadata_object_ids = [
377            self.cached_object_ids.seconds_remaining,
378            self.cached_object_ids.replicated_state_name,
379            self.cached_object_ids.replicated_game_state_time_remaining,
380            self.cached_object_ids.ball_has_been_hit,
381        ];
382
383        self.actor_state
384            .actor_states
385            .iter()
386            .filter_map(|(actor_id, actor_state)| {
387                let metadata_attribute_count = metadata_object_ids
388                    .iter()
389                    .flatten()
390                    .filter(|object_id| actor_state.attributes.contains_key(object_id))
391                    .count();
392                (metadata_attribute_count > 0).then_some((
393                    metadata_attribute_count,
394                    std::cmp::Reverse(*actor_id),
395                    *actor_id,
396                ))
397            })
398            .max()
399            .map(|(_, _, actor_id)| actor_id)
400            .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::NoGameActor))
401    }
402
403    /// Returns the actor id associated with a player id.
404    pub fn get_player_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
405        self.player_to_actor_id
406            .get(player_id)
407            .ok_or_else(|| {
408                SubtrActorError::new(SubtrActorErrorVariant::ActorNotFound {
409                    name: "ActorId",
410                    player_id: player_id.clone(),
411                })
412            })
413            .cloned()
414    }
415
416    /// Returns the car actor id currently associated with a player.
417    pub fn get_car_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
418        self.player_to_car
419            .get(&self.get_player_actor_id(player_id)?)
420            .ok_or_else(|| {
421                SubtrActorError::new(SubtrActorErrorVariant::ActorNotFound {
422                    name: "Car",
423                    player_id: player_id.clone(),
424                })
425            })
426            .cloned()
427    }
428
429    /// Resolves a player to a connected component actor through the supplied mapping.
430    pub fn get_car_connected_actor_id(
431        &self,
432        player_id: &PlayerId,
433        map: &HashMap<boxcars::ActorId, boxcars::ActorId>,
434        name: &'static str,
435    ) -> SubtrActorResult<boxcars::ActorId> {
436        map.get(&self.get_car_actor_id(player_id)?)
437            .ok_or_else(|| {
438                SubtrActorError::new(SubtrActorErrorVariant::ActorNotFound {
439                    name,
440                    player_id: player_id.clone(),
441                })
442            })
443            .cloned()
444    }
445
446    /// Returns the player's boost component actor id.
447    pub fn get_boost_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
448        self.get_car_connected_actor_id(player_id, &self.car_to_boost, "Boost")
449    }
450
451    /// Returns the player's jump component actor id.
452    pub fn get_jump_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
453        self.get_car_connected_actor_id(player_id, &self.car_to_jump, "Jump")
454    }
455
456    /// Returns the player's double-jump component actor id.
457    pub fn get_double_jump_actor_id(
458        &self,
459        player_id: &PlayerId,
460    ) -> SubtrActorResult<boxcars::ActorId> {
461        self.get_car_connected_actor_id(player_id, &self.car_to_double_jump, "Double Jump")
462    }
463
464    /// Returns the player's dodge component actor id.
465    pub fn get_dodge_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
466        self.get_car_connected_actor_id(player_id, &self.car_to_dodge, "Dodge")
467    }
468
469    /// Returns an actor's rigid body together with the frame index of its last update.
470    pub fn get_actor_rigid_body(
471        &self,
472        actor_id: &boxcars::ActorId,
473    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
474        get_attribute_and_updated!(
475            self,
476            &self.get_actor_state(actor_id)?.attributes,
477            RIGID_BODY_STATE_KEY,
478            boxcars::Attribute::RigidBody
479        )
480    }
481
482    /// Like [`Self::get_actor_rigid_body`], but falls back to recently deleted actor state.
483    pub fn get_actor_rigid_body_or_recently_deleted(
484        &self,
485        actor_id: &boxcars::ActorId,
486    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
487        get_attribute_and_updated!(
488            self,
489            &self
490                .get_actor_state_or_recently_deleted(actor_id)?
491                .attributes,
492            RIGID_BODY_STATE_KEY,
493            boxcars::Attribute::RigidBody
494        )
495    }
496
497    /// Iterates over players in the stable team-zero, then team-one ordering.
498    pub fn iter_player_ids_in_order(&self) -> impl Iterator<Item = &PlayerId> {
499        self.team_zero.iter().chain(self.team_one.iter())
500    }
501
502    /// Counts currently in-game players per team from live actor state.
503    pub fn current_in_game_team_player_counts(&self) -> [usize; 2] {
504        let mut counts = [0, 0];
505        let Ok(player_actor_ids) = self.get_actor_ids_by_type(PLAYER_TYPE) else {
506            return counts;
507        };
508        let mut seen_players = std::collections::HashSet::new();
509
510        for actor_id in player_actor_ids {
511            let Ok(player_id) = self.get_player_id_from_actor_id(actor_id) else {
512                continue;
513            };
514            if !seen_players.insert(player_id) {
515                continue;
516            }
517
518            let Some(team_actor_id) = self.player_to_team.get(actor_id) else {
519                continue;
520            };
521            let Ok(team_state) = self.get_actor_state(team_actor_id) else {
522                continue;
523            };
524            let Some(team_name) = self.object_id_to_name.get(&team_state.object_id) else {
525                continue;
526            };
527
528            match team_name.chars().last() {
529                Some('0') => counts[0] += 1,
530                Some('1') => counts[1] += 1,
531                _ => {}
532            }
533        }
534
535        counts
536    }
537
538    /// Returns the number of players in the stored replay ordering.
539    pub fn player_count(&self) -> usize {
540        self.iter_player_ids_in_order().count()
541    }
542
543    /// Returns a map from player ids to their resolved display names.
544    pub fn get_player_names(&self) -> HashMap<PlayerId, String> {
545        self.iter_player_ids_in_order()
546            .filter_map(|player_id| {
547                self.get_player_name(player_id)
548                    .ok()
549                    .map(|name| (player_id.clone(), name))
550            })
551            .collect()
552    }
553
554    /// Iterates over actors of a named object type, returning an error if the type is unknown.
555    pub(crate) fn iter_actors_by_type_err(
556        &self,
557        name: &'static str,
558    ) -> SubtrActorResult<impl Iterator<Item = (&boxcars::ActorId, &ActorState)>> {
559        Ok(self.iter_actors_by_object_id(self.get_object_id_for_key(name)?))
560    }
561
562    /// Iterates over actors of a named object type, if that type exists in the replay.
563    pub fn iter_actors_by_type(
564        &self,
565        name: &'static str,
566    ) -> Option<impl Iterator<Item = (&boxcars::ActorId, &ActorState)>> {
567        self.iter_actors_by_type_err(name).ok()
568    }
569
570    /// Iterates over actors for a concrete object id.
571    pub fn iter_actors_by_object_id<'b>(
572        &'b self,
573        object_id: &'b boxcars::ObjectId,
574    ) -> impl Iterator<Item = (&'b boxcars::ActorId, &'b ActorState)> + 'b {
575        let actor_ids = self
576            .actor_state
577            .actor_ids_by_type
578            .get(object_id)
579            .map(|v| &v[..])
580            .unwrap_or_else(|| &EMPTY_ACTOR_IDS);
581
582        actor_ids
583            .iter()
584            .map(move |id| (id, self.actor_state.actor_states.get(id).unwrap()))
585    }
586
587    /// Returns the replicated match clock in whole seconds.
588    pub fn get_seconds_remaining(&self) -> SubtrActorResult<i32> {
589        let seconds_remaining_object_id =
590            self.cached_object_ids.seconds_remaining.ok_or_else(|| {
591                SubtrActorError::new(SubtrActorErrorVariant::ObjectIdNotFound {
592                    name: SECONDS_REMAINING_KEY,
593                })
594            })?;
595        let metadata_actor_id = self.get_metadata_actor_id()?;
596        let metadata_state = self.get_actor_state(&metadata_actor_id)?;
597        metadata_state
598            .attributes
599            .get(&seconds_remaining_object_id)
600            .ok_or_else(|| {
601                SubtrActorError::new(SubtrActorErrorVariant::PropertyNotFoundInState {
602                    property: SECONDS_REMAINING_KEY,
603                })
604            })
605            .and_then(|(attribute, _)| attribute_match!(attribute, boxcars::Attribute::Int))
606            .copied()
607    }
608
609    /// Returns the replicated game-state enum value from the metadata actor.
610    pub fn get_replicated_state_name(&self) -> SubtrActorResult<i32> {
611        get_actor_attribute_matching!(
612            self,
613            &self.get_metadata_actor_id()?,
614            REPLICATED_STATE_NAME_KEY,
615            boxcars::Attribute::Int
616        )
617        .cloned()
618    }
619
620    /// Returns the replicated kickoff countdown / time-remaining field.
621    pub fn get_replicated_game_state_time_remaining(&self) -> SubtrActorResult<i32> {
622        get_actor_attribute_matching!(
623            self,
624            &self.get_metadata_actor_id()?,
625            REPLICATED_GAME_STATE_TIME_REMAINING_KEY,
626            boxcars::Attribute::Int
627        )
628        .cloned()
629    }
630
631    /// Returns whether the replay currently reports that the ball has been hit.
632    pub fn get_ball_has_been_hit(&self) -> SubtrActorResult<bool> {
633        get_actor_attribute_matching!(
634            self,
635            &self.get_metadata_actor_id()?,
636            BALL_HAS_BEEN_HIT_KEY,
637            boxcars::Attribute::Boolean
638        )
639        .cloned()
640    }
641
642    /// Returns the ball actor's ignore-syncing flag.
643    pub fn get_ignore_ball_syncing(&self) -> SubtrActorResult<bool> {
644        let actor_id = self.get_ball_actor_id()?;
645        get_actor_attribute_matching!(
646            self,
647            &actor_id,
648            IGNORE_SYNCING_KEY,
649            boxcars::Attribute::Boolean
650        )
651        .cloned()
652    }
653
654    /// Returns the current ball rigid body from live actor state.
655    pub fn get_ball_rigid_body(&self) -> SubtrActorResult<&boxcars::RigidBody> {
656        self.ball_actor_id
657            .ok_or(SubtrActorError::new(
658                SubtrActorErrorVariant::BallActorNotFound,
659            ))
660            .and_then(|actor_id| self.get_actor_rigid_body(&actor_id).map(|v| v.0))
661    }
662
663    /// Returns the current ball rigid body after spatial normalization.
664    pub fn get_normalized_ball_rigid_body(&self) -> SubtrActorResult<boxcars::RigidBody> {
665        self.get_ball_rigid_body()
666            .map(|rigid_body| self.normalize_rigid_body(rigid_body))
667    }
668
669    /// Returns whether a non-sleeping ball rigid body is currently available.
670    pub fn ball_rigid_body_exists(&self) -> SubtrActorResult<bool> {
671        Ok(self
672            .get_ball_rigid_body()
673            .map(|rb| !rb.sleeping)
674            .unwrap_or(false))
675    }
676
677    /// Returns the current ball rigid body and the frame where it was last updated.
678    pub fn get_ball_rigid_body_and_updated(
679        &self,
680    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
681        self.ball_actor_id
682            .ok_or(SubtrActorError::new(
683                SubtrActorErrorVariant::BallActorNotFound,
684            ))
685            .and_then(|actor_id| {
686                get_attribute_and_updated!(
687                    self,
688                    &self.get_actor_state(&actor_id)?.attributes,
689                    RIGID_BODY_STATE_KEY,
690                    boxcars::Attribute::RigidBody
691                )
692            })
693    }
694
695    /// Applies stored ball velocity forward to the requested time.
696    pub fn get_velocity_applied_ball_rigid_body(
697        &self,
698        target_time: f32,
699    ) -> SubtrActorResult<boxcars::RigidBody> {
700        let (current_rigid_body, frame_index) = self.get_ball_rigid_body_and_updated()?;
701        self.velocities_applied_rigid_body(current_rigid_body, *frame_index, target_time)
702    }
703
704    /// Interpolates the ball rigid body to the requested time.
705    pub fn get_interpolated_ball_rigid_body(
706        &self,
707        time: f32,
708        close_enough: f32,
709    ) -> SubtrActorResult<boxcars::RigidBody> {
710        self.get_interpolated_actor_rigid_body(&self.get_ball_actor_id()?, time, close_enough)
711    }
712
713    /// Returns the player's replicated display name.
714    pub fn get_player_name(&self, player_id: &PlayerId) -> SubtrActorResult<String> {
715        get_actor_attribute_matching!(
716            self,
717            &self.get_player_actor_id(player_id)?,
718            PLAYER_NAME_KEY,
719            boxcars::Attribute::String
720        )
721        .cloned()
722    }
723
724    fn get_player_int_stat(
725        &self,
726        player_id: &PlayerId,
727        key: &'static str,
728    ) -> SubtrActorResult<i32> {
729        get_actor_attribute_matching!(
730            self,
731            &self.get_player_actor_id(player_id)?,
732            key,
733            boxcars::Attribute::Int
734        )
735        .cloned()
736    }
737
738    /// Returns the replay object-name key for the player's team actor.
739    pub fn get_player_team_key(&self, player_id: &PlayerId) -> SubtrActorResult<String> {
740        let team_actor_id = self
741            .player_to_team
742            .get(&self.get_player_actor_id(player_id)?)
743            .ok_or_else(|| {
744                SubtrActorError::new(SubtrActorErrorVariant::UnknownPlayerTeam {
745                    player_id: player_id.clone(),
746                })
747            })?;
748        let state = self.get_actor_state(team_actor_id)?;
749        self.object_id_to_name
750            .get(&state.object_id)
751            .ok_or_else(|| {
752                SubtrActorError::new(SubtrActorErrorVariant::UnknownPlayerTeam {
753                    player_id: player_id.clone(),
754                })
755            })
756            .cloned()
757    }
758
759    /// Returns whether the player belongs to team 0.
760    pub fn get_player_is_team_0(&self, player_id: &PlayerId) -> SubtrActorResult<bool> {
761        Ok(self
762            .get_player_team_key(player_id)?
763            .chars()
764            .last()
765            .ok_or_else(|| {
766                SubtrActorError::new(SubtrActorErrorVariant::EmptyTeamName {
767                    player_id: player_id.clone(),
768                })
769            })?
770            == '0')
771    }
772
773    /// Returns the team actor id for the requested side.
774    pub(crate) fn get_team_actor_id_for_side(
775        &self,
776        is_team_0: bool,
777    ) -> SubtrActorResult<boxcars::ActorId> {
778        let player_id = if is_team_0 {
779            self.team_zero.first()
780        } else {
781            self.team_one.first()
782        }
783        .ok_or(SubtrActorError::new(SubtrActorErrorVariant::NoGameActor))?;
784
785        self.player_to_team
786            .get(&self.get_player_actor_id(player_id)?)
787            .copied()
788            .ok_or_else(|| {
789                SubtrActorError::new(SubtrActorErrorVariant::ActorNotFound {
790                    name: "Team",
791                    player_id: player_id.clone(),
792                })
793            })
794    }
795
796    /// Returns the score for the requested team side.
797    pub fn get_team_score(&self, is_team_0: bool) -> SubtrActorResult<i32> {
798        let team_actor_id = self.get_team_actor_id_for_side(is_team_0)?;
799        get_actor_attribute_matching!(
800            self,
801            &team_actor_id,
802            TEAM_GAME_SCORE_KEY,
803            boxcars::Attribute::Int
804        )
805        .cloned()
806    }
807
808    /// Returns `(team_zero_score, team_one_score)`.
809    pub fn get_team_scores(&self) -> SubtrActorResult<(i32, i32)> {
810        Ok((self.get_team_score(true)?, self.get_team_score(false)?))
811    }
812
813    /// Returns the player's current car rigid body.
814    pub fn get_player_rigid_body(
815        &self,
816        player_id: &PlayerId,
817    ) -> SubtrActorResult<&boxcars::RigidBody> {
818        self.get_car_actor_id(player_id)
819            .and_then(|actor_id| self.get_actor_rigid_body(&actor_id).map(|v| v.0))
820    }
821
822    /// Returns the player's current car rigid body after spatial normalization.
823    pub fn get_normalized_player_rigid_body(
824        &self,
825        player_id: &PlayerId,
826    ) -> SubtrActorResult<boxcars::RigidBody> {
827        self.get_player_rigid_body(player_id)
828            .map(|rigid_body| self.normalize_rigid_body(rigid_body))
829    }
830
831    /// Returns the player's rigid body and the frame where it was last updated.
832    pub fn get_player_rigid_body_and_updated(
833        &self,
834        player_id: &PlayerId,
835    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
836        self.get_car_actor_id(player_id).and_then(|actor_id| {
837            get_attribute_and_updated!(
838                self,
839                &self.get_actor_state(&actor_id)?.attributes,
840                RIGID_BODY_STATE_KEY,
841                boxcars::Attribute::RigidBody
842            )
843        })
844    }
845
846    /// Like [`Self::get_player_rigid_body_and_updated`], but can use recently deleted state.
847    pub fn get_player_rigid_body_and_updated_or_recently_deleted(
848        &self,
849        player_id: &PlayerId,
850    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
851        self.get_car_actor_id(player_id)
852            .and_then(|actor_id| self.get_actor_rigid_body_or_recently_deleted(&actor_id))
853    }
854
855    /// Applies stored player velocity forward to the requested time.
856    pub fn get_velocity_applied_player_rigid_body(
857        &self,
858        player_id: &PlayerId,
859        target_time: f32,
860    ) -> SubtrActorResult<boxcars::RigidBody> {
861        let (current_rigid_body, frame_index) =
862            self.get_player_rigid_body_and_updated(player_id)?;
863        self.velocities_applied_rigid_body(current_rigid_body, *frame_index, target_time)
864    }
865
866    /// Interpolates the player's car rigid body to the requested time.
867    pub fn get_interpolated_player_rigid_body(
868        &self,
869        player_id: &PlayerId,
870        time: f32,
871        close_enough: f32,
872    ) -> SubtrActorResult<boxcars::RigidBody> {
873        self.get_car_actor_id(player_id).and_then(|car_actor_id| {
874            self.get_interpolated_actor_rigid_body(&car_actor_id, time, close_enough)
875        })
876    }
877
878    /// Returns the player's current boost amount in raw replay units.
879    pub fn get_player_boost_level(&self, player_id: &PlayerId) -> SubtrActorResult<f32> {
880        self.get_boost_actor_id(player_id).and_then(|actor_id| {
881            let boost_state = self.get_actor_state(&actor_id)?;
882            get_derived_attribute!(
883                boost_state.derived_attributes,
884                BOOST_AMOUNT_KEY,
885                boxcars::Attribute::Float
886            )
887            .cloned()
888        })
889    }
890
891    /// Returns the previous boost amount recorded for the player in raw replay units.
892    pub fn get_player_last_boost_level(&self, player_id: &PlayerId) -> SubtrActorResult<f32> {
893        self.get_boost_actor_id(player_id).and_then(|actor_id| {
894            let boost_state = self.get_actor_state(&actor_id)?;
895            get_derived_attribute!(
896                boost_state.derived_attributes,
897                LAST_BOOST_AMOUNT_KEY,
898                boxcars::Attribute::Byte
899            )
900            .map(|value| *value as f32)
901        })
902    }
903
904    /// Returns the player's boost level scaled to the conventional 0.0-100.0 range.
905    pub fn get_player_boost_percentage(&self, player_id: &PlayerId) -> SubtrActorResult<f32> {
906        self.get_player_boost_level(player_id)
907            .map(boost_amount_to_percent)
908    }
909
910    /// Returns the player's match assists counter.
911    pub fn get_player_match_assists(&self, player_id: &PlayerId) -> SubtrActorResult<i32> {
912        self.get_player_int_stat(player_id, MATCH_ASSISTS_KEY)
913    }
914
915    /// Returns the player's match goals counter.
916    pub fn get_player_match_goals(&self, player_id: &PlayerId) -> SubtrActorResult<i32> {
917        self.get_player_int_stat(player_id, MATCH_GOALS_KEY)
918    }
919
920    /// Returns the player's match saves counter.
921    pub fn get_player_match_saves(&self, player_id: &PlayerId) -> SubtrActorResult<i32> {
922        self.get_player_int_stat(player_id, MATCH_SAVES_KEY)
923    }
924
925    /// Returns the player's match score counter.
926    pub fn get_player_match_score(&self, player_id: &PlayerId) -> SubtrActorResult<i32> {
927        self.get_player_int_stat(player_id, MATCH_SCORE_KEY)
928    }
929
930    /// Returns the player's match shots counter.
931    pub fn get_player_match_shots(&self, player_id: &PlayerId) -> SubtrActorResult<i32> {
932        self.get_player_int_stat(player_id, MATCH_SHOTS_KEY)
933    }
934
935    /// Returns the team number recorded as the last ball-touching side.
936    pub fn get_ball_hit_team_num(&self) -> SubtrActorResult<u8> {
937        let ball_actor_id = self.get_ball_actor_id()?;
938        get_actor_attribute_matching!(
939            self,
940            &ball_actor_id,
941            BALL_HIT_TEAM_NUM_KEY,
942            boxcars::Attribute::Byte
943        )
944        .cloned()
945    }
946
947    /// Returns the team number currently marked as having been scored on.
948    pub fn get_scored_on_team_num(&self) -> SubtrActorResult<u8> {
949        get_actor_attribute_matching!(
950            self,
951            &self.get_metadata_actor_id()?,
952            REPLICATED_SCORED_ON_TEAM_KEY,
953            boxcars::Attribute::Byte
954        )
955        .cloned()
956    }
957
958    /// Returns a component actor's active byte.
959    pub fn get_component_active(&self, actor_id: &boxcars::ActorId) -> SubtrActorResult<u8> {
960        get_actor_attribute_matching!(
961            self,
962            &actor_id,
963            COMPONENT_ACTIVE_KEY,
964            boxcars::Attribute::Byte
965        )
966        .cloned()
967    }
968
969    /// Returns the active byte for the player's boost component.
970    pub fn get_boost_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
971        self.get_boost_actor_id(player_id)
972            .and_then(|actor_id| self.get_component_active(&actor_id))
973    }
974
975    /// Returns the active byte for the player's jump component.
976    pub fn get_jump_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
977        self.get_jump_actor_id(player_id)
978            .and_then(|actor_id| self.get_component_active(&actor_id))
979    }
980
981    /// Returns the active byte for the player's double-jump component.
982    pub fn get_double_jump_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
983        self.get_double_jump_actor_id(player_id)
984            .and_then(|actor_id| self.get_component_active(&actor_id))
985    }
986
987    /// Returns the active byte for the player's dodge component.
988    pub fn get_dodge_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
989        self.get_dodge_actor_id(player_id)
990            .and_then(|actor_id| self.get_component_active(&actor_id))
991    }
992
993    /// Returns whether the player's handbrake / powerslide flag is active.
994    pub fn get_powerslide_active(&self, player_id: &PlayerId) -> SubtrActorResult<bool> {
995        get_actor_attribute_matching!(
996            self,
997            &self.get_car_actor_id(player_id)?,
998            HANDBRAKE_KEY,
999            boxcars::Attribute::Boolean
1000        )
1001        .cloned()
1002    }
1003}