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