subtr_actor_spec/
processor.rs

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