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    /// The function also handles the deletion of actors. When an actor is
587    /// deleted, the function removes the actor's ID from the `player_to_car`
588    /// mapping.
589    fn update_mappings(&mut self, frame: &boxcars::Frame) -> SubtrActorResult<()> {
590        for update in frame.updated_actors.iter() {
591            macro_rules! maintain_link {
592                ($map:expr, $actor_type:expr, $attr:expr, $get_key: expr, $get_value: expr, $type:path) => {{
593                    if &update.object_id == self.get_object_id_for_key(&$attr)? {
594                        if self
595                            .get_actor_ids_by_type($actor_type)?
596                            .iter()
597                            .any(|id| id == &update.actor_id)
598                        {
599                            let value = get_actor_attribute_matching!(
600                                self,
601                                &update.actor_id,
602                                $attr,
603                                $type
604                            )?;
605                            let _key = $get_key(update.actor_id, value);
606                            let _new_value = $get_value(update.actor_id, value);
607                            let _old_value = $map.insert(
608                                $get_key(update.actor_id, value),
609                                $get_value(update.actor_id, value),
610                            );
611                        }
612                    }
613                }};
614            }
615            macro_rules! maintain_actor_link {
616                ($map:expr, $actor_type:expr, $attr:expr) => {
617                    maintain_link!(
618                        $map,
619                        $actor_type,
620                        $attr,
621                        // This is slightly confusing, but in these cases we are
622                        // using the attribute as the key to the current actor.
623                        get_actor_id_from_active_actor,
624                        use_update_actor,
625                        boxcars::Attribute::ActiveActor
626                    )
627                };
628            }
629            macro_rules! maintain_vehicle_key_link {
630                ($map:expr, $actor_type:expr) => {
631                    maintain_actor_link!($map, $actor_type, VEHICLE_KEY)
632                };
633            }
634            maintain_link!(
635                self.player_to_actor_id,
636                PLAYER_TYPE,
637                UNIQUE_ID_KEY,
638                |_, unique_id: &boxcars::UniqueId| unique_id.remote_id.clone(),
639                use_update_actor,
640                boxcars::Attribute::UniqueId
641            );
642            maintain_link!(
643                self.player_to_team,
644                PLAYER_TYPE,
645                TEAM_KEY,
646                // In this case we are using the update actor as the key.
647                use_update_actor,
648                get_actor_id_from_active_actor,
649                boxcars::Attribute::ActiveActor
650            );
651            maintain_actor_link!(self.player_to_car, CAR_TYPE, PLAYER_REPLICATION_KEY);
652            // Reverse of player_to_car. Not cleaned up on deletion so it
653            // persists after car destruction (needed for demolition tracking).
654            maintain_link!(
655                self.car_to_player,
656                CAR_TYPE,
657                PLAYER_REPLICATION_KEY,
658                use_update_actor,
659                get_actor_id_from_active_actor,
660                boxcars::Attribute::ActiveActor
661            );
662            maintain_vehicle_key_link!(self.car_to_boost, BOOST_TYPE);
663            maintain_vehicle_key_link!(self.car_to_dodge, DODGE_TYPE);
664            maintain_vehicle_key_link!(self.car_to_jump, JUMP_TYPE);
665            maintain_vehicle_key_link!(self.car_to_double_jump, DOUBLE_JUMP_TYPE);
666        }
667
668        for actor_id in frame.deleted_actors.iter() {
669            if let Some(car_id) = self.player_to_car.remove(actor_id) {
670                log::info!("Player actor {actor_id:?} deleted, car id: {car_id:?}.");
671            }
672        }
673
674        Ok(())
675    }
676
677    fn update_ball_id(&mut self, frame: &boxcars::Frame) -> SubtrActorResult<()> {
678        // XXX: This assumes there is only ever one ball, which is safe (I think?)
679        if let Some(actor_id) = self.ball_actor_id {
680            if frame.deleted_actors.contains(&actor_id) {
681                self.ball_actor_id = None;
682            }
683        } else {
684            self.ball_actor_id = self.find_ball_actor();
685            if self.ball_actor_id.is_some() {
686                return self.update_ball_id(frame);
687            }
688        }
689        Ok(())
690    }
691
692    /// Updates the boost amounts for all the actors in a given frame.
693    ///
694    /// This function works by iterating over all the actors of a particular
695    /// boost type. For each actor, it retrieves the current boost value. If the
696    /// actor's boost value hasn't been updated, it continues using the derived
697    /// boost value from the last frame. If the actor's boost is active, it
698    /// subtracts from the current boost value according to the frame delta and
699    /// the constant `BOOST_USED_RAW_UNITS_PER_SECOND`.
700    ///
701    /// The updated boost values are then stored in the actor's derived
702    /// attributes.
703    ///
704    /// # Arguments
705    ///
706    /// * `frame` - A reference to the [`Frame`] in which the boost amounts are to be updated.
707    /// * `frame_index` - The index of the frame in the replay.
708    ///   [`Frame`]: boxcars::Frame
709    fn update_boost_amounts(
710        &mut self,
711        frame: &boxcars::Frame,
712        frame_index: usize,
713    ) -> SubtrActorResult<()> {
714        let updates: Vec<_> = self
715            .iter_actors_by_type_err(BOOST_TYPE)?
716            .map(|(actor_id, actor_state)| {
717                let (actor_amount_value, last_value, _, derived_value, is_active) =
718                    self.get_current_boost_values(actor_state);
719                let mut current_value = if actor_amount_value == last_value {
720                    // If we don't have an update in the actor, just continue
721                    // using our derived value
722                    derived_value
723                } else {
724                    // If we do have an update in the actor, use that value.
725                    actor_amount_value.into()
726                };
727                if is_active {
728                    current_value -= frame.delta * BOOST_USED_RAW_UNITS_PER_SECOND;
729                }
730                (*actor_id, current_value.max(0.0), actor_amount_value)
731            })
732            .collect();
733
734        for (actor_id, current_value, new_last_value) in updates {
735            let derived_attributes = &mut self
736                .actor_state
737                .actor_states
738                .get_mut(&actor_id)
739                // This actor is known to exist, so unwrap is fine
740                .unwrap()
741                .derived_attributes;
742
743            derived_attributes.insert(
744                LAST_BOOST_AMOUNT_KEY.to_string(),
745                (boxcars::Attribute::Byte(new_last_value), frame_index),
746            );
747            derived_attributes.insert(
748                BOOST_AMOUNT_KEY.to_string(),
749                (boxcars::Attribute::Float(current_value), frame_index),
750            );
751        }
752        Ok(())
753    }
754
755    /// Gets the current boost values for a given actor state.
756    ///
757    /// This function retrieves the current boost amount, whether the boost is active,
758    /// the derived boost amount, and the last known boost amount from the actor's state.
759    /// The derived value is retrieved from the actor's derived attributes, while
760    /// the other values are retrieved directly from the actor's attributes.
761    ///
762    /// # Arguments
763    ///
764    /// * `actor_state` - A reference to the actor's [`ActorState`] from which
765    ///   the boost values are to be retrieved.
766    ///
767    /// # Returns
768    ///
769    /// This function returns a tuple consisting of the following:
770    /// * Current boost amount
771    /// * Last known boost amount
772    /// * Boost active value (1 if active, 0 otherwise)
773    /// * Derived boost amount
774    /// * Whether the boost is active (true if active, false otherwise)
775    fn get_current_boost_values(&self, actor_state: &ActorState) -> (u8, u8, u8, f32, bool) {
776        // Try to get boost amount from ReplicatedBoost attribute first (new format)
777        let amount_value = if let Ok(boxcars::Attribute::ReplicatedBoost(replicated_boost)) =
778            self.get_attribute(&actor_state.attributes, BOOST_REPLICATED_KEY)
779        {
780            replicated_boost.boost_amount
781        } else {
782            // Fall back to ReplicatedBoostAmount (old format)
783            get_attribute_errors_expected!(
784                self,
785                &actor_state.attributes,
786                BOOST_AMOUNT_KEY,
787                boxcars::Attribute::Byte
788            )
789            .cloned()
790            .unwrap_or(0)
791        };
792        let active_value = get_attribute_errors_expected!(
793            self,
794            &actor_state.attributes,
795            COMPONENT_ACTIVE_KEY,
796            boxcars::Attribute::Byte
797        )
798        .cloned()
799        .unwrap_or(0);
800        let is_active = active_value % 2 == 1;
801        let derived_value = actor_state
802            .derived_attributes
803            .get(BOOST_AMOUNT_KEY)
804            .cloned()
805            .and_then(|v| attribute_match!(v.0, boxcars::Attribute::Float).ok())
806            .unwrap_or(0.0);
807        let last_boost_amount = attribute_match!(
808            actor_state
809                .derived_attributes
810                .get(LAST_BOOST_AMOUNT_KEY)
811                .cloned()
812                .map(|v| v.0)
813                .unwrap_or_else(|| boxcars::Attribute::Byte(amount_value)),
814            boxcars::Attribute::Byte
815        )
816        .unwrap_or(0);
817        (
818            amount_value,
819            last_boost_amount,
820            active_value,
821            derived_value,
822            is_active,
823        )
824    }
825
826    fn update_demolishes(
827        &mut self,
828        frame: &boxcars::Frame,
829        frame_index: usize,
830    ) -> SubtrActorResult<()> {
831        if self.demolish_format.is_none() {
832            self.demolish_format = self.detect_demolish_format();
833        }
834
835        let new_demolishes: Vec<_> = self.get_active_demos()?.collect();
836
837        for demolish in new_demolishes {
838            if self.demolish_is_known(&demolish, frame_index) {
839                continue;
840            }
841            self.known_demolishes.push((demolish.clone(), frame_index));
842            match self.build_demolish_info(&demolish, frame, frame_index) {
843                Ok(demolish_info) => self.demolishes.push(demolish_info),
844                Err(_e) => {
845                    log::warn!(
846                        "Error building demolish info: {}; \
847                         attacker_car={}, victim_car={}, attacker={}, victim={}",
848                        _e.variant,
849                        demolish.attacker_actor_id(),
850                        demolish.victim_actor_id(),
851                        self.car_to_player
852                            .get(&demolish.attacker_actor_id())
853                            .unwrap_or(&boxcars::ActorId(-1)),
854                        self.car_to_player
855                            .get(&demolish.victim_actor_id())
856                            .unwrap_or(&boxcars::ActorId(-1)),
857                    );
858                }
859            }
860        }
861
862        Ok(())
863    }
864
865    fn build_demolish_info(
866        &self,
867        demo: &DemolishAttribute,
868        frame: &boxcars::Frame,
869        frame_index: usize,
870    ) -> SubtrActorResult<DemolishInfo> {
871        let attacker = self.get_player_id_from_car_id(&demo.attacker_actor_id())?;
872        let victim = self.get_player_id_from_car_id(&demo.victim_actor_id())?;
873        let (current_rigid_body, _) = self.get_player_rigid_body_and_updated(&victim)?;
874        Ok(DemolishInfo {
875            time: frame.time,
876            seconds_remaining: self.get_seconds_remaining()?,
877            frame: frame_index,
878            attacker,
879            victim,
880            attacker_velocity: demo.attacker_velocity(),
881            victim_velocity: demo.victim_velocity(),
882            victim_location: current_rigid_body.location,
883        })
884    }
885
886    // ID Mapping functions
887
888    pub fn get_player_id_from_car_id(
889        &self,
890        actor_id: &boxcars::ActorId,
891    ) -> SubtrActorResult<PlayerId> {
892        self.get_player_id_from_actor_id(&self.get_player_actor_id_from_car_actor_id(actor_id)?)
893    }
894
895    fn get_player_id_from_actor_id(
896        &self,
897        actor_id: &boxcars::ActorId,
898    ) -> SubtrActorResult<PlayerId> {
899        for (player_id, player_actor_id) in self.player_to_actor_id.iter() {
900            if actor_id == player_actor_id {
901                return Ok(player_id.clone());
902            }
903        }
904        SubtrActorError::new_result(SubtrActorErrorVariant::NoMatchingPlayerId {
905            actor_id: *actor_id,
906        })
907    }
908
909    fn get_player_actor_id_from_car_actor_id(
910        &self,
911        actor_id: &boxcars::ActorId,
912    ) -> SubtrActorResult<boxcars::ActorId> {
913        self.car_to_player.get(actor_id).copied().ok_or_else(|| {
914            SubtrActorError::new(SubtrActorErrorVariant::NoMatchingPlayerId {
915                actor_id: *actor_id,
916            })
917        })
918    }
919
920    fn demolish_is_known(&self, demo: &DemolishAttribute, frame_index: usize) -> bool {
921        self.known_demolishes
922            .iter()
923            .any(|(existing, existing_frame_index)| {
924                existing == demo
925                    && frame_index
926                        .checked_sub(*existing_frame_index)
927                        .or_else(|| existing_frame_index.checked_sub(frame_index))
928                        .unwrap()
929                        < MAX_DEMOLISH_KNOWN_FRAMES_PASSED
930            })
931    }
932
933    /// Returns the detected demolition format, if any demolitions have been encountered.
934    pub fn get_demolish_format(&self) -> Option<DemolishFormat> {
935        self.demolish_format
936    }
937
938    /// Detects which demolition format this replay uses by checking car actor attributes.
939    pub fn detect_demolish_format(&self) -> Option<DemolishFormat> {
940        let actors = self.iter_actors_by_type_err(CAR_TYPE).ok()?;
941        for (_actor_id, state) in actors {
942            if get_attribute_errors_expected!(
943                self,
944                &state.attributes,
945                DEMOLISH_EXTENDED_KEY,
946                boxcars::Attribute::DemolishExtended
947            )
948            .is_ok()
949            {
950                return Some(DemolishFormat::Extended);
951            }
952            if get_attribute_errors_expected!(
953                self,
954                &state.attributes,
955                DEMOLISH_GOAL_EXPLOSION_KEY,
956                boxcars::Attribute::DemolishFx
957            )
958            .is_ok()
959            {
960                return Some(DemolishFormat::Fx);
961            }
962        }
963        None
964    }
965
966    /// Provides an iterator over the active demolition events in the current frame.
967    ///
968    /// Uses the cached `demolish_format` to determine which attribute to check.
969    pub fn get_active_demos(
970        &self,
971    ) -> SubtrActorResult<impl Iterator<Item = DemolishAttribute> + '_> {
972        let format = self.demolish_format;
973        let actors: Vec<_> = self.iter_actors_by_type_err(CAR_TYPE)?.collect();
974        Ok(actors
975            .into_iter()
976            .filter_map(move |(_actor_id, state)| match format {
977                Some(DemolishFormat::Extended) => get_attribute_errors_expected!(
978                    self,
979                    &state.attributes,
980                    DEMOLISH_EXTENDED_KEY,
981                    boxcars::Attribute::DemolishExtended
982                )
983                .ok()
984                .map(|demo| DemolishAttribute::Extended(**demo)),
985                Some(DemolishFormat::Fx) => get_attribute_errors_expected!(
986                    self,
987                    &state.attributes,
988                    DEMOLISH_GOAL_EXPLOSION_KEY,
989                    boxcars::Attribute::DemolishFx
990                )
991                .ok()
992                .map(|demo| DemolishAttribute::Fx(**demo)),
993                None => None,
994            }))
995    }
996
997    // Interpolation Support functions
998
999    fn get_frame(&self, frame_index: usize) -> SubtrActorResult<&boxcars::Frame> {
1000        self.replay
1001            .network_frames
1002            .as_ref()
1003            .ok_or(SubtrActorError::new(
1004                SubtrActorErrorVariant::NoNetworkFrames,
1005            ))?
1006            .frames
1007            .get(frame_index)
1008            .ok_or(SubtrActorError::new(
1009                SubtrActorErrorVariant::FrameIndexOutOfBounds,
1010            ))
1011    }
1012
1013    fn velocities_applied_rigid_body(
1014        &self,
1015        rigid_body: &boxcars::RigidBody,
1016        rb_frame_index: usize,
1017        target_time: f32,
1018    ) -> SubtrActorResult<boxcars::RigidBody> {
1019        let rb_frame = self.get_frame(rb_frame_index)?;
1020        let interpolation_amount = target_time - rb_frame.time;
1021        Ok(apply_velocities_to_rigid_body(
1022            rigid_body,
1023            interpolation_amount,
1024        ))
1025    }
1026
1027    /// This function first retrieves the actor's [`RigidBody`] at the current
1028    /// frame. If the time difference between the current frame and the provided
1029    /// time is within the `close_enough` threshold, the function returns the
1030    /// current frame's [`RigidBody`].
1031    ///
1032    /// If the [`RigidBody`] at the exact time is not available, the function
1033    /// searches in the appropriate direction (either forwards or backwards in
1034    /// time) to find another [`RigidBody`] to interpolate from. If the found
1035    /// [`RigidBody`]'s time is within the `close_enough` threshold, it is
1036    /// returned.
1037    ///
1038    /// Otherwise, it interpolates between the two [`RigidBody`]s (from the
1039    /// current frame and the found frame) to produce a [`RigidBody`] for the
1040    /// specified time. This is done using the [`get_interpolated_rigid_body`]
1041    /// function from the `util` module.
1042    ///
1043    /// # Arguments
1044    ///
1045    /// * `actor_id` - The ID of the actor whose [`RigidBody`] is to be retrieved.
1046    /// * `time` - The time at which the actor's [`RigidBody`] is to be retrieved.
1047    /// * `close_enough` - The acceptable threshold for time difference when
1048    ///   determining if a [`RigidBody`] is close enough to the desired time to not
1049    ///   require interpolation.
1050    ///
1051    /// # Returns
1052    ///
1053    /// A [`RigidBody`] for the actor at the specified time.
1054    ///
1055    /// [`RigidBody`]: boxcars::RigidBody
1056    /// [`get_interpolated_rigid_body`]: util::get_interpolated_rigid_body
1057    pub fn get_interpolated_actor_rigid_body(
1058        &self,
1059        actor_id: &boxcars::ActorId,
1060        time: f32,
1061        close_enough: f32,
1062    ) -> SubtrActorResult<boxcars::RigidBody> {
1063        let (frame_body, frame_index) = self.get_actor_rigid_body(actor_id)?;
1064        let frame_time = self.get_frame(*frame_index)?.time;
1065        let time_and_frame_difference = time - frame_time;
1066
1067        if (time_and_frame_difference).abs() <= close_enough.abs() {
1068            return Ok(*frame_body);
1069        }
1070
1071        let search_direction = if time_and_frame_difference > 0.0 {
1072            util::SearchDirection::Forward
1073        } else {
1074            util::SearchDirection::Backward
1075        };
1076
1077        let object_id = self.get_object_id_for_key(RIGID_BODY_STATE_KEY)?;
1078
1079        let (attribute, found_frame) =
1080            self.find_update_in_direction(*frame_index, actor_id, object_id, search_direction)?;
1081        let found_time = self.get_frame(found_frame)?.time;
1082
1083        let found_body = attribute_match!(attribute, boxcars::Attribute::RigidBody)?;
1084
1085        if (found_time - time).abs() <= close_enough {
1086            return Ok(found_body);
1087        }
1088
1089        let (start_body, start_time, end_body, end_time) = match search_direction {
1090            util::SearchDirection::Forward => (frame_body, frame_time, &found_body, found_time),
1091            util::SearchDirection::Backward => (&found_body, found_time, frame_body, frame_time),
1092        };
1093
1094        util::get_interpolated_rigid_body(start_body, start_time, end_body, end_time, time)
1095    }
1096
1097    // Actor functions
1098
1099    pub fn get_object_id_for_key(
1100        &self,
1101        name: &'static str,
1102    ) -> SubtrActorResult<&boxcars::ObjectId> {
1103        self.name_to_object_id
1104            .get(name)
1105            .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::ObjectIdNotFound { name }))
1106    }
1107
1108    pub fn get_actor_ids_by_type(
1109        &self,
1110        name: &'static str,
1111    ) -> SubtrActorResult<&[boxcars::ActorId]> {
1112        self.get_object_id_for_key(name)
1113            .map(|object_id| self.get_actor_ids_by_object_id(object_id))
1114    }
1115
1116    fn get_actor_ids_by_object_id(&self, object_id: &boxcars::ObjectId) -> &[boxcars::ActorId] {
1117        self.actor_state
1118            .actor_ids_by_type
1119            .get(object_id)
1120            .map(|v| &v[..])
1121            .unwrap_or_else(|| &EMPTY_ACTOR_IDS)
1122    }
1123
1124    fn get_actor_state(&self, actor_id: &boxcars::ActorId) -> SubtrActorResult<&ActorState> {
1125        self.actor_state.actor_states.get(actor_id).ok_or_else(|| {
1126            SubtrActorError::new(SubtrActorErrorVariant::NoStateForActorId {
1127                actor_id: *actor_id,
1128            })
1129        })
1130    }
1131
1132    fn get_actor_attribute<'b>(
1133        &'b self,
1134        actor_id: &boxcars::ActorId,
1135        property: &'static str,
1136    ) -> SubtrActorResult<&'b boxcars::Attribute> {
1137        self.get_attribute(&self.get_actor_state(actor_id)?.attributes, property)
1138    }
1139
1140    pub fn get_attribute<'b>(
1141        &'b self,
1142        map: &'b HashMap<boxcars::ObjectId, (boxcars::Attribute, usize)>,
1143        property: &'static str,
1144    ) -> SubtrActorResult<&'b boxcars::Attribute> {
1145        self.get_attribute_and_updated(map, property).map(|v| &v.0)
1146    }
1147
1148    pub fn get_attribute_and_updated<'b>(
1149        &'b self,
1150        map: &'b HashMap<boxcars::ObjectId, (boxcars::Attribute, usize)>,
1151        property: &'static str,
1152    ) -> SubtrActorResult<&'b (boxcars::Attribute, usize)> {
1153        let attribute_object_id = self.get_object_id_for_key(property)?;
1154        map.get(attribute_object_id).ok_or_else(|| {
1155            SubtrActorError::new(SubtrActorErrorVariant::PropertyNotFoundInState { property })
1156        })
1157    }
1158
1159    fn find_ball_actor(&self) -> Option<boxcars::ActorId> {
1160        BALL_TYPES
1161            .iter()
1162            .filter_map(|ball_type| self.iter_actors_by_type(ball_type))
1163            .flatten()
1164            .map(|(actor_id, _)| *actor_id)
1165            .next()
1166    }
1167
1168    pub fn get_ball_actor_id(&self) -> SubtrActorResult<boxcars::ActorId> {
1169        self.ball_actor_id.ok_or(SubtrActorError::new(
1170            SubtrActorErrorVariant::BallActorNotFound,
1171        ))
1172    }
1173
1174    pub fn get_metadata_actor_id(&self) -> SubtrActorResult<&boxcars::ActorId> {
1175        self.get_actor_ids_by_type(GAME_TYPE)?
1176            .iter()
1177            .next()
1178            .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::NoGameActor))
1179    }
1180
1181    pub fn get_player_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
1182        self.player_to_actor_id
1183            .get(player_id)
1184            .ok_or_else(|| {
1185                SubtrActorError::new(SubtrActorErrorVariant::ActorNotFound {
1186                    name: "ActorId",
1187                    player_id: player_id.clone(),
1188                })
1189            })
1190            .cloned()
1191    }
1192
1193    pub fn get_car_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
1194        self.player_to_car
1195            .get(&self.get_player_actor_id(player_id)?)
1196            .ok_or_else(|| {
1197                SubtrActorError::new(SubtrActorErrorVariant::ActorNotFound {
1198                    name: "Car",
1199                    player_id: player_id.clone(),
1200                })
1201            })
1202            .cloned()
1203    }
1204
1205    pub fn get_car_connected_actor_id(
1206        &self,
1207        player_id: &PlayerId,
1208        map: &HashMap<boxcars::ActorId, boxcars::ActorId>,
1209        name: &'static str,
1210    ) -> SubtrActorResult<boxcars::ActorId> {
1211        map.get(&self.get_car_actor_id(player_id)?)
1212            .ok_or_else(|| {
1213                SubtrActorError::new(SubtrActorErrorVariant::ActorNotFound {
1214                    name,
1215                    player_id: player_id.clone(),
1216                })
1217            })
1218            .cloned()
1219    }
1220
1221    pub fn get_boost_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
1222        self.get_car_connected_actor_id(player_id, &self.car_to_boost, "Boost")
1223    }
1224
1225    pub fn get_jump_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
1226        self.get_car_connected_actor_id(player_id, &self.car_to_jump, "Jump")
1227    }
1228
1229    pub fn get_double_jump_actor_id(
1230        &self,
1231        player_id: &PlayerId,
1232    ) -> SubtrActorResult<boxcars::ActorId> {
1233        self.get_car_connected_actor_id(player_id, &self.car_to_double_jump, "Double Jump")
1234    }
1235
1236    pub fn get_dodge_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
1237        self.get_car_connected_actor_id(player_id, &self.car_to_dodge, "Dodge")
1238    }
1239
1240    pub fn get_actor_rigid_body(
1241        &self,
1242        actor_id: &boxcars::ActorId,
1243    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
1244        get_attribute_and_updated!(
1245            self,
1246            &self.get_actor_state(actor_id)?.attributes,
1247            RIGID_BODY_STATE_KEY,
1248            boxcars::Attribute::RigidBody
1249        )
1250    }
1251
1252    // Actor iteration functions
1253
1254    pub fn iter_player_ids_in_order(&self) -> impl Iterator<Item = &PlayerId> {
1255        self.team_zero.iter().chain(self.team_one.iter())
1256    }
1257
1258    pub fn player_count(&self) -> usize {
1259        self.iter_player_ids_in_order().count()
1260    }
1261
1262    /// Returns a map of all player IDs to their names.
1263    pub fn get_player_names(&self) -> HashMap<PlayerId, String> {
1264        self.iter_player_ids_in_order()
1265            .filter_map(|player_id| {
1266                self.get_player_name(player_id)
1267                    .ok()
1268                    .map(|name| (player_id.clone(), name))
1269            })
1270            .collect()
1271    }
1272
1273    fn iter_actors_by_type_err(
1274        &self,
1275        name: &'static str,
1276    ) -> SubtrActorResult<impl Iterator<Item = (&boxcars::ActorId, &ActorState)>> {
1277        Ok(self.iter_actors_by_object_id(self.get_object_id_for_key(name)?))
1278    }
1279
1280    pub fn iter_actors_by_type(
1281        &self,
1282        name: &'static str,
1283    ) -> Option<impl Iterator<Item = (&boxcars::ActorId, &ActorState)>> {
1284        self.iter_actors_by_type_err(name).ok()
1285    }
1286
1287    pub fn iter_actors_by_object_id<'b>(
1288        &'b self,
1289        object_id: &'b boxcars::ObjectId,
1290    ) -> impl Iterator<Item = (&'b boxcars::ActorId, &'b ActorState)> + 'b {
1291        let actor_ids = self
1292            .actor_state
1293            .actor_ids_by_type
1294            .get(object_id)
1295            .map(|v| &v[..])
1296            .unwrap_or_else(|| &EMPTY_ACTOR_IDS);
1297
1298        actor_ids
1299            .iter()
1300            // This unwrap is fine because we know the actor will exist as it is
1301            // in the actor_ids_by_type
1302            .map(move |id| (id, self.actor_state.actor_states.get(id).unwrap()))
1303    }
1304
1305    // Properties
1306
1307    /// Returns the remaining time in seconds in the game as an `i32`.
1308    pub fn get_seconds_remaining(&self) -> SubtrActorResult<i32> {
1309        get_actor_attribute_matching!(
1310            self,
1311            self.get_metadata_actor_id()?,
1312            SECONDS_REMAINING_KEY,
1313            boxcars::Attribute::Int
1314        )
1315        .cloned()
1316    }
1317
1318    /// Returns the current game state as an `i32`.
1319    ///
1320    /// Known values:
1321    /// - 55: Kickoff/Countdown state (players frozen, waiting for countdown)
1322    /// - 58: Active play (players can move)
1323    /// - 86: Goal scored (replay mode)
1324    pub fn get_replicated_state_name(&self) -> SubtrActorResult<i32> {
1325        get_actor_attribute_matching!(
1326            self,
1327            self.get_metadata_actor_id()?,
1328            REPLICATED_STATE_NAME_KEY,
1329            boxcars::Attribute::Int
1330        )
1331        .cloned()
1332    }
1333
1334    /// Returns the game state time remaining (countdown timer) as an `i32`.
1335    ///
1336    /// During kickoff:
1337    /// - 3: Countdown starts (players frozen)
1338    /// - 2, 1: Countdown continues
1339    /// - 0: Countdown ends (players can move)
1340    ///
1341    /// This is useful for detecting when players transition from frozen to movable.
1342    pub fn get_replicated_game_state_time_remaining(&self) -> SubtrActorResult<i32> {
1343        get_actor_attribute_matching!(
1344            self,
1345            self.get_metadata_actor_id()?,
1346            REPLICATED_GAME_STATE_TIME_REMAINING_KEY,
1347            boxcars::Attribute::Int
1348        )
1349        .cloned()
1350    }
1351
1352    /// Returns whether the ball has been hit in the current play.
1353    ///
1354    /// This resets to false at the start of each kickoff and becomes true
1355    /// once any player touches the ball.
1356    pub fn get_ball_has_been_hit(&self) -> SubtrActorResult<bool> {
1357        get_actor_attribute_matching!(
1358            self,
1359            self.get_metadata_actor_id()?,
1360            BALL_HAS_BEEN_HIT_KEY,
1361            boxcars::Attribute::Boolean
1362        )
1363        .cloned()
1364    }
1365
1366    /// Returns a boolean indicating whether ball syncing is ignored.
1367    pub fn get_ignore_ball_syncing(&self) -> SubtrActorResult<bool> {
1368        let actor_id = self.get_ball_actor_id()?;
1369        get_actor_attribute_matching!(
1370            self,
1371            &actor_id,
1372            IGNORE_SYNCING_KEY,
1373            boxcars::Attribute::Boolean
1374        )
1375        .cloned()
1376    }
1377
1378    /// Returns a reference to the [`RigidBody`](boxcars::RigidBody) of the ball.
1379    pub fn get_ball_rigid_body(&self) -> SubtrActorResult<&boxcars::RigidBody> {
1380        self.ball_actor_id
1381            .ok_or(SubtrActorError::new(
1382                SubtrActorErrorVariant::BallActorNotFound,
1383            ))
1384            .and_then(|actor_id| self.get_actor_rigid_body(&actor_id).map(|v| v.0))
1385    }
1386
1387    /// Returns a boolean indicating whether the ball's
1388    /// [`RigidBody`](boxcars::RigidBody) exists and is not sleeping.
1389    pub fn ball_rigid_body_exists(&self) -> SubtrActorResult<bool> {
1390        Ok(self
1391            .get_ball_rigid_body()
1392            .map(|rb| !rb.sleeping)
1393            .unwrap_or(false))
1394    }
1395
1396    /// Returns a reference to the ball's [`RigidBody`](boxcars::RigidBody) and
1397    /// its last updated frame.
1398    pub fn get_ball_rigid_body_and_updated(
1399        &self,
1400    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
1401        self.ball_actor_id
1402            .ok_or(SubtrActorError::new(
1403                SubtrActorErrorVariant::BallActorNotFound,
1404            ))
1405            .and_then(|actor_id| {
1406                get_attribute_and_updated!(
1407                    self,
1408                    &self.get_actor_state(&actor_id)?.attributes,
1409                    RIGID_BODY_STATE_KEY,
1410                    boxcars::Attribute::RigidBody
1411                )
1412            })
1413    }
1414
1415    /// Returns a [`RigidBody`](boxcars::RigidBody) of the ball with applied
1416    /// velocity at the target time.
1417    pub fn get_velocity_applied_ball_rigid_body(
1418        &self,
1419        target_time: f32,
1420    ) -> SubtrActorResult<boxcars::RigidBody> {
1421        let (current_rigid_body, frame_index) = self.get_ball_rigid_body_and_updated()?;
1422        self.velocities_applied_rigid_body(current_rigid_body, *frame_index, target_time)
1423    }
1424
1425    /// Returns an interpolated [`RigidBody`](boxcars::RigidBody) of the ball at
1426    /// a specified time.
1427    pub fn get_interpolated_ball_rigid_body(
1428        &self,
1429        time: f32,
1430        close_enough: f32,
1431    ) -> SubtrActorResult<boxcars::RigidBody> {
1432        self.get_interpolated_actor_rigid_body(&self.get_ball_actor_id()?, time, close_enough)
1433    }
1434
1435    /// Returns the name of the specified player.
1436    pub fn get_player_name(&self, player_id: &PlayerId) -> SubtrActorResult<String> {
1437        get_actor_attribute_matching!(
1438            self,
1439            &self.get_player_actor_id(player_id)?,
1440            PLAYER_NAME_KEY,
1441            boxcars::Attribute::String
1442        )
1443        .cloned()
1444    }
1445
1446    /// Returns the team key for the specified player.
1447    pub fn get_player_team_key(&self, player_id: &PlayerId) -> SubtrActorResult<String> {
1448        let team_actor_id = self
1449            .player_to_team
1450            .get(&self.get_player_actor_id(player_id)?)
1451            .ok_or_else(|| {
1452                SubtrActorError::new(SubtrActorErrorVariant::UnknownPlayerTeam {
1453                    player_id: player_id.clone(),
1454                })
1455            })?;
1456        let state = self.get_actor_state(team_actor_id)?;
1457        self.object_id_to_name
1458            .get(&state.object_id)
1459            .ok_or_else(|| {
1460                SubtrActorError::new(SubtrActorErrorVariant::UnknownPlayerTeam {
1461                    player_id: player_id.clone(),
1462                })
1463            })
1464            .cloned()
1465    }
1466
1467    /// Determines if the player is on team 0.
1468    pub fn get_player_is_team_0(&self, player_id: &PlayerId) -> SubtrActorResult<bool> {
1469        Ok(self
1470            .get_player_team_key(player_id)?
1471            .chars()
1472            .last()
1473            .ok_or_else(|| {
1474                SubtrActorError::new(SubtrActorErrorVariant::EmptyTeamName {
1475                    player_id: player_id.clone(),
1476                })
1477            })?
1478            == '0')
1479    }
1480
1481    /// Returns a reference to the [`RigidBody`](boxcars::RigidBody) of the player's car.
1482    pub fn get_player_rigid_body(
1483        &self,
1484        player_id: &PlayerId,
1485    ) -> SubtrActorResult<&boxcars::RigidBody> {
1486        self.get_car_actor_id(player_id)
1487            .and_then(|actor_id| self.get_actor_rigid_body(&actor_id).map(|v| v.0))
1488    }
1489
1490    /// Returns the most recent update to the [`RigidBody`](boxcars::RigidBody)
1491    /// of the player's car along with the index of the frame in which it was
1492    /// updated.
1493    pub fn get_player_rigid_body_and_updated(
1494        &self,
1495        player_id: &PlayerId,
1496    ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
1497        self.get_car_actor_id(player_id).and_then(|actor_id| {
1498            get_attribute_and_updated!(
1499                self,
1500                &self.get_actor_state(&actor_id)?.attributes,
1501                RIGID_BODY_STATE_KEY,
1502                boxcars::Attribute::RigidBody
1503            )
1504        })
1505    }
1506
1507    pub fn get_velocity_applied_player_rigid_body(
1508        &self,
1509        player_id: &PlayerId,
1510        target_time: f32,
1511    ) -> SubtrActorResult<boxcars::RigidBody> {
1512        let (current_rigid_body, frame_index) =
1513            self.get_player_rigid_body_and_updated(player_id)?;
1514        self.velocities_applied_rigid_body(current_rigid_body, *frame_index, target_time)
1515    }
1516
1517    pub fn get_interpolated_player_rigid_body(
1518        &self,
1519        player_id: &PlayerId,
1520        time: f32,
1521        close_enough: f32,
1522    ) -> SubtrActorResult<boxcars::RigidBody> {
1523        self.get_interpolated_actor_rigid_body(
1524            &self.get_car_actor_id(player_id).unwrap(),
1525            time,
1526            close_enough,
1527        )
1528    }
1529
1530    /// Returns the player's boost amount in raw replay units (`0.0..=255.0`).
1531    ///
1532    /// Use [`boost_amount_to_percent`] or [`ReplayProcessor::get_player_boost_percentage`]
1533    /// if you need a `0.0..=100.0` percentage value.
1534    pub fn get_player_boost_level(&self, player_id: &PlayerId) -> SubtrActorResult<f32> {
1535        self.get_boost_actor_id(player_id).and_then(|actor_id| {
1536            let boost_state = self.get_actor_state(&actor_id)?;
1537            get_derived_attribute!(
1538                boost_state.derived_attributes,
1539                BOOST_AMOUNT_KEY,
1540                boxcars::Attribute::Float
1541            )
1542            .cloned()
1543        })
1544    }
1545
1546    /// Returns the player's boost amount as a percentage (`0.0..=100.0`).
1547    pub fn get_player_boost_percentage(&self, player_id: &PlayerId) -> SubtrActorResult<f32> {
1548        self.get_player_boost_level(player_id)
1549            .map(boost_amount_to_percent)
1550    }
1551
1552    pub fn get_component_active(&self, actor_id: &boxcars::ActorId) -> SubtrActorResult<u8> {
1553        get_actor_attribute_matching!(
1554            self,
1555            &actor_id,
1556            COMPONENT_ACTIVE_KEY,
1557            boxcars::Attribute::Byte
1558        )
1559        .cloned()
1560    }
1561
1562    pub fn get_boost_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
1563        self.get_boost_actor_id(player_id)
1564            .and_then(|actor_id| self.get_component_active(&actor_id))
1565    }
1566
1567    pub fn get_jump_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
1568        self.get_jump_actor_id(player_id)
1569            .and_then(|actor_id| self.get_component_active(&actor_id))
1570    }
1571
1572    pub fn get_double_jump_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
1573        self.get_double_jump_actor_id(player_id)
1574            .and_then(|actor_id| self.get_component_active(&actor_id))
1575    }
1576
1577    pub fn get_dodge_active(&self, player_id: &PlayerId) -> SubtrActorResult<u8> {
1578        self.get_dodge_actor_id(player_id)
1579            .and_then(|actor_id| self.get_component_active(&actor_id))
1580    }
1581
1582    // Debugging
1583
1584    pub fn map_attribute_keys(
1585        &self,
1586        hash_map: &HashMap<boxcars::ObjectId, (boxcars::Attribute, usize)>,
1587    ) -> HashMap<String, boxcars::Attribute> {
1588        hash_map
1589            .iter()
1590            .map(|(k, (v, _updated))| {
1591                self.object_id_to_name
1592                    .get(k)
1593                    .map(|name| (name.clone(), v.clone()))
1594                    .unwrap()
1595            })
1596            .collect()
1597    }
1598
1599    pub fn all_mappings_string(&self) -> String {
1600        let pairs = [
1601            ("player_to_car", &self.player_to_car),
1602            ("player_to_team", &self.player_to_team),
1603            ("car_to_player", &self.car_to_player),
1604            ("car_to_boost", &self.car_to_boost),
1605            ("car_to_jump", &self.car_to_jump),
1606            ("car_to_double_jump", &self.car_to_double_jump),
1607            ("car_to_dodge", &self.car_to_dodge),
1608        ];
1609        let mut strings: Vec<_> = pairs
1610            .iter()
1611            .map(|(map_name, map)| format!("{map_name:?}: {map:?}"))
1612            .collect();
1613        strings.push(format!("name_to_object_id: {:?}", &self.name_to_object_id));
1614        strings.join("\n")
1615    }
1616
1617    pub fn actor_state_string(&self, actor_id: &boxcars::ActorId) -> String {
1618        if let Ok(actor_state) = self.get_actor_state(actor_id) {
1619            format!("{:?}", self.map_attribute_keys(&actor_state.attributes))
1620        } else {
1621            String::from("error")
1622        }
1623    }
1624
1625    pub fn print_actors_by_id<'b>(&self, actor_ids: impl Iterator<Item = &'b boxcars::ActorId>) {
1626        actor_ids.for_each(|actor_id| {
1627            let state = self.get_actor_state(actor_id).unwrap();
1628            println!(
1629                "{:?}\n\n\n",
1630                self.object_id_to_name.get(&state.object_id).unwrap()
1631            );
1632            println!("{:?}", self.map_attribute_keys(&state.attributes))
1633        })
1634    }
1635
1636    pub fn print_actors_of_type(&self, actor_type: &'static str) {
1637        self.iter_actors_by_type(actor_type)
1638            .unwrap()
1639            .for_each(|(_actor_id, state)| {
1640                log::debug!("{:?}", self.map_attribute_keys(&state.attributes));
1641            });
1642    }
1643
1644    pub fn print_actor_types(&self) {
1645        let types: Vec<_> = self
1646            .actor_state
1647            .actor_ids_by_type
1648            .keys()
1649            .filter_map(|id| self.object_id_to_name.get(id))
1650            .collect();
1651        log::debug!("{types:?}");
1652    }
1653
1654    pub fn print_all_actors(&self) {
1655        self.actor_state
1656            .actor_states
1657            .iter()
1658            .for_each(|(actor_id, actor_state)| {
1659                log::debug!(
1660                    "{}: {:?}",
1661                    self.object_id_to_name
1662                        .get(&actor_state.object_id)
1663                        .unwrap_or(&String::from("unknown")),
1664                    self.actor_state_string(actor_id)
1665                )
1666            });
1667    }
1668}