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