subtr_actor_spec/
util.rs

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