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