Skip to main content

subtr_actor/processor/
mod.rs

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