Skip to main content

subtr_actor/
util.rs

1use boxcars::{HeaderProp, RemoteId};
2use serde::Serialize;
3
4use crate::*;
5
6macro_rules! fmt_err {
7    ($( $item:expr ),* $(,)?) => {
8        Err(format!($( $item ),*))
9    };
10}
11
12pub type PlayerId = boxcars::RemoteId;
13
14/// Represents which demolition format a replay uses.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum DemolishFormat {
17    /// Old format (pre-September 2024): uses `ReplicatedDemolishGoalExplosion`
18    Fx,
19    /// New format (September 2024+): uses `ReplicatedDemolishExtended`
20    Extended,
21}
22
23/// Wrapper enum for different demolition attribute formats across Rocket League versions.
24///
25/// Rocket League changed the demolition data structure around September 2024 (v2.43+),
26/// moving from `DemolishFx` to `DemolishExtended`. This enum provides a unified interface
27/// for both formats.
28#[derive(Debug, Clone, PartialEq)]
29pub enum DemolishAttribute {
30    Fx(boxcars::DemolishFx),
31    Extended(boxcars::DemolishExtended),
32}
33
34impl DemolishAttribute {
35    pub fn attacker_actor_id(&self) -> boxcars::ActorId {
36        match self {
37            DemolishAttribute::Fx(fx) => fx.attacker,
38            DemolishAttribute::Extended(ext) => ext.attacker.actor,
39        }
40    }
41
42    pub fn victim_actor_id(&self) -> boxcars::ActorId {
43        match self {
44            DemolishAttribute::Fx(fx) => fx.victim,
45            DemolishAttribute::Extended(ext) => ext.victim.actor,
46        }
47    }
48
49    pub fn attacker_velocity(&self) -> boxcars::Vector3f {
50        match self {
51            DemolishAttribute::Fx(fx) => fx.attack_velocity,
52            DemolishAttribute::Extended(ext) => ext.attacker_velocity,
53        }
54    }
55
56    pub fn victim_velocity(&self) -> boxcars::Vector3f {
57        match self {
58            DemolishAttribute::Fx(fx) => fx.victim_velocity,
59            DemolishAttribute::Extended(ext) => ext.victim_velocity,
60        }
61    }
62}
63
64/// [`DemolishInfo`] struct represents data related to a demolition event in the game.
65///
66/// Demolition events occur when one player 'demolishes' or 'destroys' another by
67/// hitting them at a sufficiently high speed. This results in the demolished player
68/// being temporarily removed from play.
69#[derive(Debug, Clone, PartialEq, Serialize)]
70pub struct DemolishInfo {
71    /// The exact game time (in seconds) at which the demolition event occurred.
72    pub time: f32,
73    /// The remaining time in the match when the demolition event occurred.
74    pub seconds_remaining: i32,
75    /// The frame number at which the demolition occurred.
76    pub frame: usize,
77    /// The [`PlayerId`] of the player who initiated the demolition.
78    pub attacker: PlayerId,
79    /// The [`PlayerId`] of the player who was demolished.
80    pub victim: PlayerId,
81    /// The velocity of the attacker at the time of demolition.
82    pub attacker_velocity: boxcars::Vector3f,
83    /// The velocity of the victim at the time of demolition.
84    pub victim_velocity: boxcars::Vector3f,
85    /// The location of the victim at the time of demolition.
86    pub victim_location: boxcars::Vector3f,
87}
88
89/// [`ReplayMeta`] struct represents metadata about the replay being processed.
90///
91/// This includes information about the players in the match and all replay headers.
92#[derive(Debug, Clone, PartialEq, Serialize)]
93pub struct ReplayMeta {
94    /// A vector of [`PlayerInfo`] instances representing the players on team zero.
95    pub team_zero: Vec<PlayerInfo>,
96    /// A vector of [`PlayerInfo`] instances representing the players on team one.
97    pub team_one: Vec<PlayerInfo>,
98    /// A vector of tuples containing the names and properties of all the headers in the replay.
99    pub all_headers: Vec<(String, HeaderProp)>,
100}
101
102impl ReplayMeta {
103    /// Returns the total number of players involved in the game.
104    pub fn player_count(&self) -> usize {
105        self.team_one.len() + self.team_zero.len()
106    }
107
108    /// Returns an iterator over the [`PlayerInfo`] instances representing the players,
109    /// in the order they are listed in the replay file.
110    pub fn player_order(&self) -> impl Iterator<Item = &PlayerInfo> {
111        self.team_zero.iter().chain(self.team_one.iter())
112    }
113}
114
115/// [`PlayerInfo`] struct provides detailed information about a specific player in the replay.
116///
117/// This includes player's unique remote ID, player stats if available, and their name.
118#[derive(Debug, Clone, PartialEq, Serialize)]
119pub struct PlayerInfo {
120    /// The unique remote ID of the player. This could be their online ID or local ID.
121    pub remote_id: RemoteId,
122    /// An optional HashMap containing player-specific stats.
123    /// The keys of this HashMap are the names of the stats,
124    /// and the values are the corresponding `HeaderProp` instances.
125    pub stats: Option<std::collections::HashMap<String, HeaderProp>>,
126    /// The name of the player as represented in the replay.
127    pub name: String,
128}
129
130pub fn find_player_stats(
131    player_id: &RemoteId,
132    name: &String,
133    all_player_stats: &Vec<Vec<(String, HeaderProp)>>,
134) -> Result<std::collections::HashMap<String, HeaderProp>, String> {
135    Ok(all_player_stats
136        .iter()
137        .find(|player_stats| matches_stats(player_id, name, player_stats))
138        .ok_or(format!(
139            "Player not found {player_id:?} {all_player_stats:?}"
140        ))?
141        .iter()
142        .cloned()
143        .collect())
144}
145
146fn matches_stats(player_id: &RemoteId, name: &String, props: &Vec<(String, HeaderProp)>) -> bool {
147    if platform_matches(player_id, props) != Ok(true) {
148        return false;
149    }
150    match player_id {
151        RemoteId::Epic(_) => name_matches(name, props),
152        RemoteId::Steam(id) => online_id_matches(*id, props),
153        RemoteId::Xbox(id) => online_id_matches(*id, props),
154        RemoteId::PlayStation(ps4id) => online_id_matches(ps4id.online_id, props),
155        RemoteId::PsyNet(psynet_id) => online_id_matches(psynet_id.online_id, props),
156        RemoteId::Switch(switch_id) => online_id_matches(switch_id.online_id, props),
157        _ => false,
158    }
159}
160
161fn name_matches(name: &String, props: &[(String, HeaderProp)]) -> bool {
162    if let Ok((_, HeaderProp::Str(stat_name))) = get_prop("Name", props) {
163        *name == stat_name
164    } else {
165        false
166    }
167}
168
169fn online_id_matches(id: u64, props: &[(String, HeaderProp)]) -> bool {
170    if let Ok((_, HeaderProp::QWord(props_id))) = get_prop("OnlineID", props) {
171        id == props_id
172    } else {
173        false
174    }
175}
176
177fn platform_matches(
178    player_id: &RemoteId,
179    props: &Vec<(String, HeaderProp)>,
180) -> Result<bool, String> {
181    if let (
182        _,
183        HeaderProp::Byte {
184            kind: _,
185            value: Some(value),
186        },
187    ) = get_prop("Platform", props)?
188    {
189        Ok(match (player_id, value.as_ref()) {
190            (RemoteId::Steam(_), "OnlinePlatform_Steam") => true,
191            (RemoteId::PlayStation(_), "OnlinePlatform_PS4") => true,
192            (RemoteId::Epic(_), "OnlinePlatform_Epic") => true,
193            (RemoteId::PsyNet(_), "OnlinePlatform_PS4") => true,
194            (RemoteId::Xbox(_), "OnlinePlatform_Dingo") => true,
195            // XXX: not sure if this is right.
196            (RemoteId::Switch(_), "OnlinePlatform_Switch") => true,
197            // TODO: There are still a few cases remaining.
198            _ => false,
199        })
200    } else {
201        fmt_err!("Unexpected platform value {:?}", props)
202    }
203}
204
205fn get_prop(prop: &str, props: &[(String, HeaderProp)]) -> Result<(String, HeaderProp), String> {
206    props
207        .iter()
208        .find(|(attr, _)| attr == prop)
209        .ok_or("Coudn't find name property".to_string())
210        .cloned()
211}
212
213pub(crate) trait VecMapEntry<K: PartialEq, V> {
214    fn get_entry(&mut self, key: K) -> Entry<'_, K, V>;
215}
216
217pub(crate) enum Entry<'a, K: PartialEq, V> {
218    Occupied(OccupiedEntry<'a, K, V>),
219    Vacant(VacantEntry<'a, K, V>),
220}
221
222impl<'a, K: PartialEq, V> Entry<'a, K, V> {
223    pub fn or_insert_with<F: FnOnce() -> V>(self, default: F) -> &'a mut V {
224        match self {
225            Entry::Occupied(occupied) => &mut occupied.entry.1,
226            Entry::Vacant(vacant) => {
227                vacant.vec.push((vacant.key, default()));
228                &mut vacant.vec.last_mut().unwrap().1
229            }
230        }
231    }
232}
233
234pub(crate) struct OccupiedEntry<'a, K: PartialEq, V> {
235    entry: &'a mut (K, V),
236}
237
238pub(crate) struct VacantEntry<'a, K: PartialEq, V> {
239    vec: &'a mut Vec<(K, V)>,
240    key: K,
241}
242
243impl<K: PartialEq + Clone, V> VecMapEntry<K, V> for Vec<(K, V)> {
244    fn get_entry(&mut self, key: K) -> Entry<'_, K, V> {
245        match self.iter_mut().position(|(k, _)| k == &key) {
246            Some(index) => Entry::Occupied(OccupiedEntry {
247                entry: &mut self[index],
248            }),
249            None => Entry::Vacant(VacantEntry { vec: self, key }),
250        }
251    }
252}
253
254pub fn vec_to_glam(v: &boxcars::Vector3f) -> glam::f32::Vec3 {
255    glam::f32::Vec3::new(v.x, v.y, v.z)
256}
257
258pub fn glam_to_vec(v: &glam::f32::Vec3) -> boxcars::Vector3f {
259    boxcars::Vector3f {
260        x: v.x,
261        y: v.y,
262        z: v.z,
263    }
264}
265
266pub fn quat_to_glam(q: &boxcars::Quaternion) -> glam::Quat {
267    glam::Quat::from_xyzw(q.x, q.y, q.z, q.w)
268}
269
270pub fn glam_to_quat(rotation: &glam::Quat) -> boxcars::Quaternion {
271    boxcars::Quaternion {
272        x: rotation.x,
273        y: rotation.y,
274        z: rotation.z,
275        w: rotation.w,
276    }
277}
278
279pub fn apply_velocities_to_rigid_body(
280    rigid_body: &boxcars::RigidBody,
281    time_delta: f32,
282) -> boxcars::RigidBody {
283    let mut interpolated = *rigid_body;
284    if time_delta == 0.0 {
285        return interpolated;
286    }
287    let linear_velocity = interpolated.linear_velocity.unwrap_or(boxcars::Vector3f {
288        x: 0.0,
289        y: 0.0,
290        z: 0.0,
291    });
292    let location = vec_to_glam(&rigid_body.location) + (time_delta * vec_to_glam(&linear_velocity));
293    interpolated.location = glam_to_vec(&location);
294    interpolated.rotation = apply_angular_velocity(rigid_body, time_delta);
295    interpolated
296}
297
298fn apply_angular_velocity(rigid_body: &boxcars::RigidBody, time_delta: f32) -> boxcars::Quaternion {
299    // XXX: This approach seems to give some unexpected results. There may be a
300    // unit mismatch or some other type of issue.
301    let rbav = rigid_body.angular_velocity.unwrap_or(boxcars::Vector3f {
302        x: 0.0,
303        y: 0.0,
304        z: 0.0,
305    });
306    let angular_velocity = glam::Vec3::new(rbav.x, rbav.y, rbav.z);
307    let magnitude = angular_velocity.length();
308    let angular_velocity_unit_vector = angular_velocity.normalize_or_zero();
309
310    let mut rotation = glam::Quat::from_xyzw(
311        rigid_body.rotation.x,
312        rigid_body.rotation.y,
313        rigid_body.rotation.z,
314        rigid_body.rotation.w,
315    );
316
317    if angular_velocity_unit_vector.length() != 0.0 {
318        let delta_rotation =
319            glam::Quat::from_axis_angle(angular_velocity_unit_vector, magnitude * time_delta);
320        rotation *= delta_rotation;
321    }
322
323    boxcars::Quaternion {
324        x: rotation.x,
325        y: rotation.y,
326        z: rotation.z,
327        w: rotation.w,
328    }
329}
330
331/// Interpolates between two [`boxcars::RigidBody`] states based on the provided time.
332///
333/// # Arguments
334///
335/// * `start_body` - The initial `RigidBody` state.
336/// * `start_time` - The timestamp of the initial `RigidBody` state.
337/// * `end_body` - The final `RigidBody` state.
338/// * `end_time` - The timestamp of the final `RigidBody` state.
339/// * `time` - The desired timestamp to interpolate to.
340///
341/// # Returns
342///
343/// A new [`boxcars::RigidBody`] that represents the interpolated state at the specified time.
344pub fn get_interpolated_rigid_body(
345    start_body: &boxcars::RigidBody,
346    start_time: f32,
347    end_body: &boxcars::RigidBody,
348    end_time: f32,
349    time: f32,
350) -> SubtrActorResult<boxcars::RigidBody> {
351    if !(start_time <= time && time <= end_time) {
352        return SubtrActorError::new_result(SubtrActorErrorVariant::InterpolationTimeOrderError {
353            start_time,
354            time,
355            end_time,
356        });
357    }
358
359    let duration = end_time - start_time;
360    let interpolation_amount = (time - start_time) / duration;
361    let start_position = util::vec_to_glam(&start_body.location);
362    let end_position = util::vec_to_glam(&end_body.location);
363    let interpolated_location = start_position.lerp(end_position, interpolation_amount);
364    let start_rotation = quat_to_glam(&start_body.rotation);
365    let end_rotation = quat_to_glam(&end_body.rotation);
366    let interpolated_rotation = start_rotation.slerp(end_rotation, interpolation_amount);
367
368    Ok(boxcars::RigidBody {
369        location: glam_to_vec(&interpolated_location),
370        rotation: glam_to_quat(&interpolated_rotation),
371        sleeping: start_body.sleeping,
372        linear_velocity: start_body.linear_velocity,
373        angular_velocity: start_body.angular_velocity,
374    })
375}
376
377/// Enum to define the direction of searching within a collection.
378#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
379pub enum SearchDirection {
380    Forward,
381    Backward,
382}
383
384/// Searches for an item in a slice in a specified direction and returns the
385/// first item that matches the provided predicate.
386///
387/// # Arguments
388///
389/// * `items` - The list of items to search.
390/// * `current_index` - The index to start the search from.
391/// * `direction` - The direction to search in.
392/// * `predicate` - A function that takes an item and returns an [`Option<R>`].
393///   When this function returns `Some(R)`, the item is considered a match.
394///
395/// # Returns
396///
397/// Returns a tuple of the index and the result `R` of the predicate for the first item that matches.
398pub fn find_in_direction<T, F, R>(
399    items: &[T],
400    current_index: usize,
401    direction: SearchDirection,
402    predicate: F,
403) -> Option<(usize, R)>
404where
405    F: Fn(&T) -> Option<R>,
406{
407    let mut iter: Box<dyn Iterator<Item = (usize, &T)>> = match direction {
408        SearchDirection::Forward => Box::new(
409            items[current_index + 1..]
410                .iter()
411                .enumerate()
412                .map(move |(i, item)| (i + current_index + 1, item)),
413        ),
414        SearchDirection::Backward => Box::new(items[..current_index].iter().enumerate().rev()),
415    };
416
417    iter.find_map(|(i, item)| predicate(item).map(|res| (i, res)))
418}