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