Skip to main content

subtr_actor/processor/
mod.rs

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