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