Skip to main content

subtr_actor/processor/
mod.rs

1use crate::*;
2use boxcars;
3use std::collections::HashMap;
4
5pub mod actor_state;
6mod boost_pad_resolution;
7mod replay_keys;
8pub mod view;
9pub use actor_state::*;
10pub(crate) use boost_pad_resolution::*;
11pub(crate) use replay_keys::*;
12pub use view::*;
13
14pub(crate) fn attribute_type_name(attribute: &boxcars::Attribute) -> &'static str {
15    match attribute {
16        boxcars::Attribute::Boolean(_) => "Boolean",
17        boxcars::Attribute::Byte(_) => "Byte",
18        boxcars::Attribute::AppliedDamage(_) => "AppliedDamage",
19        boxcars::Attribute::DamageState(_) => "DamageState",
20        boxcars::Attribute::CamSettings(_) => "CamSettings",
21        boxcars::Attribute::ClubColors(_) => "ClubColors",
22        boxcars::Attribute::Demolish(_) => "Demolish",
23        boxcars::Attribute::DemolishExtended(_) => "DemolishExtended",
24        boxcars::Attribute::DemolishFx(_) => "DemolishFx",
25        boxcars::Attribute::Enum(_) => "Enum",
26        boxcars::Attribute::Explosion(_) => "Explosion",
27        boxcars::Attribute::ExtendedExplosion(_) => "ExtendedExplosion",
28        boxcars::Attribute::FlaggedByte(_, _) => "FlaggedByte",
29        boxcars::Attribute::ActiveActor(_) => "ActiveActor",
30        boxcars::Attribute::Float(_) => "Float",
31        boxcars::Attribute::GameMode(_, _) => "GameMode",
32        boxcars::Attribute::Int(_) => "Int",
33        boxcars::Attribute::Int64(_) => "Int64",
34        boxcars::Attribute::Loadout(_) => "Loadout",
35        boxcars::Attribute::TeamLoadout(_) => "TeamLoadout",
36        boxcars::Attribute::Location(_) => "Location",
37        boxcars::Attribute::MusicStinger(_) => "MusicStinger",
38        boxcars::Attribute::PlayerHistoryKey(_) => "PlayerHistoryKey",
39        boxcars::Attribute::Pickup(_) => "Pickup",
40        boxcars::Attribute::PickupNew(_) => "PickupNew",
41        boxcars::Attribute::QWord(_) => "QWord",
42        boxcars::Attribute::Welded(_) => "Welded",
43        boxcars::Attribute::Title(_, _, _, _, _, _, _, _) => "Title",
44        boxcars::Attribute::TeamPaint(_) => "TeamPaint",
45        boxcars::Attribute::RigidBody(_) => "RigidBody",
46        boxcars::Attribute::String(_) => "String",
47        boxcars::Attribute::UniqueId(_) => "UniqueId",
48        boxcars::Attribute::Reservation(_) => "Reservation",
49        boxcars::Attribute::PartyLeader(_) => "PartyLeader",
50        boxcars::Attribute::PrivateMatch(_) => "PrivateMatch",
51        boxcars::Attribute::LoadoutOnline(_) => "LoadoutOnline",
52        boxcars::Attribute::LoadoutsOnline(_) => "LoadoutsOnline",
53        boxcars::Attribute::StatEvent(_) => "StatEvent",
54        boxcars::Attribute::Rotation(_) => "Rotation",
55        boxcars::Attribute::RepStatTitle(_) => "RepStatTitle",
56        boxcars::Attribute::PickupInfo(_) => "PickupInfo",
57        boxcars::Attribute::Impulse(_) => "Impulse",
58        boxcars::Attribute::ReplicatedBoost(_) => "ReplicatedBoost",
59        boxcars::Attribute::LogoData(_) => "LogoData",
60    }
61}
62
63/// Attempts to match an attribute value with the given type.
64///
65/// # Arguments
66///
67/// * `$value` - An expression that yields the attribute value.
68/// * `$type` - The expected enum path.
69///
70/// If the attribute matches the specified type, it is returned wrapped in an
71/// [`Ok`] variant of a [`Result`]. If the attribute doesn't match, it results in an
72/// [`Err`] variant with a [`SubtrActorError`], specifying the expected type and
73/// the actual type.
74#[macro_export]
75macro_rules! attribute_match {
76    ($value:expr_2021, $type:path $(,)?) => {{
77        let attribute = $value;
78        if let $type(value) = attribute {
79            Ok(value)
80        } else {
81            SubtrActorError::new_result(SubtrActorErrorVariant::UnexpectedAttributeType {
82                expected_type: stringify!($type),
83                actual_type: attribute_type_name(&attribute),
84            })
85        }
86    }};
87}
88
89/// Obtains an attribute from a map and ensures it matches the expected type.
90///
91/// # Arguments
92///
93/// * `$self` - The struct or instance on which the function is invoked.
94/// * `$map` - The data map.
95/// * `$prop` - The attribute key.
96/// * `$type` - The expected enum path.
97#[macro_export]
98macro_rules! get_attribute_errors_expected {
99    ($self:ident, $map:expr_2021, $prop:expr_2021, $type:path) => {
100        $self
101            .get_attribute($map, $prop)
102            .and_then(|found| attribute_match!(found, $type))
103    };
104}
105
106/// Obtains an attribute and its updated status from a map and ensures the
107/// attribute matches the expected type.
108///
109/// # Arguments
110///
111/// * `$self` - The struct or instance on which the function is invoked.
112/// * `$map` - The data map.
113/// * `$prop` - The attribute key.
114/// * `$type` - The expected enum path.
115///
116/// It returns a [`Result`] with a tuple of the matched attribute and its updated
117/// status, after invoking [`attribute_match!`] on the found attribute.
118macro_rules! get_attribute_and_updated {
119    ($self:ident, $map:expr_2021, $prop:expr_2021, $type:path) => {
120        $self
121            .get_attribute_and_updated($map, $prop)
122            .and_then(|(found, updated)| attribute_match!(found, $type).map(|v| (v, updated)))
123    };
124}
125
126/// Obtains an actor attribute and ensures it matches the expected type.
127///
128/// # Arguments
129///
130/// * `$self` - The struct or instance on which the function is invoked.
131/// * `$actor` - The actor identifier.
132/// * `$prop` - The attribute key.
133/// * `$type` - The expected enum path.
134macro_rules! get_actor_attribute_matching {
135    ($self:ident, $actor:expr_2021, $prop:expr_2021, $type:path) => {
136        $self
137            .get_actor_attribute($actor, $prop)
138            .and_then(|found| attribute_match!(found, $type))
139    };
140}
141
142/// Obtains a derived attribute from a map and ensures it matches the expected
143/// type.
144///
145/// # Arguments
146///
147/// * `$map` - The data map.
148/// * `$key` - The attribute key.
149/// * `$type` - The expected enum path.
150macro_rules! get_derived_attribute {
151    ($map:expr_2021, $key:expr_2021, $type:path) => {
152        $map.get($key)
153            .ok_or_else(|| {
154                SubtrActorError::new(SubtrActorErrorVariant::DerivedKeyValueNotFound {
155                    name: $key.to_string(),
156                })
157            })
158            .and_then(|found| attribute_match!(&found.0, $type))
159    };
160}
161
162fn get_actor_id_from_active_actor<T>(
163    _: T,
164    active_actor: &boxcars::ActiveActor,
165) -> boxcars::ActorId {
166    active_actor.actor
167}
168
169fn use_update_actor<T>(id: boxcars::ActorId, _: T) -> boxcars::ActorId {
170    id
171}
172
173#[derive(Clone, Copy, Default)]
174struct CachedObjectIds {
175    player_type: Option<boxcars::ObjectId>,
176    car_type: Option<boxcars::ObjectId>,
177    boost_type: Option<boxcars::ObjectId>,
178    dodge_type: Option<boxcars::ObjectId>,
179    jump_type: Option<boxcars::ObjectId>,
180    double_jump_type: Option<boxcars::ObjectId>,
181    unique_id: Option<boxcars::ObjectId>,
182    team: Option<boxcars::ObjectId>,
183    bot: Option<boxcars::ObjectId>,
184    client_loadouts: Option<boxcars::ObjectId>,
185    player_replication: Option<boxcars::ObjectId>,
186    vehicle: Option<boxcars::ObjectId>,
187    boost_replicated: Option<boxcars::ObjectId>,
188    boost_amount: Option<boxcars::ObjectId>,
189    component_active: Option<boxcars::ObjectId>,
190    seconds_remaining: Option<boxcars::ObjectId>,
191    replicated_state_name: Option<boxcars::ObjectId>,
192    replicated_game_state_time_remaining: Option<boxcars::ObjectId>,
193    match_type_class: Option<boxcars::ObjectId>,
194    ball_has_been_hit: Option<boxcars::ObjectId>,
195    replicated_game_playlist: Option<boxcars::ObjectId>,
196    ball_hit_team_num: Option<boxcars::ObjectId>,
197    dodges_refreshed_counter: Option<boxcars::ObjectId>,
198    camera_settings_pri: Option<boxcars::ObjectId>,
199    camera_settings_profile: Option<boxcars::ObjectId>,
200}
201
202impl CachedObjectIds {
203    fn from_name_map(name_to_object_id: &HashMap<String, boxcars::ObjectId>) -> Self {
204        let cached = |name| name_to_object_id.get(name).copied();
205        Self {
206            player_type: cached(PLAYER_TYPE),
207            car_type: cached(CAR_TYPE),
208            boost_type: cached(BOOST_TYPE),
209            dodge_type: cached(DODGE_TYPE),
210            jump_type: cached(JUMP_TYPE),
211            double_jump_type: cached(DOUBLE_JUMP_TYPE),
212            unique_id: cached(UNIQUE_ID_KEY),
213            team: cached(TEAM_KEY),
214            bot: cached(BOT_KEY),
215            client_loadouts: cached(CLIENT_LOADOUTS_KEY),
216            player_replication: cached(PLAYER_REPLICATION_KEY),
217            vehicle: cached(VEHICLE_KEY),
218            boost_replicated: cached(BOOST_REPLICATED_KEY),
219            boost_amount: cached(BOOST_AMOUNT_KEY),
220            component_active: cached(COMPONENT_ACTIVE_KEY),
221            seconds_remaining: cached(SECONDS_REMAINING_KEY),
222            replicated_state_name: cached(REPLICATED_STATE_NAME_KEY),
223            replicated_game_state_time_remaining: cached(REPLICATED_GAME_STATE_TIME_REMAINING_KEY),
224            match_type_class: cached(MATCH_TYPE_CLASS_KEY),
225            ball_has_been_hit: cached(BALL_HAS_BEEN_HIT_KEY),
226            replicated_game_playlist: cached(REPLICATED_GAME_PLAYLIST_KEY),
227            ball_hit_team_num: cached(BALL_HIT_TEAM_NUM_KEY),
228            dodges_refreshed_counter: cached(DODGES_REFRESHED_COUNTER_KEY),
229            camera_settings_pri: cached(CAMERA_SETTINGS_PRI_KEY),
230            camera_settings_profile: cached(CAMERA_SETTINGS_PROFILE_KEY),
231        }
232    }
233}
234
235mod bootstrap;
236mod debug;
237mod player_queries;
238mod queries;
239mod updaters;
240
241/// The [`ReplayProcessor`] struct is a pivotal component in `subtr-actor`'s
242/// replay parsing pipeline. It is designed to process and traverse an actor
243/// graph of a Rocket League replay, and expose methods for collectors to gather
244/// specific data points as it progresses through the replay.
245///
246/// The processor pushes frames from a replay through an [`ActorStateModeler`],
247/// which models the state all actors in the replay at a given point in time.
248/// The [`ReplayProcessor`] also maintains various mappings to allow efficient
249/// lookup and traversal of the actor graph, thus assisting [`Collector`]
250/// instances in their data accumulation tasks.
251///
252/// The primary method of this struct is [`process`](ReplayProcessor::process),
253/// which takes a collector and processes the replay. As it traverses the
254/// replay, it calls the [`Collector::process_frame`] method of the passed
255/// collector, passing the current frame along with its contextual data. This
256/// allows the collector to extract specific data from each frame as needed.
257///
258/// The [`ReplayProcessor`] also provides a number of helper methods for
259/// navigating the actor graph and extracting information, such as
260/// [`get_ball_rigid_body`](ReplayProcessor::get_ball_rigid_body),
261/// [`get_player_name`](ReplayProcessor::get_player_name),
262/// [`get_player_team_key`](ReplayProcessor::get_player_team_key),
263/// [`get_player_is_team_0`](ReplayProcessor::get_player_is_team_0), and
264/// [`get_player_rigid_body`](ReplayProcessor::get_player_rigid_body).
265///
266/// # See Also
267///
268/// * [`ActorStateModeler`]: A struct used to model the states of multiple
269///   actors at a given point in time.
270/// * [`Collector`]: A trait implemented by objects that wish to collect data as
271///   the `ReplayProcessor` processes a replay.
272pub struct ReplayProcessor<'a> {
273    /// The replay currently being traversed.
274    pub replay: &'a boxcars::Replay,
275    spatial_normalization_factor: f32,
276    rigid_body_velocity_normalization_factor: f32,
277    uses_legacy_rigid_body_rotation: bool,
278    cached_object_ids: CachedObjectIds,
279    is_boost_pad_object: Vec<bool>,
280    /// Modeled actor state for the current replay frame.
281    pub actor_state: ActorStateModeler,
282    /// Mapping from object ids to their replay object names.
283    pub object_id_to_name: HashMap<boxcars::ObjectId, String>,
284    /// Reverse lookup from replay object names to object ids.
285    pub name_to_object_id: HashMap<String, boxcars::ObjectId>,
286    /// Cached actor id for the replay ball when known.
287    pub ball_actor_id: Option<boxcars::ActorId>,
288    /// Best known game-type metadata for this replay.
289    pub game_type_details: ReplayGameTypeDetails,
290    /// Stable ordering of team 0 players.
291    pub team_zero: Vec<PlayerId>,
292    /// Stable ordering of team 1 players.
293    pub team_one: Vec<PlayerId>,
294    /// Mapping from player ids to their player-controller actor ids.
295    pub player_to_actor_id: HashMap<PlayerId, boxcars::ActorId>,
296    /// Mapping from player-controller actors to their replicated loadouts.
297    pub player_actor_to_loadout: HashMap<boxcars::ActorId, boxcars::TeamLoadout>,
298    /// Mapping from player-controller actors to their replicated camera presets.
299    ///
300    /// Like `player_actor_to_loadout`, this is captured while frames are
301    /// processed (camera-settings actors can be deleted mid-replay) and
302    /// persists across [`reset`](Self::reset).
303    pub player_actor_to_camera_settings: HashMap<boxcars::ActorId, PlayerCameraSettings>,
304    /// Mapping from camera-settings actors to the player-controller actor they
305    /// replicate for, used to join the two camera attributes whichever order
306    /// they arrive in.
307    camera_settings_actor_to_player_actor: HashMap<boxcars::ActorId, boxcars::ActorId>,
308    /// Mapping from camera-settings actors to their last replicated preset.
309    camera_settings_actor_to_settings: HashMap<boxcars::ActorId, PlayerCameraSettings>,
310    /// Mapping from player-controller actors to car actors.
311    pub player_to_car: HashMap<boxcars::ActorId, boxcars::ActorId>,
312    /// Mapping from player-controller actors to team actors.
313    pub player_to_team: HashMap<boxcars::ActorId, boxcars::ActorId>,
314    /// Reverse mapping from car actors to player-controller actors.
315    pub car_to_player: HashMap<boxcars::ActorId, boxcars::ActorId>,
316    /// Mapping from car actors to boost component actors.
317    pub car_to_boost: HashMap<boxcars::ActorId, boxcars::ActorId>,
318    /// Mapping from car actors to jump component actors.
319    pub car_to_jump: HashMap<boxcars::ActorId, boxcars::ActorId>,
320    /// Mapping from car actors to double-jump component actors.
321    pub car_to_double_jump: HashMap<boxcars::ActorId, boxcars::ActorId>,
322    /// Mapping from car actors to dodge component actors.
323    pub car_to_dodge: HashMap<boxcars::ActorId, boxcars::ActorId>,
324    /// All boost-pad events observed so far in the replay.
325    pub boost_pad_events: Vec<BoostPadEvent>,
326    current_frame_boost_pad_events: Vec<BoostPadEvent>,
327    boost_pad_pickup_sequence_times: HashMap<(String, u8), f32>,
328    boost_pad_resolution: BoostPadResolutionState,
329    /// All touch events observed so far in the replay.
330    pub touch_events: Vec<TouchEvent>,
331    current_frame_touch_events: Vec<TouchEvent>,
332    /// All dodge-refresh events observed so far in the replay.
333    pub dodge_refreshed_events: Vec<DodgeRefreshedEvent>,
334    current_frame_dodge_refreshed_events: Vec<DodgeRefreshedEvent>,
335    dodge_refreshed_counters: HashMap<PlayerId, i32>,
336    /// All goal events observed so far in the replay.
337    pub goal_events: Vec<GoalEvent>,
338    current_frame_goal_events: Vec<GoalEvent>,
339    /// All shot/save/assist-style stat events observed so far in the replay.
340    pub player_stat_events: Vec<PlayerStatEvent>,
341    current_frame_player_stat_events: Vec<PlayerStatEvent>,
342    player_stat_counters: HashMap<(PlayerId, PlayerStatEventKind), i32>,
343    /// All demolishes observed so far in the replay.
344    pub demolishes: Vec<DemolishInfo>,
345    known_demolishes: Vec<(DemolishAttribute, usize)>,
346    demolish_format: Option<DemolishFormat>,
347    kickoff_phase_active_last_frame: bool,
348}
349
350impl<'a> ReplayProcessor<'a> {
351    const LEGACY_RIGID_BODY_NET_VERSION_CUTOFF: i32 = 5;
352    const LEGACY_RIGID_BODY_ROTATION_NET_VERSION_CUTOFF: i32 = 7;
353    const LEGACY_RIGID_BODY_LOCATION_FACTOR: f32 = 100.0;
354    const LEGACY_RIGID_BODY_VELOCITY_FACTOR: f32 = 10.0;
355
356    fn uses_legacy_rigid_body_vector_scale(net_version: Option<i32>) -> bool {
357        net_version.is_none_or(|version| version < Self::LEGACY_RIGID_BODY_NET_VERSION_CUTOFF)
358    }
359
360    fn uses_legacy_rigid_body_rotation_for_net_version(net_version: Option<i32>) -> bool {
361        net_version
362            .is_none_or(|version| version < Self::LEGACY_RIGID_BODY_ROTATION_NET_VERSION_CUTOFF)
363    }
364
365    fn rigid_body_location_normalization_factor_for_net_version(net_version: Option<i32>) -> f32 {
366        if Self::uses_legacy_rigid_body_vector_scale(net_version) {
367            Self::LEGACY_RIGID_BODY_LOCATION_FACTOR
368        } else {
369            1.0
370        }
371    }
372
373    fn rigid_body_velocity_normalization_factor_for_net_version(net_version: Option<i32>) -> f32 {
374        if Self::uses_legacy_rigid_body_vector_scale(net_version) {
375            Self::LEGACY_RIGID_BODY_VELOCITY_FACTOR
376        } else {
377            1.0
378        }
379    }
380
381    /// Constructs a new [`ReplayProcessor`] instance with the provided replay.
382    ///
383    /// # Arguments
384    ///
385    /// * `replay` - A reference to the [`boxcars::Replay`] to be processed.
386    ///
387    /// # Returns
388    ///
389    /// Returns a [`SubtrActorResult`] of [`ReplayProcessor`]. In the process of
390    /// initialization, the [`ReplayProcessor`]: - Maps each object id in the
391    /// replay to its corresponding name. - Initializes empty state and
392    /// attribute maps. - Sets the player order from either replay headers or
393    /// frames, if available.
394    pub fn new(replay: &'a boxcars::Replay) -> SubtrActorResult<Self> {
395        let mut object_id_to_name = HashMap::new();
396        let mut name_to_object_id = HashMap::new();
397        let spatial_normalization_factor =
398            Self::rigid_body_location_normalization_factor_for_net_version(replay.net_version);
399        let rigid_body_velocity_normalization_factor =
400            Self::rigid_body_velocity_normalization_factor_for_net_version(replay.net_version);
401        let uses_legacy_rigid_body_rotation =
402            Self::uses_legacy_rigid_body_rotation_for_net_version(replay.net_version);
403        for (id, name) in replay.objects.iter().enumerate() {
404            let object_id = boxcars::ObjectId(id as i32);
405            object_id_to_name.insert(object_id, name.clone());
406            name_to_object_id.insert(name.clone(), object_id);
407        }
408        let cached_object_ids = CachedObjectIds::from_name_map(&name_to_object_id);
409        let game_type_details = ReplayGameTypeDetails::from_headers(&replay.properties);
410        let mut processor = Self {
411            actor_state: ActorStateModeler::new(),
412            replay,
413            spatial_normalization_factor,
414            rigid_body_velocity_normalization_factor,
415            uses_legacy_rigid_body_rotation,
416            cached_object_ids,
417            is_boost_pad_object: replay
418                .objects
419                .iter()
420                .map(|name| name.contains("VehiclePickup_Boost_TA"))
421                .collect(),
422            object_id_to_name,
423            name_to_object_id,
424            game_type_details,
425            team_zero: Vec::new(),
426            team_one: Vec::new(),
427            ball_actor_id: None,
428            player_to_car: HashMap::new(),
429            player_to_team: HashMap::new(),
430            player_to_actor_id: HashMap::new(),
431            player_actor_to_loadout: HashMap::new(),
432            player_actor_to_camera_settings: HashMap::new(),
433            camera_settings_actor_to_player_actor: HashMap::new(),
434            camera_settings_actor_to_settings: HashMap::new(),
435            car_to_player: HashMap::new(),
436            car_to_boost: HashMap::new(),
437            car_to_jump: HashMap::new(),
438            car_to_double_jump: HashMap::new(),
439            car_to_dodge: HashMap::new(),
440            boost_pad_events: Vec::new(),
441            current_frame_boost_pad_events: Vec::new(),
442            boost_pad_pickup_sequence_times: HashMap::new(),
443            boost_pad_resolution: BoostPadResolutionState::default(),
444            touch_events: Vec::new(),
445            current_frame_touch_events: Vec::new(),
446            dodge_refreshed_events: Vec::new(),
447            current_frame_dodge_refreshed_events: Vec::new(),
448            dodge_refreshed_counters: HashMap::new(),
449            goal_events: Vec::new(),
450            current_frame_goal_events: Vec::new(),
451            player_stat_events: Vec::new(),
452            current_frame_player_stat_events: Vec::new(),
453            player_stat_counters: HashMap::new(),
454            demolishes: Vec::new(),
455            known_demolishes: Vec::new(),
456            demolish_format: None,
457            kickoff_phase_active_last_frame: false,
458        };
459        processor
460            .set_player_order_from_headers()
461            .or_else(|_| processor.set_player_order_from_frames())?;
462
463        Ok(processor)
464    }
465
466    /// Returns the scale factor applied when normalizing replay spatial values.
467    pub fn spatial_normalization_factor(&self) -> f32 {
468        self.spatial_normalization_factor
469    }
470
471    /// Returns the scale factor applied when normalizing rigid-body linear and angular velocity.
472    pub fn rigid_body_velocity_normalization_factor(&self) -> f32 {
473        self.rigid_body_velocity_normalization_factor
474    }
475
476    fn sync_player_order_from_known_mappings(&mut self) {
477        let player_ids: Vec<_> = self.player_to_actor_id.keys().cloned().collect();
478        for player_id in player_ids {
479            let already_ordered =
480                self.team_zero.contains(&player_id) || self.team_one.contains(&player_id);
481            if already_ordered {
482                continue;
483            }
484
485            let Ok(is_team_0) = self.get_player_is_team_0(&player_id) else {
486                continue;
487            };
488            if is_team_0 {
489                self.team_zero.push(player_id);
490            } else {
491                self.team_one.push(player_id);
492            }
493        }
494    }
495
496    pub(crate) fn insert_player_actor_id(
497        &mut self,
498        player_id: PlayerId,
499        actor_id: boxcars::ActorId,
500    ) {
501        let stale_player_ids = self
502            .player_to_actor_id
503            .iter()
504            .filter(|(existing_player_id, existing_actor_id)| {
505                **existing_actor_id == actor_id && **existing_player_id != player_id
506            })
507            .map(|(existing_player_id, _existing_actor_id)| existing_player_id.clone())
508            .collect::<Vec<_>>();
509
510        for stale_player_id in stale_player_ids {
511            self.player_to_actor_id.remove(&stale_player_id);
512            self.team_zero
513                .retain(|ordered_player_id| ordered_player_id != &stale_player_id);
514            self.team_one
515                .retain(|ordered_player_id| ordered_player_id != &stale_player_id);
516        }
517
518        self.player_to_actor_id.insert(player_id, actor_id);
519    }
520
521    fn normalize_vector_by_factor(
522        &self,
523        vector: boxcars::Vector3f,
524        factor: f32,
525    ) -> boxcars::Vector3f {
526        if (factor - 1.0).abs() < f32::EPSILON {
527            vector
528        } else {
529            boxcars::Vector3f {
530                x: vector.x * factor,
531                y: vector.y * factor,
532                z: vector.z * factor,
533            }
534        }
535    }
536
537    fn normalize_vector(&self, vector: boxcars::Vector3f) -> boxcars::Vector3f {
538        self.normalize_vector_by_factor(vector, self.spatial_normalization_factor)
539    }
540
541    fn normalize_rigid_body_velocity(&self, vector: boxcars::Vector3f) -> boxcars::Vector3f {
542        self.normalize_vector_by_factor(vector, self.rigid_body_velocity_normalization_factor)
543    }
544
545    fn normalize_optional_rigid_body_velocity(
546        &self,
547        vector: Option<boxcars::Vector3f>,
548    ) -> Option<boxcars::Vector3f> {
549        vector.map(|value| self.normalize_rigid_body_velocity(value))
550    }
551
552    fn normalize_rigid_body_rotation(&self, rotation: boxcars::Quaternion) -> boxcars::Quaternion {
553        if !self.uses_legacy_rigid_body_rotation {
554            return rotation;
555        }
556
557        // Older replays store rigid-body rotation as fixed compressed
558        // (pitch, yaw, roll), not as the modern quaternion shape. The decoded
559        // legacy roll component is opposite the modern angular-velocity sign.
560        let normalized = glam::Quat::from_euler(
561            glam::EulerRot::ZYX,
562            rotation.y * std::f32::consts::PI,
563            rotation.x * std::f32::consts::PI,
564            -rotation.z * std::f32::consts::PI,
565        );
566        boxcars::Quaternion {
567            x: normalized.x,
568            y: normalized.y,
569            z: normalized.z,
570            w: normalized.w,
571        }
572    }
573
574    fn normalize_rigid_body(&self, rigid_body: &boxcars::RigidBody) -> boxcars::RigidBody {
575        if (self.spatial_normalization_factor - 1.0).abs() < f32::EPSILON
576            && (self.rigid_body_velocity_normalization_factor - 1.0).abs() < f32::EPSILON
577            && !self.uses_legacy_rigid_body_rotation
578        {
579            *rigid_body
580        } else {
581            boxcars::RigidBody {
582                sleeping: rigid_body.sleeping,
583                location: self.normalize_vector(rigid_body.location),
584                rotation: self.normalize_rigid_body_rotation(rigid_body.rotation),
585                linear_velocity: self
586                    .normalize_optional_rigid_body_velocity(rigid_body.linear_velocity),
587                angular_velocity: self
588                    .normalize_optional_rigid_body_velocity(rigid_body.angular_velocity),
589            }
590        }
591    }
592
593    fn required_cached_object_id(
594        &self,
595        object_id: Option<boxcars::ObjectId>,
596        name: &'static str,
597    ) -> SubtrActorResult<boxcars::ObjectId> {
598        object_id
599            .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::ObjectIdNotFound { name }))
600    }
601
602    /// [`Self::process`] takes a [`Collector`] as an argument and iterates over
603    /// each frame in the replay, updating the internal state of the processor
604    /// and other relevant mappings based on the current frame.
605    ///
606    /// After each a frame is processed, [`Collector::process_frame`] of the
607    /// collector is called. The [`TimeAdvance`] return value of this call into
608    /// [`Collector::process_frame`] is used to determine what happens next: in
609    /// the case of [`TimeAdvance::Time`], the notion of current time is
610    /// advanced by the provided amount, and only the timestamp of the frame is
611    /// exceeded, do we process the next frame. This mechanism allows fine
612    /// grained control of frame processing, and the frequency of invocations of
613    /// the [`Collector`]. If time is advanced by less than the delay between
614    /// frames, the collector will be called more than once per frame, and can
615    /// use functions like [`Self::get_interpolated_player_rigid_body`] to get
616    /// values that are interpolated between frames. Its also possible to skip
617    /// over frames by providing time advance values that are sufficiently
618    /// large.
619    ///
620    /// At the end of processing, it checks to make sure that no unknown players
621    /// were encountered during the replay. If any unknown players are found, an
622    /// error is returned.
623    pub fn process<H: Collector>(&mut self, handler: &mut H) -> SubtrActorResult<()> {
624        // Initially, we set target_time to NextFrame to ensure the collector
625        // will process the first frame.
626        let mut target_time = TimeAdvance::NextFrame;
627        for (index, frame) in self
628            .replay
629            .network_frames
630            .as_ref()
631            .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::NoNetworkFrames))?
632            .frames
633            .iter()
634            .enumerate()
635        {
636            // Update the internal state of the processor based on the current frame
637            self.actor_state.process_frame(frame, index)?;
638            self.update_game_type_details();
639            self.update_mappings(frame)?;
640            self.update_ball_id(frame)?;
641            self.update_boost_amounts(frame, index)?;
642            self.update_boost_pad_events(frame, index)?;
643            self.update_touch_events(frame, index)?;
644            self.update_dodge_refreshed_events(frame, index)?;
645            self.update_goal_events(frame, index)?;
646            self.update_player_stat_events(frame, index)?;
647            self.update_demolishes(frame, index)?;
648
649            // Get the time to process for this frame. If target_time is set to
650            // NextFrame, we use the time of the current frame.
651            let mut current_time = match &target_time {
652                TimeAdvance::Time(t) => *t,
653                TimeAdvance::NextFrame => frame.time,
654            };
655
656            while current_time <= frame.time {
657                // Call the handler to process the frame and get the time for
658                // the next frame the handler wants to process
659                target_time = handler.process_frame(self, frame, index, current_time)?;
660                // If the handler specified a specific time, update current_time
661                // to that time. If the handler specified NextFrame, we break
662                // out of the loop to move on to the next frame in the replay.
663                // This design allows the handler to have control over the frame
664                // rate, including the possibility of skipping frames.
665                if let TimeAdvance::Time(new_target) = target_time {
666                    current_time = new_target;
667                } else {
668                    break;
669                }
670            }
671        }
672        handler.finish_replay(self)?;
673        Ok(())
674    }
675
676    /// Process multiple collectors simultaneously over the same replay frames.
677    ///
678    /// All collectors receive the same frame data for each frame. This is useful
679    /// when you have multiple independent collectors that each gather different
680    /// aspects of replay data.
681    ///
682    /// Note: This method always advances frame-by-frame. If collectors return
683    /// [`TimeAdvance::Time`] values, those are ignored.
684    pub fn process_all(&mut self, collectors: &mut [&mut dyn Collector]) -> SubtrActorResult<()> {
685        for (index, frame) in self
686            .replay
687            .network_frames
688            .as_ref()
689            .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::NoNetworkFrames))?
690            .frames
691            .iter()
692            .enumerate()
693        {
694            self.actor_state.process_frame(frame, index)?;
695            self.update_game_type_details();
696            self.update_mappings(frame)?;
697            self.update_ball_id(frame)?;
698            self.update_boost_amounts(frame, index)?;
699            self.update_boost_pad_events(frame, index)?;
700            self.update_touch_events(frame, index)?;
701            self.update_dodge_refreshed_events(frame, index)?;
702            self.update_goal_events(frame, index)?;
703            self.update_player_stat_events(frame, index)?;
704            self.update_demolishes(frame, index)?;
705
706            for collector in collectors.iter_mut() {
707                collector.process_frame(self, frame, index, frame.time)?;
708            }
709        }
710        for collector in collectors.iter_mut() {
711            collector.finish_replay(self)?;
712        }
713        Ok(())
714    }
715
716    /// Reset the state of the [`ReplayProcessor`].
717    pub fn reset(&mut self) {
718        self.ball_actor_id = None;
719        // Keep bootstrapped player/car mappings. Some old replays expose
720        // player identity late during bootstrap, but the actor ids are already
721        // valid for earlier frames once frame processing restarts.
722        self.actor_state = ActorStateModeler::new();
723        self.boost_pad_events = Vec::new();
724        self.current_frame_boost_pad_events = Vec::new();
725        self.boost_pad_pickup_sequence_times = HashMap::new();
726        self.boost_pad_resolution = BoostPadResolutionState::default();
727        self.touch_events = Vec::new();
728        self.current_frame_touch_events = Vec::new();
729        self.dodge_refreshed_events = Vec::new();
730        self.current_frame_dodge_refreshed_events = Vec::new();
731        self.dodge_refreshed_counters = HashMap::new();
732        self.goal_events = Vec::new();
733        self.current_frame_goal_events = Vec::new();
734        self.player_stat_events = Vec::new();
735        self.current_frame_player_stat_events = Vec::new();
736        self.player_stat_counters = HashMap::new();
737        self.demolishes = Vec::new();
738        self.known_demolishes = Vec::new();
739        self.demolish_format = None;
740        self.kickoff_phase_active_last_frame = false;
741    }
742}
743
744#[cfg(test)]
745#[path = "mod_tests.rs"]
746mod tests;