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,no_run
23//! use subtr_actor::collector::replay_data::ReplayDataCollector;
24//! use boxcars::ParserBuilder;
25//!
26//! let data = std::fs::read("replay.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#[derive(Debug, Clone, PartialEq, Serialize)]
395pub struct MetadataFrame {
396 /// The current time in seconds since the start of the replay
397 pub time: f32,
398 /// The number of seconds remaining in the current game period
399 pub seconds_remaining: i32,
400}
401
402impl MetadataFrame {
403 /// Creates a new [`MetadataFrame`] from a [`ReplayProcessor`] at the specified time.
404 ///
405 /// # Arguments
406 ///
407 /// * `processor` - The [`ReplayProcessor`] containing the replay data
408 /// * `time` - The current time in seconds since the start of the replay
409 ///
410 /// # Returns
411 ///
412 /// Returns a [`SubtrActorResult`] containing a [`MetadataFrame`] with the
413 /// current time and remaining seconds extracted from the processor.
414 ///
415 /// # Errors
416 ///
417 /// Returns a [`SubtrActorError`] if the seconds remaining cannot be retrieved
418 /// from the processor.
419 fn new_from_processor(processor: &ReplayProcessor, time: f32) -> SubtrActorResult<Self> {
420 Ok(Self::new(time, processor.get_seconds_remaining()?))
421 }
422
423 /// Creates a new [`MetadataFrame`] with the specified time and seconds remaining.
424 ///
425 /// # Arguments
426 ///
427 /// * `time` - The current time in seconds since the start of the replay
428 /// * `seconds_remaining` - The number of seconds remaining in the current game period
429 ///
430 /// # Returns
431 ///
432 /// Returns a new [`MetadataFrame`] with the provided values.
433 fn new(time: f32, seconds_remaining: i32) -> Self {
434 MetadataFrame {
435 time,
436 seconds_remaining,
437 }
438 }
439}
440
441/// Contains all frame-by-frame data for a Rocket League replay.
442///
443/// This structure organizes ball data, player data, and metadata for each
444/// frame of the replay, providing a complete picture of the game state
445/// throughout the match.
446///
447/// # Fields
448///
449/// * `ball_data` - All ball state information across all frames
450/// * `players` - Player data for each player, indexed by [`PlayerId`]
451/// * `metadata_frames` - Game metadata for each frame including timing information
452#[derive(Debug, Clone, PartialEq, Serialize)]
453pub struct FrameData {
454 /// All ball state information across all frames
455 pub ball_data: BallData,
456 /// Player data for each player, indexed by PlayerId
457 pub players: Vec<(PlayerId, PlayerData)>,
458 /// Game metadata for each frame including timing information
459 pub metadata_frames: Vec<MetadataFrame>,
460}
461
462/// Complete replay data structure containing all extracted information from a Rocket League replay.
463///
464/// This is the top-level structure that contains all processed replay data including
465/// frame-by-frame information, replay metadata, and special events like demolitions.
466///
467/// # Fields
468///
469/// * `frame_data` - All frame-by-frame data including ball, player, and metadata information
470/// * `meta` - Replay metadata including player information, game settings, and statistics
471/// * `demolish_infos` - Information about all demolition events that occurred during the replay
472///
473/// # Example
474///
475/// ```rust,no_run
476/// use subtr_actor::collector::replay_data::ReplayDataCollector;
477/// use boxcars::ParserBuilder;
478///
479/// let data = std::fs::read("replay.replay").unwrap();
480/// let replay = ParserBuilder::new(&data).parse().unwrap();
481/// let collector = ReplayDataCollector::new();
482/// let replay_data = collector.get_replay_data(&replay).unwrap();
483///
484/// // Access replay metadata
485/// println!("Team 0 players: {}", replay_data.meta.team_zero.len());
486///
487/// // Access frame data
488/// println!("Total frames: {}", replay_data.frame_data.metadata_frames.len());
489///
490/// // Access demolition events
491/// println!("Total demolitions: {}", replay_data.demolish_infos.len());
492/// ```
493#[derive(Debug, Clone, PartialEq, Serialize)]
494pub struct ReplayData {
495 /// All frame-by-frame data including ball, player, and metadata information
496 pub frame_data: FrameData,
497 /// Replay metadata including player information, game settings, and statistics
498 pub meta: ReplayMeta,
499 /// Information about all demolition events that occurred during the replay
500 pub demolish_infos: Vec<DemolishInfo>,
501}
502
503impl ReplayData {
504 /// Serializes the replay data to a JSON string.
505 ///
506 /// # Returns
507 ///
508 /// Returns a [`Result`] containing either the JSON string representation
509 /// of the replay data or a [`serde_json::Error`] if serialization fails.
510 ///
511 /// # Example
512 ///
513 /// ```rust,no_run
514 /// use subtr_actor::collector::replay_data::ReplayDataCollector;
515 /// use boxcars::ParserBuilder;
516 ///
517 /// let data = std::fs::read("replay.replay").unwrap();
518 /// let replay = ParserBuilder::new(&data).parse().unwrap();
519 /// let collector = ReplayDataCollector::new();
520 /// let replay_data = collector.get_replay_data(&replay).unwrap();
521 ///
522 /// let json_string = replay_data.as_json().unwrap();
523 /// println!("Replay as JSON: {}", json_string);
524 /// ```
525 pub fn as_json(&self) -> Result<String, serde_json::Error> {
526 serde_json::to_string(self)
527 }
528
529 /// Serializes the replay data to a pretty-printed JSON string.
530 ///
531 /// # Returns
532 ///
533 /// Returns a [`Result`] containing either the pretty-printed JSON string
534 /// representation of the replay data or a [`serde_json::Error`] if serialization fails.
535 pub fn as_pretty_json(&self) -> Result<String, serde_json::Error> {
536 serde_json::to_string_pretty(self)
537 }
538}
539
540impl FrameData {
541 /// Creates a new empty [`FrameData`] instance.
542 ///
543 /// # Returns
544 ///
545 /// Returns a new [`FrameData`] with empty ball data, player data, and metadata frames.
546 fn new() -> Self {
547 FrameData {
548 ball_data: BallData::new(),
549 players: Vec::new(),
550 metadata_frames: Vec::new(),
551 }
552 }
553
554 /// Returns the total number of frames in this frame data.
555 ///
556 /// # Returns
557 ///
558 /// Returns the number of metadata frames, which represents the total frame count.
559 pub fn frame_count(&self) -> usize {
560 self.metadata_frames.len()
561 }
562
563 /// Returns the duration of the replay in seconds.
564 ///
565 /// # Returns
566 ///
567 /// Returns the time of the last frame, or 0.0 if no frames exist.
568 pub fn duration(&self) -> f32 {
569 self.metadata_frames.last().map(|f| f.time).unwrap_or(0.0)
570 }
571
572 /// Adds a complete frame of data to the frame data structure.
573 ///
574 /// This method adds metadata, ball data, and player data for a single frame
575 /// to their respective collections, maintaining frame synchronization across
576 /// all data types.
577 ///
578 /// # Arguments
579 ///
580 /// * `frame_metadata` - The metadata for this frame (time, game state, etc.)
581 /// * `ball_frame` - The ball state for this frame
582 /// * `player_frames` - Player state data for all players in this frame
583 ///
584 /// # Returns
585 ///
586 /// Returns a [`SubtrActorResult`] indicating success or failure of the operation.
587 ///
588 /// # Errors
589 ///
590 /// May return a [`SubtrActorError`] if frame data cannot be processed correctly.
591 fn add_frame(
592 &mut self,
593 frame_metadata: MetadataFrame,
594 ball_frame: BallFrame,
595 player_frames: Vec<(PlayerId, PlayerFrame)>,
596 ) -> SubtrActorResult<()> {
597 let frame_index = self.metadata_frames.len();
598 self.metadata_frames.push(frame_metadata);
599 self.ball_data.add_frame(frame_index, ball_frame);
600 for (player_id, frame) in player_frames {
601 self.players
602 .get_entry(player_id)
603 .or_insert_with(PlayerData::new)
604 .add_frame(frame_index, frame)
605 }
606 Ok(())
607 }
608}
609
610/// A collector that extracts comprehensive frame-by-frame data from Rocket League replays.
611///
612/// [`ReplayDataCollector`] implements the [`Collector`] trait to process replay frames
613/// and extract detailed information about ball movement, player actions, and game state.
614/// It builds a complete [`ReplayData`] structure containing all available information
615/// from the replay.
616///
617/// # Usage
618///
619/// The collector is designed to be used with the [`ReplayProcessor`] to extract
620/// comprehensive replay data:
621///
622/// ```rust,no_run
623/// use subtr_actor::collector::replay_data::ReplayDataCollector;
624/// use boxcars::ParserBuilder;
625///
626/// let data = std::fs::read("replay.replay").unwrap();
627/// let replay = ParserBuilder::new(&data).parse().unwrap();
628///
629/// let collector = ReplayDataCollector::new();
630/// let replay_data = collector.get_replay_data(&replay).unwrap();
631///
632/// // Process the extracted data
633/// for (frame_idx, metadata) in replay_data.frame_data.metadata_frames.iter().enumerate() {
634/// println!("Frame {}: Time={:.2}s, Remaining={}s",
635/// frame_idx, metadata.time, metadata.seconds_remaining);
636/// }
637/// ```
638///
639/// # Fields
640///
641/// * `frame_data` - Internal storage for frame-by-frame data during collection
642pub struct ReplayDataCollector {
643 /// Internal storage for frame-by-frame data during collection
644 frame_data: FrameData,
645}
646
647impl Default for ReplayDataCollector {
648 /// Creates a default [`ReplayDataCollector`] instance.
649 ///
650 /// This is equivalent to calling [`ReplayDataCollector::new()`].
651 fn default() -> Self {
652 Self::new()
653 }
654}
655
656impl ReplayDataCollector {
657 /// Creates a new [`ReplayDataCollector`] instance.
658 ///
659 /// # Returns
660 ///
661 /// Returns a new collector ready to process replay frames.
662 pub fn new() -> Self {
663 ReplayDataCollector {
664 frame_data: FrameData::new(),
665 }
666 }
667
668 /// Consumes the collector and returns the collected frame data.
669 ///
670 /// # Returns
671 ///
672 /// Returns the [`FrameData`] containing all processed frame information.
673 pub fn get_frame_data(self) -> FrameData {
674 self.frame_data
675 }
676
677 /// Processes a replay and returns complete replay data.
678 ///
679 /// This method processes the entire replay using a [`ReplayProcessor`] and
680 /// extracts all available information including frame-by-frame data, metadata,
681 /// and special events like demolitions.
682 ///
683 /// # Arguments
684 ///
685 /// * `replay` - The parsed replay data from the [`boxcars`] library
686 ///
687 /// # Returns
688 ///
689 /// Returns a [`SubtrActorResult`] containing the complete [`ReplayData`] structure
690 /// with all extracted information.
691 ///
692 /// # Errors
693 ///
694 /// Returns a [`SubtrActorError`] if:
695 /// - The replay processor cannot be created
696 /// - Frame processing fails
697 /// - Replay metadata cannot be extracted
698 ///
699 /// # Example
700 ///
701 /// ```rust,no_run
702 /// use subtr_actor::collector::replay_data::ReplayDataCollector;
703 /// use boxcars::ParserBuilder;
704 ///
705 /// let data = std::fs::read("replay.replay").unwrap();
706 /// let replay = ParserBuilder::new(&data).parse().unwrap();
707 ///
708 /// let collector = ReplayDataCollector::new();
709 /// let replay_data = collector.get_replay_data(&replay).unwrap();
710 ///
711 /// println!("Processed {} frames", replay_data.frame_data.frame_count());
712 /// ```
713 pub fn get_replay_data(mut self, replay: &boxcars::Replay) -> SubtrActorResult<ReplayData> {
714 let mut processor = ReplayProcessor::new(replay)?;
715 processor.process(&mut self)?;
716 let meta = processor.get_replay_meta()?;
717 Ok(ReplayData {
718 meta,
719 demolish_infos: processor.demolishes,
720 frame_data: self.get_frame_data(),
721 })
722 }
723
724 /// Extracts player frame data for all players at the specified time.
725 ///
726 /// This method iterates through all players in the replay and extracts their
727 /// state information at the given time, returning a vector of player frames
728 /// indexed by player ID.
729 ///
730 /// # Arguments
731 ///
732 /// * `processor` - The [`ReplayProcessor`] containing the replay data
733 /// * `current_time` - The time in seconds at which to extract player states
734 ///
735 /// # Returns
736 ///
737 /// Returns a [`SubtrActorResult`] containing a vector of tuples with player IDs
738 /// and their corresponding [`PlayerFrame`] data.
739 ///
740 /// # Errors
741 ///
742 /// Returns a [`SubtrActorError`] if player frame data cannot be extracted.
743 fn get_player_frames(
744 &self,
745 processor: &ReplayProcessor,
746 current_time: f32,
747 ) -> SubtrActorResult<Vec<(PlayerId, PlayerFrame)>> {
748 Ok(processor
749 .iter_player_ids_in_order()
750 .map(|player_id| {
751 (
752 player_id.clone(),
753 PlayerFrame::new_from_processor(processor, player_id, current_time)
754 .unwrap_or(PlayerFrame::Empty),
755 )
756 })
757 .collect())
758 }
759}
760
761impl Collector for ReplayDataCollector {
762 /// Processes a single frame of the replay and extracts all relevant data.
763 ///
764 /// This method is called by the [`ReplayProcessor`] for each frame in the replay.
765 /// It extracts metadata, ball state, and player state information and adds them
766 /// to the internal frame data structure.
767 ///
768 /// # Arguments
769 ///
770 /// * `processor` - The [`ReplayProcessor`] containing the replay data and context
771 /// * `_frame` - The current frame data (unused in this implementation)
772 /// * `_frame_number` - The current frame number (unused in this implementation)
773 /// * `current_time` - The current time in seconds since the start of the replay
774 ///
775 /// # Returns
776 ///
777 /// Returns a [`SubtrActorResult`] containing [`TimeAdvance::NextFrame`] to
778 /// indicate that processing should continue to the next frame.
779 ///
780 /// # Errors
781 ///
782 /// Returns a [`SubtrActorError`] if:
783 /// - Metadata frame cannot be created
784 /// - Player frame data cannot be extracted
785 /// - Frame data cannot be added to the collection
786 fn process_frame(
787 &mut self,
788 processor: &ReplayProcessor,
789 _frame: &boxcars::Frame,
790 _frame_number: usize,
791 current_time: f32,
792 ) -> SubtrActorResult<TimeAdvance> {
793 let metadata_frame = MetadataFrame::new_from_processor(processor, current_time)?;
794 let ball_frame = BallFrame::new_from_processor(processor, current_time);
795 let player_frames = self.get_player_frames(processor, current_time)?;
796 self.frame_data
797 .add_frame(metadata_frame, ball_frame, player_frames)?;
798 Ok(TimeAdvance::NextFrame)
799 }
800}