subtr_actor/
processor.rs

1use crate::*;
2use boxcars;
3use std::collections::HashMap;
4
5/// Attempts to match an attribute value with the given type.
6///
7/// # Arguments
8///
9/// * `$value` - An expression that yields the attribute value.
10/// * `$type` - The expected enum path.
11///
12/// If the attribute matches the specified type, it is returned wrapped in an
13/// [`Ok`] variant of a [`Result`]. If the attribute doesn't match, it results in an
14/// [`Err`] variant with a [`SubtrActorError`], specifying the expected type and
15/// the actual type.
16macro_rules! attribute_match {
17    ($value:expr, $type:path $(,)?) => {{
18        let attribute = $value;
19        if let $type(value) = attribute {
20            Ok(value)
21        } else {
22            SubtrActorError::new_result(SubtrActorErrorVariant::UnexpectedAttributeType {
23                expected_type: stringify!(path).to_string(),
24                actual_type: attribute_to_tag(&attribute).to_string(),
25            })
26        }
27    }};
28}
29
30/// Obtains an attribute from a map and ensures it matches the expected type.
31///
32/// # Arguments
33///
34/// * `$self` - The struct or instance on which the function is invoked.
35/// * `$map` - The data map.
36/// * `$prop` - The attribute key.
37/// * `$type` - The expected enum path.
38macro_rules! get_attribute_errors_expected {
39    ($self:ident, $map:expr, $prop:expr, $type:path) => {
40        $self
41            .get_attribute($map, $prop)
42            .and_then(|found| attribute_match!(found, $type))
43    };
44}
45
46/// Obtains an attribute and its updated status from a map and ensures the
47/// attribute matches the expected type.
48///
49/// # Arguments
50///
51/// * `$self` - The struct or instance on which the function is invoked.
52/// * `$map` - The data map.
53/// * `$prop` - The attribute key.
54/// * `$type` - The expected enum path.
55///
56/// It returns a [`Result`] with a tuple of the matched attribute and its updated
57/// status, after invoking [`attribute_match!`] on the found attribute.
58macro_rules! get_attribute_and_updated {
59    ($self:ident, $map:expr, $prop:expr, $type:path) => {
60        $self
61            .get_attribute_and_updated($map, $prop)
62            .and_then(|(found, updated)| attribute_match!(found, $type).map(|v| (v, updated)))
63    };
64}
65
66/// Obtains an actor attribute and ensures it matches the expected type.
67///
68/// # Arguments
69///
70/// * `$self` - The struct or instance on which the function is invoked.
71/// * `$actor` - The actor identifier.
72/// * `$prop` - The attribute key.
73/// * `$type` - The expected enum path.
74macro_rules! get_actor_attribute_matching {
75    ($self:ident, $actor:expr, $prop:expr, $type:path) => {
76        $self
77            .get_actor_attribute($actor, $prop)
78            .and_then(|found| attribute_match!(found, $type))
79    };
80}
81
82/// Obtains a derived attribute from a map and ensures it matches the expected
83/// type.
84///
85/// # Arguments
86///
87/// * `$map` - The data map.
88/// * `$key` - The attribute key.
89/// * `$type` - The expected enum path.
90macro_rules! get_derived_attribute {
91    ($map:expr, $key:expr, $type:path) => {
92        $map.get($key)
93            .ok_or_else(|| {
94                SubtrActorError::new(SubtrActorErrorVariant::DerivedKeyValueNotFound {
95                    name: $key.to_string(),
96                })
97            })
98            .and_then(|found| attribute_match!(&found.0, $type))
99    };
100}
101
102fn get_actor_id_from_active_actor<T>(
103    _: T,
104    active_actor: &boxcars::ActiveActor,
105) -> boxcars::ActorId {
106    active_actor.actor
107}
108
109fn use_update_actor<T>(id: boxcars::ActorId, _: T) -> boxcars::ActorId {
110    id
111}
112
113/// The [`ReplayProcessor`] struct is a pivotal component in `subtr-actor`'s
114/// replay parsing pipeline. It is designed to process and traverse an actor
115/// graph of a Rocket League replay, and expose methods for collectors to gather
116/// specific data points as it progresses through the replay.
117///
118/// The processor pushes frames from a replay through an [`ActorStateModeler`],
119/// which models the state all actors in the replay at a given point in time.
120/// The [`ReplayProcessor`] also maintains various mappings to allow efficient
121/// lookup and traversal of the actor graph, thus assisting [`Collector`]
122/// instances in their data accumulation tasks.
123///
124/// The primary method of this struct is [`process`](ReplayProcessor::process),
125/// which takes a collector and processes the replay. As it traverses the
126/// replay, it calls the [`Collector::process_frame`] method of the passed
127/// collector, passing the current frame along with its contextual data. This
128/// allows the collector to extract specific data from each frame as needed.
129///
130/// The [`ReplayProcessor`] also provides a number of helper methods for
131/// navigating the actor graph and extracting information, such as
132/// [`get_ball_rigid_body`](ReplayProcessor::get_ball_rigid_body),
133/// [`get_player_name`](ReplayProcessor::get_player_name),
134/// [`get_player_team_key`](ReplayProcessor::get_player_team_key),
135/// [`get_player_is_team_0`](ReplayProcessor::get_player_is_team_0), and
136/// [`get_player_rigid_body`](ReplayProcessor::get_player_rigid_body).
137///
138/// # See Also
139///
140/// * [`ActorStateModeler`]: A struct used to model the states of multiple
141///   actors at a given point in time.
142/// * [`Collector`]: A trait implemented by objects that wish to collect data as
143///   the `ReplayProcessor` processes a replay.
144pub struct ReplayProcessor<'a> {
145    pub replay: &'a boxcars::Replay,
146    pub actor_state: ActorStateModeler,
147    pub object_id_to_name: HashMap<boxcars::ObjectId, String>,
148    pub name_to_object_id: HashMap<String, boxcars::ObjectId>,
149    pub ball_actor_id: Option<boxcars::ActorId>,
150    pub team_zero: Vec<PlayerId>,
151    pub team_one: Vec<PlayerId>,
152    pub player_to_actor_id: HashMap<PlayerId, boxcars::ActorId>,
153    pub player_to_car: HashMap<boxcars::ActorId, boxcars::ActorId>,
154    pub player_to_team: HashMap<boxcars::ActorId, boxcars::ActorId>,
155    pub car_to_boost: HashMap<boxcars::ActorId, boxcars::ActorId>,
156    pub car_to_jump: HashMap<boxcars::ActorId, boxcars::ActorId>,
157    pub car_to_double_jump: HashMap<boxcars::ActorId, boxcars::ActorId>,
158    pub car_to_dodge: HashMap<boxcars::ActorId, boxcars::ActorId>,
159    pub demolishes: Vec<DemolishInfo>,
160    known_demolishes: Vec<(boxcars::DemolishFx, usize)>,
161}
162
163impl<'a> ReplayProcessor<'a> {
164    /// Constructs a new [`ReplayProcessor`] instance with the provided replay.
165    ///
166    /// # Arguments
167    ///
168    /// * `replay` - A reference to the [`boxcars::Replay`] to be processed.
169    ///
170    /// # Returns
171    ///
172    /// Returns a [`SubtrActorResult`] of [`ReplayProcessor`]. In the process of
173    /// initialization, the [`ReplayProcessor`]: - Maps each object id in the
174    /// replay to its corresponding name. - Initializes empty state and
175    /// attribute maps. - Sets the player order from either replay headers or
176    /// frames, if available.
177    pub fn new(replay: &'a boxcars::Replay) -> SubtrActorResult<Self> {
178        let mut object_id_to_name = HashMap::new();
179        let mut name_to_object_id = HashMap::new();
180        for (id, name) in replay.objects.iter().enumerate() {
181            let object_id = boxcars::ObjectId(id as i32);
182            object_id_to_name.insert(object_id, name.clone());
183            name_to_object_id.insert(name.clone(), object_id);
184        }
185        let mut processor = Self {
186            actor_state: ActorStateModeler::new(),
187            replay,
188            object_id_to_name,
189            name_to_object_id,
190            team_zero: Vec::new(),
191            team_one: Vec::new(),
192            ball_actor_id: None,
193            player_to_car: HashMap::new(),
194            player_to_team: HashMap::new(),
195            player_to_actor_id: HashMap::new(),
196            car_to_boost: HashMap::new(),
197            car_to_jump: HashMap::new(),
198            car_to_double_jump: HashMap::new(),
199            car_to_dodge: HashMap::new(),
200            demolishes: Vec::new(),
201            known_demolishes: Vec::new(),
202        };
203        processor
204            .set_player_order_from_headers()
205            .or_else(|_| processor.set_player_order_from_frames())?;
206
207        Ok(processor)
208    }
209
210    /// [`Self::process`] takes a [`Collector`] as an argument and iterates over
211    /// each frame in the replay, updating the internal state of the processor
212    /// and other relevant mappings based on the current frame.
213    ///
214    /// After each a frame is processed, [`Collector::process_frame`] of the
215    /// collector is called. The [`TimeAdvance`] return value of this call into
216    /// [`Collector::process_frame`] is used to determine what happens next: in
217    /// the case of [`TimeAdvance::Time`], the notion of current time is
218    /// advanced by the provided amount, and only the timestamp of the frame is
219    /// exceeded, do we process the next frame. This mechanism allows fine
220    /// grained control of frame processing, and the frequency of invocations of
221    /// the [`Collector`]. If time is advanced by less than the delay between
222    /// frames, the collector will be called more than once per frame, and can
223    /// use functions like [`Self::get_interpolated_player_rigid_body`] to get
224    /// values that are interpolated between frames. Its also possible to skip
225    /// over frames by providing time advance values that are sufficiently
226    /// large.
227    ///
228    /// At the end of processing, it checks to make sure that no unknown players
229    /// were encountered during the replay. If any unknown players are found, an
230    /// error is returned.
231    pub fn process<H: Collector>(&mut self, handler: &mut H) -> SubtrActorResult<()> {
232        // Initially, we set target_time to NextFrame to ensure the collector
233        // will process the first frame.
234        let mut target_time = TimeAdvance::NextFrame;
235        for (index, frame) in self
236            .replay
237            .network_frames
238            .as_ref()
239            .ok_or(SubtrActorError::new(
240                SubtrActorErrorVariant::NoNetworkFrames,
241            ))?
242            .frames
243            .iter()
244            .enumerate()
245        {
246            // Update the internal state of the processor based on the current frame
247            self.actor_state.process_frame(frame, index)?;
248            self.update_mappings(frame)?;
249            self.update_ball_id(frame)?;
250            self.update_boost_amounts(frame, index)?;
251            self.update_demolishes(frame, index)?;
252
253            // Get the time to process for this frame. If target_time is set to
254            // NextFrame, we use the time of the current frame.
255            let mut current_time = match &target_time {
256                TimeAdvance::Time(t) => *t,
257                TimeAdvance::NextFrame => frame.time,
258            };
259
260            while current_time <= frame.time {
261                // Call the handler to process the frame and get the time for
262                // the next frame the handler wants to process
263                target_time = handler.process_frame(self, frame, index, current_time)?;
264                // If the handler specified a specific time, update current_time
265                // to that time. If the handler specified NextFrame, we break
266                // out of the loop to move on to the next frame in the replay.
267                // This design allows the handler to have control over the frame
268                // rate, including the possibility of skipping frames.
269                if let TimeAdvance::Time(new_target) = target_time {
270                    current_time = new_target;
271                } else {
272                    break;
273                }
274            }
275        }
276        Ok(())
277    }
278
279    /// Reset the state of the [`ReplayProcessor`].
280    pub fn reset(&mut self) {
281        self.player_to_car = HashMap::new();
282        self.player_to_team = HashMap::new();
283        self.player_to_actor_id = HashMap::new();
284        self.car_to_boost = HashMap::new();
285        self.car_to_jump = HashMap::new();
286        self.car_to_double_jump = HashMap::new();
287        self.car_to_dodge = HashMap::new();
288        self.actor_state = ActorStateModeler::new();
289        self.demolishes = Vec::new();
290        self.known_demolishes = Vec::new();
291    }
292
293    fn set_player_order_from_headers(&mut self) -> SubtrActorResult<()> {
294        let _player_stats = self
295            .replay
296            .properties
297            .iter()
298            .find(|(key, _)| key == "PlayerStats")
299            .ok_or_else(|| {
300                SubtrActorError::new(SubtrActorErrorVariant::PlayerStatsHeaderNotFound)
301            })?;
302        // XXX: implementation incomplete
303        SubtrActorError::new_result(SubtrActorErrorVariant::PlayerStatsHeaderNotFound)
304    }
305
306    /// Processes the replay until it has gathered enough information to map
307    /// players to their actor IDs.
308    ///
309    /// This function is designed to ensure that each player that participated
310    /// in the game is associated with a corresponding actor ID. It runs the
311    /// processing operation for approximately the first 10 seconds of the
312    /// replay (10 * 30 frames), as this time span is generally sufficient to
313    /// identify all players.
314    ///
315    /// Note that this function is particularly necessary because the headers of
316    /// replays sometimes omit some players.
317    ///
318    /// # Errors
319    ///
320    /// If any error other than `FinishProcessingEarly` occurs during the
321    /// processing operation, it is propagated up by this function.
322    pub fn process_long_enough_to_get_actor_ids(&mut self) -> SubtrActorResult<()> {
323        let mut handler = |_p: &ReplayProcessor, _f: &boxcars::Frame, n: usize, _current_time| {
324            // XXX: 10 seconds should be enough to find everyone, right?
325            if n > 10 * 30 {
326                SubtrActorError::new_result(SubtrActorErrorVariant::FinishProcessingEarly)
327            } else {
328                Ok(TimeAdvance::NextFrame)
329            }
330        };
331        let process_result = self.process(&mut handler);
332        if let Some(SubtrActorErrorVariant::FinishProcessingEarly) =
333            process_result.as_ref().err().map(|e| e.variant.clone())
334        {
335            Ok(())
336        } else {
337            process_result
338        }
339    }
340
341    fn set_player_order_from_frames(&mut self) -> SubtrActorResult<()> {
342        self.process_long_enough_to_get_actor_ids()?;
343        let player_to_team_0: HashMap<PlayerId, bool> = self
344            .player_to_actor_id
345            .keys()
346            .filter_map(|player_id| {
347                self.get_player_is_team_0(player_id)
348                    .ok()
349                    .map(|is_team_0| (player_id.clone(), is_team_0))
350            })
351            .collect();
352
353        let (team_zero, team_one): (Vec<_>, Vec<_>) = player_to_team_0
354            .keys()
355            .cloned()
356            // The unwrap here is fine because we know the get will succeed
357            .partition(|player_id| *player_to_team_0.get(player_id).unwrap());
358
359        self.team_zero = team_zero;
360        self.team_one = team_one;
361
362        self.team_zero
363            .sort_by(|a, b| format!("{a:?}").cmp(&format!("{b:?}")));
364        self.team_one
365            .sort_by(|a, b| format!("{a:?}").cmp(&format!("{b:?}")));
366
367        self.reset();
368        Ok(())
369    }
370
371    pub fn check_player_id_set(&self) -> SubtrActorResult<()> {
372        let known_players =
373            std::collections::HashSet::<_>::from_iter(self.player_to_actor_id.keys());
374        let original_players =
375            std::collections::HashSet::<_>::from_iter(self.iter_player_ids_in_order());
376
377        if original_players != known_players {
378            SubtrActorError::new_result(SubtrActorErrorVariant::InconsistentPlayerSet {
379                found: known_players.into_iter().cloned().collect(),
380                original: original_players.into_iter().cloned().collect(),
381            })
382        } else {
383            Ok(())
384        }
385    }
386
387    /// Processes the replay enough to get the actor IDs and then retrieves the replay metadata.
388    ///
389    /// This method is a convenience function that combines the functionalities
390    /// of
391    /// [`process_long_enough_to_get_actor_ids`](Self::process_long_enough_to_get_actor_ids)
392    /// and [`get_replay_meta`](Self::get_replay_meta) into a single operation.
393    /// It's meant to be used when you don't necessarily want to process the
394    /// whole replay and need only the replay's metadata.
395    pub fn process_and_get_replay_meta(&mut self) -> SubtrActorResult<ReplayMeta> {
396        if self.player_to_actor_id.is_empty() {
397            self.process_long_enough_to_get_actor_ids()?;
398        }
399        self.get_replay_meta()
400    }
401
402    /// Retrieves the replay metadata.
403    ///
404    /// This function collects information about each player in the replay and
405    /// groups them by team. For each player, it gets the player's name and
406    /// statistics. All this information is then wrapped into a [`ReplayMeta`]
407    /// object along with the properties from the replay.
408    pub fn get_replay_meta(&self) -> SubtrActorResult<ReplayMeta> {
409        let empty_player_stats = Vec::new();
410        let player_stats = if let Some((_, boxcars::HeaderProp::Array(per_player))) = self
411            .replay
412            .properties
413            .iter()
414            .find(|(key, _)| key == "PlayerStats")
415        {
416            per_player
417        } else {
418            &empty_player_stats
419        };
420        let known_count = self.iter_player_ids_in_order().count();
421        if player_stats.len() != known_count {
422            log::warn!(
423                "Replay does not have player stats for all players. encountered {:?} {:?}",
424                known_count,
425                player_stats.len()
426            )
427        }
428        let get_player_info = |player_id| {
429            let name = self.get_player_name(player_id)?;
430            let stats = find_player_stats(player_id, &name, player_stats).ok();
431            Ok(PlayerInfo {
432                name,
433                stats,
434                remote_id: player_id.clone(),
435            })
436        };
437        let team_zero: SubtrActorResult<Vec<PlayerInfo>> =
438            self.team_zero.iter().map(get_player_info).collect();
439        let team_one: SubtrActorResult<Vec<PlayerInfo>> =
440            self.team_one.iter().map(get_player_info).collect();
441        Ok(ReplayMeta {
442            team_zero: team_zero?,
443            team_one: team_one?,
444            all_headers: self.replay.properties.clone(),
445        })
446    }
447
448    /// Searches for the next or previous update for a specified actor and
449    /// object in the replay's network frames.
450    ///
451    /// This method uses the [`find_in_direction`](util::find_in_direction)
452    /// function to search through the network frames of the replay to find the
453    /// next (or previous, depending on the direction provided) attribute update
454    /// for a specified actor and object.
455    ///
456    /// # Arguments
457    ///
458    /// * `current_index` - The index of the network frame from where the search should start.
459    /// * `actor_id` - The ID of the actor for which the update is being searched.
460    /// * `object_id` - The ID of the object associated with the actor for which
461    ///   the update is being searched.
462    /// * `direction` - The direction of search, specified as either
463    ///   [`SearchDirection::Backward`] or [`SearchDirection::Forward`].
464    ///
465    /// # Returns
466    ///
467    /// If a matching update is found, this function returns a
468    /// [`SubtrActorResult`] tuple containing the found attribute and its index
469    /// in the replay's network frames.
470    ///
471    /// # Errors
472    ///
473    /// If no matching update is found, or if the replay has no network frames,
474    /// this function returns a [`SubtrActorError`]. Specifically, it returns
475    /// `NoUpdateAfterFrame` error variant if no update is found after the
476    /// specified frame, or `NoNetworkFrames` if the replay lacks network
477    /// frames.
478    ///
479    /// [`SearchDirection::Backward`]: enum.SearchDirection.html#variant.Backward
480    /// [`SearchDirection::Forward`]: enum.SearchDirection.html#variant.Forward
481    /// [`SubtrActorResult`]: type.SubtrActorResult.html
482    /// [`SubtrActorError`]: struct.SubtrActorError.html
483    pub fn find_update_in_direction(
484        &self,
485        current_index: usize,
486        actor_id: &boxcars::ActorId,
487        object_id: &boxcars::ObjectId,
488        direction: SearchDirection,
489    ) -> SubtrActorResult<(boxcars::Attribute, usize)> {
490        let frames = self
491            .replay
492            .network_frames
493            .as_ref()
494            .ok_or(SubtrActorError::new(
495                SubtrActorErrorVariant::NoNetworkFrames,
496            ))?;
497
498        let predicate = |frame: &boxcars::Frame| {
499            frame
500                .updated_actors
501                .iter()
502                .find(|update| &update.actor_id == actor_id && &update.object_id == object_id)
503                .map(|update| &update.attribute)
504                .cloned()
505        };
506
507        match util::find_in_direction(&frames.frames, current_index, direction, predicate) {
508            Some((index, attribute)) => Ok((attribute, index)),
509            None => SubtrActorError::new_result(SubtrActorErrorVariant::NoUpdateAfterFrame {
510                actor_id: *actor_id,
511                object_id: *object_id,
512                frame_index: current_index,
513            }),
514        }
515    }
516
517    // Update functions
518
519    /// This method is responsible for updating various mappings that are used
520    /// to track and link different actors in the replay.
521    ///
522    /// The replay data is a stream of [`boxcars::Frame`] objects that contain
523    /// information about the game at a specific point in time. These frames
524    /// contain updates for different actors, and the goal of this method is to
525    /// maintain and update the mappings for these actors as the frames are
526    /// processed.
527    ///
528    /// The method loops over each `updated_actors` field in the
529    /// [`boxcars::Frame`]. For each updated actor, it checks whether the
530    /// actor's object ID matches the object ID of various keys in the actor
531    /// state. If a match is found, the corresponding map is updated with a new
532    /// entry linking the actor ID to the value of the attribute in the replay
533    /// frame.
534    ///
535    /// The mappings updated are:
536    /// - `player_to_actor_id`: maps a player's [`boxcars::UniqueId`] to their actor ID.
537    /// - `player_to_team`: maps a player's actor ID to their team actor ID.
538    /// - `player_to_car`: maps a player's actor ID to their car actor ID.
539    /// - `car_to_boost`: maps a car's actor ID to its associated boost actor ID.
540    /// - `car_to_dodge`: maps a car's actor ID to its associated dodge actor ID.
541    /// - `car_to_jump`: maps a car's actor ID to its associated jump actor ID.
542    /// - `car_to_double_jump`: maps a car's actor ID to its associated double jump actor ID.
543    ///
544    /// The function also handles the deletion of actors. When an actor is
545    /// deleted, the function removes the actor's ID from the `player_to_car`
546    /// mapping.
547    fn update_mappings(&mut self, frame: &boxcars::Frame) -> SubtrActorResult<()> {
548        for update in frame.updated_actors.iter() {
549            macro_rules! maintain_link {
550                ($map:expr, $actor_type:expr, $attr:expr, $get_key: expr, $get_value: expr, $type:path) => {{
551                    if &update.object_id == self.get_object_id_for_key(&$attr)? {
552                        if self
553                            .get_actor_ids_by_type($actor_type)?
554                            .iter()
555                            .any(|id| id == &update.actor_id)
556                        {
557                            let value = get_actor_attribute_matching!(
558                                self,
559                                &update.actor_id,
560                                $attr,
561                                $type
562                            )?;
563                            let _key = $get_key(update.actor_id, value);
564                            let _new_value = $get_value(update.actor_id, value);
565                            let _old_value = $map.insert(
566                                $get_key(update.actor_id, value),
567                                $get_value(update.actor_id, value),
568                            );
569                        }
570                    }
571                }};
572            }
573            macro_rules! maintain_actor_link {
574                ($map:expr, $actor_type:expr, $attr:expr) => {
575                    maintain_link!(
576                        $map,
577                        $actor_type,
578                        $attr,
579                        // This is slightly confusing, but in these cases we are
580                        // using the attribute as the key to the current actor.
581                        get_actor_id_from_active_actor,
582                        use_update_actor,
583                        boxcars::Attribute::ActiveActor
584                    )
585                };
586            }
587            macro_rules! maintain_vehicle_key_link {
588                ($map:expr, $actor_type:expr) => {
589                    maintain_actor_link!($map, $actor_type, VEHICLE_KEY)
590                };
591            }
592            maintain_link!(
593                self.player_to_actor_id,
594                PLAYER_TYPE,
595                UNIQUE_ID_KEY,
596                |_, unique_id: &boxcars::UniqueId| unique_id.remote_id.clone(),
597                use_update_actor,
598                boxcars::Attribute::UniqueId
599            );
600            maintain_link!(
601                self.player_to_team,
602                PLAYER_TYPE,
603                TEAM_KEY,
604                // In this case we are using the update actor as the key.
605                use_update_actor,
606                get_actor_id_from_active_actor,
607                boxcars::Attribute::ActiveActor
608            );
609            maintain_actor_link!(self.player_to_car, CAR_TYPE, PLAYER_REPLICATION_KEY);
610            maintain_vehicle_key_link!(self.car_to_boost, BOOST_TYPE);
611            maintain_vehicle_key_link!(self.car_to_dodge, DODGE_TYPE);
612            maintain_vehicle_key_link!(self.car_to_jump, JUMP_TYPE);
613            maintain_vehicle_key_link!(self.car_to_double_jump, DOUBLE_JUMP_TYPE);
614        }
615
616        for actor_id in frame.deleted_actors.iter() {
617            if let Some(car_id) = self.player_to_car.remove(actor_id) {
618                log::info!("Player actor {actor_id:?} deleted, car id: {car_id:?}.");
619            }
620        }
621
622        Ok(())
623    }
624
625    fn update_ball_id(&mut self, frame: &boxcars::Frame) -> SubtrActorResult<()> {
626        // XXX: This assumes there is only ever one ball, which is safe (I think?)
627        if let Some(actor_id) = self.ball_actor_id {
628            if frame.deleted_actors.contains(&actor_id) {
629                self.ball_actor_id = None;
630            }
631        } else {
632            self.ball_actor_id = self.find_ball_actor();
633            if self.ball_actor_id.is_some() {
634                return self.update_ball_id(frame);
635            }
636        }
637        Ok(())
638    }
639
640    /// Updates the boost amounts for all the actors in a given frame.
641    ///
642    /// This function works by iterating over all the actors of a particular
643    /// boost type. For each actor, it retrieves the current boost value. If the
644    /// actor's boost value hasn't been updated, it continues using the derived
645    /// boost value from the last frame. If the actor's boost is active, it
646    /// subtracts from the current boost value according to the frame delta and
647    /// the constant `BOOST_USED_PER_SECOND`.
648    ///
649    /// The updated boost values are then stored in the actor's derived
650    /// attributes.
651    ///
652    /// # Arguments
653    ///
654    /// * `frame` - A reference to the [`Frame`] in which the boost amounts are to be updated.
655    /// * `frame_index` - The index of the frame in the replay.
656    ///   [`Frame`]: boxcars::Frame
657    fn update_boost_amounts(
658        &mut self,
659        frame: &boxcars::Frame,
660        frame_index: usize,
661    ) -> SubtrActorResult<()> {
662        let updates: Vec<_> = self
663            .iter_actors_by_type_err(BOOST_TYPE)?
664            .map(|(actor_id, actor_state)| {
665                let (actor_amount_value, last_value, _, derived_value, is_active) =
666                    self.get_current_boost_values(actor_state);
667                let mut current_value = if actor_amount_value == last_value {
668                    // If we don't have an update in the actor, just continue
669                    // using our derived value
670                    derived_value
671                } else {
672                    // If we do have an update in the actor, use that value.
673                    actor_amount_value.into()
674                };
675                if is_active {
676                    current_value -= frame.delta * BOOST_USED_PER_SECOND;
677                }
678                (*actor_id, current_value.max(0.0), actor_amount_value)
679            })
680            .collect();
681
682        for (actor_id, current_value, new_last_value) in updates {
683            let derived_attributes = &mut self
684                .actor_state
685                .actor_states
686                .get_mut(&actor_id)
687                // This actor is known to exist, so unwrap is fine
688                .unwrap()
689                .derived_attributes;
690
691            derived_attributes.insert(
692                LAST_BOOST_AMOUNT_KEY.to_string(),
693                (boxcars::Attribute::Byte(new_last_value), frame_index),
694            );
695            derived_attributes.insert(
696                BOOST_AMOUNT_KEY.to_string(),
697                (boxcars::Attribute::Float(current_value), frame_index),
698            );
699        }
700        Ok(())
701    }
702
703    /// Gets the current boost values for a given actor state.
704    ///
705    /// This function retrieves the current boost amount, whether the boost is active,
706    /// the derived boost amount, and the last known boost amount from the actor's state.
707    /// The derived value is retrieved from the actor's derived attributes, while
708    /// the other values are retrieved directly from the actor's attributes.
709    ///
710    /// # Arguments
711    ///
712    /// * `actor_state` - A reference to the actor's [`ActorState`] from which
713    ///   the boost values are to be retrieved.
714    ///
715    /// # Returns
716    ///
717    /// This function returns a tuple consisting of the following:
718    /// * Current boost amount
719    /// * Last known boost amount
720    /// * Boost active value (1 if active, 0 otherwise)
721    /// * Derived boost amount
722    /// * Whether the boost is active (true if active, false otherwise)
723    fn get_current_boost_values(&self, actor_state: &ActorState) -> (u8, u8, u8, f32, bool) {
724        // Try to get boost amount from ReplicatedBoost attribute first (new format)
725        let amount_value = if let Ok(boxcars::Attribute::ReplicatedBoost(replicated_boost)) =
726            self.get_attribute(&actor_state.attributes, BOOST_REPLICATED_KEY)
727        {
728            replicated_boost.boost_amount
729        } else {
730            // Fall back to ReplicatedBoostAmount (old format)
731            get_attribute_errors_expected!(
732                self,
733                &actor_state.attributes,
734                BOOST_AMOUNT_KEY,
735                boxcars::Attribute::Byte
736            )
737            .cloned()
738            .unwrap_or(0)
739        };
740        let active_value = get_attribute_errors_expected!(
741            self,
742            &actor_state.attributes,
743            COMPONENT_ACTIVE_KEY,
744            boxcars::Attribute::Byte
745        )
746        .cloned()
747        .unwrap_or(0);
748        let is_active = active_value % 2 == 1;
749        let derived_value = actor_state
750            .derived_attributes
751            .get(BOOST_AMOUNT_KEY)
752            .cloned()
753            .and_then(|v| attribute_match!(v.0, boxcars::Attribute::Float).ok())
754            .unwrap_or(0.0);
755        let last_boost_amount = attribute_match!(
756            actor_state
757                .derived_attributes
758                .get(LAST_BOOST_AMOUNT_KEY)
759                .cloned()
760                .map(|v| v.0)
761                .unwrap_or_else(|| boxcars::Attribute::Byte(amount_value)),
762            boxcars::Attribute::Byte
763        )
764        .unwrap_or(0);
765        (
766            amount_value,
767            last_boost_amount,
768            active_value,
769            derived_value,
770            is_active,
771        )
772    }
773
774    fn update_demolishes(&mut self, frame: &boxcars::Frame, index: usize) -> SubtrActorResult<()> {
775        let new_demolishes: Vec<_> = self
776            .get_active_demolish_fx()?
777            .flat_map(|demolish_fx| {
778                if !self.demolish_is_known(demolish_fx, index) {
779                    Some(*demolish_fx.as_ref())
780                } else {
781                    None
782                }
783            })
784            .collect();
785
786        for demolish in new_demolishes {
787            match self.build_demolish_info(&demolish, frame, index) {
788                Ok(demolish_info) => self.demolishes.push(demolish_info),
789                Err(_e) => {
790                    log::warn!("Error building demolish info");
791                }
792            }
793            self.known_demolishes.push((demolish, index))
794        }
795
796        Ok(())
797    }
798
799    fn build_demolish_info(
800        &self,
801        demolish_fx: &boxcars::DemolishFx,
802        frame: &boxcars::Frame,
803        index: usize,
804    ) -> SubtrActorResult<DemolishInfo> {
805        let attacker = self.get_player_id_from_car_id(&demolish_fx.attacker)?;
806        let victim = self.get_player_id_from_car_id(&demolish_fx.victim)?;
807        Ok(DemolishInfo {
808            time: frame.time,
809            seconds_remaining: self.get_seconds_remaining()?,
810            frame: index,
811            attacker,
812            victim,
813            attacker_velocity: demolish_fx.attack_velocity,
814            victim_velocity: demolish_fx.victim_velocity,
815        })
816    }
817
818    // ID Mapping functions
819
820    fn get_player_id_from_car_id(&self, actor_id: &boxcars::ActorId) -> SubtrActorResult<PlayerId> {
821        self.get_player_id_from_actor_id(&self.get_player_actor_id_from_car_actor_id(actor_id)?)
822    }
823
824    fn get_player_id_from_actor_id(
825        &self,
826        actor_id: &boxcars::ActorId,
827    ) -> SubtrActorResult<PlayerId> {
828        for (player_id, player_actor_id) in self.player_to_actor_id.iter() {
829            if actor_id == player_actor_id {
830                return Ok(player_id.clone());
831            }
832        }
833        SubtrActorError::new_result(SubtrActorErrorVariant::NoMatchingPlayerId {
834            actor_id: *actor_id,
835        })
836    }
837
838    fn get_player_actor_id_from_car_actor_id(
839        &self,
840        actor_id: &boxcars::ActorId,
841    ) -> SubtrActorResult<boxcars::ActorId> {
842        for (player_id, car_id) in self.player_to_car.iter() {
843            if actor_id == car_id {
844                return Ok(*player_id);
845            }
846        }
847        SubtrActorError::new_result(SubtrActorErrorVariant::NoMatchingPlayerId {
848            actor_id: *actor_id,
849        })
850    }
851
852    fn demolish_is_known(&self, demolish_fx: &boxcars::DemolishFx, frame_index: usize) -> bool {
853        self.known_demolishes.iter().any(|(existing, index)| {
854            existing == demolish_fx
855                && frame_index
856                    .checked_sub(*index)
857                    .or_else(|| index.checked_sub(frame_index))
858                    .unwrap()
859                    < MAX_DEMOLISH_KNOWN_FRAMES_PASSED
860        })
861    }
862
863    /// Provides an iterator over the active demolition effects,
864    /// [`boxcars::DemolishFx`], in the current frame.
865    pub fn get_active_demolish_fx(
866        &self,
867    ) -> SubtrActorResult<impl Iterator<Item = &Box<boxcars::DemolishFx>>> {
868        Ok(self
869            .iter_actors_by_type_err(CAR_TYPE)?
870            .flat_map(|(_actor_id, state)| {
871                get_attribute_errors_expected!(
872                    self,
873                    &state.attributes,
874                    DEMOLISH_GOAL_EXPLOSION_KEY,
875                    boxcars::Attribute::DemolishFx
876                )
877                .ok()
878            }))
879    }
880
881    // Interpolation Support functions
882
883    fn get_frame(&self, frame_index: usize) -> SubtrActorResult<&boxcars::Frame> {
884        self.replay
885            .network_frames
886            .as_ref()
887            .ok_or(SubtrActorError::new(
888                SubtrActorErrorVariant::NoNetworkFrames,
889            ))?
890            .frames
891            .get(frame_index)
892            .ok_or(SubtrActorError::new(
893                SubtrActorErrorVariant::FrameIndexOutOfBounds,
894            ))
895    }
896
897    fn velocities_applied_rigid_body(
898        &self,
899        rigid_body: &boxcars::RigidBody,
900        rb_frame_index: usize,
901        target_time: f32,
902    ) -> SubtrActorResult<boxcars::RigidBody> {
903        let rb_frame = self.get_frame(rb_frame_index)?;
904        let interpolation_amount = target_time - rb_frame.time;
905        Ok(apply_velocities_to_rigid_body(
906            rigid_body,
907            interpolation_amount,
908        ))
909    }
910
911    /// This function first retrieves the actor's [`RigidBody`] at the current
912    /// frame. If the time difference between the current frame and the provided
913    /// time is within the `close_enough` threshold, the function returns the
914    /// current frame's [`RigidBody`].
915    ///
916    /// If the [`RigidBody`] at the exact time is not available, the function
917    /// searches in the appropriate direction (either forwards or backwards in
918    /// time) to find another [`RigidBody`] to interpolate from. If the found
919    /// [`RigidBody`]'s time is within the `close_enough` threshold, it is
920    /// returned.
921    ///
922    /// Otherwise, it interpolates between the two [`RigidBody`]s (from the
923    /// current frame and the found frame) to produce a [`RigidBody`] for the
924    /// specified time. This is done using the [`get_interpolated_rigid_body`]
925    /// function from the `util` module.
926    ///
927    /// # Arguments
928    ///
929    /// * `actor_id` - The ID of the actor whose [`RigidBody`] is to be retrieved.
930    /// * `time` - The time at which the actor's [`RigidBody`] is to be retrieved.
931    /// * `close_enough` - The acceptable threshold for time difference when
932    ///   determining if a [`RigidBody`] is close enough to the desired time to not
933    ///   require interpolation.
934    ///
935    /// # Returns
936    ///
937    /// A [`RigidBody`] for the actor at the specified time.
938    ///
939    /// [`RigidBody`]: boxcars::RigidBody
940    /// [`get_interpolated_rigid_body`]: util::get_interpolated_rigid_body
941    pub fn get_interpolated_actor_rigid_body(
942        &self,
943        actor_id: &boxcars::ActorId,
944        time: f32,
945        close_enough: f32,
946    ) -> SubtrActorResult<boxcars::RigidBody> {
947        let (frame_body, frame_index) = self.get_actor_rigid_body(actor_id)?;
948        let frame_time = self.get_frame(*frame_index)?.time;
949        let time_and_frame_difference = time - frame_time;
950
951        if (time_and_frame_difference).abs() <= close_enough.abs() {
952            return Ok(*frame_body);
953        }
954
955        let search_direction = if time_and_frame_difference > 0.0 {
956            util::SearchDirection::Forward
957        } else {
958            util::SearchDirection::Backward
959        };
960
961        let object_id = self.get_object_id_for_key(RIGID_BODY_STATE_KEY)?;
962
963        let (attribute, found_frame) =
964            self.find_update_in_direction(*frame_index, actor_id, object_id, search_direction)?;
965        let found_time = self.get_frame(found_frame)?.time;
966
967        let found_body = attribute_match!(attribute, boxcars::Attribute::RigidBody)?;
968
969        if (found_time - time).abs() <= close_enough {
970            return Ok(found_body);
971        }
972
973        let (start_body, start_time, end_body, end_time) = match search_direction {
974            util::SearchDirection::Forward => (frame_body, frame_time, &found_body, found_time),
975            util::SearchDirection::Backward => (&found_body, found_time, frame_body, frame_time),
976        };
977
978        util::get_interpolated_rigid_body(start_body, start_time, end_body, end_time, time)
979    }
980
981    // Actor functions
982
983    fn get_object_id_for_key(&self, name: &'static str) -> SubtrActorResult<&boxcars::ObjectId> {
984        self.name_to_object_id
985            .get(name)
986            .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::ObjectIdNotFound { name }))
987    }
988
989    fn get_actor_ids_by_type(&self, name: &'static str) -> SubtrActorResult<&[boxcars::ActorId]> {
990        self.get_object_id_for_key(name)
991            .map(|object_id| self.get_actor_ids_by_object_id(object_id))
992    }
993
994    fn get_actor_ids_by_object_id(&self, object_id: &boxcars::ObjectId) -> &[boxcars::ActorId] {
995        self.actor_state
996            .actor_ids_by_type
997            .get(object_id)
998            .map(|v| &v[..])
999            .unwrap_or_else(|| &EMPTY_ACTOR_IDS)
1000    }
1001
1002    fn get_actor_state(&self, actor_id: &boxcars::ActorId) -> SubtrActorResult<&ActorState> {
1003        self.actor_state.actor_states.get(actor_id).ok_or_else(|| {
1004            SubtrActorError::new(SubtrActorErrorVariant::NoStateForActorId {
1005                actor_id: *actor_id,
1006            })
1007        })
1008    }
1009
1010    fn get_actor_attribute<'b>(
1011        &'b self,
1012        actor_id: &boxcars::ActorId,
1013        property: &'static str,
1014    ) -> SubtrActorResult<&'b boxcars::Attribute> {
1015        self.get_attribute(&self.get_actor_state(actor_id)?.attributes, property)
1016    }
1017
1018    fn get_attribute<'b>(
1019        &'b self,
1020        map: &'b HashMap<boxcars::ObjectId, (boxcars::Attribute, usize)>,
1021        property: &'static str,
1022    ) -> SubtrActorResult<&'b boxcars::Attribute> {
1023        self.get_attribute_and_updated(map, property).map(|v| &v.0)
1024    }
1025
1026    fn get_attribute_and_updated<'b>(
1027        &'b self,
1028        map: &'b HashMap<boxcars::ObjectId, (boxcars::Attribute, usize)>,
1029        property: &'static str,
1030    ) -> SubtrActorResult<&'b (boxcars::Attribute, usize)> {
1031        let attribute_object_id = self.get_object_id_for_key(property)?;
1032        map.get(attribute_object_id).ok_or_else(|| {
1033            SubtrActorError::new(SubtrActorErrorVariant::PropertyNotFoundInState { property })
1034        })
1035    }
1036
1037    fn find_ball_actor(&self) -> Option<boxcars::ActorId> {
1038        BALL_TYPES
1039            .iter()
1040            .filter_map(|ball_type| self.iter_actors_by_type(ball_type))
1041            .flatten()
1042            .map(|(actor_id, _)| *actor_id)
1043            .next()
1044    }
1045
1046    pub fn get_ball_actor_id(&self) -> SubtrActorResult<boxcars::ActorId> {
1047        self.ball_actor_id.ok_or(SubtrActorError::new(
1048            SubtrActorErrorVariant::BallActorNotFound,
1049        ))
1050    }
1051
1052    pub fn get_metadata_actor_id(&self) -> SubtrActorResult<&boxcars::ActorId> {
1053        self.get_actor_ids_by_type(GAME_TYPE)?
1054            .iter()
1055            .next()
1056            .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::NoGameActor))
1057    }
1058
1059    pub fn get_player_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
1060        self.player_to_actor_id
1061            .get(player_id)
1062            .ok_or_else(|| {
1063                SubtrActorError::new(SubtrActorErrorVariant::ActorNotFound {
1064                    name: "ActorId",
1065                    player_id: player_id.clone(),
1066                })
1067            })
1068            .cloned()
1069    }
1070
1071    pub fn get_car_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
1072        self.player_to_car
1073            .get(&self.get_player_actor_id(player_id)?)
1074            .ok_or_else(|| {
1075                SubtrActorError::new(SubtrActorErrorVariant::ActorNotFound {
1076                    name: "Car",
1077                    player_id: player_id.clone(),
1078                })
1079            })
1080            .cloned()
1081    }
1082
1083    pub fn get_car_connected_actor_id(
1084        &self,
1085        player_id: &PlayerId,
1086        map: &HashMap<boxcars::ActorId, boxcars::ActorId>,
1087        name: &'static str,
1088    ) -> SubtrActorResult<boxcars::ActorId> {
1089        map.get(&self.get_car_actor_id(player_id)?)
1090            .ok_or_else(|| {
1091                SubtrActorError::new(SubtrActorErrorVariant::ActorNotFound {
1092                    name,
1093                    player_id: player_id.clone(),
1094                })
1095            })
1096            .cloned()
1097    }
1098
1099    pub fn get_boost_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
1100        self.get_car_connected_actor_id(player_id, &self.car_to_boost, "Boost")
1101    }
1102
1103    pub fn get_jump_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
1104        self.get_car_connected_actor_id(player_id, &self.car_to_jump, "Jump")
1105    }
1106
1107    pub fn get_double_jump_actor_id(
1108        &self,
1109        player_id: &PlayerId,
1110    ) -> SubtrActorResult<boxcars::ActorId> {
1111        self.get_car_connected_actor_id(player_id, &self.car_to_double_jump, "Double Jump")
1112    }
1113
1114    pub fn get_dodge_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
1115        self.get_car_connected_actor_id(player_id, &self.car_to_dodge, "Dodge")
1116    }
1117
1118    pub fn get_actor_rigid_body(
1119        &self,
1120        actor_id: &boxcars::ActorId,
1121    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
1122        get_attribute_and_updated!(
1123            self,
1124            &self.get_actor_state(actor_id)?.attributes,
1125            RIGID_BODY_STATE_KEY,
1126            boxcars::Attribute::RigidBody
1127        )
1128    }
1129
1130    // Actor iteration functions
1131
1132    pub fn iter_player_ids_in_order(&self) -> impl Iterator<Item = &PlayerId> {
1133        self.team_zero.iter().chain(self.team_one.iter())
1134    }
1135
1136    pub fn player_count(&self) -> usize {
1137        self.iter_player_ids_in_order().count()
1138    }
1139
1140    fn iter_actors_by_type_err(
1141        &self,
1142        name: &'static str,
1143    ) -> SubtrActorResult<impl Iterator<Item = (&boxcars::ActorId, &ActorState)>> {
1144        Ok(self.iter_actors_by_object_id(self.get_object_id_for_key(name)?))
1145    }
1146
1147    pub fn iter_actors_by_type(
1148        &self,
1149        name: &'static str,
1150    ) -> Option<impl Iterator<Item = (&boxcars::ActorId, &ActorState)>> {
1151        self.iter_actors_by_type_err(name).ok()
1152    }
1153
1154    pub fn iter_actors_by_object_id<'b>(
1155        &'b self,
1156        object_id: &'b boxcars::ObjectId,
1157    ) -> impl Iterator<Item = (&'b boxcars::ActorId, &'b ActorState)> + 'b {
1158        let actor_ids = self
1159            .actor_state
1160            .actor_ids_by_type
1161            .get(object_id)
1162            .map(|v| &v[..])
1163            .unwrap_or_else(|| &EMPTY_ACTOR_IDS);
1164
1165        actor_ids
1166            .iter()
1167            // This unwrap is fine because we know the actor will exist as it is
1168            // in the actor_ids_by_type
1169            .map(move |id| (id, self.actor_state.actor_states.get(id).unwrap()))
1170    }
1171
1172    // Properties
1173
1174    /// Returns the remaining time in seconds in the game as an `i32`.
1175    pub fn get_seconds_remaining(&self) -> SubtrActorResult<i32> {
1176        get_actor_attribute_matching!(
1177            self,
1178            self.get_metadata_actor_id()?,
1179            SECONDS_REMAINING_KEY,
1180            boxcars::Attribute::Int
1181        )
1182        .cloned()
1183    }
1184
1185    /// Returns a boolean indicating whether ball syncing is ignored.
1186    pub fn get_ignore_ball_syncing(&self) -> SubtrActorResult<bool> {
1187        let actor_id = self.get_ball_actor_id()?;
1188        get_actor_attribute_matching!(
1189            self,
1190            &actor_id,
1191            IGNORE_SYNCING_KEY,
1192            boxcars::Attribute::Boolean
1193        )
1194        .cloned()
1195    }
1196
1197    /// Returns a reference to the [`RigidBody`](boxcars::RigidBody) of the ball.
1198    pub fn get_ball_rigid_body(&self) -> SubtrActorResult<&boxcars::RigidBody> {
1199        self.ball_actor_id
1200            .ok_or(SubtrActorError::new(
1201                SubtrActorErrorVariant::BallActorNotFound,
1202            ))
1203            .and_then(|actor_id| self.get_actor_rigid_body(&actor_id).map(|v| v.0))
1204    }
1205
1206    /// Returns a boolean indicating whether the ball's
1207    /// [`RigidBody`](boxcars::RigidBody) exists and is not sleeping.
1208    pub fn ball_rigid_body_exists(&self) -> SubtrActorResult<bool> {
1209        Ok(self
1210            .get_ball_rigid_body()
1211            .map(|rb| !rb.sleeping)
1212            .unwrap_or(false))
1213    }
1214
1215    /// Returns a reference to the ball's [`RigidBody`](boxcars::RigidBody) and
1216    /// its last updated frame.
1217    pub fn get_ball_rigid_body_and_updated(
1218        &self,
1219    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
1220        self.ball_actor_id
1221            .ok_or(SubtrActorError::new(
1222                SubtrActorErrorVariant::BallActorNotFound,
1223            ))
1224            .and_then(|actor_id| {
1225                get_attribute_and_updated!(
1226                    self,
1227                    &self.get_actor_state(&actor_id)?.attributes,
1228                    RIGID_BODY_STATE_KEY,
1229                    boxcars::Attribute::RigidBody
1230                )
1231            })
1232    }
1233
1234    /// Returns a [`RigidBody`](boxcars::RigidBody) of the ball with applied
1235    /// velocity at the target time.
1236    pub fn get_velocity_applied_ball_rigid_body(
1237        &self,
1238        target_time: f32,
1239    ) -> SubtrActorResult<boxcars::RigidBody> {
1240        let (current_rigid_body, frame_index) = self.get_ball_rigid_body_and_updated()?;
1241        self.velocities_applied_rigid_body(current_rigid_body, *frame_index, target_time)
1242    }
1243
1244    /// Returns an interpolated [`RigidBody`](boxcars::RigidBody) of the ball at
1245    /// a specified time.
1246    pub fn get_interpolated_ball_rigid_body(
1247        &self,
1248        time: f32,
1249        close_enough: f32,
1250    ) -> SubtrActorResult<boxcars::RigidBody> {
1251        self.get_interpolated_actor_rigid_body(&self.get_ball_actor_id()?, time, close_enough)
1252    }
1253
1254    /// Returns the name of the specified player.
1255    pub fn get_player_name(&self, player_id: &PlayerId) -> SubtrActorResult<String> {
1256        get_actor_attribute_matching!(
1257            self,
1258            &self.get_player_actor_id(player_id)?,
1259            PLAYER_NAME_KEY,
1260            boxcars::Attribute::String
1261        )
1262        .cloned()
1263    }
1264
1265    /// Returns the team key for the specified player.
1266    pub fn get_player_team_key(&self, player_id: &PlayerId) -> SubtrActorResult<String> {
1267        let team_actor_id = self
1268            .player_to_team
1269            .get(&self.get_player_actor_id(player_id)?)
1270            .ok_or_else(|| {
1271                SubtrActorError::new(SubtrActorErrorVariant::UnknownPlayerTeam {
1272                    player_id: player_id.clone(),
1273                })
1274            })?;
1275        let state = self.get_actor_state(team_actor_id)?;
1276        self.object_id_to_name
1277            .get(&state.object_id)
1278            .ok_or_else(|| {
1279                SubtrActorError::new(SubtrActorErrorVariant::UnknownPlayerTeam {
1280                    player_id: player_id.clone(),
1281                })
1282            })
1283            .cloned()
1284    }
1285
1286    /// Determines if the player is on team 0.
1287    pub fn get_player_is_team_0(&self, player_id: &PlayerId) -> SubtrActorResult<bool> {
1288        Ok(self
1289            .get_player_team_key(player_id)?
1290            .chars()
1291            .last()
1292            .ok_or_else(|| {
1293                SubtrActorError::new(SubtrActorErrorVariant::EmptyTeamName {
1294                    player_id: player_id.clone(),
1295                })
1296            })?
1297            == '0')
1298    }
1299
1300    /// Returns a reference to the [`RigidBody`](boxcars::RigidBody) of the player's car.
1301    pub fn get_player_rigid_body(
1302        &self,
1303        player_id: &PlayerId,
1304    ) -> SubtrActorResult<&boxcars::RigidBody> {
1305        self.get_car_actor_id(player_id)
1306            .and_then(|actor_id| self.get_actor_rigid_body(&actor_id).map(|v| v.0))
1307    }
1308
1309    /// Returns the most recent update to the [`RigidBody`](boxcars::RigidBody)
1310    /// of the player's car along with the index of the frame in which it was
1311    /// updated.
1312    pub fn get_player_rigid_body_and_updated(
1313        &self,
1314        player_id: &PlayerId,
1315    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
1316        self.get_car_actor_id(player_id).and_then(|actor_id| {
1317            get_attribute_and_updated!(
1318                self,
1319                &self.get_actor_state(&actor_id)?.attributes,
1320                RIGID_BODY_STATE_KEY,
1321                boxcars::Attribute::RigidBody
1322            )
1323        })
1324    }
1325
1326    pub fn get_velocity_applied_player_rigid_body(
1327        &self,
1328        player_id: &PlayerId,
1329        target_time: f32,
1330    ) -> SubtrActorResult<boxcars::RigidBody> {
1331        let (current_rigid_body, frame_index) =
1332            self.get_player_rigid_body_and_updated(player_id)?;
1333        self.velocities_applied_rigid_body(current_rigid_body, *frame_index, target_time)
1334    }
1335
1336    pub fn get_interpolated_player_rigid_body(
1337        &self,
1338        player_id: &PlayerId,
1339        time: f32,
1340        close_enough: f32,
1341    ) -> SubtrActorResult<boxcars::RigidBody> {
1342        self.get_interpolated_actor_rigid_body(
1343            &self.get_car_actor_id(player_id).unwrap(),
1344            time,
1345            close_enough,
1346        )
1347    }
1348
1349    pub fn get_player_boost_level(&self, player_id: &PlayerId) -> SubtrActorResult<f32> {
1350        self.get_boost_actor_id(player_id).and_then(|actor_id| {
1351            let boost_state = self.get_actor_state(&actor_id)?;
1352            get_derived_attribute!(
1353                boost_state.derived_attributes,
1354                BOOST_AMOUNT_KEY,
1355                boxcars::Attribute::Float
1356            )
1357            .cloned()
1358        })
1359    }
1360
1361    pub fn get_component_active(&self, actor_id: &boxcars::ActorId) -> SubtrActorResult<u8> {
1362        get_actor_attribute_matching!(
1363            self,
1364            &actor_id,
1365            COMPONENT_ACTIVE_KEY,
1366            boxcars::Attribute::Byte
1367        )
1368        .cloned()
1369    }
1370
1371    pub fn get_boost_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
1372        self.get_boost_actor_id(player_id)
1373            .and_then(|actor_id| self.get_component_active(&actor_id))
1374    }
1375
1376    pub fn get_jump_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
1377        self.get_jump_actor_id(player_id)
1378            .and_then(|actor_id| self.get_component_active(&actor_id))
1379    }
1380
1381    pub fn get_double_jump_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
1382        self.get_double_jump_actor_id(player_id)
1383            .and_then(|actor_id| self.get_component_active(&actor_id))
1384    }
1385
1386    pub fn get_dodge_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
1387        self.get_dodge_actor_id(player_id)
1388            .and_then(|actor_id| self.get_component_active(&actor_id))
1389    }
1390
1391    // Debugging
1392
1393    pub fn map_attribute_keys(
1394        &self,
1395        hash_map: &HashMap<boxcars::ObjectId, (boxcars::Attribute, usize)>,
1396    ) -> HashMap<String, boxcars::Attribute> {
1397        hash_map
1398            .iter()
1399            .map(|(k, (v, _updated))| {
1400                self.object_id_to_name
1401                    .get(k)
1402                    .map(|name| (name.clone(), v.clone()))
1403                    .unwrap()
1404            })
1405            .collect()
1406    }
1407
1408    pub fn all_mappings_string(&self) -> String {
1409        let pairs = [
1410            ("player_to_car", &self.player_to_car),
1411            ("player_to_team", &self.player_to_team),
1412            ("car_to_boost", &self.car_to_boost),
1413            ("car_to_jump", &self.car_to_jump),
1414            ("car_to_double_jump", &self.car_to_double_jump),
1415            ("car_to_dodge", &self.car_to_dodge),
1416        ];
1417        let strings: Vec<_> = pairs
1418            .iter()
1419            .map(|(map_name, map)| format!("{map_name:?}: {map:?}"))
1420            .collect();
1421        strings.join("\n")
1422    }
1423
1424    pub fn actor_state_string(&self, actor_id: &boxcars::ActorId) -> String {
1425        format!(
1426            "{:?}",
1427            self.get_actor_state(actor_id)
1428                .map(|s| self.map_attribute_keys(&s.attributes))
1429        )
1430    }
1431
1432    pub fn print_actors_by_id<'b>(&self, actor_ids: impl Iterator<Item = &'b boxcars::ActorId>) {
1433        actor_ids.for_each(|actor_id| {
1434            let state = self.get_actor_state(actor_id).unwrap();
1435            println!(
1436                "{:?}\n\n\n",
1437                self.object_id_to_name.get(&state.object_id).unwrap()
1438            );
1439            println!("{:?}", self.map_attribute_keys(&state.attributes))
1440        })
1441    }
1442
1443    pub fn print_actors_of_type(&self, actor_type: &'static str) {
1444        self.iter_actors_by_type(actor_type)
1445            .unwrap()
1446            .for_each(|(_actor_id, state)| {
1447                println!("{:?}", self.map_attribute_keys(&state.attributes));
1448            });
1449    }
1450
1451    pub fn print_actor_types(&self) {
1452        let types: Vec<_> = self
1453            .actor_state
1454            .actor_ids_by_type
1455            .keys()
1456            .filter_map(|id| self.object_id_to_name.get(id))
1457            .collect();
1458        println!("{types:?}");
1459    }
1460
1461    pub fn print_all_actors(&self) {
1462        self.actor_state
1463            .actor_states
1464            .iter()
1465            .for_each(|(actor_id, _actor_state)| {
1466                println!("{:?}", self.actor_state_string(actor_id))
1467            })
1468    }
1469}