subtr_actor/collector/replay_data.rs
1//! # Replay Data Collection Module
2//!
3//! This module provides comprehensive data structures and collection mechanisms
4//! for extracting and organizing Rocket League replay data. It offers a complete
5//! representation of ball, player, and game state information across all frames
6//! of a replay.
7//!
8//! The module is built around the [`ReplayDataCollector`] which implements the
9//! [`Collector`] trait, allowing it to process replay frames and extract
10//! detailed information about player actions, ball movement, and game state.
11//!
12//! # Key Components
13//!
14//! - [`ReplayData`] - The complete replay data structure containing all extracted information
15//! - [`FrameData`] - Frame-by-frame data including ball, player, and metadata information
16//! - [`PlayerFrame`] - Detailed player state including position, controls, and actions
17//! - [`BallFrame`] - Ball state including rigid body physics information
18//! - [`MetadataFrame`] - Game state metadata including time and score information
19//!
20//! # Example Usage
21//!
22//! ```rust
23//! use subtr_actor::collector::replay_data::ReplayDataCollector;
24//! use boxcars::ParserBuilder;
25//!
26//! let data = std::fs::read("assets/replays/new_boost_format.replay").unwrap();
27//! let replay = ParserBuilder::new(&data).parse().unwrap();
28//!
29//! let collector = ReplayDataCollector::new();
30//! let replay_data = collector.get_replay_data(&replay).unwrap();
31//!
32//! // Access frame-by-frame data
33//! for metadata_frame in &replay_data.frame_data.metadata_frames {
34//! println!("Time: {:.2}s, Remaining: {}s",
35//! metadata_frame.time, metadata_frame.seconds_remaining);
36//! }
37//! ```
38
39use boxcars;
40use serde::Serialize;
41
42use crate::*;
43
44/// Represents the ball state for a single frame in a Rocket League replay.
45///
46/// The ball can either be in an empty state (when ball syncing is disabled or
47/// the rigid body is unavailable) or contain full physics data including
48/// position, rotation, and velocity information.
49///
50/// # Variants
51///
52/// - [`Empty`](BallFrame::Empty) - Indicates the ball is unavailable or ball syncing is disabled
53/// - [`Data`](BallFrame::Data) - Contains the ball's rigid body physics information
54#[derive(Debug, Clone, PartialEq, Serialize)]
55pub enum BallFrame {
56 /// Empty frame indicating the ball is unavailable or ball syncing is disabled
57 Empty,
58 /// Frame containing the ball's rigid body physics data
59 Data {
60 /// The ball's rigid body containing position, rotation, and velocity information
61 rigid_body: boxcars::RigidBody,
62 },
63}
64
65impl BallFrame {
66 /// Creates a new [`BallFrame`] from a [`ReplayProcessor`] at the specified time.
67 ///
68 /// This method extracts the ball's state from the replay processor, handling
69 /// cases where ball syncing is disabled or the rigid body is unavailable.
70 ///
71 /// # Arguments
72 ///
73 /// * `processor` - The [`ReplayProcessor`] containing the replay data
74 /// * `current_time` - The time in seconds at which to extract the ball state
75 ///
76 /// # Returns
77 ///
78 /// Returns a [`BallFrame`] which will be [`Empty`](BallFrame::Empty) if:
79 /// - Ball syncing is disabled in the replay
80 /// - The ball's rigid body cannot be retrieved
81 ///
82 /// Otherwise returns [`Data`](BallFrame::Data) containing the ball's rigid body.
83 fn new_from_processor(processor: &ReplayProcessor, current_time: f32) -> Self {
84 if processor.get_ignore_ball_syncing().unwrap_or(false) {
85 Self::Empty
86 } else if let Ok(rigid_body) = processor.get_interpolated_ball_rigid_body(current_time, 0.0)
87 {
88 Self::new_from_rigid_body(rigid_body)
89 } else {
90 Self::Empty
91 }
92 }
93
94 /// Creates a new [`BallFrame`] from a rigid body.
95 ///
96 /// # Arguments
97 ///
98 /// * `rigid_body` - The ball's rigid body containing physics information
99 ///
100 /// # Returns
101 ///
102 /// Returns [`Data`](BallFrame::Data) containing the rigid body even when the
103 /// ball is sleeping, so stationary kickoff frames still retain the ball's
104 /// position for downstream consumers such as the JS player.
105 fn new_from_rigid_body(rigid_body: boxcars::RigidBody) -> Self {
106 Self::Data { rigid_body }
107 }
108}
109
110/// Represents a player's state for a single frame in a Rocket League replay.
111///
112/// Contains comprehensive information about a player's position, movement,
113/// and control inputs during a specific frame of the replay.
114///
115/// # Variants
116///
117/// - [`Empty`](PlayerFrame::Empty) - Indicates the player is inactive or sleeping
118/// - [`Data`](PlayerFrame::Data) - Contains the player's complete state information
119#[derive(Debug, Clone, PartialEq, Serialize)]
120pub enum PlayerFrame {
121 /// Empty frame indicating the player is inactive or sleeping
122 Empty,
123 /// Frame containing the player's complete state data
124 Data {
125 /// The player's rigid body containing position, rotation, and velocity information
126 rigid_body: boxcars::RigidBody,
127 /// The player's current boost amount in raw replay units (0.0 to 255.0)
128 boost_amount: f32,
129 /// Whether the player is actively using boost
130 boost_active: bool,
131 /// Whether the player is actively powersliding / holding handbrake
132 powerslide_active: bool,
133 /// Whether the player is actively jumping
134 jump_active: bool,
135 /// Whether the player is performing a double jump
136 double_jump_active: bool,
137 /// Whether the player is performing a dodge maneuver
138 dodge_active: bool,
139 /// The player's name as it appears in the replay
140 player_name: Option<String>,
141 /// The team the player belongs to (0 or 1)
142 team: Option<i32>,
143 /// Whether the player is on team 0 (blue team typically)
144 is_team_0: Option<bool>,
145 },
146}
147
148impl PlayerFrame {
149 /// Creates a new [`PlayerFrame`] from a [`ReplayProcessor`] for a specific player at the specified time.
150 ///
151 /// This method extracts comprehensive player state information from the replay processor,
152 /// including position, control inputs, and team information.
153 ///
154 /// # Arguments
155 ///
156 /// * `processor` - The [`ReplayProcessor`] containing the replay data
157 /// * `player_id` - The unique identifier for the player
158 /// * `current_time` - The time in seconds at which to extract the player state
159 ///
160 /// # Returns
161 ///
162 /// Returns a [`SubtrActorResult`] containing a [`PlayerFrame`] which will be:
163 /// - [`Empty`](PlayerFrame::Empty) if the player's rigid body is in a sleeping state
164 /// - [`Data`](PlayerFrame::Data) containing the player's complete state information
165 ///
166 /// # Errors
167 ///
168 /// Returns a [`SubtrActorError`] if:
169 /// - The player's rigid body cannot be retrieved
170 /// - The player's boost level cannot be accessed
171 /// - Other player state information is inaccessible
172 fn new_from_processor(
173 processor: &ReplayProcessor,
174 player_id: &PlayerId,
175 current_time: f32,
176 ) -> SubtrActorResult<Self> {
177 let rigid_body =
178 processor.get_interpolated_player_rigid_body(player_id, current_time, 0.0)?;
179
180 if rigid_body.sleeping {
181 return Ok(PlayerFrame::Empty);
182 }
183
184 let boost_amount = processor.get_player_boost_level(player_id)?;
185 let boost_active = processor.get_boost_active(player_id).unwrap_or(0) % 2 == 1;
186 let powerslide_active = processor.get_powerslide_active(player_id).unwrap_or(false);
187 let jump_active = processor.get_jump_active(player_id).unwrap_or(0) % 2 == 1;
188 let double_jump_active = processor.get_double_jump_active(player_id).unwrap_or(0) % 2 == 1;
189 let dodge_active = processor.get_dodge_active(player_id).unwrap_or(0) % 2 == 1;
190
191 // Extract player identity information
192 let player_name = processor.get_player_name(player_id).ok();
193 let team = processor
194 .get_player_team_key(player_id)
195 .ok()
196 .and_then(|team_key| team_key.parse::<i32>().ok());
197 let is_team_0 = processor.get_player_is_team_0(player_id).ok();
198
199 Ok(Self::from_data(
200 rigid_body,
201 boost_amount,
202 boost_active,
203 powerslide_active,
204 jump_active,
205 double_jump_active,
206 dodge_active,
207 player_name,
208 team,
209 is_team_0,
210 ))
211 }
212
213 /// Creates a [`PlayerFrame`] from the provided data components.
214 ///
215 /// # Arguments
216 ///
217 /// * `rigid_body` - The player's rigid body physics information
218 /// * `boost_amount` - The player's current boost level in raw replay units (0.0 to 255.0)
219 /// * `boost_active` - Whether the player is actively using boost
220 /// * `powerslide_active` - Whether the player is actively powersliding
221 /// * `jump_active` - Whether the player is actively jumping
222 /// * `double_jump_active` - Whether the player is performing a double jump
223 /// * `dodge_active` - Whether the player is performing a dodge maneuver
224 /// * `player_name` - The player's name, if available
225 /// * `team` - The player's team number, if available
226 /// * `is_team_0` - Whether the player is on team 0, if available
227 ///
228 /// # Returns
229 ///
230 /// Returns [`Empty`](PlayerFrame::Empty) if the rigid body is sleeping,
231 /// otherwise returns [`Data`](PlayerFrame::Data) with all provided information.
232 #[allow(clippy::too_many_arguments)]
233 fn from_data(
234 rigid_body: boxcars::RigidBody,
235 boost_amount: f32,
236 boost_active: bool,
237 powerslide_active: bool,
238 jump_active: bool,
239 double_jump_active: bool,
240 dodge_active: bool,
241 player_name: Option<String>,
242 team: Option<i32>,
243 is_team_0: Option<bool>,
244 ) -> Self {
245 if rigid_body.sleeping {
246 Self::Empty
247 } else {
248 Self::Data {
249 rigid_body,
250 boost_amount,
251 boost_active,
252 powerslide_active,
253 jump_active,
254 double_jump_active,
255 dodge_active,
256 player_name,
257 team,
258 is_team_0,
259 }
260 }
261 }
262}
263
264/// Contains all frame data for a single player throughout the replay.
265///
266/// This structure holds a chronological sequence of [`PlayerFrame`] instances
267/// representing the player's state at each processed frame of the replay.
268///
269/// # Fields
270///
271/// * `frames` - A vector of [`PlayerFrame`] instances in chronological order
272#[derive(Debug, Clone, PartialEq, Serialize)]
273pub struct PlayerData {
274 /// Vector of player frames in chronological order
275 frames: Vec<PlayerFrame>,
276}
277
278impl PlayerData {
279 /// Creates a new empty [`PlayerData`] instance.
280 ///
281 /// # Returns
282 ///
283 /// Returns a new [`PlayerData`] with an empty frames vector.
284 fn new() -> Self {
285 Self { frames: Vec::new() }
286 }
287
288 /// Adds a player frame at the specified frame index.
289 ///
290 /// If the frame index is beyond the current length of the frames vector,
291 /// empty frames will be inserted to fill the gap before adding the new frame.
292 ///
293 /// # Arguments
294 ///
295 /// * `frame_index` - The index at which to insert the frame
296 /// * `frame` - The [`PlayerFrame`] to add
297 fn add_frame(&mut self, frame_index: usize, frame: PlayerFrame) {
298 let empty_frames_to_add = frame_index - self.frames.len();
299 if empty_frames_to_add > 0 {
300 for _ in 0..empty_frames_to_add {
301 self.frames.push(PlayerFrame::Empty)
302 }
303 }
304 self.frames.push(frame)
305 }
306
307 /// Returns a reference to the frames vector.
308 ///
309 /// # Returns
310 ///
311 /// Returns a reference to the vector of [`PlayerFrame`] instances.
312 pub fn frames(&self) -> &Vec<PlayerFrame> {
313 &self.frames
314 }
315
316 /// Returns the number of frames in this player's data.
317 ///
318 /// # Returns
319 ///
320 /// Returns the total number of frames stored for this player.
321 pub fn frame_count(&self) -> usize {
322 self.frames.len()
323 }
324}
325
326/// Contains all frame data for the ball throughout the replay.
327///
328/// This structure holds a chronological sequence of [`BallFrame`] instances
329/// representing the ball's state at each processed frame of the replay.
330///
331/// # Fields
332///
333/// * `frames` - A vector of [`BallFrame`] instances in chronological order
334#[derive(Debug, Clone, PartialEq, Serialize)]
335pub struct BallData {
336 /// Vector of ball frames in chronological order
337 frames: Vec<BallFrame>,
338}
339
340impl BallData {
341 /// Creates a new empty [`BallData`] instance.
342 ///
343 /// # Returns
344 ///
345 /// Returns a new [`BallData`] with an empty frames vector.
346 fn new() -> Self {
347 Self { frames: Vec::new() }
348 }
349
350 /// Adds a ball frame at the specified frame index.
351 ///
352 /// If the frame index is beyond the current length of the frames vector,
353 /// empty frames will be inserted to fill the gap before adding the new frame.
354 ///
355 /// # Arguments
356 ///
357 /// * `frame_index` - The index at which to insert the frame
358 /// * `frame` - The [`BallFrame`] to add
359 fn add_frame(&mut self, frame_index: usize, frame: BallFrame) {
360 let empty_frames_to_add = frame_index - self.frames.len();
361 if empty_frames_to_add > 0 {
362 for _ in 0..empty_frames_to_add {
363 self.frames.push(BallFrame::Empty)
364 }
365 }
366 self.frames.push(frame)
367 }
368
369 /// Returns a reference to the frames vector.
370 ///
371 /// # Returns
372 ///
373 /// Returns a reference to the vector of [`BallFrame`] instances.
374 pub fn frames(&self) -> &Vec<BallFrame> {
375 &self.frames
376 }
377
378 /// Returns the number of frames in the ball data.
379 ///
380 /// # Returns
381 ///
382 /// Returns the total number of frames stored for the ball.
383 pub fn frame_count(&self) -> usize {
384 self.frames.len()
385 }
386}
387
388/// Represents game metadata for a single frame in a Rocket League replay.
389///
390/// Contains timing information and game state data that applies to the entire
391/// game at a specific point in time.
392///
393/// # Fields
394///
395/// * `time` - The current time in seconds since the start of the replay
396/// * `seconds_remaining` - The number of seconds remaining in the current game period
397/// * `replicated_game_state_name` - The game state enum value (indicates countdown, playing, goal, etc.)
398/// * `replicated_game_state_time_remaining` - The kickoff countdown timer, usually 3 to 0
399#[derive(Debug, Clone, PartialEq, Serialize)]
400pub struct MetadataFrame {
401 /// The current time in seconds since the start of the replay
402 pub time: f32,
403 /// The number of seconds remaining in the current game period
404 pub seconds_remaining: i32,
405 /// The game state enum value (indicates countdown, playing, goal scored, etc.)
406 pub replicated_game_state_name: i32,
407 /// The kickoff countdown timer exposed by the replay metadata actor.
408 pub replicated_game_state_time_remaining: i32,
409}
410
411impl MetadataFrame {
412 /// Creates a new [`MetadataFrame`] from a [`ReplayProcessor`] at the specified time.
413 ///
414 /// # Arguments
415 ///
416 /// * `processor` - The [`ReplayProcessor`] containing the replay data
417 /// * `time` - The current time in seconds since the start of the replay
418 ///
419 /// # Returns
420 ///
421 /// Returns a [`SubtrActorResult`] containing a [`MetadataFrame`] with the
422 /// current time and remaining seconds extracted from the processor.
423 ///
424 /// # Errors
425 ///
426 /// Returns a [`SubtrActorError`] if the seconds remaining cannot be retrieved
427 /// from the processor.
428 fn new_from_processor(processor: &ReplayProcessor, time: f32) -> SubtrActorResult<Self> {
429 Ok(Self::new(
430 time,
431 processor.get_seconds_remaining()?,
432 processor.get_replicated_state_name().unwrap_or(0),
433 processor
434 .get_replicated_game_state_time_remaining()
435 .unwrap_or(0),
436 ))
437 }
438
439 /// Creates a new [`MetadataFrame`] with the specified time, seconds remaining, game state,
440 /// and kickoff countdown value.
441 ///
442 /// # Arguments
443 ///
444 /// * `time` - The current time in seconds since the start of the replay
445 /// * `seconds_remaining` - The number of seconds remaining in the current game period
446 /// * `replicated_game_state_name` - The game state enum value
447 /// * `replicated_game_state_time_remaining` - The kickoff countdown timer
448 ///
449 /// # Returns
450 ///
451 /// Returns a new [`MetadataFrame`] with the provided values.
452 fn new(
453 time: f32,
454 seconds_remaining: i32,
455 replicated_game_state_name: i32,
456 replicated_game_state_time_remaining: i32,
457 ) -> Self {
458 MetadataFrame {
459 time,
460 seconds_remaining,
461 replicated_game_state_name,
462 replicated_game_state_time_remaining,
463 }
464 }
465}
466
467/// Contains all frame-by-frame data for a Rocket League replay.
468///
469/// This structure organizes ball data, player data, and metadata for each
470/// frame of the replay, providing a complete picture of the game state
471/// throughout the match.
472///
473/// # Fields
474///
475/// * `ball_data` - All ball state information across all frames
476/// * `players` - Player data for each player, indexed by [`PlayerId`]
477/// * `metadata_frames` - Game metadata for each frame including timing information
478#[derive(Debug, Clone, PartialEq, Serialize)]
479pub struct FrameData {
480 /// All ball state information across all frames
481 pub ball_data: BallData,
482 /// Player data for each player, indexed by PlayerId
483 pub players: Vec<(PlayerId, PlayerData)>,
484 /// Game metadata for each frame including timing information
485 pub metadata_frames: Vec<MetadataFrame>,
486}
487
488/// Complete replay data structure containing all extracted information from a Rocket League replay.
489///
490/// This is the top-level structure that contains all processed replay data including
491/// frame-by-frame information, replay metadata, and special events like demolitions.
492///
493/// # Fields
494///
495/// * `frame_data` - All frame-by-frame data including ball, player, and metadata information
496/// * `meta` - Replay metadata including player information, game settings, and statistics
497/// * `demolish_infos` - Information about all demolition events that occurred during the replay
498/// * `boost_pad_events` - Exact boost pad pickup/availability events detected while processing
499/// * `boost_pads` - Resolved standard boost pad layout annotated with replay pad ids when known
500/// * `touch_events` - Exact team touch events plus attributed player when available
501/// * `dodge_refreshed_events` - Exact counter-derived dodge refresh events from the replay
502/// * `flip_reset_events` - Heuristic sparse flip-reset candidates derived from airborne touch geometry
503/// * `post_wall_dodge_events` - Heuristic airborne dodge uses that occur after wall contact
504/// * `flip_reset_followup_dodge_events` - Heuristic airborne dodge uses following a likely reset touch
505/// * `player_stat_events` - Exact shot/save/assist counter increment events
506/// * `goal_events` - Exact goal explosion events with scorer and cumulative score when available
507///
508/// # Example
509///
510/// ```rust
511/// use subtr_actor::collector::replay_data::ReplayDataCollector;
512/// use boxcars::ParserBuilder;
513///
514/// let data = std::fs::read("assets/replays/new_boost_format.replay").unwrap();
515/// let replay = ParserBuilder::new(&data).parse().unwrap();
516/// let collector = ReplayDataCollector::new();
517/// let replay_data = collector.get_replay_data(&replay).unwrap();
518///
519/// // Access replay metadata
520/// println!("Team 0 players: {}", replay_data.meta.team_zero.len());
521///
522/// // Access frame data
523/// println!("Total frames: {}", replay_data.frame_data.metadata_frames.len());
524///
525/// // Access demolition events
526/// println!("Total demolitions: {}", replay_data.demolish_infos.len());
527/// ```
528#[derive(Debug, Clone, PartialEq, Serialize)]
529pub struct ReplayData {
530 /// All frame-by-frame data including ball, player, and metadata information
531 pub frame_data: FrameData,
532 /// Replay metadata including player information, game settings, and statistics
533 pub meta: ReplayMeta,
534 /// Information about all demolition events that occurred during the replay
535 pub demolish_infos: Vec<DemolishInfo>,
536 /// Exact boost pad pickup and availability events observed during the replay
537 pub boost_pad_events: Vec<BoostPadEvent>,
538 /// Resolved standard boost pad layout annotated with replay pad ids when known
539 pub boost_pads: Vec<ResolvedBoostPad>,
540 /// Exact touch events observed during the replay
541 pub touch_events: Vec<TouchEvent>,
542 /// Exact dodge refresh events observed via the replay's refreshed-dodge counter
543 pub dodge_refreshed_events: Vec<DodgeRefreshedEvent>,
544 /// Heuristic flip-reset candidates observed during the replay
545 pub flip_reset_events: Vec<FlipResetEvent>,
546 /// Heuristic airborne dodge uses observed after wall contact
547 pub post_wall_dodge_events: Vec<PostWallDodgeEvent>,
548 /// Heuristic airborne dodge uses observed after a likely reset touch
549 pub flip_reset_followup_dodge_events: Vec<FlipResetFollowupDodgeEvent>,
550 /// Exact player stat counter increments observed during the replay
551 pub player_stat_events: Vec<PlayerStatEvent>,
552 /// Exact goal events observed during the replay
553 pub goal_events: Vec<GoalEvent>,
554}
555
556/// Optional replay-data enrichments produced by collectors that run alongside
557/// [`ReplayDataCollector`] in the same processor pass.
558#[derive(Debug, Clone, Default)]
559pub struct ReplayDataSupplementalData {
560 pub boost_pads: Vec<ResolvedBoostPad>,
561 pub flip_reset_events: Vec<FlipResetEvent>,
562 pub post_wall_dodge_events: Vec<PostWallDodgeEvent>,
563 pub flip_reset_followup_dodge_events: Vec<FlipResetFollowupDodgeEvent>,
564}
565
566impl ReplayDataSupplementalData {
567 pub fn from_flip_reset_tracker(tracker: FlipResetTracker) -> Self {
568 let (flip_reset_events, post_wall_dodge_events, flip_reset_followup_dodge_events) =
569 tracker.into_events();
570 Self {
571 boost_pads: Vec::new(),
572 flip_reset_events,
573 post_wall_dodge_events,
574 flip_reset_followup_dodge_events,
575 }
576 }
577
578 pub fn with_boost_pads(mut self, boost_pads: Vec<ResolvedBoostPad>) -> Self {
579 self.boost_pads = boost_pads;
580 self
581 }
582}
583
584impl ReplayData {
585 /// Serializes the replay data to a JSON string.
586 ///
587 /// # Returns
588 ///
589 /// Returns a [`Result`] containing either the JSON string representation
590 /// of the replay data or a [`serde_json::Error`] if serialization fails.
591 ///
592 /// # Example
593 ///
594 /// ```rust
595 /// use subtr_actor::collector::replay_data::ReplayDataCollector;
596 /// use boxcars::ParserBuilder;
597 ///
598 /// let data = std::fs::read("assets/replays/new_boost_format.replay").unwrap();
599 /// let replay = ParserBuilder::new(&data).parse().unwrap();
600 /// let collector = ReplayDataCollector::new();
601 /// let replay_data = collector.get_replay_data(&replay).unwrap();
602 ///
603 /// let json_string = replay_data.as_json().unwrap();
604 /// println!("Replay as JSON: {}", json_string);
605 /// ```
606 pub fn as_json(&self) -> Result<String, serde_json::Error> {
607 serde_json::to_string(self)
608 }
609
610 /// Serializes the replay data to a pretty-printed JSON string.
611 ///
612 /// # Returns
613 ///
614 /// Returns a [`Result`] containing either the pretty-printed JSON string
615 /// representation of the replay data or a [`serde_json::Error`] if serialization fails.
616 pub fn as_pretty_json(&self) -> Result<String, serde_json::Error> {
617 serde_json::to_string_pretty(self)
618 }
619}
620
621impl FrameData {
622 /// Creates a new empty [`FrameData`] instance.
623 ///
624 /// # Returns
625 ///
626 /// Returns a new [`FrameData`] with empty ball data, player data, and metadata frames.
627 fn new() -> Self {
628 FrameData {
629 ball_data: BallData::new(),
630 players: Vec::new(),
631 metadata_frames: Vec::new(),
632 }
633 }
634
635 /// Returns the total number of frames in this frame data.
636 ///
637 /// # Returns
638 ///
639 /// Returns the number of metadata frames, which represents the total frame count.
640 pub fn frame_count(&self) -> usize {
641 self.metadata_frames.len()
642 }
643
644 /// Returns the duration of the replay in seconds.
645 ///
646 /// # Returns
647 ///
648 /// Returns the time of the last frame, or 0.0 if no frames exist.
649 pub fn duration(&self) -> f32 {
650 self.metadata_frames.last().map(|f| f.time).unwrap_or(0.0)
651 }
652
653 /// Adds a complete frame of data to the frame data structure.
654 ///
655 /// This method adds metadata, ball data, and player data for a single frame
656 /// to their respective collections, maintaining frame synchronization across
657 /// all data types.
658 ///
659 /// # Arguments
660 ///
661 /// * `frame_metadata` - The metadata for this frame (time, game state, etc.)
662 /// * `ball_frame` - The ball state for this frame
663 /// * `player_frames` - Player state data for all players in this frame
664 ///
665 /// # Returns
666 ///
667 /// Returns a [`SubtrActorResult`] indicating success or failure of the operation.
668 ///
669 /// # Errors
670 ///
671 /// May return a [`SubtrActorError`] if frame data cannot be processed correctly.
672 fn add_frame(
673 &mut self,
674 frame_metadata: MetadataFrame,
675 ball_frame: BallFrame,
676 player_frames: Vec<(PlayerId, PlayerFrame)>,
677 ) -> SubtrActorResult<()> {
678 let frame_index = self.metadata_frames.len();
679 self.metadata_frames.push(frame_metadata);
680 self.ball_data.add_frame(frame_index, ball_frame);
681 for (player_id, frame) in player_frames {
682 self.players
683 .get_entry(player_id)
684 .or_insert_with(PlayerData::new)
685 .add_frame(frame_index, frame)
686 }
687 Ok(())
688 }
689}
690
691/// A collector that extracts comprehensive frame-by-frame data from Rocket League replays.
692///
693/// [`ReplayDataCollector`] implements the [`Collector`] trait to process replay frames
694/// and extract detailed information about ball movement, player actions, and game state.
695/// It builds a complete [`ReplayData`] structure containing all available information
696/// from the replay.
697///
698/// # Usage
699///
700/// The collector is designed to be used with the [`ReplayProcessor`] to extract
701/// comprehensive replay data:
702///
703/// ```rust
704/// use subtr_actor::collector::replay_data::ReplayDataCollector;
705/// use boxcars::ParserBuilder;
706///
707/// let data = std::fs::read("assets/replays/new_boost_format.replay").unwrap();
708/// let replay = ParserBuilder::new(&data).parse().unwrap();
709///
710/// let collector = ReplayDataCollector::new();
711/// let replay_data = collector.get_replay_data(&replay).unwrap();
712///
713/// // Process the extracted data
714/// for (frame_idx, metadata) in replay_data.frame_data.metadata_frames.iter().enumerate() {
715/// println!("Frame {}: Time={:.2}s, Remaining={}s",
716/// frame_idx, metadata.time, metadata.seconds_remaining);
717/// }
718/// ```
719///
720/// # Fields
721///
722/// * `frame_data` - Internal storage for frame-by-frame data during collection
723pub struct ReplayDataCollector {
724 /// Internal storage for frame-by-frame data during collection
725 frame_data: FrameData,
726}
727
728impl Default for ReplayDataCollector {
729 /// Creates a default [`ReplayDataCollector`] instance.
730 ///
731 /// This is equivalent to calling [`ReplayDataCollector::new()`].
732 fn default() -> Self {
733 Self::new()
734 }
735}
736
737impl ReplayDataCollector {
738 /// Creates a new [`ReplayDataCollector`] instance.
739 ///
740 /// # Returns
741 ///
742 /// Returns a new collector ready to process replay frames.
743 pub fn new() -> Self {
744 ReplayDataCollector {
745 frame_data: FrameData::new(),
746 }
747 }
748
749 /// Consumes the collector and returns the collected frame data.
750 ///
751 /// # Returns
752 ///
753 /// Returns the [`FrameData`] containing all processed frame information.
754 pub fn get_frame_data(self) -> FrameData {
755 self.frame_data
756 }
757
758 /// Builds replay data from this collector and an already-processed
759 /// [`ReplayProcessor`].
760 ///
761 /// This keeps replay-data collection composable: callers can run
762 /// [`ReplayDataCollector`] alongside any other collectors with
763 /// [`ReplayProcessor::process_all`] and then decide which enrichments to
764 /// merge into the final payload.
765 pub fn into_replay_data(self, processor: ReplayProcessor<'_>) -> SubtrActorResult<ReplayData> {
766 self.into_replay_data_with_supplemental_data(
767 processor,
768 ReplayDataSupplementalData::default(),
769 )
770 }
771
772 pub fn into_replay_data_with_supplemental_data(
773 self,
774 processor: ReplayProcessor<'_>,
775 supplemental_data: ReplayDataSupplementalData,
776 ) -> SubtrActorResult<ReplayData> {
777 let meta = processor.get_replay_meta()?;
778 Ok(ReplayData {
779 meta,
780 demolish_infos: processor.demolishes,
781 boost_pad_events: processor.boost_pad_events,
782 boost_pads: supplemental_data.boost_pads,
783 touch_events: processor.touch_events,
784 dodge_refreshed_events: processor.dodge_refreshed_events,
785 flip_reset_events: supplemental_data.flip_reset_events,
786 post_wall_dodge_events: supplemental_data.post_wall_dodge_events,
787 flip_reset_followup_dodge_events: supplemental_data.flip_reset_followup_dodge_events,
788 player_stat_events: processor.player_stat_events,
789 goal_events: processor.goal_events,
790 frame_data: self.get_frame_data(),
791 })
792 }
793
794 /// Processes a replay and returns complete replay data.
795 ///
796 /// This method processes the entire replay using a [`ReplayProcessor`] and
797 /// extracts all available information including frame-by-frame data, metadata,
798 /// and special events like demolitions.
799 ///
800 /// # Arguments
801 ///
802 /// * `replay` - The parsed replay data from the [`boxcars`] library
803 ///
804 /// # Returns
805 ///
806 /// Returns a [`SubtrActorResult`] containing the complete [`ReplayData`] structure
807 /// with all extracted information.
808 ///
809 /// # Errors
810 ///
811 /// Returns a [`SubtrActorError`] if:
812 /// - The replay processor cannot be created
813 /// - Frame processing fails
814 /// - Replay metadata cannot be extracted
815 ///
816 /// # Example
817 ///
818 /// ```rust
819 /// use subtr_actor::collector::replay_data::ReplayDataCollector;
820 /// use boxcars::ParserBuilder;
821 ///
822 /// let data = std::fs::read("assets/replays/new_boost_format.replay").unwrap();
823 /// let replay = ParserBuilder::new(&data).parse().unwrap();
824 ///
825 /// let collector = ReplayDataCollector::new();
826 /// let replay_data = collector.get_replay_data(&replay).unwrap();
827 ///
828 /// println!("Processed {} frames", replay_data.frame_data.frame_count());
829 /// ```
830 pub fn get_replay_data(mut self, replay: &boxcars::Replay) -> SubtrActorResult<ReplayData> {
831 let mut processor = ReplayProcessor::new(replay)?;
832 let mut flip_reset_tracker = FlipResetTracker::new();
833 let mut boost_pad_collector = ReducerCollector::new(BoostReducer::new());
834 processor.process_all(&mut [
835 &mut self,
836 &mut flip_reset_tracker,
837 &mut boost_pad_collector,
838 ])?;
839 let supplemental_data =
840 ReplayDataSupplementalData::from_flip_reset_tracker(flip_reset_tracker)
841 .with_boost_pads(boost_pad_collector.into_inner().resolved_boost_pads());
842 self.into_replay_data_with_supplemental_data(processor, supplemental_data)
843 }
844
845 /// Extracts player frame data for all players at the specified time.
846 ///
847 /// This method iterates through all players in the replay and extracts their
848 /// state information at the given time, returning a vector of player frames
849 /// indexed by player ID.
850 ///
851 /// # Arguments
852 ///
853 /// * `processor` - The [`ReplayProcessor`] containing the replay data
854 /// * `current_time` - The time in seconds at which to extract player states
855 ///
856 /// # Returns
857 ///
858 /// Returns a [`SubtrActorResult`] containing a vector of tuples with player IDs
859 /// and their corresponding [`PlayerFrame`] data.
860 ///
861 /// # Errors
862 ///
863 /// Returns a [`SubtrActorError`] if player frame data cannot be extracted.
864 fn get_player_frames(
865 &self,
866 processor: &ReplayProcessor,
867 current_time: f32,
868 ) -> SubtrActorResult<Vec<(PlayerId, PlayerFrame)>> {
869 Ok(processor
870 .iter_player_ids_in_order()
871 .map(|player_id| {
872 (
873 player_id.clone(),
874 PlayerFrame::new_from_processor(processor, player_id, current_time)
875 .unwrap_or(PlayerFrame::Empty),
876 )
877 })
878 .collect())
879 }
880}
881
882impl Collector for ReplayDataCollector {
883 /// Processes a single frame of the replay and extracts all relevant data.
884 ///
885 /// This method is called by the [`ReplayProcessor`] for each frame in the replay.
886 /// It extracts metadata, ball state, and player state information and adds them
887 /// to the internal frame data structure.
888 ///
889 /// # Arguments
890 ///
891 /// * `processor` - The [`ReplayProcessor`] containing the replay data and context
892 /// * `_frame` - The current frame data (unused in this implementation)
893 /// * `_frame_number` - The current frame number (unused in this implementation)
894 /// * `current_time` - The current time in seconds since the start of the replay
895 ///
896 /// # Returns
897 ///
898 /// Returns a [`SubtrActorResult`] containing [`TimeAdvance::NextFrame`] to
899 /// indicate that processing should continue to the next frame.
900 ///
901 /// # Errors
902 ///
903 /// Returns a [`SubtrActorError`] if:
904 /// - Metadata frame cannot be created
905 /// - Player frame data cannot be extracted
906 /// - Frame data cannot be added to the collection
907 fn process_frame(
908 &mut self,
909 processor: &ReplayProcessor,
910 _frame: &boxcars::Frame,
911 _frame_number: usize,
912 current_time: f32,
913 ) -> SubtrActorResult<TimeAdvance> {
914 let metadata_frame = MetadataFrame::new_from_processor(processor, current_time)?;
915 let ball_frame = BallFrame::new_from_processor(processor, current_time);
916 let player_frames = self.get_player_frames(processor, current_time)?;
917 self.frame_data
918 .add_frame(metadata_frame, ball_frame, player_frames)?;
919 Ok(TimeAdvance::NextFrame)
920 }
921}