Skip to main content

subtr_actor/processor/
mod.rs

1use crate::*;
2use boxcars;
3use std::collections::HashMap;
4
5/// Attempts to match an attribute value with the given type.
6///
7/// # Arguments
8///
9/// * `$value` - An expression that yields the attribute value.
10/// * `$type` - The expected enum path.
11///
12/// If the attribute matches the specified type, it is returned wrapped in an
13/// [`Ok`] variant of a [`Result`]. If the attribute doesn't match, it results in an
14/// [`Err`] variant with a [`SubtrActorError`], specifying the expected type and
15/// the actual type.
16#[macro_export]
17macro_rules! attribute_match {
18    ($value:expr, $type:path $(,)?) => {{
19        let attribute = $value;
20        if let $type(value) = attribute {
21            Ok(value)
22        } else {
23            SubtrActorError::new_result(SubtrActorErrorVariant::UnexpectedAttributeType {
24                expected_type: stringify!(path).to_string(),
25                actual_type: attribute_to_tag(&attribute).to_string(),
26            })
27        }
28    }};
29}
30
31/// Obtains an attribute from a map and ensures it matches the expected type.
32///
33/// # Arguments
34///
35/// * `$self` - The struct or instance on which the function is invoked.
36/// * `$map` - The data map.
37/// * `$prop` - The attribute key.
38/// * `$type` - The expected enum path.
39#[macro_export]
40macro_rules! get_attribute_errors_expected {
41    ($self:ident, $map:expr, $prop:expr, $type:path) => {
42        $self
43            .get_attribute($map, $prop)
44            .and_then(|found| attribute_match!(found, $type))
45    };
46}
47
48/// Obtains an attribute and its updated status from a map and ensures the
49/// attribute matches the expected type.
50///
51/// # Arguments
52///
53/// * `$self` - The struct or instance on which the function is invoked.
54/// * `$map` - The data map.
55/// * `$prop` - The attribute key.
56/// * `$type` - The expected enum path.
57///
58/// It returns a [`Result`] with a tuple of the matched attribute and its updated
59/// status, after invoking [`attribute_match!`] on the found attribute.
60macro_rules! get_attribute_and_updated {
61    ($self:ident, $map:expr, $prop:expr, $type:path) => {
62        $self
63            .get_attribute_and_updated($map, $prop)
64            .and_then(|(found, updated)| attribute_match!(found, $type).map(|v| (v, updated)))
65    };
66}
67
68/// Obtains an actor attribute and ensures it matches the expected type.
69///
70/// # Arguments
71///
72/// * `$self` - The struct or instance on which the function is invoked.
73/// * `$actor` - The actor identifier.
74/// * `$prop` - The attribute key.
75/// * `$type` - The expected enum path.
76macro_rules! get_actor_attribute_matching {
77    ($self:ident, $actor:expr, $prop:expr, $type:path) => {
78        $self
79            .get_actor_attribute($actor, $prop)
80            .and_then(|found| attribute_match!(found, $type))
81    };
82}
83
84/// Obtains a derived attribute from a map and ensures it matches the expected
85/// type.
86///
87/// # Arguments
88///
89/// * `$map` - The data map.
90/// * `$key` - The attribute key.
91/// * `$type` - The expected enum path.
92macro_rules! get_derived_attribute {
93    ($map:expr, $key:expr, $type:path) => {
94        $map.get($key)
95            .ok_or_else(|| {
96                SubtrActorError::new(SubtrActorErrorVariant::DerivedKeyValueNotFound {
97                    name: $key.to_string(),
98                })
99            })
100            .and_then(|found| attribute_match!(&found.0, $type))
101    };
102}
103
104fn get_actor_id_from_active_actor<T>(
105    _: T,
106    active_actor: &boxcars::ActiveActor,
107) -> boxcars::ActorId {
108    active_actor.actor
109}
110
111fn use_update_actor<T>(id: boxcars::ActorId, _: T) -> boxcars::ActorId {
112    id
113}
114
115mod bootstrap;
116mod debug;
117mod queries;
118mod updaters;
119
120/// The [`ReplayProcessor`] struct is a pivotal component in `subtr-actor`'s
121/// replay parsing pipeline. It is designed to process and traverse an actor
122/// graph of a Rocket League replay, and expose methods for collectors to gather
123/// specific data points as it progresses through the replay.
124///
125/// The processor pushes frames from a replay through an [`ActorStateModeler`],
126/// which models the state all actors in the replay at a given point in time.
127/// The [`ReplayProcessor`] also maintains various mappings to allow efficient
128/// lookup and traversal of the actor graph, thus assisting [`Collector`]
129/// instances in their data accumulation tasks.
130///
131/// The primary method of this struct is [`process`](ReplayProcessor::process),
132/// which takes a collector and processes the replay. As it traverses the
133/// replay, it calls the [`Collector::process_frame`] method of the passed
134/// collector, passing the current frame along with its contextual data. This
135/// allows the collector to extract specific data from each frame as needed.
136///
137/// The [`ReplayProcessor`] also provides a number of helper methods for
138/// navigating the actor graph and extracting information, such as
139/// [`get_ball_rigid_body`](ReplayProcessor::get_ball_rigid_body),
140/// [`get_player_name`](ReplayProcessor::get_player_name),
141/// [`get_player_team_key`](ReplayProcessor::get_player_team_key),
142/// [`get_player_is_team_0`](ReplayProcessor::get_player_is_team_0), and
143/// [`get_player_rigid_body`](ReplayProcessor::get_player_rigid_body).
144///
145/// # See Also
146///
147/// * [`ActorStateModeler`]: A struct used to model the states of multiple
148///   actors at a given point in time.
149/// * [`Collector`]: A trait implemented by objects that wish to collect data as
150///   the `ReplayProcessor` processes a replay.
151pub struct ReplayProcessor<'a> {
152    /// The replay currently being traversed.
153    pub replay: &'a boxcars::Replay,
154    spatial_normalization_factor: f32,
155    /// Modeled actor state for the current replay frame.
156    pub actor_state: ActorStateModeler,
157    /// Mapping from object ids to their replay object names.
158    pub object_id_to_name: HashMap<boxcars::ObjectId, String>,
159    /// Reverse lookup from replay object names to object ids.
160    pub name_to_object_id: HashMap<String, boxcars::ObjectId>,
161    /// Cached actor id for the replay ball when known.
162    pub ball_actor_id: Option<boxcars::ActorId>,
163    /// Stable ordering of team 0 players.
164    pub team_zero: Vec<PlayerId>,
165    /// Stable ordering of team 1 players.
166    pub team_one: Vec<PlayerId>,
167    /// Mapping from player ids to their player-controller actor ids.
168    pub player_to_actor_id: HashMap<PlayerId, boxcars::ActorId>,
169    /// Mapping from player-controller actors to car actors.
170    pub player_to_car: HashMap<boxcars::ActorId, boxcars::ActorId>,
171    /// Mapping from player-controller actors to team actors.
172    pub player_to_team: HashMap<boxcars::ActorId, boxcars::ActorId>,
173    /// Reverse mapping from car actors to player-controller actors.
174    pub car_to_player: HashMap<boxcars::ActorId, boxcars::ActorId>,
175    /// Mapping from car actors to boost component actors.
176    pub car_to_boost: HashMap<boxcars::ActorId, boxcars::ActorId>,
177    /// Mapping from car actors to jump component actors.
178    pub car_to_jump: HashMap<boxcars::ActorId, boxcars::ActorId>,
179    /// Mapping from car actors to double-jump component actors.
180    pub car_to_double_jump: HashMap<boxcars::ActorId, boxcars::ActorId>,
181    /// Mapping from car actors to dodge component actors.
182    pub car_to_dodge: HashMap<boxcars::ActorId, boxcars::ActorId>,
183    /// All boost-pad events observed so far in the replay.
184    pub boost_pad_events: Vec<BoostPadEvent>,
185    current_frame_boost_pad_events: Vec<BoostPadEvent>,
186    /// All touch events observed so far in the replay.
187    pub touch_events: Vec<TouchEvent>,
188    current_frame_touch_events: Vec<TouchEvent>,
189    /// All dodge-refresh events observed so far in the replay.
190    pub dodge_refreshed_events: Vec<DodgeRefreshedEvent>,
191    current_frame_dodge_refreshed_events: Vec<DodgeRefreshedEvent>,
192    dodge_refreshed_counters: HashMap<PlayerId, i32>,
193    /// All goal events observed so far in the replay.
194    pub goal_events: Vec<GoalEvent>,
195    current_frame_goal_events: Vec<GoalEvent>,
196    /// All shot/save/assist-style stat events observed so far in the replay.
197    pub player_stat_events: Vec<PlayerStatEvent>,
198    current_frame_player_stat_events: Vec<PlayerStatEvent>,
199    player_stat_counters: HashMap<(PlayerId, PlayerStatEventKind), i32>,
200    /// All demolishes observed so far in the replay.
201    pub demolishes: Vec<DemolishInfo>,
202    known_demolishes: Vec<(DemolishAttribute, usize)>,
203    demolish_format: Option<DemolishFormat>,
204    kickoff_phase_active_last_frame: bool,
205}
206
207impl<'a> ReplayProcessor<'a> {
208    /// Constructs a new [`ReplayProcessor`] instance with the provided replay.
209    ///
210    /// # Arguments
211    ///
212    /// * `replay` - A reference to the [`boxcars::Replay`] to be processed.
213    ///
214    /// # Returns
215    ///
216    /// Returns a [`SubtrActorResult`] of [`ReplayProcessor`]. In the process of
217    /// initialization, the [`ReplayProcessor`]: - Maps each object id in the
218    /// replay to its corresponding name. - Initializes empty state and
219    /// attribute maps. - Sets the player order from either replay headers or
220    /// frames, if available.
221    pub fn new(replay: &'a boxcars::Replay) -> SubtrActorResult<Self> {
222        let mut object_id_to_name = HashMap::new();
223        let mut name_to_object_id = HashMap::new();
224        for (id, name) in replay.objects.iter().enumerate() {
225            let object_id = boxcars::ObjectId(id as i32);
226            object_id_to_name.insert(object_id, name.clone());
227            name_to_object_id.insert(name.clone(), object_id);
228        }
229        let mut processor = Self {
230            actor_state: ActorStateModeler::new(),
231            replay,
232            spatial_normalization_factor: if replay.net_version.unwrap_or(0) < 7 {
233                100.0
234            } else {
235                1.0
236            },
237            object_id_to_name,
238            name_to_object_id,
239            team_zero: Vec::new(),
240            team_one: Vec::new(),
241            ball_actor_id: None,
242            player_to_car: HashMap::new(),
243            player_to_team: HashMap::new(),
244            player_to_actor_id: HashMap::new(),
245            car_to_player: HashMap::new(),
246            car_to_boost: HashMap::new(),
247            car_to_jump: HashMap::new(),
248            car_to_double_jump: HashMap::new(),
249            car_to_dodge: HashMap::new(),
250            boost_pad_events: Vec::new(),
251            current_frame_boost_pad_events: Vec::new(),
252            touch_events: Vec::new(),
253            current_frame_touch_events: Vec::new(),
254            dodge_refreshed_events: Vec::new(),
255            current_frame_dodge_refreshed_events: Vec::new(),
256            dodge_refreshed_counters: HashMap::new(),
257            goal_events: Vec::new(),
258            current_frame_goal_events: Vec::new(),
259            player_stat_events: Vec::new(),
260            current_frame_player_stat_events: Vec::new(),
261            player_stat_counters: HashMap::new(),
262            demolishes: Vec::new(),
263            known_demolishes: Vec::new(),
264            demolish_format: None,
265            kickoff_phase_active_last_frame: false,
266        };
267        processor
268            .set_player_order_from_headers()
269            .or_else(|_| processor.set_player_order_from_frames())?;
270
271        Ok(processor)
272    }
273
274    /// Returns the scale factor applied when normalizing replay spatial values.
275    pub fn spatial_normalization_factor(&self) -> f32 {
276        self.spatial_normalization_factor
277    }
278
279    fn normalize_vector(&self, vector: boxcars::Vector3f) -> boxcars::Vector3f {
280        if (self.spatial_normalization_factor - 1.0).abs() < f32::EPSILON {
281            vector
282        } else {
283            boxcars::Vector3f {
284                x: vector.x * self.spatial_normalization_factor,
285                y: vector.y * self.spatial_normalization_factor,
286                z: vector.z * self.spatial_normalization_factor,
287            }
288        }
289    }
290
291    fn normalize_optional_vector(
292        &self,
293        vector: Option<boxcars::Vector3f>,
294    ) -> Option<boxcars::Vector3f> {
295        vector.map(|value| self.normalize_vector(value))
296    }
297
298    fn normalize_rigid_body(&self, rigid_body: &boxcars::RigidBody) -> boxcars::RigidBody {
299        if (self.spatial_normalization_factor - 1.0).abs() < f32::EPSILON {
300            *rigid_body
301        } else {
302            boxcars::RigidBody {
303                sleeping: rigid_body.sleeping,
304                location: self.normalize_vector(rigid_body.location),
305                rotation: rigid_body.rotation,
306                linear_velocity: self.normalize_optional_vector(rigid_body.linear_velocity),
307                angular_velocity: self.normalize_optional_vector(rigid_body.angular_velocity),
308            }
309        }
310    }
311
312    /// [`Self::process`] takes a [`Collector`] as an argument and iterates over
313    /// each frame in the replay, updating the internal state of the processor
314    /// and other relevant mappings based on the current frame.
315    ///
316    /// After each a frame is processed, [`Collector::process_frame`] of the
317    /// collector is called. The [`TimeAdvance`] return value of this call into
318    /// [`Collector::process_frame`] is used to determine what happens next: in
319    /// the case of [`TimeAdvance::Time`], the notion of current time is
320    /// advanced by the provided amount, and only the timestamp of the frame is
321    /// exceeded, do we process the next frame. This mechanism allows fine
322    /// grained control of frame processing, and the frequency of invocations of
323    /// the [`Collector`]. If time is advanced by less than the delay between
324    /// frames, the collector will be called more than once per frame, and can
325    /// use functions like [`Self::get_interpolated_player_rigid_body`] to get
326    /// values that are interpolated between frames. Its also possible to skip
327    /// over frames by providing time advance values that are sufficiently
328    /// large.
329    ///
330    /// At the end of processing, it checks to make sure that no unknown players
331    /// were encountered during the replay. If any unknown players are found, an
332    /// error is returned.
333    pub fn process<H: Collector>(&mut self, handler: &mut H) -> SubtrActorResult<()> {
334        // Initially, we set target_time to NextFrame to ensure the collector
335        // will process the first frame.
336        let mut target_time = TimeAdvance::NextFrame;
337        for (index, frame) in self
338            .replay
339            .network_frames
340            .as_ref()
341            .ok_or(SubtrActorError::new(
342                SubtrActorErrorVariant::NoNetworkFrames,
343            ))?
344            .frames
345            .iter()
346            .enumerate()
347        {
348            // Update the internal state of the processor based on the current frame
349            self.actor_state.process_frame(frame, index)?;
350            self.update_mappings(frame)?;
351            self.update_ball_id(frame)?;
352            self.update_boost_amounts(frame, index)?;
353            self.update_boost_pad_events(frame, index)?;
354            self.update_touch_events(frame, index)?;
355            self.update_dodge_refreshed_events(frame, index)?;
356            self.update_goal_events(frame, index)?;
357            self.update_player_stat_events(frame, index)?;
358            self.update_demolishes(frame, index)?;
359
360            // Get the time to process for this frame. If target_time is set to
361            // NextFrame, we use the time of the current frame.
362            let mut current_time = match &target_time {
363                TimeAdvance::Time(t) => *t,
364                TimeAdvance::NextFrame => frame.time,
365            };
366
367            while current_time <= frame.time {
368                // Call the handler to process the frame and get the time for
369                // the next frame the handler wants to process
370                target_time = handler.process_frame(self, frame, index, current_time)?;
371                // If the handler specified a specific time, update current_time
372                // to that time. If the handler specified NextFrame, we break
373                // out of the loop to move on to the next frame in the replay.
374                // This design allows the handler to have control over the frame
375                // rate, including the possibility of skipping frames.
376                if let TimeAdvance::Time(new_target) = target_time {
377                    current_time = new_target;
378                } else {
379                    break;
380                }
381            }
382        }
383        handler.finish_replay(self)?;
384        Ok(())
385    }
386
387    /// Process multiple collectors simultaneously over the same replay frames.
388    ///
389    /// All collectors receive the same frame data for each frame. This is useful
390    /// when you have multiple independent collectors that each gather different
391    /// aspects of replay data.
392    ///
393    /// Note: This method always advances frame-by-frame. If collectors return
394    /// [`TimeAdvance::Time`] values, those are ignored.
395    pub fn process_all(&mut self, collectors: &mut [&mut dyn Collector]) -> SubtrActorResult<()> {
396        for (index, frame) in self
397            .replay
398            .network_frames
399            .as_ref()
400            .ok_or(SubtrActorError::new(
401                SubtrActorErrorVariant::NoNetworkFrames,
402            ))?
403            .frames
404            .iter()
405            .enumerate()
406        {
407            self.actor_state.process_frame(frame, index)?;
408            self.update_mappings(frame)?;
409            self.update_ball_id(frame)?;
410            self.update_boost_amounts(frame, index)?;
411            self.update_boost_pad_events(frame, index)?;
412            self.update_touch_events(frame, index)?;
413            self.update_dodge_refreshed_events(frame, index)?;
414            self.update_goal_events(frame, index)?;
415            self.update_player_stat_events(frame, index)?;
416            self.update_demolishes(frame, index)?;
417
418            for collector in collectors.iter_mut() {
419                collector.process_frame(self, frame, index, frame.time)?;
420            }
421        }
422        for collector in collectors.iter_mut() {
423            collector.finish_replay(self)?;
424        }
425        Ok(())
426    }
427
428    /// Reset the state of the [`ReplayProcessor`].
429    pub fn reset(&mut self) {
430        self.ball_actor_id = None;
431        self.player_to_car = HashMap::new();
432        self.player_to_team = HashMap::new();
433        self.player_to_actor_id = HashMap::new();
434        self.car_to_player = HashMap::new();
435        self.car_to_boost = HashMap::new();
436        self.car_to_jump = HashMap::new();
437        self.car_to_double_jump = HashMap::new();
438        self.car_to_dodge = HashMap::new();
439        self.actor_state = ActorStateModeler::new();
440        self.boost_pad_events = Vec::new();
441        self.current_frame_boost_pad_events = Vec::new();
442        self.touch_events = Vec::new();
443        self.current_frame_touch_events = Vec::new();
444        self.dodge_refreshed_events = Vec::new();
445        self.current_frame_dodge_refreshed_events = Vec::new();
446        self.dodge_refreshed_counters = HashMap::new();
447        self.goal_events = Vec::new();
448        self.current_frame_goal_events = Vec::new();
449        self.player_stat_events = Vec::new();
450        self.current_frame_player_stat_events = Vec::new();
451        self.player_stat_counters = HashMap::new();
452        self.demolishes = Vec::new();
453        self.known_demolishes = Vec::new();
454        self.demolish_format = None;
455        self.kickoff_phase_active_last_frame = false;
456    }
457}