Skip to main content

subtr_actor/processor/
mod.rs

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