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