Skip to main content

subtr_actor/collector/ndarray/
builtins.rs

1use crate::*;
2use boxcars;
3use std::sync::Arc;
4
5fn or_zero_boxcars_3f() -> boxcars::Vector3f {
6    boxcars::Vector3f {
7        x: 0.0,
8        y: 0.0,
9        z: 0.0,
10    }
11}
12
13type RigidBodyArrayResult<F> = SubtrActorResult<[F; 12]>;
14type RigidBodyQuaternionArrayResult<F> = SubtrActorResult<[F; 13]>;
15type RigidBodyBasisArrayResult<F> = SubtrActorResult<[F; 15]>;
16
17/// Converts a rigid body into position, Euler rotation, and velocity features.
18pub fn get_rigid_body_properties<F: TryFrom<f32>>(
19    rigid_body: &boxcars::RigidBody,
20) -> RigidBodyArrayResult<F>
21where
22    <F as TryFrom<f32>>::Error: std::fmt::Debug,
23{
24    let linear_velocity = rigid_body
25        .linear_velocity
26        .unwrap_or_else(or_zero_boxcars_3f);
27    let angular_velocity = rigid_body
28        .angular_velocity
29        .unwrap_or_else(or_zero_boxcars_3f);
30    let rotation = rigid_body.rotation;
31    let location = rigid_body.location;
32    let (rx, ry, rz) =
33        glam::quat(rotation.x, rotation.y, rotation.z, rotation.w).to_euler(glam::EulerRot::XYZ);
34    convert_all_floats!(
35        location.x,
36        location.y,
37        location.z,
38        rx,
39        ry,
40        rz,
41        linear_velocity.x,
42        linear_velocity.y,
43        linear_velocity.z,
44        angular_velocity.x,
45        angular_velocity.y,
46        angular_velocity.z,
47    )
48}
49
50/// Converts a rigid body into position, quaternion rotation, and velocity features.
51pub fn get_rigid_body_properties_quaternion<F: TryFrom<f32>>(
52    rigid_body: &boxcars::RigidBody,
53) -> RigidBodyQuaternionArrayResult<F>
54where
55    <F as TryFrom<f32>>::Error: std::fmt::Debug,
56{
57    let linear_velocity = rigid_body
58        .linear_velocity
59        .unwrap_or_else(or_zero_boxcars_3f);
60    let angular_velocity = rigid_body
61        .angular_velocity
62        .unwrap_or_else(or_zero_boxcars_3f);
63    let rotation = rigid_body.rotation;
64    let location = rigid_body.location;
65    convert_all_floats!(
66        location.x,
67        location.y,
68        location.z,
69        rotation.x,
70        rotation.y,
71        rotation.z,
72        rotation.w,
73        linear_velocity.x,
74        linear_velocity.y,
75        linear_velocity.z,
76        angular_velocity.x,
77        angular_velocity.y,
78        angular_velocity.z,
79    )
80}
81
82/// Converts a rigid body into position, basis vectors, and velocity features.
83pub fn get_rigid_body_properties_basis<F: TryFrom<f32>>(
84    rigid_body: &boxcars::RigidBody,
85) -> RigidBodyBasisArrayResult<F>
86where
87    <F as TryFrom<f32>>::Error: std::fmt::Debug,
88{
89    let linear_velocity = rigid_body
90        .linear_velocity
91        .unwrap_or_else(or_zero_boxcars_3f);
92    let angular_velocity = rigid_body
93        .angular_velocity
94        .unwrap_or_else(or_zero_boxcars_3f);
95    let rotation = rigid_body.rotation;
96    let location = rigid_body.location;
97    let quat = glam::quat(rotation.x, rotation.y, rotation.z, rotation.w);
98    let forward = quat.mul_vec3(glam::Vec3::X);
99    let up = quat.mul_vec3(glam::Vec3::Z);
100    convert_all_floats!(
101        location.x,
102        location.y,
103        location.z,
104        forward.x,
105        forward.y,
106        forward.z,
107        up.x,
108        up.y,
109        up.z,
110        linear_velocity.x,
111        linear_velocity.y,
112        linear_velocity.z,
113        angular_velocity.x,
114        angular_velocity.y,
115        angular_velocity.z,
116    )
117}
118
119/// Converts a rigid body into position and quaternion-rotation features only.
120pub fn get_rigid_body_properties_no_velocities<F: TryFrom<f32>>(
121    rigid_body: &boxcars::RigidBody,
122) -> SubtrActorResult<[F; 7]>
123where
124    <F as TryFrom<f32>>::Error: std::fmt::Debug,
125{
126    let rotation = rigid_body.rotation;
127    let location = rigid_body.location;
128    convert_all_floats!(
129        location.x, location.y, location.z, rotation.x, rotation.y, rotation.z, rotation.w
130    )
131}
132
133fn default_rb_state<F: TryFrom<f32>>() -> RigidBodyArrayResult<F>
134where
135    <F as TryFrom<f32>>::Error: std::fmt::Debug,
136{
137    convert_all!(
138        convert_float_conversion_error,
139        0.0,
140        0.0,
141        0.0,
142        0.0,
143        0.0,
144        0.0,
145        0.0,
146        0.0,
147        0.0,
148        0.0,
149        0.0,
150        0.0,
151    )
152}
153
154fn default_rb_state_quaternion<F: TryFrom<f32>>() -> RigidBodyQuaternionArrayResult<F>
155where
156    <F as TryFrom<f32>>::Error: std::fmt::Debug,
157{
158    convert_all_floats!(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,)
159}
160
161fn default_rb_state_basis<F: TryFrom<f32>>() -> RigidBodyBasisArrayResult<F>
162where
163    <F as TryFrom<f32>>::Error: std::fmt::Debug,
164{
165    convert_all_floats!(0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,)
166}
167
168fn default_rb_state_no_velocities<F: TryFrom<f32>>() -> SubtrActorResult<[F; 7]>
169where
170    <F as TryFrom<f32>>::Error: std::fmt::Debug,
171{
172    convert_all_floats!(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,)
173}
174
175build_global_feature_adder!(
176    SecondsRemaining,
177    |_, processor: &ReplayProcessor, _frame, _index, _current_time| {
178        convert_all_floats!(processor.get_seconds_remaining().unwrap_or(0) as f32)
179    },
180    "seconds remaining"
181);
182
183build_global_feature_adder!(
184    CurrentTime,
185    |_, _processor, _frame, _index, current_time: f32| { convert_all_floats!(current_time) },
186    "current time"
187);
188
189build_global_feature_adder!(
190    ReplicatedStateName,
191    |_, processor: &ReplayProcessor, _frame, _index, _current_time| {
192        convert_all_floats!(processor.get_replicated_state_name().unwrap_or(0) as f32)
193    },
194    "game state"
195);
196
197build_global_feature_adder!(
198    ReplicatedGameStateTimeRemaining,
199    |_, processor: &ReplayProcessor, _frame, _index, _current_time| {
200        convert_all_floats!(processor
201            .get_replicated_game_state_time_remaining()
202            .unwrap_or(0) as f32)
203    },
204    "kickoff countdown"
205);
206
207build_global_feature_adder!(
208    BallHasBeenHit,
209    |_, processor: &ReplayProcessor, _frame, _index, _current_time| {
210        convert_all_floats!(if processor.get_ball_has_been_hit().unwrap_or(false) {
211            1.0
212        } else {
213            0.0
214        })
215    },
216    "ball has been hit"
217);
218
219build_global_feature_adder!(
220    FrameTime,
221    |_, _processor, frame: &boxcars::Frame, _index, _current_time| {
222        convert_all_floats!(frame.time)
223    },
224    "frame time"
225);
226
227build_global_feature_adder!(
228    BallRigidBody,
229    |_, processor: &ReplayProcessor, _frame, _index, _current_time| {
230        processor
231            .get_normalized_ball_rigid_body()
232            .and_then(|rb| get_rigid_body_properties(&rb))
233            .or_else(|_| default_rb_state())
234    },
235    "Ball - position x",
236    "Ball - position y",
237    "Ball - position z",
238    "Ball - rotation x",
239    "Ball - rotation y",
240    "Ball - rotation z",
241    "Ball - linear velocity x",
242    "Ball - linear velocity y",
243    "Ball - linear velocity z",
244    "Ball - angular velocity x",
245    "Ball - angular velocity y",
246    "Ball - angular velocity z",
247);
248
249build_global_feature_adder!(
250    BallRigidBodyNoVelocities,
251    |_, processor: &ReplayProcessor, _frame, _index, _current_time| {
252        processor
253            .get_normalized_ball_rigid_body()
254            .and_then(|rb| get_rigid_body_properties_no_velocities(&rb))
255            .or_else(|_| default_rb_state_no_velocities())
256    },
257    "Ball - position x",
258    "Ball - position y",
259    "Ball - position z",
260    "Ball - rotation x",
261    "Ball - rotation y",
262    "Ball - rotation z",
263    "Ball - rotation w",
264);
265
266build_global_feature_adder!(
267    VelocityAddedBallRigidBodyNoVelocities,
268    |_, processor: &ReplayProcessor, _frame, _index, current_time: f32| {
269        processor
270            .get_velocity_applied_ball_rigid_body(current_time)
271            .and_then(|rb| get_rigid_body_properties_no_velocities(&rb))
272            .or_else(|_| default_rb_state_no_velocities())
273    },
274    "Ball - position x",
275    "Ball - position y",
276    "Ball - position z",
277    "Ball - rotation x",
278    "Ball - rotation y",
279    "Ball - rotation z",
280    "Ball - rotation w",
281);
282
283/// Global feature adder that samples an interpolated ball rigid body.
284#[derive(derive_new::new)]
285pub struct InterpolatedBallRigidBodyNoVelocities<F> {
286    close_enough_to_frame_time: f32,
287    _zero: std::marker::PhantomData<F>,
288}
289
290impl<F> InterpolatedBallRigidBodyNoVelocities<F> {
291    /// Creates the feature adder with the tolerated snap-to-frame threshold.
292    pub fn arc_new(close_enough_to_frame_time: f32) -> Arc<Self> {
293        Arc::new(Self::new(close_enough_to_frame_time))
294    }
295}
296
297global_feature_adder!(
298    InterpolatedBallRigidBodyNoVelocities,
299    |s: &InterpolatedBallRigidBodyNoVelocities<F>,
300     processor: &ReplayProcessor,
301     _frame: &boxcars::Frame,
302     _index,
303     current_time: f32| {
304        processor
305            .get_interpolated_ball_rigid_body(current_time, s.close_enough_to_frame_time)
306            .map(|v| get_rigid_body_properties_no_velocities(&v))
307            .unwrap_or_else(|_| default_rb_state_no_velocities())
308    },
309    "Ball - position x",
310    "Ball - position y",
311    "Ball - position z",
312    "Ball - rotation x",
313    "Ball - rotation y",
314    "Ball - rotation z",
315    "Ball - rotation w",
316);
317
318build_player_feature_adder!(
319    PlayerRigidBody,
320    |_, player_id: &PlayerId, processor: &ReplayProcessor, _frame, _index, _current_time: f32| {
321        if let Ok(rb) = processor.get_normalized_player_rigid_body(player_id) {
322            get_rigid_body_properties(&rb)
323        } else {
324            default_rb_state()
325        }
326    },
327    "position x",
328    "position y",
329    "position z",
330    "rotation x",
331    "rotation y",
332    "rotation z",
333    "linear velocity x",
334    "linear velocity y",
335    "linear velocity z",
336    "angular velocity x",
337    "angular velocity y",
338    "angular velocity z",
339);
340
341build_player_feature_adder!(
342    PlayerRigidBodyNoVelocities,
343    |_, player_id: &PlayerId, processor: &ReplayProcessor, _frame, _index, _current_time: f32| {
344        if let Ok(rb) = processor.get_normalized_player_rigid_body(player_id) {
345            get_rigid_body_properties_no_velocities(&rb)
346        } else {
347            default_rb_state_no_velocities()
348        }
349    },
350    "position x",
351    "position y",
352    "position z",
353    "rotation x",
354    "rotation y",
355    "rotation z",
356    "rotation w"
357);
358
359build_player_feature_adder!(
360    VelocityAddedPlayerRigidBodyNoVelocities,
361    |_, player_id: &PlayerId, processor: &ReplayProcessor, _frame, _index, current_time: f32| {
362        if let Ok(rb) = processor.get_velocity_applied_player_rigid_body(player_id, current_time) {
363            get_rigid_body_properties_no_velocities(&rb)
364        } else {
365            default_rb_state_no_velocities()
366        }
367    },
368    "position x",
369    "position y",
370    "position z",
371    "rotation x",
372    "rotation y",
373    "rotation z",
374    "rotation w"
375);
376
377/// Per-player feature adder that samples an interpolated car rigid body.
378#[derive(derive_new::new)]
379pub struct InterpolatedPlayerRigidBodyNoVelocities<F> {
380    close_enough_to_frame_time: f32,
381    _zero: std::marker::PhantomData<F>,
382}
383
384impl<F> InterpolatedPlayerRigidBodyNoVelocities<F> {
385    /// Creates the feature adder with the tolerated snap-to-frame threshold.
386    pub fn arc_new(close_enough_to_frame_time: f32) -> Arc<Self> {
387        Arc::new(Self::new(close_enough_to_frame_time))
388    }
389}
390
391player_feature_adder!(
392    InterpolatedPlayerRigidBodyNoVelocities,
393    |s: &InterpolatedPlayerRigidBodyNoVelocities<F>,
394     player_id: &PlayerId,
395     processor: &ReplayProcessor,
396     _frame: &boxcars::Frame,
397     _index,
398     current_time: f32| {
399        processor
400            .get_interpolated_player_rigid_body(
401                player_id,
402                current_time,
403                s.close_enough_to_frame_time,
404            )
405            .map(|v| get_rigid_body_properties_no_velocities(&v))
406            .unwrap_or_else(|_| default_rb_state_no_velocities())
407    },
408    "i position x",
409    "i position y",
410    "i position z",
411    "i rotation x",
412    "i rotation y",
413    "i rotation z",
414    "i rotation w"
415);
416
417build_player_feature_adder!(
418    PlayerRelativeBallPosition,
419    |_, player_id: &PlayerId, processor: &ReplayProcessor, _frame, _index, _current_time: f32| {
420        let relative_position = processor
421            .get_normalized_player_rigid_body(player_id)
422            .ok()
423            .zip(processor.get_normalized_ball_rigid_body().ok())
424            .map(|(player_rigid_body, ball_rigid_body)| {
425                vec_to_glam(&ball_rigid_body.location) - vec_to_glam(&player_rigid_body.location)
426            })
427            .unwrap_or(glam::f32::Vec3::ZERO);
428        convert_all_floats!(
429            relative_position.x,
430            relative_position.y,
431            relative_position.z
432        )
433    },
434    "relative ball position x",
435    "relative ball position y",
436    "relative ball position z"
437);
438
439build_player_feature_adder!(
440    PlayerRelativeBallVelocity,
441    |_, player_id: &PlayerId, processor: &ReplayProcessor, _frame, _index, _current_time: f32| {
442        let relative_velocity = processor
443            .get_normalized_player_rigid_body(player_id)
444            .ok()
445            .zip(processor.get_normalized_ball_rigid_body().ok())
446            .map(|(player_rigid_body, ball_rigid_body)| {
447                vec_to_glam(
448                    &ball_rigid_body
449                        .linear_velocity
450                        .unwrap_or_else(or_zero_boxcars_3f),
451                ) - vec_to_glam(
452                    &player_rigid_body
453                        .linear_velocity
454                        .unwrap_or_else(or_zero_boxcars_3f),
455                )
456            })
457            .unwrap_or(glam::f32::Vec3::ZERO);
458        convert_all_floats!(
459            relative_velocity.x,
460            relative_velocity.y,
461            relative_velocity.z
462        )
463    },
464    "relative ball velocity x",
465    "relative ball velocity y",
466    "relative ball velocity z"
467);
468
469build_player_feature_adder!(
470    PlayerLocalRelativeBallPosition,
471    |_, player_id: &PlayerId, processor: &ReplayProcessor, _frame, _index, _current_time: f32| {
472        let local_relative_position = processor
473            .get_normalized_player_rigid_body(player_id)
474            .ok()
475            .zip(processor.get_normalized_ball_rigid_body().ok())
476            .map(|(player_rigid_body, ball_rigid_body)| {
477                let player_rotation = player_rigid_body.rotation;
478                let player_quat = glam::quat(
479                    player_rotation.x,
480                    player_rotation.y,
481                    player_rotation.z,
482                    player_rotation.w,
483                );
484                let world_relative_position = vec_to_glam(&ball_rigid_body.location)
485                    - vec_to_glam(&player_rigid_body.location);
486                player_quat.inverse().mul_vec3(world_relative_position)
487            })
488            .unwrap_or(glam::f32::Vec3::ZERO);
489        convert_all_floats!(
490            local_relative_position.x,
491            local_relative_position.y,
492            local_relative_position.z
493        )
494    },
495    "local relative ball position x",
496    "local relative ball position y",
497    "local relative ball position z"
498);
499
500build_player_feature_adder!(
501    PlayerLocalRelativeBallVelocity,
502    |_, player_id: &PlayerId, processor: &ReplayProcessor, _frame, _index, _current_time: f32| {
503        let local_relative_velocity = processor
504            .get_normalized_player_rigid_body(player_id)
505            .ok()
506            .zip(processor.get_normalized_ball_rigid_body().ok())
507            .map(|(player_rigid_body, ball_rigid_body)| {
508                let player_rotation = player_rigid_body.rotation;
509                let player_quat = glam::quat(
510                    player_rotation.x,
511                    player_rotation.y,
512                    player_rotation.z,
513                    player_rotation.w,
514                );
515                let world_relative_velocity = vec_to_glam(
516                    &ball_rigid_body
517                        .linear_velocity
518                        .unwrap_or_else(or_zero_boxcars_3f),
519                ) - vec_to_glam(
520                    &player_rigid_body
521                        .linear_velocity
522                        .unwrap_or_else(or_zero_boxcars_3f),
523                );
524                player_quat.inverse().mul_vec3(world_relative_velocity)
525            })
526            .unwrap_or(glam::f32::Vec3::ZERO);
527        convert_all_floats!(
528            local_relative_velocity.x,
529            local_relative_velocity.y,
530            local_relative_velocity.z
531        )
532    },
533    "local relative ball velocity x",
534    "local relative ball velocity y",
535    "local relative ball velocity z"
536);
537
538build_player_feature_adder!(
539    PlayerBallDistance,
540    |_, player_id: &PlayerId, processor: &ReplayProcessor, _frame, _index, _current_time: f32| {
541        let distance = processor
542            .get_normalized_player_rigid_body(player_id)
543            .ok()
544            .zip(processor.get_normalized_ball_rigid_body().ok())
545            .map(|(player_rigid_body, ball_rigid_body)| {
546                (vec_to_glam(&player_rigid_body.location) - vec_to_glam(&ball_rigid_body.location))
547                    .length()
548            })
549            .unwrap_or(0.0);
550        convert_all_floats!(distance)
551    },
552    "distance to ball"
553);
554
555build_player_feature_adder!(
556    PlayerBoost,
557    |_, player_id: &PlayerId, processor: &ReplayProcessor, _frame, _index, _current_time: f32| {
558        convert_all_floats!(processor.get_player_boost_level(player_id).unwrap_or(0.0))
559    },
560    "boost level (raw replay units)"
561);
562
563fn u8_get_f32(v: u8) -> SubtrActorResult<f32> {
564    Ok(v.into())
565}
566
567build_player_feature_adder!(
568    PlayerJump,
569    |_,
570     player_id: &PlayerId,
571     processor: &ReplayProcessor,
572     _frame,
573     _frame_number,
574     _current_time: f32| {
575        convert_all_floats!(
576            processor
577                .get_dodge_active(player_id)
578                .and_then(u8_get_f32)
579                .unwrap_or(0.0),
580            processor
581                .get_jump_active(player_id)
582                .and_then(u8_get_f32)
583                .unwrap_or(0.0),
584            processor
585                .get_double_jump_active(player_id)
586                .and_then(u8_get_f32)
587                .unwrap_or(0.0),
588        )
589    },
590    "dodge active",
591    "jump active",
592    "double jump active"
593);
594
595build_player_feature_adder!(
596    PlayerAnyJump,
597    |_,
598     player_id: &PlayerId,
599     processor: &ReplayProcessor,
600     _frame,
601     _frame_number,
602     _current_time: f32| {
603        let dodge_is_active = processor.get_dodge_active(player_id).unwrap_or(0) % 2;
604        let jump_is_active = processor.get_jump_active(player_id).unwrap_or(0) % 2;
605        let double_jump_is_active = processor.get_double_jump_active(player_id).unwrap_or(0) % 2;
606        let value: f32 = [dodge_is_active, jump_is_active, double_jump_is_active]
607            .into_iter()
608            .enumerate()
609            .map(|(index, is_active)| (1 << index) * is_active)
610            .sum::<u8>() as f32;
611        convert_all_floats!(value)
612    },
613    "any_jump_active"
614);
615
616build_player_feature_adder!(
617    PlayerDodgeRefreshed,
618    |_,
619     player_id: &PlayerId,
620     processor: &ReplayProcessor,
621     _frame,
622     _frame_number,
623     _current_time: f32| {
624        let dodge_refresh_count = processor
625            .current_frame_dodge_refreshed_events()
626            .iter()
627            .filter(|event| &event.player == player_id)
628            .count() as f32;
629        convert_all_floats!(dodge_refresh_count)
630    },
631    "dodge refresh count"
632);
633
634const DEMOLISH_APPEARANCE_FRAME_COUNT: usize = 30;
635
636build_player_feature_adder!(
637    PlayerDemolishedBy,
638    |_,
639     player_id: &PlayerId,
640     processor: &ReplayProcessor,
641     _frame,
642     frame_number,
643     _current_time: f32| {
644        let demolisher_index = processor
645            .demolishes
646            .iter()
647            .find(|demolish_info| {
648                &demolish_info.victim == player_id
649                    && frame_number - demolish_info.frame < DEMOLISH_APPEARANCE_FRAME_COUNT
650            })
651            .map(|demolish_info| {
652                processor
653                    .iter_player_ids_in_order()
654                    .position(|player_id| player_id == &demolish_info.attacker)
655                    .unwrap_or_else(|| processor.iter_player_ids_in_order().count())
656            })
657            .and_then(|v| i32::try_from(v).ok())
658            .unwrap_or(-1);
659        convert_all_floats!(demolisher_index as f32)
660    },
661    "player demolished by"
662);
663
664build_player_feature_adder!(
665    PlayerRigidBodyQuaternions,
666    |_, player_id: &PlayerId, processor: &ReplayProcessor, _frame, _index, _current_time: f32| {
667        if let Ok(rb) = processor.get_normalized_player_rigid_body(player_id) {
668            let rotation = rb.rotation;
669            let location = rb.location;
670            convert_all_floats!(
671                location.x, location.y, location.z, rotation.x, rotation.y, rotation.z, rotation.w
672            )
673        } else {
674            convert_all_floats!(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0)
675        }
676    },
677    "position x",
678    "position y",
679    "position z",
680    "quaternion x",
681    "quaternion y",
682    "quaternion z",
683    "quaternion w"
684);
685
686build_player_feature_adder!(
687    PlayerRigidBodyQuaternionVelocities,
688    |_, player_id: &PlayerId, processor: &ReplayProcessor, _frame, _index, _current_time: f32| {
689        if let Ok(rb) = processor.get_normalized_player_rigid_body(player_id) {
690            get_rigid_body_properties_quaternion(&rb)
691        } else {
692            default_rb_state_quaternion()
693        }
694    },
695    "position x",
696    "position y",
697    "position z",
698    "quaternion x",
699    "quaternion y",
700    "quaternion z",
701    "quaternion w",
702    "linear velocity x",
703    "linear velocity y",
704    "linear velocity z",
705    "angular velocity x",
706    "angular velocity y",
707    "angular velocity z",
708);
709
710build_player_feature_adder!(
711    PlayerRigidBodyBasis,
712    |_, player_id: &PlayerId, processor: &ReplayProcessor, _frame, _index, _current_time: f32| {
713        if let Ok(rb) = processor.get_normalized_player_rigid_body(player_id) {
714            get_rigid_body_properties_basis(&rb)
715        } else {
716            default_rb_state_basis()
717        }
718    },
719    "position x",
720    "position y",
721    "position z",
722    "forward x",
723    "forward y",
724    "forward z",
725    "up x",
726    "up y",
727    "up z",
728    "linear velocity x",
729    "linear velocity y",
730    "linear velocity z",
731    "angular velocity x",
732    "angular velocity y",
733    "angular velocity z",
734);
735
736build_global_feature_adder!(
737    BallRigidBodyQuaternions,
738    |_, processor: &ReplayProcessor, _frame, _index, _current_time| {
739        match processor.get_normalized_ball_rigid_body() {
740            Ok(rb) => {
741                let rotation = rb.rotation;
742                let location = rb.location;
743                convert_all_floats!(
744                    location.x, location.y, location.z, rotation.x, rotation.y, rotation.z,
745                    rotation.w
746                )
747            }
748            Err(_) => convert_all_floats!(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0),
749        }
750    },
751    "Ball - position x",
752    "Ball - position y",
753    "Ball - position z",
754    "Ball - quaternion x",
755    "Ball - quaternion y",
756    "Ball - quaternion z",
757    "Ball - quaternion w"
758);
759
760build_global_feature_adder!(
761    BallRigidBodyQuaternionVelocities,
762    |_, processor: &ReplayProcessor, _frame, _index, _current_time| {
763        processor
764            .get_normalized_ball_rigid_body()
765            .and_then(|rb| get_rigid_body_properties_quaternion(&rb))
766            .or_else(|_| default_rb_state_quaternion())
767    },
768    "Ball - position x",
769    "Ball - position y",
770    "Ball - position z",
771    "Ball - quaternion x",
772    "Ball - quaternion y",
773    "Ball - quaternion z",
774    "Ball - quaternion w",
775    "Ball - linear velocity x",
776    "Ball - linear velocity y",
777    "Ball - linear velocity z",
778    "Ball - angular velocity x",
779    "Ball - angular velocity y",
780    "Ball - angular velocity z",
781);
782
783build_global_feature_adder!(
784    BallRigidBodyBasis,
785    |_, processor: &ReplayProcessor, _frame, _index, _current_time| {
786        processor
787            .get_normalized_ball_rigid_body()
788            .and_then(|rb| get_rigid_body_properties_basis(&rb))
789            .or_else(|_| default_rb_state_basis())
790    },
791    "Ball - position x",
792    "Ball - position y",
793    "Ball - position z",
794    "Ball - forward x",
795    "Ball - forward y",
796    "Ball - forward z",
797    "Ball - up x",
798    "Ball - up y",
799    "Ball - up z",
800    "Ball - linear velocity x",
801    "Ball - linear velocity y",
802    "Ball - linear velocity z",
803    "Ball - angular velocity x",
804    "Ball - angular velocity y",
805    "Ball - angular velocity z",
806);