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