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