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}