Skip to main content

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