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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum DemolishFormat {
17 Fx,
19 Extended,
21}
22
23#[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#[derive(Debug, Clone, PartialEq, Serialize)]
70pub struct DemolishInfo {
71 pub time: f32,
73 pub seconds_remaining: i32,
75 pub frame: usize,
77 pub attacker: PlayerId,
79 pub victim: PlayerId,
81 pub attacker_velocity: boxcars::Vector3f,
83 pub victim_velocity: boxcars::Vector3f,
85 pub victim_location: boxcars::Vector3f,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
90pub enum BoostPadEventKind {
91 PickedUp { sequence: u8 },
92 Available,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
96pub enum BoostPadSize {
97 Big,
98 Small,
99}
100
101#[derive(Debug, Clone, PartialEq, Serialize)]
102pub struct BoostPadEvent {
103 pub time: f32,
104 pub frame: usize,
105 pub pad_id: String,
106 pub player: Option<PlayerId>,
107 pub kind: BoostPadEventKind,
108}
109
110#[derive(Debug, Clone, PartialEq, Serialize)]
111pub struct ResolvedBoostPad {
112 pub index: usize,
113 pub pad_id: Option<String>,
114 pub size: BoostPadSize,
115 pub position: boxcars::Vector3f,
116}
117
118#[derive(Debug, Clone, PartialEq, Serialize)]
119pub struct GoalEvent {
120 pub time: f32,
121 pub frame: usize,
122 pub scoring_team_is_team_0: bool,
123 pub player: Option<PlayerId>,
124 pub team_zero_score: Option<i32>,
125 pub team_one_score: Option<i32>,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
129pub enum PlayerStatEventKind {
130 Shot,
131 Save,
132 Assist,
133}
134
135#[derive(Debug, Clone, PartialEq, Serialize)]
136pub struct PlayerStatEvent {
137 pub time: f32,
138 pub frame: usize,
139 pub player: PlayerId,
140 pub is_team_0: bool,
141 pub kind: PlayerStatEventKind,
142}
143
144#[derive(Debug, Clone, PartialEq, Serialize)]
145pub struct TouchEvent {
146 pub time: f32,
147 pub frame: usize,
148 pub team_is_team_0: bool,
149 pub player: Option<PlayerId>,
150 pub closest_approach_distance: Option<f32>,
151}
152
153#[derive(Debug, Clone, PartialEq, Serialize)]
157pub struct ReplayMeta {
158 pub team_zero: Vec<PlayerInfo>,
160 pub team_one: Vec<PlayerInfo>,
162 pub all_headers: Vec<(String, HeaderProp)>,
164}
165
166impl ReplayMeta {
167 pub fn player_count(&self) -> usize {
169 self.team_one.len() + self.team_zero.len()
170 }
171
172 pub fn player_order(&self) -> impl Iterator<Item = &PlayerInfo> {
175 self.team_zero.iter().chain(self.team_one.iter())
176 }
177}
178
179#[derive(Debug, Clone, PartialEq, Serialize)]
183pub struct PlayerInfo {
184 pub remote_id: RemoteId,
186 pub stats: Option<std::collections::HashMap<String, HeaderProp>>,
190 pub name: String,
192}
193
194pub fn find_player_stats(
195 player_id: &RemoteId,
196 name: &String,
197 all_player_stats: &Vec<Vec<(String, HeaderProp)>>,
198) -> Result<std::collections::HashMap<String, HeaderProp>, String> {
199 Ok(all_player_stats
200 .iter()
201 .find(|player_stats| matches_stats(player_id, name, player_stats))
202 .ok_or(format!(
203 "Player not found {player_id:?} {all_player_stats:?}"
204 ))?
205 .iter()
206 .cloned()
207 .collect())
208}
209
210fn matches_stats(player_id: &RemoteId, name: &String, props: &Vec<(String, HeaderProp)>) -> bool {
211 if platform_matches(player_id, props) != Ok(true) {
212 return false;
213 }
214 match player_id {
215 RemoteId::Epic(_) => name_matches(name, props),
216 RemoteId::Steam(id) => online_id_matches(*id, props),
217 RemoteId::Xbox(id) => online_id_matches(*id, props),
218 RemoteId::PlayStation(ps4id) => online_id_matches(ps4id.online_id, props),
219 RemoteId::PsyNet(psynet_id) => online_id_matches(psynet_id.online_id, props),
220 RemoteId::Switch(switch_id) => online_id_matches(switch_id.online_id, props),
221 _ => false,
222 }
223}
224
225fn name_matches(name: &String, props: &[(String, HeaderProp)]) -> bool {
226 if let Ok((_, HeaderProp::Str(stat_name))) = get_prop("Name", props) {
227 *name == stat_name
228 } else {
229 false
230 }
231}
232
233fn online_id_matches(id: u64, props: &[(String, HeaderProp)]) -> bool {
234 if let Ok((_, HeaderProp::QWord(props_id))) = get_prop("OnlineID", props) {
235 id == props_id
236 } else {
237 false
238 }
239}
240
241fn platform_matches(
242 player_id: &RemoteId,
243 props: &Vec<(String, HeaderProp)>,
244) -> Result<bool, String> {
245 if let (
246 _,
247 HeaderProp::Byte {
248 kind: _,
249 value: Some(value),
250 },
251 ) = get_prop("Platform", props)?
252 {
253 Ok(match (player_id, value.as_ref()) {
254 (RemoteId::Steam(_), "OnlinePlatform_Steam") => true,
255 (RemoteId::PlayStation(_), "OnlinePlatform_PS4") => true,
256 (RemoteId::Epic(_), "OnlinePlatform_Epic") => true,
257 (RemoteId::PsyNet(_), "OnlinePlatform_PS4") => true,
258 (RemoteId::Xbox(_), "OnlinePlatform_Dingo") => true,
259 (RemoteId::Switch(_), "OnlinePlatform_Switch") => true,
261 _ => false,
263 })
264 } else {
265 fmt_err!("Unexpected platform value {:?}", props)
266 }
267}
268
269fn get_prop(prop: &str, props: &[(String, HeaderProp)]) -> Result<(String, HeaderProp), String> {
270 props
271 .iter()
272 .find(|(attr, _)| attr == prop)
273 .ok_or("Coudn't find name property".to_string())
274 .cloned()
275}
276
277pub(crate) trait VecMapEntry<K: PartialEq, V> {
278 fn get_entry(&mut self, key: K) -> Entry<'_, K, V>;
279}
280
281pub(crate) enum Entry<'a, K: PartialEq, V> {
282 Occupied(OccupiedEntry<'a, K, V>),
283 Vacant(VacantEntry<'a, K, V>),
284}
285
286impl<'a, K: PartialEq, V> Entry<'a, K, V> {
287 pub fn or_insert_with<F: FnOnce() -> V>(self, default: F) -> &'a mut V {
288 match self {
289 Entry::Occupied(occupied) => &mut occupied.entry.1,
290 Entry::Vacant(vacant) => {
291 vacant.vec.push((vacant.key, default()));
292 &mut vacant.vec.last_mut().unwrap().1
293 }
294 }
295 }
296}
297
298pub(crate) struct OccupiedEntry<'a, K: PartialEq, V> {
299 entry: &'a mut (K, V),
300}
301
302pub(crate) struct VacantEntry<'a, K: PartialEq, V> {
303 vec: &'a mut Vec<(K, V)>,
304 key: K,
305}
306
307impl<K: PartialEq + Clone, V> VecMapEntry<K, V> for Vec<(K, V)> {
308 fn get_entry(&mut self, key: K) -> Entry<'_, K, V> {
309 match self.iter_mut().position(|(k, _)| k == &key) {
310 Some(index) => Entry::Occupied(OccupiedEntry {
311 entry: &mut self[index],
312 }),
313 None => Entry::Vacant(VacantEntry { vec: self, key }),
314 }
315 }
316}
317
318pub fn vec_to_glam(v: &boxcars::Vector3f) -> glam::f32::Vec3 {
319 glam::f32::Vec3::new(v.x, v.y, v.z)
320}
321
322pub fn glam_to_vec(v: &glam::f32::Vec3) -> boxcars::Vector3f {
323 boxcars::Vector3f {
324 x: v.x,
325 y: v.y,
326 z: v.z,
327 }
328}
329
330pub fn quat_to_glam(q: &boxcars::Quaternion) -> glam::Quat {
331 glam::Quat::from_xyzw(q.x, q.y, q.z, q.w)
332}
333
334pub fn glam_to_quat(rotation: &glam::Quat) -> boxcars::Quaternion {
335 boxcars::Quaternion {
336 x: rotation.x,
337 y: rotation.y,
338 z: rotation.z,
339 w: rotation.w,
340 }
341}
342
343pub fn apply_velocities_to_rigid_body(
344 rigid_body: &boxcars::RigidBody,
345 time_delta: f32,
346) -> boxcars::RigidBody {
347 let mut interpolated = *rigid_body;
348 if time_delta == 0.0 {
349 return interpolated;
350 }
351 let linear_velocity = interpolated.linear_velocity.unwrap_or(boxcars::Vector3f {
352 x: 0.0,
353 y: 0.0,
354 z: 0.0,
355 });
356 let location = vec_to_glam(&rigid_body.location) + (time_delta * vec_to_glam(&linear_velocity));
357 interpolated.location = glam_to_vec(&location);
358 interpolated.rotation = apply_angular_velocity(rigid_body, time_delta);
359 interpolated
360}
361
362pub(crate) fn touch_candidate_rank(
369 ball_body: &boxcars::RigidBody,
370 player_body: &boxcars::RigidBody,
371) -> Option<(f32, f32)> {
372 const TOUCH_LOOKBACK_SECONDS: f32 = 0.12;
373 const TOUCH_LOOKAHEAD_SECONDS: f32 = 0.03;
374
375 let relative_position = vec_to_glam(&player_body.location) - vec_to_glam(&ball_body.location);
376 let current_distance = relative_position.length();
377 if !current_distance.is_finite() {
378 return None;
379 }
380
381 let relative_velocity =
382 vec_to_glam(&player_body.linear_velocity.unwrap_or(boxcars::Vector3f {
383 x: 0.0,
384 y: 0.0,
385 z: 0.0,
386 })) - vec_to_glam(&ball_body.linear_velocity.unwrap_or(boxcars::Vector3f {
387 x: 0.0,
388 y: 0.0,
389 z: 0.0,
390 }));
391 let relative_speed_squared = relative_velocity.length_squared();
392 let closest_time = if relative_speed_squared > f32::EPSILON {
393 (-relative_position.dot(relative_velocity) / relative_speed_squared)
394 .clamp(-TOUCH_LOOKBACK_SECONDS, TOUCH_LOOKAHEAD_SECONDS)
395 } else {
396 0.0
397 };
398 let closest_distance = (relative_position + relative_velocity * closest_time).length();
399 if !closest_distance.is_finite() {
400 return None;
401 }
402
403 Some((closest_distance, current_distance))
404}
405
406fn apply_angular_velocity(rigid_body: &boxcars::RigidBody, time_delta: f32) -> boxcars::Quaternion {
407 let rbav = rigid_body.angular_velocity.unwrap_or(boxcars::Vector3f {
410 x: 0.0,
411 y: 0.0,
412 z: 0.0,
413 });
414 let angular_velocity = glam::Vec3::new(rbav.x, rbav.y, rbav.z);
415 let magnitude = angular_velocity.length();
416 let angular_velocity_unit_vector = angular_velocity.normalize_or_zero();
417
418 let mut rotation = glam::Quat::from_xyzw(
419 rigid_body.rotation.x,
420 rigid_body.rotation.y,
421 rigid_body.rotation.z,
422 rigid_body.rotation.w,
423 );
424
425 if angular_velocity_unit_vector.length() != 0.0 {
426 let delta_rotation =
427 glam::Quat::from_axis_angle(angular_velocity_unit_vector, magnitude * time_delta);
428 rotation *= delta_rotation;
429 }
430
431 boxcars::Quaternion {
432 x: rotation.x,
433 y: rotation.y,
434 z: rotation.z,
435 w: rotation.w,
436 }
437}
438
439pub fn get_interpolated_rigid_body(
453 start_body: &boxcars::RigidBody,
454 start_time: f32,
455 end_body: &boxcars::RigidBody,
456 end_time: f32,
457 time: f32,
458) -> SubtrActorResult<boxcars::RigidBody> {
459 if !(start_time <= time && time <= end_time) {
460 return SubtrActorError::new_result(SubtrActorErrorVariant::InterpolationTimeOrderError {
461 start_time,
462 time,
463 end_time,
464 });
465 }
466
467 let duration = end_time - start_time;
468 let interpolation_amount = (time - start_time) / duration;
469 let start_position = util::vec_to_glam(&start_body.location);
470 let end_position = util::vec_to_glam(&end_body.location);
471 let interpolated_location = start_position.lerp(end_position, interpolation_amount);
472 let start_rotation = quat_to_glam(&start_body.rotation);
473 let end_rotation = quat_to_glam(&end_body.rotation);
474 let interpolated_rotation = start_rotation.slerp(end_rotation, interpolation_amount);
475
476 Ok(boxcars::RigidBody {
477 location: glam_to_vec(&interpolated_location),
478 rotation: glam_to_quat(&interpolated_rotation),
479 sleeping: start_body.sleeping,
480 linear_velocity: start_body.linear_velocity,
481 angular_velocity: start_body.angular_velocity,
482 })
483}
484
485#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
487pub enum SearchDirection {
488 Forward,
489 Backward,
490}
491
492pub fn find_in_direction<T, F, R>(
507 items: &[T],
508 current_index: usize,
509 direction: SearchDirection,
510 predicate: F,
511) -> Option<(usize, R)>
512where
513 F: Fn(&T) -> Option<R>,
514{
515 match direction {
516 SearchDirection::Forward => items
517 .iter()
518 .enumerate()
519 .skip(current_index + 1)
520 .find_map(|(i, item)| predicate(item).map(|res| (i, res))),
521 SearchDirection::Backward => items[..current_index]
522 .iter()
523 .enumerate()
524 .rev()
525 .find_map(|(i, item)| predicate(item).map(|res| (i, res))),
526 }
527}