Skip to main content

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