Skip to main content

subtr_actor/collector/
ndarray.rs

1use crate::*;
2use ::ndarray;
3use boxcars;
4pub use derive_new;
5pub use paste;
6use serde::Serialize;
7use std::sync::Arc;
8
9/// Represents the column headers in the collected data of an [`NDArrayCollector`].
10///
11/// # Fields
12///
13/// * `global_headers`: A list of strings that represent the global,
14///   player-independent features' column headers.
15/// * `player_headers`: A list of strings that represent the player-specific
16///   features' column headers.
17///
18/// Use [`Self::new`] to construct an instance of this struct.
19#[derive(Debug, Clone, PartialEq, Serialize)]
20pub struct NDArrayColumnHeaders {
21    pub global_headers: Vec<String>,
22    pub player_headers: Vec<String>,
23}
24
25impl NDArrayColumnHeaders {
26    pub fn new(global_headers: Vec<String>, player_headers: Vec<String>) -> Self {
27        Self {
28            global_headers,
29            player_headers,
30        }
31    }
32}
33
34/// A struct that contains both the metadata of a replay and the associated
35/// column headers.
36///
37/// # Fields
38///
39/// * `replay_meta`: Contains metadata about a [`boxcars::Replay`].
40/// * `column_headers`: The [`NDArrayColumnHeaders`] associated with the data
41///   collected from the replay.
42#[derive(Debug, Clone, PartialEq, Serialize)]
43pub struct ReplayMetaWithHeaders {
44    pub replay_meta: ReplayMeta,
45    pub column_headers: NDArrayColumnHeaders,
46}
47
48impl ReplayMetaWithHeaders {
49    pub fn headers_vec(&self) -> Vec<String> {
50        self.headers_vec_from(|_, _info, index| format!("Player {index} - "))
51    }
52
53    pub fn headers_vec_from<F>(&self, player_prefix_getter: F) -> Vec<String>
54    where
55        F: Fn(&Self, &PlayerInfo, usize) -> String,
56    {
57        self.column_headers
58            .global_headers
59            .iter()
60            .cloned()
61            .chain(self.replay_meta.player_order().enumerate().flat_map(
62                move |(player_index, info)| {
63                    let player_prefix = player_prefix_getter(self, info, player_index);
64                    self.column_headers
65                        .player_headers
66                        .iter()
67                        .map(move |header| format!("{player_prefix}{header}"))
68                },
69            ))
70            .collect()
71    }
72}
73
74/// [`NDArrayCollector`] is a [`Collector`] which transforms frame-based replay
75/// data into a 2-dimensional array of type [`ndarray::Array2`], where each
76/// element is of a specified floating point type.
77///
78/// It's initialized with collections of [`FeatureAdder`] instances which
79/// extract global, player independent features for each frame, and
80/// [`PlayerFeatureAdder`], which add player specific features for each frame.
81///
82/// It's main entrypoint is [`Self::get_meta_and_ndarray`], which provides
83/// [`ndarray::Array2`] along with column headers and replay metadata.
84pub struct NDArrayCollector<F> {
85    feature_adders: FeatureAdders<F>,
86    player_feature_adders: PlayerFeatureAdders<F>,
87    data: Vec<F>,
88    replay_meta: Option<ReplayMeta>,
89    frames_added: usize,
90}
91
92impl<F> NDArrayCollector<F> {
93    /// Creates a new instance of `NDArrayCollector`.
94    ///
95    /// # Arguments
96    ///
97    /// * `feature_adders` - A vector of [`Arc<dyn FeatureAdder<F>>`], each
98    ///   implementing the [`FeatureAdder`] trait. These are used to add global
99    ///   features to the replay data.
100    ///
101    /// * `player_feature_adders` - A vector of [`Arc<dyn PlayerFeatureAdder<F>>`],
102    ///   each implementing the [`PlayerFeatureAdder`]
103    ///   trait. These are used to add player-specific features to the replay
104    ///   data.
105    ///
106    /// # Returns
107    ///
108    /// A new [`NDArrayCollector`] instance. This instance is initialized with
109    /// empty data, no replay metadata and zero frames added.
110    pub fn new(
111        feature_adders: FeatureAdders<F>,
112        player_feature_adders: PlayerFeatureAdders<F>,
113    ) -> Self {
114        Self {
115            feature_adders,
116            player_feature_adders,
117            data: Vec::new(),
118            replay_meta: None,
119            frames_added: 0,
120        }
121    }
122
123    /// Returns the column headers of the 2-dimensional array produced by the
124    /// [`NDArrayCollector`].
125    ///
126    /// # Returns
127    ///
128    /// An instance of [`NDArrayColumnHeaders`] representing the column headers
129    /// in the collected data.
130    pub fn get_column_headers(&self) -> NDArrayColumnHeaders {
131        let global_headers = self
132            .feature_adders
133            .iter()
134            .flat_map(move |fa| {
135                fa.get_column_headers()
136                    .iter()
137                    .map(move |column_name| column_name.to_string())
138            })
139            .collect();
140        let player_headers = self
141            .player_feature_adders
142            .iter()
143            .flat_map(move |pfa| {
144                pfa.get_column_headers()
145                    .iter()
146                    .map(move |base_name| base_name.to_string())
147            })
148            .collect();
149        NDArrayColumnHeaders::new(global_headers, player_headers)
150    }
151
152    /// This function consumes the [`NDArrayCollector`] instance and returns the
153    /// data collected as an [`ndarray::Array2`].
154    ///
155    /// # Returns
156    ///
157    /// A [`SubtrActorResult`] containing the collected data as an
158    /// [`ndarray::Array2`].
159    ///
160    /// This method is a shorthand for calling [`Self::get_meta_and_ndarray`]
161    /// and discarding the replay metadata and headers.
162    pub fn get_ndarray(self) -> SubtrActorResult<ndarray::Array2<F>> {
163        self.get_meta_and_ndarray().map(|a| a.1)
164    }
165
166    /// Consumes the [`NDArrayCollector`] and returns the collected features as a
167    /// 2D ndarray, along with replay metadata and headers.
168    ///
169    /// # Returns
170    ///
171    /// A [`SubtrActorResult`] containing a tuple:
172    /// - [`ReplayMetaWithHeaders`]: The replay metadata along with the headers
173    ///   for each column in the ndarray.
174    /// - [`ndarray::Array2<F>`]: The collected features as a 2D ndarray.
175    pub fn get_meta_and_ndarray(
176        self,
177    ) -> SubtrActorResult<(ReplayMetaWithHeaders, ndarray::Array2<F>)> {
178        let features_per_row = self.try_get_frame_feature_count()?;
179        let expected_length = features_per_row * self.frames_added;
180        assert!(self.data.len() == expected_length);
181        let column_headers = self.get_column_headers();
182        Ok((
183            ReplayMetaWithHeaders {
184                replay_meta: self.replay_meta.ok_or(SubtrActorError::new(
185                    SubtrActorErrorVariant::CouldNotBuildReplayMeta,
186                ))?,
187                column_headers,
188            },
189            ndarray::Array2::from_shape_vec((self.frames_added, features_per_row), self.data)
190                .map_err(SubtrActorErrorVariant::NDArrayShapeError)
191                .map_err(SubtrActorError::new)?,
192        ))
193    }
194
195    /// Processes a [`boxcars::Replay`] and returns its metadata along with column headers.
196    ///
197    /// This method first processes the replay using a [`ReplayProcessor`]. It
198    /// then updates the `replay_meta` field if it's not already set, and
199    /// returns a clone of the `replay_meta` field along with column headers of
200    /// the data.
201    ///
202    /// # Arguments
203    ///
204    /// * `replay`: A reference to the [`boxcars::Replay`] to process.
205    ///
206    /// # Returns
207    ///
208    /// A [`SubtrActorResult`] containing a [`ReplayMetaWithHeaders`] that
209    /// includes the metadata of the replay and column headers.
210    pub fn process_and_get_meta_and_headers(
211        &mut self,
212        replay: &boxcars::Replay,
213    ) -> SubtrActorResult<ReplayMetaWithHeaders> {
214        let mut processor = ReplayProcessor::new(replay)?;
215        processor.process_long_enough_to_get_actor_ids()?;
216        self.maybe_set_replay_meta(&processor)?;
217        Ok(ReplayMetaWithHeaders {
218            replay_meta: self
219                .replay_meta
220                .as_ref()
221                .ok_or(SubtrActorError::new(
222                    SubtrActorErrorVariant::CouldNotBuildReplayMeta,
223                ))?
224                .clone(),
225            column_headers: self.get_column_headers(),
226        })
227    }
228
229    fn try_get_frame_feature_count(&self) -> SubtrActorResult<usize> {
230        let player_count = self
231            .replay_meta
232            .as_ref()
233            .ok_or(SubtrActorError::new(
234                SubtrActorErrorVariant::CouldNotBuildReplayMeta,
235            ))?
236            .player_count();
237        let global_feature_count: usize = self
238            .feature_adders
239            .iter()
240            .map(|fa| fa.features_added())
241            .sum();
242        let player_feature_count: usize = self
243            .player_feature_adders
244            .iter() // iterate
245            .map(|pfa| pfa.features_added() * player_count)
246            .sum();
247        Ok(global_feature_count + player_feature_count)
248    }
249
250    fn maybe_set_replay_meta(&mut self, processor: &ReplayProcessor) -> SubtrActorResult<()> {
251        if self.replay_meta.is_none() {
252            self.replay_meta = Some(processor.get_replay_meta()?);
253        }
254        Ok(())
255    }
256}
257
258impl<F> Collector for NDArrayCollector<F> {
259    fn process_frame(
260        &mut self,
261        processor: &ReplayProcessor,
262        frame: &boxcars::Frame,
263        frame_number: usize,
264        current_time: f32,
265    ) -> SubtrActorResult<collector::TimeAdvance> {
266        self.maybe_set_replay_meta(processor)?;
267
268        for feature_adder in self.feature_adders.iter() {
269            feature_adder.add_features(
270                processor,
271                frame,
272                frame_number,
273                current_time,
274                &mut self.data,
275            )?;
276        }
277
278        for player_id in processor.iter_player_ids_in_order() {
279            for player_feature_adder in self.player_feature_adders.iter() {
280                player_feature_adder.add_features(
281                    player_id,
282                    processor,
283                    frame,
284                    frame_number,
285                    current_time,
286                    &mut self.data,
287                )?;
288            }
289        }
290
291        self.frames_added += 1;
292
293        Ok(collector::TimeAdvance::NextFrame)
294    }
295}
296
297fn global_feature_adder_from_name<F>(
298    name: &str,
299) -> Option<Arc<dyn FeatureAdder<F> + Send + Sync + 'static>>
300where
301    F: TryFrom<f32> + Send + Sync + 'static,
302    <F as TryFrom<f32>>::Error: std::fmt::Debug,
303{
304    match name {
305        "BallRigidBody" => Some(BallRigidBody::<F>::arc_new()),
306        "BallRigidBodyNoVelocities" => Some(BallRigidBodyNoVelocities::<F>::arc_new()),
307        "BallRigidBodyQuaternions" => Some(BallRigidBodyQuaternions::<F>::arc_new()),
308        "VelocityAddedBallRigidBodyNoVelocities" => {
309            Some(VelocityAddedBallRigidBodyNoVelocities::<F>::arc_new())
310        }
311        "InterpolatedBallRigidBodyNoVelocities" => {
312            Some(InterpolatedBallRigidBodyNoVelocities::<F>::arc_new(0.0))
313        }
314        "SecondsRemaining" => Some(SecondsRemaining::<F>::arc_new()),
315        "CurrentTime" => Some(CurrentTime::<F>::arc_new()),
316        "FrameTime" => Some(FrameTime::<F>::arc_new()),
317        "ReplicatedStateName" => Some(ReplicatedStateName::<F>::arc_new()),
318        "ReplicatedGameStateTimeRemaining" => {
319            Some(ReplicatedGameStateTimeRemaining::<F>::arc_new())
320        }
321        "BallHasBeenHit" => Some(BallHasBeenHit::<F>::arc_new()),
322        _ => None,
323    }
324}
325
326fn player_feature_adder_from_name<F>(
327    name: &str,
328) -> Option<Arc<dyn PlayerFeatureAdder<F> + Send + Sync + 'static>>
329where
330    F: TryFrom<f32> + Send + Sync + 'static,
331    <F as TryFrom<f32>>::Error: std::fmt::Debug,
332{
333    match name {
334        "PlayerRigidBody" => Some(PlayerRigidBody::<F>::arc_new()),
335        "PlayerRigidBodyNoVelocities" => Some(PlayerRigidBodyNoVelocities::<F>::arc_new()),
336        "PlayerRigidBodyQuaternions" => Some(PlayerRigidBodyQuaternions::<F>::arc_new()),
337        "VelocityAddedPlayerRigidBodyNoVelocities" => {
338            Some(VelocityAddedPlayerRigidBodyNoVelocities::<F>::arc_new())
339        }
340        "InterpolatedPlayerRigidBodyNoVelocities" => {
341            Some(InterpolatedPlayerRigidBodyNoVelocities::<F>::arc_new(0.003))
342        }
343        "PlayerBoost" => Some(PlayerBoost::<F>::arc_new()),
344        "PlayerJump" => Some(PlayerJump::<F>::arc_new()),
345        "PlayerAnyJump" => Some(PlayerAnyJump::<F>::arc_new()),
346        "PlayerDemolishedBy" => Some(PlayerDemolishedBy::<F>::arc_new()),
347        _ => None,
348    }
349}
350
351impl<F> NDArrayCollector<F>
352where
353    F: TryFrom<f32> + Send + Sync + 'static,
354    <F as TryFrom<f32>>::Error: std::fmt::Debug,
355{
356    /// Builds an [`NDArrayCollector`] from feature-adder names for an explicit
357    /// output type `F`.
358    pub fn from_strings_typed(fa_names: &[&str], pfa_names: &[&str]) -> SubtrActorResult<Self> {
359        let feature_adders: Vec<Arc<dyn FeatureAdder<F> + Send + Sync>> = fa_names
360            .iter()
361            .map(|name| {
362                global_feature_adder_from_name(name).ok_or_else(|| {
363                    SubtrActorError::new(SubtrActorErrorVariant::UnknownFeatureAdderName(
364                        name.to_string(),
365                    ))
366                })
367            })
368            .collect::<SubtrActorResult<Vec<_>>>()?;
369        let player_feature_adders: Vec<Arc<dyn PlayerFeatureAdder<F> + Send + Sync>> = pfa_names
370            .iter()
371            .map(|name| {
372                player_feature_adder_from_name(name).ok_or_else(|| {
373                    SubtrActorError::new(SubtrActorErrorVariant::UnknownFeatureAdderName(
374                        name.to_string(),
375                    ))
376                })
377            })
378            .collect::<SubtrActorResult<Vec<_>>>()?;
379        Ok(Self::new(feature_adders, player_feature_adders))
380    }
381}
382
383impl NDArrayCollector<f32> {
384    /// Backward-compatible shorthand for string-based collector construction
385    /// using `f32` output features.
386    pub fn from_strings(fa_names: &[&str], pfa_names: &[&str]) -> SubtrActorResult<Self> {
387        Self::from_strings_typed(fa_names, pfa_names)
388    }
389}
390
391impl<F: TryFrom<f32> + Send + Sync + 'static> Default for NDArrayCollector<F>
392where
393    <F as TryFrom<f32>>::Error: std::fmt::Debug,
394{
395    fn default() -> Self {
396        NDArrayCollector::new(
397            vec![BallRigidBody::arc_new()],
398            vec![
399                PlayerRigidBody::arc_new(),
400                PlayerBoost::arc_new(),
401                PlayerAnyJump::arc_new(),
402            ],
403        )
404    }
405}
406
407/// This trait acts as an abstraction over a feature adder, and is primarily
408/// used to allow for heterogeneous collections of feature adders in the
409/// [`NDArrayCollector`]. While it provides methods for adding features and
410/// retrieving column headers, it is generally recommended to implement the
411/// [`LengthCheckedFeatureAdder`] trait instead, which provides compile-time
412/// guarantees about the number of features returned.
413pub trait FeatureAdder<F> {
414    fn features_added(&self) -> usize {
415        self.get_column_headers().len()
416    }
417
418    fn get_column_headers(&self) -> &[&str];
419
420    fn add_features(
421        &self,
422        processor: &ReplayProcessor,
423        frame: &boxcars::Frame,
424        frame_count: usize,
425        current_time: f32,
426        vector: &mut Vec<F>,
427    ) -> SubtrActorResult<()>;
428}
429
430pub type FeatureAdders<F> = Vec<Arc<dyn FeatureAdder<F> + Send + Sync>>;
431
432/// This trait is stricter version of the [`FeatureAdder`] trait, enforcing at
433/// compile time that the number of features added is equal to the number of
434/// column headers provided. Implementations of this trait can be automatically
435/// adapted to the [`FeatureAdder`] trait using the [`impl_feature_adder!`]
436/// macro.
437pub trait LengthCheckedFeatureAdder<F, const N: usize> {
438    fn get_column_headers_array(&self) -> &[&str; N];
439
440    fn get_features(
441        &self,
442        processor: &ReplayProcessor,
443        frame: &boxcars::Frame,
444        frame_count: usize,
445        current_time: f32,
446    ) -> SubtrActorResult<[F; N]>;
447}
448
449/// A macro to provide an automatic implementation of the [`FeatureAdder`] trait
450/// for types that implement [`LengthCheckedFeatureAdder`]. This allows you to
451/// take advantage of the compile-time guarantees provided by
452/// [`LengthCheckedFeatureAdder`], while still being able to use your type in
453/// contexts that require a [`FeatureAdder`] object. This macro is used to
454/// bridge the gap between the two traits, as Rust's type system does not
455/// currently provide a way to prove to the compiler that there will always be
456/// exactly one implementation of [`LengthCheckedFeatureAdder`] for each type.
457#[macro_export]
458macro_rules! impl_feature_adder {
459    ($struct_name:ident) => {
460        impl<F: TryFrom<f32>> FeatureAdder<F> for $struct_name<F>
461        where
462            <F as TryFrom<f32>>::Error: std::fmt::Debug,
463        {
464            fn add_features(
465                &self,
466                processor: &ReplayProcessor,
467                frame: &boxcars::Frame,
468                frame_count: usize,
469                current_time: f32,
470                vector: &mut Vec<F>,
471            ) -> SubtrActorResult<()> {
472                Ok(
473                    vector.extend(self.get_features(
474                        processor,
475                        frame,
476                        frame_count,
477                        current_time,
478                    )?),
479                )
480            }
481
482            fn get_column_headers(&self) -> &[&str] {
483                self.get_column_headers_array()
484            }
485        }
486    };
487}
488
489/// This trait acts as an abstraction over a player-specific feature adder, and
490/// is primarily used to allow for heterogeneous collections of player feature
491/// adders in the [`NDArrayCollector`]. While it provides methods for adding
492/// player-specific features and retrieving column headers, it is generally
493/// recommended to implement the [`LengthCheckedPlayerFeatureAdder`] trait
494/// instead, which provides compile-time guarantees about the number of features
495/// returned.
496pub trait PlayerFeatureAdder<F> {
497    fn features_added(&self) -> usize {
498        self.get_column_headers().len()
499    }
500
501    fn get_column_headers(&self) -> &[&str];
502
503    fn add_features(
504        &self,
505        player_id: &PlayerId,
506        processor: &ReplayProcessor,
507        frame: &boxcars::Frame,
508        frame_count: usize,
509        current_time: f32,
510        vector: &mut Vec<F>,
511    ) -> SubtrActorResult<()>;
512}
513
514pub type PlayerFeatureAdders<F> = Vec<Arc<dyn PlayerFeatureAdder<F> + Send + Sync>>;
515
516/// This trait is a more strict version of the [`PlayerFeatureAdder`] trait,
517/// enforcing at compile time that the number of player-specific features added
518/// is equal to the number of column headers provided. Implementations of this
519/// trait can be automatically adapted to the [`PlayerFeatureAdder`] trait using
520/// the [`impl_player_feature_adder!`] macro.
521pub trait LengthCheckedPlayerFeatureAdder<F, const N: usize> {
522    fn get_column_headers_array(&self) -> &[&str; N];
523
524    fn get_features(
525        &self,
526        player_id: &PlayerId,
527        processor: &ReplayProcessor,
528        frame: &boxcars::Frame,
529        frame_count: usize,
530        current_time: f32,
531    ) -> SubtrActorResult<[F; N]>;
532}
533
534/// A macro to provide an automatic implementation of the [`PlayerFeatureAdder`]
535/// trait for types that implement [`LengthCheckedPlayerFeatureAdder`]. This
536/// allows you to take advantage of the compile-time guarantees provided by
537/// [`LengthCheckedPlayerFeatureAdder`], while still being able to use your type
538/// in contexts that require a [`PlayerFeatureAdder`] object. This macro is used
539/// to bridge the gap between the two traits, as Rust's type system does not
540/// currently provide a way to prove to the compiler that there will always be
541/// exactly one implementation of [`LengthCheckedPlayerFeatureAdder`] for each
542/// type.
543#[macro_export]
544macro_rules! impl_player_feature_adder {
545    ($struct_name:ident) => {
546        impl<F: TryFrom<f32>> PlayerFeatureAdder<F> for $struct_name<F>
547        where
548            <F as TryFrom<f32>>::Error: std::fmt::Debug,
549        {
550            fn add_features(
551                &self,
552                player_id: &PlayerId,
553                processor: &ReplayProcessor,
554                frame: &boxcars::Frame,
555                frame_count: usize,
556                current_time: f32,
557                vector: &mut Vec<F>,
558            ) -> SubtrActorResult<()> {
559                Ok(vector.extend(self.get_features(
560                    player_id,
561                    processor,
562                    frame,
563                    frame_count,
564                    current_time,
565                )?))
566            }
567
568            fn get_column_headers(&self) -> &[&str] {
569                self.get_column_headers_array()
570            }
571        }
572    };
573}
574
575impl<G, F, const N: usize> FeatureAdder<F> for (G, &[&str; N])
576where
577    G: Fn(&ReplayProcessor, &boxcars::Frame, usize, f32) -> SubtrActorResult<[F; N]>,
578{
579    fn add_features(
580        &self,
581        processor: &ReplayProcessor,
582        frame: &boxcars::Frame,
583        frame_count: usize,
584        current_time: f32,
585        vector: &mut Vec<F>,
586    ) -> SubtrActorResult<()> {
587        vector.extend(self.0(processor, frame, frame_count, current_time)?);
588        Ok(())
589    }
590
591    fn get_column_headers(&self) -> &[&str] {
592        self.1.as_slice()
593    }
594}
595
596impl<G, F, const N: usize> PlayerFeatureAdder<F> for (G, &[&str; N])
597where
598    G: Fn(&PlayerId, &ReplayProcessor, &boxcars::Frame, usize, f32) -> SubtrActorResult<[F; N]>,
599{
600    fn add_features(
601        &self,
602        player_id: &PlayerId,
603        processor: &ReplayProcessor,
604        frame: &boxcars::Frame,
605        frame_count: usize,
606        current_time: f32,
607        vector: &mut Vec<F>,
608    ) -> SubtrActorResult<()> {
609        vector.extend(self.0(
610            player_id,
611            processor,
612            frame,
613            frame_count,
614            current_time,
615        )?);
616        Ok(())
617    }
618
619    fn get_column_headers(&self) -> &[&str] {
620        self.1.as_slice()
621    }
622}
623
624/// This macro creates a global [`FeatureAdder`] struct and implements the
625/// necessary traits to add the calculated features to the data matrix. The
626/// macro exports a struct with the same name as passed in the parameter. The
627/// number of column names and the length of the feature array returned by
628/// `$prop_getter` are checked at compile time to ensure they match, in line
629/// with the [`LengthCheckedFeatureAdder`] trait. The output struct also
630/// provides an implementation of the [`FeatureAdder`] trait via the
631/// [`impl_feature_adder!`] macro, allowing it to be used in contexts where a
632/// [`FeatureAdder`] object is required.
633///
634/// # Parameters
635///
636/// * `$struct_name`: The name of the struct to be created.
637/// * `$prop_getter`: The function or closure used to calculate the features.
638/// * `$( $column_names:expr ),*`: A comma-separated list of column names as strings.
639///
640/// # Example
641///
642/// ```
643/// use subtr_actor::*;
644///
645/// build_global_feature_adder!(
646///     SecondsRemainingExample,
647///     |_, processor: &ReplayProcessor, _frame, _index, _current_time| {
648///         convert_all_floats!(processor.get_seconds_remaining()?.clone() as f32)
649///     },
650///     "seconds remaining"
651/// );
652/// ```
653///
654/// This will create a struct named `SecondsRemaining` and implement necessary
655/// traits to calculate features using the provided closure. The feature will be
656/// added under the column name "seconds remaining". Note, however, that it is
657/// possible to add more than one feature with each feature adder
658#[macro_export]
659macro_rules! build_global_feature_adder {
660    ($struct_name:ident, $prop_getter:expr, $( $column_names:expr ),* $(,)?) => {
661
662        #[derive(derive_new::new)]
663        pub struct $struct_name<F> {
664            _zero: std::marker::PhantomData<F>,
665        }
666
667        impl<F: Sync + Send + TryFrom<f32> + 'static> $struct_name<F> where
668            <F as TryFrom<f32>>::Error: std::fmt::Debug,
669        {
670            pub fn arc_new() -> std::sync::Arc<dyn FeatureAdder<F> + Send + Sync + 'static> {
671                std::sync::Arc::new(Self::new())
672            }
673        }
674
675        global_feature_adder!(
676            $struct_name,
677            $prop_getter,
678            $( $column_names ),*
679        );
680    }
681}
682
683/// This macro is used to implement necessary traits for an existing struct to
684/// add the calculated features to the data matrix. This macro is particularly
685/// useful when the feature adder needs to be instantiated with specific
686/// parameters. The number of column names and the length of the feature array
687/// returned by `$prop_getter` are checked at compile time to ensure they match.
688///
689/// # Parameters
690///
691/// * `$struct_name`: The name of the existing struct.
692/// * `$prop_getter`: The function or closure used to calculate the features.
693/// * `$( $column_names:expr ),*`: A comma-separated list of column names as strings.
694#[macro_export]
695macro_rules! global_feature_adder {
696    ($struct_name:ident, $prop_getter:expr, $( $column_names:expr ),* $(,)?) => {
697        macro_rules! _global_feature_adder {
698            ($count:ident) => {
699                impl<F: TryFrom<f32>> LengthCheckedFeatureAdder<F, $count> for $struct_name<F>
700                where
701                    <F as TryFrom<f32>>::Error: std::fmt::Debug,
702                {
703                    fn get_column_headers_array(&self) -> &[&str; $count] {
704                        &[$( $column_names ),*]
705                    }
706
707                    fn get_features(
708                        &self,
709                        processor: &ReplayProcessor,
710                        frame: &boxcars::Frame,
711                        frame_count: usize,
712                        current_time: f32,
713                    ) -> SubtrActorResult<[F; $count]> {
714                        $prop_getter(self, processor, frame, frame_count, current_time)
715                    }
716                }
717
718                impl_feature_adder!($struct_name);
719            };
720        }
721        paste::paste! {
722            const [<$struct_name:snake:upper _LENGTH>]: usize = [$($column_names),*].len();
723            _global_feature_adder!([<$struct_name:snake:upper _LENGTH>]);
724        }
725    }
726}
727
728/// This macro creates a player feature adder struct and implements the
729/// necessary traits to add the calculated player-specific features to the data
730/// matrix. The macro exports a struct with the same name as passed in the
731/// parameter. The number of column names and the length of the feature array
732/// returned by `$prop_getter` are checked at compile time to ensure they match,
733/// in line with the [`LengthCheckedPlayerFeatureAdder`] trait. The output
734/// struct also provides an implementation of the [`PlayerFeatureAdder`] trait
735/// via the [`impl_player_feature_adder!`] macro, allowing it to be used in
736/// contexts where a [`PlayerFeatureAdder`] object is required.
737///
738/// # Parameters
739///
740/// * `$struct_name`: The name of the struct to be created.
741/// * `$prop_getter`: The function or closure used to calculate the features.
742/// * `$( $column_names:expr ),*`: A comma-separated list of column names as strings.
743///
744/// # Example
745///
746/// ```
747/// use subtr_actor::*;
748///
749/// fn u8_get_f32(v: u8) -> SubtrActorResult<f32> {
750///    v.try_into().map_err(convert_float_conversion_error)
751/// }
752///
753/// build_player_feature_adder!(
754///     PlayerJump,
755///     |_,
756///      player_id: &PlayerId,
757///      processor: &ReplayProcessor,
758///      _frame,
759///      _frame_number,
760///      _current_time: f32| {
761///         convert_all_floats!(
762///             processor
763///                 .get_dodge_active(player_id)
764///                 .and_then(u8_get_f32)
765///                 .unwrap_or(0.0),
766///             processor
767///                 .get_jump_active(player_id)
768///                 .and_then(u8_get_f32)
769///                 .unwrap_or(0.0),
770///             processor
771///                 .get_double_jump_active(player_id)
772///                 .and_then(u8_get_f32)
773///                 .unwrap_or(0.0),
774///         )
775///     },
776///     "dodge active",
777///     "jump active",
778///     "double jump active"
779/// );
780/// ```
781///
782/// This will create a struct named `PlayerJump` and implement necessary
783/// traits to calculate features using the provided closure. The player-specific
784/// features will be added under the column names "dodge active",
785/// "jump active", and "double jump active" respectively.
786#[macro_export]
787macro_rules! build_player_feature_adder {
788    ($struct_name:ident, $prop_getter:expr, $( $column_names:expr ),* $(,)?) => {
789        #[derive(derive_new::new)]
790        pub struct $struct_name<F> {
791            _zero: std::marker::PhantomData<F>,
792        }
793
794        impl<F: Sync + Send + TryFrom<f32> + 'static> $struct_name<F> where
795            <F as TryFrom<f32>>::Error: std::fmt::Debug,
796        {
797            pub fn arc_new() -> std::sync::Arc<dyn PlayerFeatureAdder<F> + Send + Sync + 'static> {
798                std::sync::Arc::new(Self::new())
799            }
800        }
801
802        player_feature_adder!(
803            $struct_name,
804            $prop_getter,
805            $( $column_names ),*
806        );
807    }
808}
809
810/// This macro is used to implement necessary traits for an existing struct to
811/// add the calculated player-specific features to the data matrix. This macro
812/// is particularly useful when the feature adder needs to be instantiated with
813/// specific parameters. The number of column names and the length of the
814/// feature array returned by `$prop_getter` are checked at compile time to
815/// ensure they match.
816///
817/// # Parameters
818///
819/// * `$struct_name`: The name of the existing struct.
820/// * `$prop_getter`: The function or closure used to calculate the features.
821/// * `$( $column_names:expr ),*`: A comma-separated list of column names as strings.
822#[macro_export]
823macro_rules! player_feature_adder {
824    ($struct_name:ident, $prop_getter:expr, $( $column_names:expr ),* $(,)?) => {
825        macro_rules! _player_feature_adder {
826            ($count:ident) => {
827                impl<F: TryFrom<f32>> LengthCheckedPlayerFeatureAdder<F, $count> for $struct_name<F>
828                where
829                    <F as TryFrom<f32>>::Error: std::fmt::Debug,
830                {
831                    fn get_column_headers_array(&self) -> &[&str; $count] {
832                        &[$( $column_names ),*]
833                    }
834
835                    fn get_features(
836                        &self,
837                        player_id: &PlayerId,
838                        processor: &ReplayProcessor,
839                        frame: &boxcars::Frame,
840                        frame_count: usize,
841                        current_time: f32,
842                    ) -> SubtrActorResult<[F; $count]> {
843                        $prop_getter(self, player_id, processor, frame, frame_count, current_time)
844                    }
845                }
846
847                impl_player_feature_adder!($struct_name);
848            };
849        }
850        paste::paste! {
851            const [<$struct_name:snake:upper _LENGTH>]: usize = [$($column_names),*].len();
852            _player_feature_adder!([<$struct_name:snake:upper _LENGTH>]);
853        }
854    }
855}
856
857/// Unconditionally convert any error into a [`SubtrActorError`] of with the
858/// [`SubtrActorErrorVariant::FloatConversionError`] variant.
859pub fn convert_float_conversion_error<T>(_: T) -> SubtrActorError {
860    SubtrActorError::new(SubtrActorErrorVariant::FloatConversionError)
861}
862
863/// A macro that tries to convert each provided item into a type. If any of the
864/// conversions fail, it short-circuits and returns the error.
865///
866/// The first argument `$err` is a closure that accepts an error and returns a
867/// [`SubtrActorResult`]. It is used to map any conversion errors into a
868/// [`SubtrActorResult`].
869///
870/// Subsequent arguments should be expressions that implement the [`TryInto`]
871/// trait, with the type they're being converted into being the one used in the
872/// `Ok` variant of the return value.
873#[macro_export]
874macro_rules! convert_all {
875    ($err:expr, $( $item:expr ),* $(,)?) => {{
876		Ok([
877			$( $item.try_into().map_err($err)? ),*
878		])
879	}};
880}
881
882/// A convenience macro that uses the [`convert_all`] macro with the
883/// [`convert_float_conversion_error`] function for error handling.
884///
885/// Each item provided is attempted to be converted into a floating point
886/// number. If any of the conversions fail, it short-circuits and returns the
887/// error. This macro must be used in the context of a function that returns a
888/// [`Result`] because it uses the ? operator. It is primarily useful for
889/// defining function like the one shown in the example below that are generic
890/// in some parameter that can implements [`TryFrom`].
891///
892/// # Example
893///
894/// ```
895/// use subtr_actor::*;
896///
897/// pub fn some_constant_function<F: TryFrom<f32>>(
898///     rigid_body: &boxcars::RigidBody,
899/// ) -> SubtrActorResult<[F; 3]> {
900///     convert_all_floats!(42.0, 0.0, 1.234)
901/// }
902/// ```
903#[macro_export]
904macro_rules! convert_all_floats {
905    ($( $item:expr ),* $(,)?) => {{
906        convert_all!(convert_float_conversion_error, $( $item ),*)
907    }};
908}
909
910fn or_zero_boxcars_3f() -> boxcars::Vector3f {
911    boxcars::Vector3f {
912        x: 0.0,
913        y: 0.0,
914        z: 0.0,
915    }
916}
917
918type RigidBodyArrayResult<F> = SubtrActorResult<[F; 12]>;
919
920/// Extracts the location, rotation, linear velocity and angular velocity from a
921/// [`boxcars::RigidBody`] and converts them to a type implementing [`TryFrom<f32>`].
922///
923/// If any of the components of the rigid body are not set (`None`), they are
924/// treated as zero.
925///
926/// The returned array contains twelve elements in the following order: x, y, z
927/// location, x, y, z rotation (as Euler angles), x, y, z linear velocity, x, y,
928/// z angular velocity.
929pub fn get_rigid_body_properties<F: TryFrom<f32>>(
930    rigid_body: &boxcars::RigidBody,
931) -> RigidBodyArrayResult<F>
932where
933    <F as TryFrom<f32>>::Error: std::fmt::Debug,
934{
935    let linear_velocity = rigid_body
936        .linear_velocity
937        .unwrap_or_else(or_zero_boxcars_3f);
938    let angular_velocity = rigid_body
939        .angular_velocity
940        .unwrap_or_else(or_zero_boxcars_3f);
941    let rotation = rigid_body.rotation;
942    let location = rigid_body.location;
943    let (rx, ry, rz) =
944        glam::quat(rotation.x, rotation.y, rotation.z, rotation.w).to_euler(glam::EulerRot::XYZ);
945    convert_all_floats!(
946        location.x,
947        location.y,
948        location.z,
949        rx,
950        ry,
951        rz,
952        linear_velocity.x,
953        linear_velocity.y,
954        linear_velocity.z,
955        angular_velocity.x,
956        angular_velocity.y,
957        angular_velocity.z,
958    )
959}
960
961/// Extracts the location and rotation from a [`boxcars::RigidBody`] and
962/// converts them to a type implementing [`TryFrom<f32>`].
963///
964/// If any of the components of the rigid body are not set (`None`), they are
965/// treated as zero.
966///
967/// The returned array contains seven elements in the following order: x, y, z
968/// location, x, y, z, w rotation.
969pub fn get_rigid_body_properties_no_velocities<F: TryFrom<f32>>(
970    rigid_body: &boxcars::RigidBody,
971) -> SubtrActorResult<[F; 7]>
972where
973    <F as TryFrom<f32>>::Error: std::fmt::Debug,
974{
975    let rotation = rigid_body.rotation;
976    let location = rigid_body.location;
977    convert_all_floats!(
978        location.x, location.y, location.z, rotation.x, rotation.y, rotation.z, rotation.w
979    )
980}
981
982fn default_rb_state<F: TryFrom<f32>>() -> RigidBodyArrayResult<F>
983where
984    <F as TryFrom<f32>>::Error: std::fmt::Debug,
985{
986    convert_all!(
987        convert_float_conversion_error,
988        // We use huge values for location instead of 0s so that hopefully any
989        // model built on this data can understand that the player is not
990        // actually on the field.
991        0.0,
992        0.0,
993        0.0,
994        0.0,
995        0.0,
996        0.0,
997        0.0,
998        0.0,
999        0.0,
1000        0.0,
1001        0.0,
1002        0.0,
1003    )
1004}
1005
1006fn default_rb_state_no_velocities<F: TryFrom<f32>>() -> SubtrActorResult<[F; 7]>
1007where
1008    <F as TryFrom<f32>>::Error: std::fmt::Debug,
1009{
1010    convert_all_floats!(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,)
1011}
1012
1013build_global_feature_adder!(
1014    SecondsRemaining,
1015    |_, processor: &ReplayProcessor, _frame, _index, _current_time| {
1016        convert_all_floats!(processor.get_seconds_remaining().unwrap_or(0) as f32)
1017    },
1018    "seconds remaining"
1019);
1020
1021build_global_feature_adder!(
1022    CurrentTime,
1023    |_, _processor, _frame, _index, current_time: f32| { convert_all_floats!(current_time) },
1024    "current time"
1025);
1026
1027// Game state indicator for kickoff detection and game phase tracking.
1028build_global_feature_adder!(
1029    ReplicatedStateName,
1030    |_, processor: &ReplayProcessor, _frame, _index, _current_time| {
1031        convert_all_floats!(processor.get_replicated_state_name().unwrap_or(0) as f32)
1032    },
1033    "game state"
1034);
1035
1036// Countdown timer for kickoff detection (3->0 during kickoff).
1037build_global_feature_adder!(
1038    ReplicatedGameStateTimeRemaining,
1039    |_, processor: &ReplayProcessor, _frame, _index, _current_time| {
1040        convert_all_floats!(processor
1041            .get_replicated_game_state_time_remaining()
1042            .unwrap_or(0) as f32)
1043    },
1044    "kickoff countdown"
1045);
1046
1047// Whether the ball has been hit in the current play.
1048build_global_feature_adder!(
1049    BallHasBeenHit,
1050    |_, processor: &ReplayProcessor, _frame, _index, _current_time| {
1051        convert_all_floats!(if processor.get_ball_has_been_hit().unwrap_or(false) {
1052            1.0
1053        } else {
1054            0.0
1055        })
1056    },
1057    "ball has been hit"
1058);
1059
1060build_global_feature_adder!(
1061    FrameTime,
1062    |_, _processor, frame: &boxcars::Frame, _index, _current_time| {
1063        convert_all_floats!(frame.time)
1064    },
1065    "frame time"
1066);
1067
1068build_global_feature_adder!(
1069    BallRigidBody,
1070    |_, processor: &ReplayProcessor, _frame, _index, _current_time| {
1071        processor
1072            .get_ball_rigid_body()
1073            .and_then(|rb| get_rigid_body_properties(rb))
1074            .or_else(|_| default_rb_state())
1075    },
1076    "Ball - position x",
1077    "Ball - position y",
1078    "Ball - position z",
1079    "Ball - rotation x",
1080    "Ball - rotation y",
1081    "Ball - rotation z",
1082    "Ball - linear velocity x",
1083    "Ball - linear velocity y",
1084    "Ball - linear velocity z",
1085    "Ball - angular velocity x",
1086    "Ball - angular velocity y",
1087    "Ball - angular velocity z",
1088);
1089
1090build_global_feature_adder!(
1091    BallRigidBodyNoVelocities,
1092    |_, processor: &ReplayProcessor, _frame, _index, _current_time| {
1093        processor
1094            .get_ball_rigid_body()
1095            .and_then(|rb| get_rigid_body_properties_no_velocities(rb))
1096            .or_else(|_| default_rb_state_no_velocities())
1097    },
1098    "Ball - position x",
1099    "Ball - position y",
1100    "Ball - position z",
1101    "Ball - rotation x",
1102    "Ball - rotation y",
1103    "Ball - rotation z",
1104    "Ball - rotation w",
1105);
1106
1107// XXX: This approach seems to give some unexpected results with rotation
1108// changes. There may be a unit mismatch or some other type of issue.
1109build_global_feature_adder!(
1110    VelocityAddedBallRigidBodyNoVelocities,
1111    |_, processor: &ReplayProcessor, _frame, _index, current_time: f32| {
1112        processor
1113            .get_velocity_applied_ball_rigid_body(current_time)
1114            .and_then(|rb| get_rigid_body_properties_no_velocities(&rb))
1115            .or_else(|_| default_rb_state_no_velocities())
1116    },
1117    "Ball - position x",
1118    "Ball - position y",
1119    "Ball - position z",
1120    "Ball - rotation x",
1121    "Ball - rotation y",
1122    "Ball - rotation z",
1123    "Ball - rotation w",
1124);
1125
1126#[derive(derive_new::new)]
1127pub struct InterpolatedBallRigidBodyNoVelocities<F> {
1128    close_enough_to_frame_time: f32,
1129    _zero: std::marker::PhantomData<F>,
1130}
1131
1132impl<F> InterpolatedBallRigidBodyNoVelocities<F> {
1133    pub fn arc_new(close_enough_to_frame_time: f32) -> Arc<Self> {
1134        Arc::new(Self::new(close_enough_to_frame_time))
1135    }
1136}
1137
1138global_feature_adder!(
1139    InterpolatedBallRigidBodyNoVelocities,
1140    |s: &InterpolatedBallRigidBodyNoVelocities<F>,
1141     processor: &ReplayProcessor,
1142     _frame: &boxcars::Frame,
1143     _index,
1144     current_time: f32| {
1145        processor
1146            .get_interpolated_ball_rigid_body(current_time, s.close_enough_to_frame_time)
1147            .map(|v| get_rigid_body_properties_no_velocities(&v))
1148            .unwrap_or_else(|_| default_rb_state_no_velocities())
1149    },
1150    "Ball - position x",
1151    "Ball - position y",
1152    "Ball - position z",
1153    "Ball - rotation x",
1154    "Ball - rotation y",
1155    "Ball - rotation z",
1156    "Ball - rotation w",
1157);
1158
1159build_player_feature_adder!(
1160    PlayerRigidBody,
1161    |_, player_id: &PlayerId, processor: &ReplayProcessor, _frame, _index, _current_time: f32| {
1162        if let Ok(rb) = processor.get_player_rigid_body(player_id) {
1163            get_rigid_body_properties(rb)
1164        } else {
1165            default_rb_state()
1166        }
1167    },
1168    "position x",
1169    "position y",
1170    "position z",
1171    "rotation x",
1172    "rotation y",
1173    "rotation z",
1174    "linear velocity x",
1175    "linear velocity y",
1176    "linear velocity z",
1177    "angular velocity x",
1178    "angular velocity y",
1179    "angular velocity z",
1180);
1181
1182build_player_feature_adder!(
1183    PlayerRigidBodyNoVelocities,
1184    |_, player_id: &PlayerId, processor: &ReplayProcessor, _frame, _index, _current_time: f32| {
1185        if let Ok(rb) = processor.get_player_rigid_body(player_id) {
1186            get_rigid_body_properties_no_velocities(rb)
1187        } else {
1188            default_rb_state_no_velocities()
1189        }
1190    },
1191    "position x",
1192    "position y",
1193    "position z",
1194    "rotation x",
1195    "rotation y",
1196    "rotation z",
1197    "rotation w"
1198);
1199
1200// XXX: This approach seems to give some unexpected results with rotation
1201// changes. There may be a unit mismatch or some other type of issue.
1202build_player_feature_adder!(
1203    VelocityAddedPlayerRigidBodyNoVelocities,
1204    |_, player_id: &PlayerId, processor: &ReplayProcessor, _frame, _index, current_time: f32| {
1205        if let Ok(rb) = processor.get_velocity_applied_player_rigid_body(player_id, current_time) {
1206            get_rigid_body_properties_no_velocities(&rb)
1207        } else {
1208            default_rb_state_no_velocities()
1209        }
1210    },
1211    "position x",
1212    "position y",
1213    "position z",
1214    "rotation x",
1215    "rotation y",
1216    "rotation z",
1217    "rotation w"
1218);
1219
1220#[derive(derive_new::new)]
1221pub struct InterpolatedPlayerRigidBodyNoVelocities<F> {
1222    close_enough_to_frame_time: f32,
1223    _zero: std::marker::PhantomData<F>,
1224}
1225
1226impl<F> InterpolatedPlayerRigidBodyNoVelocities<F> {
1227    pub fn arc_new(close_enough_to_frame_time: f32) -> Arc<Self> {
1228        Arc::new(Self::new(close_enough_to_frame_time))
1229    }
1230}
1231
1232player_feature_adder!(
1233    InterpolatedPlayerRigidBodyNoVelocities,
1234    |s: &InterpolatedPlayerRigidBodyNoVelocities<F>,
1235     player_id: &PlayerId,
1236     processor: &ReplayProcessor,
1237     _frame: &boxcars::Frame,
1238     _index,
1239     current_time: f32| {
1240        processor
1241            .get_interpolated_player_rigid_body(
1242                player_id,
1243                current_time,
1244                s.close_enough_to_frame_time,
1245            )
1246            .map(|v| get_rigid_body_properties_no_velocities(&v))
1247            .unwrap_or_else(|_| default_rb_state_no_velocities())
1248    },
1249    "i position x",
1250    "i position y",
1251    "i position z",
1252    "i rotation x",
1253    "i rotation y",
1254    "i rotation z",
1255    "i rotation w"
1256);
1257
1258build_player_feature_adder!(
1259    PlayerBoost,
1260    |_, player_id: &PlayerId, processor: &ReplayProcessor, _frame, _index, _current_time: f32| {
1261        convert_all_floats!(processor.get_player_boost_level(player_id).unwrap_or(0.0))
1262    },
1263    "boost level (raw replay units)"
1264);
1265
1266fn u8_get_f32(v: u8) -> SubtrActorResult<f32> {
1267    Ok(v.into())
1268}
1269
1270build_player_feature_adder!(
1271    PlayerJump,
1272    |_,
1273     player_id: &PlayerId,
1274     processor: &ReplayProcessor,
1275     _frame,
1276     _frame_number,
1277     _current_time: f32| {
1278        convert_all_floats!(
1279            processor
1280                .get_dodge_active(player_id)
1281                .and_then(u8_get_f32)
1282                .unwrap_or(0.0),
1283            processor
1284                .get_jump_active(player_id)
1285                .and_then(u8_get_f32)
1286                .unwrap_or(0.0),
1287            processor
1288                .get_double_jump_active(player_id)
1289                .and_then(u8_get_f32)
1290                .unwrap_or(0.0),
1291        )
1292    },
1293    "dodge active",
1294    "jump active",
1295    "double jump active"
1296);
1297
1298build_player_feature_adder!(
1299    PlayerAnyJump,
1300    |_,
1301     player_id: &PlayerId,
1302     processor: &ReplayProcessor,
1303     _frame,
1304     _frame_number,
1305     _current_time: f32| {
1306        let dodge_is_active = processor.get_dodge_active(player_id).unwrap_or(0) % 2;
1307        let jump_is_active = processor.get_jump_active(player_id).unwrap_or(0) % 2;
1308        let double_jump_is_active = processor.get_double_jump_active(player_id).unwrap_or(0) % 2;
1309        let value: f32 = [dodge_is_active, jump_is_active, double_jump_is_active]
1310            .into_iter()
1311            .enumerate()
1312            .map(|(index, is_active)| (1 << index) * is_active)
1313            .sum::<u8>() as f32;
1314        convert_all_floats!(value)
1315    },
1316    "any_jump_active"
1317);
1318
1319const DEMOLISH_APPEARANCE_FRAME_COUNT: usize = 30;
1320
1321build_player_feature_adder!(
1322    PlayerDemolishedBy,
1323    |_,
1324     player_id: &PlayerId,
1325     processor: &ReplayProcessor,
1326     _frame,
1327     frame_number,
1328     _current_time: f32| {
1329        let demolisher_index = processor
1330            .demolishes
1331            .iter()
1332            .find(|demolish_info| {
1333                &demolish_info.victim == player_id
1334                    && frame_number - demolish_info.frame < DEMOLISH_APPEARANCE_FRAME_COUNT
1335            })
1336            .map(|demolish_info| {
1337                processor
1338                    .iter_player_ids_in_order()
1339                    .position(|player_id| player_id == &demolish_info.attacker)
1340                    .unwrap_or_else(|| processor.iter_player_ids_in_order().count())
1341            })
1342            .and_then(|v| i32::try_from(v).ok())
1343            .unwrap_or(-1);
1344        convert_all_floats!(demolisher_index as f32)
1345    },
1346    "player demolished by"
1347);
1348
1349build_player_feature_adder!(
1350    PlayerRigidBodyQuaternions,
1351    |_, player_id: &PlayerId, processor: &ReplayProcessor, _frame, _index, _current_time: f32| {
1352        if let Ok(rb) = processor.get_player_rigid_body(player_id) {
1353            let rotation = rb.rotation;
1354            let location = rb.location;
1355            convert_all_floats!(
1356                location.x, location.y, location.z, rotation.x, rotation.y, rotation.z, rotation.w
1357            )
1358        } else {
1359            convert_all_floats!(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0)
1360        }
1361    },
1362    "position x",
1363    "position y",
1364    "position z",
1365    "quaternion x",
1366    "quaternion y",
1367    "quaternion z",
1368    "quaternion w"
1369);
1370
1371build_global_feature_adder!(
1372    BallRigidBodyQuaternions,
1373    |_, processor: &ReplayProcessor, _frame, _index, _current_time| {
1374        match processor.get_ball_rigid_body() {
1375            Ok(rb) => {
1376                let rotation = rb.rotation;
1377                let location = rb.location;
1378                convert_all_floats!(
1379                    location.x, location.y, location.z, rotation.x, rotation.y, rotation.z,
1380                    rotation.w
1381                )
1382            }
1383            Err(_) => convert_all_floats!(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0),
1384        }
1385    },
1386    "Ball - position x",
1387    "Ball - position y",
1388    "Ball - position z",
1389    "Ball - quaternion x",
1390    "Ball - quaternion y",
1391    "Ball - quaternion z",
1392    "Ball - quaternion w"
1393);