1use crate::*;
2use boxcars;
3use std::collections::HashMap;
4
5pub mod actor_state;
6pub use actor_state::*;
7
8pub(crate) fn attribute_type_name(attribute: &boxcars::Attribute) -> &'static str {
9 match attribute {
10 boxcars::Attribute::Boolean(_) => "Boolean",
11 boxcars::Attribute::Byte(_) => "Byte",
12 boxcars::Attribute::AppliedDamage(_) => "AppliedDamage",
13 boxcars::Attribute::DamageState(_) => "DamageState",
14 boxcars::Attribute::CamSettings(_) => "CamSettings",
15 boxcars::Attribute::ClubColors(_) => "ClubColors",
16 boxcars::Attribute::Demolish(_) => "Demolish",
17 boxcars::Attribute::DemolishExtended(_) => "DemolishExtended",
18 boxcars::Attribute::DemolishFx(_) => "DemolishFx",
19 boxcars::Attribute::Enum(_) => "Enum",
20 boxcars::Attribute::Explosion(_) => "Explosion",
21 boxcars::Attribute::ExtendedExplosion(_) => "ExtendedExplosion",
22 boxcars::Attribute::FlaggedByte(_, _) => "FlaggedByte",
23 boxcars::Attribute::ActiveActor(_) => "ActiveActor",
24 boxcars::Attribute::Float(_) => "Float",
25 boxcars::Attribute::GameMode(_, _) => "GameMode",
26 boxcars::Attribute::Int(_) => "Int",
27 boxcars::Attribute::Int64(_) => "Int64",
28 boxcars::Attribute::Loadout(_) => "Loadout",
29 boxcars::Attribute::TeamLoadout(_) => "TeamLoadout",
30 boxcars::Attribute::Location(_) => "Location",
31 boxcars::Attribute::MusicStinger(_) => "MusicStinger",
32 boxcars::Attribute::PlayerHistoryKey(_) => "PlayerHistoryKey",
33 boxcars::Attribute::Pickup(_) => "Pickup",
34 boxcars::Attribute::PickupNew(_) => "PickupNew",
35 boxcars::Attribute::QWord(_) => "QWord",
36 boxcars::Attribute::Welded(_) => "Welded",
37 boxcars::Attribute::Title(_, _, _, _, _, _, _, _) => "Title",
38 boxcars::Attribute::TeamPaint(_) => "TeamPaint",
39 boxcars::Attribute::RigidBody(_) => "RigidBody",
40 boxcars::Attribute::String(_) => "String",
41 boxcars::Attribute::UniqueId(_) => "UniqueId",
42 boxcars::Attribute::Reservation(_) => "Reservation",
43 boxcars::Attribute::PartyLeader(_) => "PartyLeader",
44 boxcars::Attribute::PrivateMatch(_) => "PrivateMatch",
45 boxcars::Attribute::LoadoutOnline(_) => "LoadoutOnline",
46 boxcars::Attribute::LoadoutsOnline(_) => "LoadoutsOnline",
47 boxcars::Attribute::StatEvent(_) => "StatEvent",
48 boxcars::Attribute::Rotation(_) => "Rotation",
49 boxcars::Attribute::RepStatTitle(_) => "RepStatTitle",
50 boxcars::Attribute::PickupInfo(_) => "PickupInfo",
51 boxcars::Attribute::Impulse(_) => "Impulse",
52 boxcars::Attribute::ReplicatedBoost(_) => "ReplicatedBoost",
53 boxcars::Attribute::LogoData(_) => "LogoData",
54 }
55}
56
57#[macro_export]
69macro_rules! attribute_match {
70 ($value:expr, $type:path $(,)?) => {{
71 let attribute = $value;
72 if let $type(value) = attribute {
73 Ok(value)
74 } else {
75 SubtrActorError::new_result(SubtrActorErrorVariant::UnexpectedAttributeType {
76 expected_type: stringify!($type),
77 actual_type: attribute_type_name(&attribute),
78 })
79 }
80 }};
81}
82
83#[macro_export]
92macro_rules! get_attribute_errors_expected {
93 ($self:ident, $map:expr, $prop:expr, $type:path) => {
94 $self
95 .get_attribute($map, $prop)
96 .and_then(|found| attribute_match!(found, $type))
97 };
98}
99
100macro_rules! get_attribute_and_updated {
113 ($self:ident, $map:expr, $prop:expr, $type:path) => {
114 $self
115 .get_attribute_and_updated($map, $prop)
116 .and_then(|(found, updated)| attribute_match!(found, $type).map(|v| (v, updated)))
117 };
118}
119
120macro_rules! get_actor_attribute_matching {
129 ($self:ident, $actor:expr, $prop:expr, $type:path) => {
130 $self
131 .get_actor_attribute($actor, $prop)
132 .and_then(|found| attribute_match!(found, $type))
133 };
134}
135
136macro_rules! get_derived_attribute {
145 ($map:expr, $key:expr, $type:path) => {
146 $map.get($key)
147 .ok_or_else(|| {
148 SubtrActorError::new(SubtrActorErrorVariant::DerivedKeyValueNotFound {
149 name: $key.to_string(),
150 })
151 })
152 .and_then(|found| attribute_match!(&found.0, $type))
153 };
154}
155
156fn get_actor_id_from_active_actor<T>(
157 _: T,
158 active_actor: &boxcars::ActiveActor,
159) -> boxcars::ActorId {
160 active_actor.actor
161}
162
163fn use_update_actor<T>(id: boxcars::ActorId, _: T) -> boxcars::ActorId {
164 id
165}
166
167#[derive(Clone, Copy, Default)]
168struct CachedObjectIds {
169 player_type: Option<boxcars::ObjectId>,
170 car_type: Option<boxcars::ObjectId>,
171 boost_type: Option<boxcars::ObjectId>,
172 dodge_type: Option<boxcars::ObjectId>,
173 jump_type: Option<boxcars::ObjectId>,
174 double_jump_type: Option<boxcars::ObjectId>,
175 unique_id: Option<boxcars::ObjectId>,
176 team: Option<boxcars::ObjectId>,
177 player_replication: Option<boxcars::ObjectId>,
178 vehicle: Option<boxcars::ObjectId>,
179 boost_replicated: Option<boxcars::ObjectId>,
180 boost_amount: Option<boxcars::ObjectId>,
181 component_active: Option<boxcars::ObjectId>,
182 seconds_remaining: Option<boxcars::ObjectId>,
183 replicated_state_name: Option<boxcars::ObjectId>,
184 replicated_game_state_time_remaining: Option<boxcars::ObjectId>,
185 ball_has_been_hit: Option<boxcars::ObjectId>,
186 ball_hit_team_num: Option<boxcars::ObjectId>,
187 dodges_refreshed_counter: Option<boxcars::ObjectId>,
188}
189
190impl CachedObjectIds {
191 fn from_name_map(name_to_object_id: &HashMap<String, boxcars::ObjectId>) -> Self {
192 let cached = |name| name_to_object_id.get(name).copied();
193 Self {
194 player_type: cached(PLAYER_TYPE),
195 car_type: cached(CAR_TYPE),
196 boost_type: cached(BOOST_TYPE),
197 dodge_type: cached(DODGE_TYPE),
198 jump_type: cached(JUMP_TYPE),
199 double_jump_type: cached(DOUBLE_JUMP_TYPE),
200 unique_id: cached(UNIQUE_ID_KEY),
201 team: cached(TEAM_KEY),
202 player_replication: cached(PLAYER_REPLICATION_KEY),
203 vehicle: cached(VEHICLE_KEY),
204 boost_replicated: cached(BOOST_REPLICATED_KEY),
205 boost_amount: cached(BOOST_AMOUNT_KEY),
206 component_active: cached(COMPONENT_ACTIVE_KEY),
207 seconds_remaining: cached(SECONDS_REMAINING_KEY),
208 replicated_state_name: cached(REPLICATED_STATE_NAME_KEY),
209 replicated_game_state_time_remaining: cached(REPLICATED_GAME_STATE_TIME_REMAINING_KEY),
210 ball_has_been_hit: cached(BALL_HAS_BEEN_HIT_KEY),
211 ball_hit_team_num: cached(BALL_HIT_TEAM_NUM_KEY),
212 dodges_refreshed_counter: cached(DODGES_REFRESHED_COUNTER_KEY),
213 }
214 }
215}
216
217mod bootstrap;
218mod debug;
219mod queries;
220mod updaters;
221
222pub struct ReplayProcessor<'a> {
254 pub replay: &'a boxcars::Replay,
256 spatial_normalization_factor: f32,
257 rigid_body_velocity_normalization_factor: f32,
258 uses_legacy_rigid_body_rotation: bool,
259 cached_object_ids: CachedObjectIds,
260 is_boost_pad_object: Vec<bool>,
261 pub actor_state: ActorStateModeler,
263 pub object_id_to_name: HashMap<boxcars::ObjectId, String>,
265 pub name_to_object_id: HashMap<String, boxcars::ObjectId>,
267 pub ball_actor_id: Option<boxcars::ActorId>,
269 pub team_zero: Vec<PlayerId>,
271 pub team_one: Vec<PlayerId>,
273 pub player_to_actor_id: HashMap<PlayerId, boxcars::ActorId>,
275 pub player_to_car: HashMap<boxcars::ActorId, boxcars::ActorId>,
277 pub player_to_team: HashMap<boxcars::ActorId, boxcars::ActorId>,
279 pub car_to_player: HashMap<boxcars::ActorId, boxcars::ActorId>,
281 pub car_to_boost: HashMap<boxcars::ActorId, boxcars::ActorId>,
283 pub car_to_jump: HashMap<boxcars::ActorId, boxcars::ActorId>,
285 pub car_to_double_jump: HashMap<boxcars::ActorId, boxcars::ActorId>,
287 pub car_to_dodge: HashMap<boxcars::ActorId, boxcars::ActorId>,
289 pub boost_pad_events: Vec<BoostPadEvent>,
291 current_frame_boost_pad_events: Vec<BoostPadEvent>,
292 boost_pad_pickup_sequence_times: HashMap<(String, u8), f32>,
293 pub touch_events: Vec<TouchEvent>,
295 current_frame_touch_events: Vec<TouchEvent>,
296 pub dodge_refreshed_events: Vec<DodgeRefreshedEvent>,
298 current_frame_dodge_refreshed_events: Vec<DodgeRefreshedEvent>,
299 dodge_refreshed_counters: HashMap<PlayerId, i32>,
300 pub goal_events: Vec<GoalEvent>,
302 current_frame_goal_events: Vec<GoalEvent>,
303 pub player_stat_events: Vec<PlayerStatEvent>,
305 current_frame_player_stat_events: Vec<PlayerStatEvent>,
306 player_stat_counters: HashMap<(PlayerId, PlayerStatEventKind), i32>,
307 pub demolishes: Vec<DemolishInfo>,
309 known_demolishes: Vec<(DemolishAttribute, usize)>,
310 demolish_format: Option<DemolishFormat>,
311 kickoff_phase_active_last_frame: bool,
312}
313
314impl<'a> ReplayProcessor<'a> {
315 const LEGACY_RIGID_BODY_NET_VERSION_CUTOFF: i32 = 5;
316 const LEGACY_RIGID_BODY_ROTATION_NET_VERSION_CUTOFF: i32 = 7;
317 const LEGACY_RIGID_BODY_LOCATION_FACTOR: f32 = 100.0;
318 const LEGACY_RIGID_BODY_VELOCITY_FACTOR: f32 = 10.0;
319
320 fn uses_legacy_rigid_body_vector_scale(net_version: Option<i32>) -> bool {
321 net_version.is_none_or(|version| version < Self::LEGACY_RIGID_BODY_NET_VERSION_CUTOFF)
322 }
323
324 fn uses_legacy_rigid_body_rotation_for_net_version(net_version: Option<i32>) -> bool {
325 net_version
326 .is_none_or(|version| version < Self::LEGACY_RIGID_BODY_ROTATION_NET_VERSION_CUTOFF)
327 }
328
329 fn rigid_body_location_normalization_factor_for_net_version(net_version: Option<i32>) -> f32 {
330 if Self::uses_legacy_rigid_body_vector_scale(net_version) {
331 Self::LEGACY_RIGID_BODY_LOCATION_FACTOR
332 } else {
333 1.0
334 }
335 }
336
337 fn rigid_body_velocity_normalization_factor_for_net_version(net_version: Option<i32>) -> f32 {
338 if Self::uses_legacy_rigid_body_vector_scale(net_version) {
339 Self::LEGACY_RIGID_BODY_VELOCITY_FACTOR
340 } else {
341 1.0
342 }
343 }
344
345 pub fn new(replay: &'a boxcars::Replay) -> SubtrActorResult<Self> {
359 let mut object_id_to_name = HashMap::new();
360 let mut name_to_object_id = HashMap::new();
361 let spatial_normalization_factor =
362 Self::rigid_body_location_normalization_factor_for_net_version(replay.net_version);
363 let rigid_body_velocity_normalization_factor =
364 Self::rigid_body_velocity_normalization_factor_for_net_version(replay.net_version);
365 let uses_legacy_rigid_body_rotation =
366 Self::uses_legacy_rigid_body_rotation_for_net_version(replay.net_version);
367 for (id, name) in replay.objects.iter().enumerate() {
368 let object_id = boxcars::ObjectId(id as i32);
369 object_id_to_name.insert(object_id, name.clone());
370 name_to_object_id.insert(name.clone(), object_id);
371 }
372 let cached_object_ids = CachedObjectIds::from_name_map(&name_to_object_id);
373 let mut processor = Self {
374 actor_state: ActorStateModeler::new(),
375 replay,
376 spatial_normalization_factor,
377 rigid_body_velocity_normalization_factor,
378 uses_legacy_rigid_body_rotation,
379 cached_object_ids,
380 is_boost_pad_object: replay
381 .objects
382 .iter()
383 .map(|name| name.contains("VehiclePickup_Boost_TA"))
384 .collect(),
385 object_id_to_name,
386 name_to_object_id,
387 team_zero: Vec::new(),
388 team_one: Vec::new(),
389 ball_actor_id: None,
390 player_to_car: HashMap::new(),
391 player_to_team: HashMap::new(),
392 player_to_actor_id: HashMap::new(),
393 car_to_player: HashMap::new(),
394 car_to_boost: HashMap::new(),
395 car_to_jump: HashMap::new(),
396 car_to_double_jump: HashMap::new(),
397 car_to_dodge: HashMap::new(),
398 boost_pad_events: Vec::new(),
399 current_frame_boost_pad_events: Vec::new(),
400 boost_pad_pickup_sequence_times: HashMap::new(),
401 touch_events: Vec::new(),
402 current_frame_touch_events: Vec::new(),
403 dodge_refreshed_events: Vec::new(),
404 current_frame_dodge_refreshed_events: Vec::new(),
405 dodge_refreshed_counters: HashMap::new(),
406 goal_events: Vec::new(),
407 current_frame_goal_events: Vec::new(),
408 player_stat_events: Vec::new(),
409 current_frame_player_stat_events: Vec::new(),
410 player_stat_counters: HashMap::new(),
411 demolishes: Vec::new(),
412 known_demolishes: Vec::new(),
413 demolish_format: None,
414 kickoff_phase_active_last_frame: false,
415 };
416 processor
417 .set_player_order_from_headers()
418 .or_else(|_| processor.set_player_order_from_frames())?;
419
420 Ok(processor)
421 }
422
423 pub fn spatial_normalization_factor(&self) -> f32 {
425 self.spatial_normalization_factor
426 }
427
428 pub fn rigid_body_velocity_normalization_factor(&self) -> f32 {
430 self.rigid_body_velocity_normalization_factor
431 }
432
433 fn normalize_vector_by_factor(
434 &self,
435 vector: boxcars::Vector3f,
436 factor: f32,
437 ) -> boxcars::Vector3f {
438 if (factor - 1.0).abs() < f32::EPSILON {
439 vector
440 } else {
441 boxcars::Vector3f {
442 x: vector.x * factor,
443 y: vector.y * factor,
444 z: vector.z * factor,
445 }
446 }
447 }
448
449 fn normalize_vector(&self, vector: boxcars::Vector3f) -> boxcars::Vector3f {
450 self.normalize_vector_by_factor(vector, self.spatial_normalization_factor)
451 }
452
453 fn normalize_rigid_body_velocity(&self, vector: boxcars::Vector3f) -> boxcars::Vector3f {
454 self.normalize_vector_by_factor(vector, self.rigid_body_velocity_normalization_factor)
455 }
456
457 fn normalize_optional_rigid_body_velocity(
458 &self,
459 vector: Option<boxcars::Vector3f>,
460 ) -> Option<boxcars::Vector3f> {
461 vector.map(|value| self.normalize_rigid_body_velocity(value))
462 }
463
464 fn normalize_rigid_body_rotation(&self, rotation: boxcars::Quaternion) -> boxcars::Quaternion {
465 if !self.uses_legacy_rigid_body_rotation {
466 return rotation;
467 }
468
469 let normalized = glam::Quat::from_euler(
473 glam::EulerRot::ZYX,
474 rotation.y * std::f32::consts::PI,
475 rotation.x * std::f32::consts::PI,
476 -rotation.z * std::f32::consts::PI,
477 );
478 boxcars::Quaternion {
479 x: normalized.x,
480 y: normalized.y,
481 z: normalized.z,
482 w: normalized.w,
483 }
484 }
485
486 fn normalize_rigid_body(&self, rigid_body: &boxcars::RigidBody) -> boxcars::RigidBody {
487 if (self.spatial_normalization_factor - 1.0).abs() < f32::EPSILON
488 && (self.rigid_body_velocity_normalization_factor - 1.0).abs() < f32::EPSILON
489 && !self.uses_legacy_rigid_body_rotation
490 {
491 *rigid_body
492 } else {
493 boxcars::RigidBody {
494 sleeping: rigid_body.sleeping,
495 location: self.normalize_vector(rigid_body.location),
496 rotation: self.normalize_rigid_body_rotation(rigid_body.rotation),
497 linear_velocity: self
498 .normalize_optional_rigid_body_velocity(rigid_body.linear_velocity),
499 angular_velocity: self
500 .normalize_optional_rigid_body_velocity(rigid_body.angular_velocity),
501 }
502 }
503 }
504
505 fn required_cached_object_id(
506 &self,
507 object_id: Option<boxcars::ObjectId>,
508 name: &'static str,
509 ) -> SubtrActorResult<boxcars::ObjectId> {
510 object_id
511 .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::ObjectIdNotFound { name }))
512 }
513
514 pub fn process<H: Collector>(&mut self, handler: &mut H) -> SubtrActorResult<()> {
536 let mut target_time = TimeAdvance::NextFrame;
539 for (index, frame) in self
540 .replay
541 .network_frames
542 .as_ref()
543 .ok_or(SubtrActorError::new(
544 SubtrActorErrorVariant::NoNetworkFrames,
545 ))?
546 .frames
547 .iter()
548 .enumerate()
549 {
550 self.actor_state.process_frame(frame, index)?;
552 self.update_mappings(frame)?;
553 self.update_ball_id(frame)?;
554 self.update_boost_amounts(frame, index)?;
555 self.update_boost_pad_events(frame, index)?;
556 self.update_touch_events(frame, index)?;
557 self.update_dodge_refreshed_events(frame, index)?;
558 self.update_goal_events(frame, index)?;
559 self.update_player_stat_events(frame, index)?;
560 self.update_demolishes(frame, index)?;
561
562 let mut current_time = match &target_time {
565 TimeAdvance::Time(t) => *t,
566 TimeAdvance::NextFrame => frame.time,
567 };
568
569 while current_time <= frame.time {
570 target_time = handler.process_frame(self, frame, index, current_time)?;
573 if let TimeAdvance::Time(new_target) = target_time {
579 current_time = new_target;
580 } else {
581 break;
582 }
583 }
584 }
585 handler.finish_replay(self)?;
586 Ok(())
587 }
588
589 pub fn process_all(&mut self, collectors: &mut [&mut dyn Collector]) -> SubtrActorResult<()> {
598 for (index, frame) in self
599 .replay
600 .network_frames
601 .as_ref()
602 .ok_or(SubtrActorError::new(
603 SubtrActorErrorVariant::NoNetworkFrames,
604 ))?
605 .frames
606 .iter()
607 .enumerate()
608 {
609 self.actor_state.process_frame(frame, index)?;
610 self.update_mappings(frame)?;
611 self.update_ball_id(frame)?;
612 self.update_boost_amounts(frame, index)?;
613 self.update_boost_pad_events(frame, index)?;
614 self.update_touch_events(frame, index)?;
615 self.update_dodge_refreshed_events(frame, index)?;
616 self.update_goal_events(frame, index)?;
617 self.update_player_stat_events(frame, index)?;
618 self.update_demolishes(frame, index)?;
619
620 for collector in collectors.iter_mut() {
621 collector.process_frame(self, frame, index, frame.time)?;
622 }
623 }
624 for collector in collectors.iter_mut() {
625 collector.finish_replay(self)?;
626 }
627 Ok(())
628 }
629
630 pub fn reset(&mut self) {
632 self.ball_actor_id = None;
633 self.player_to_car = HashMap::new();
634 self.player_to_team = HashMap::new();
635 self.player_to_actor_id = HashMap::new();
636 self.car_to_player = HashMap::new();
637 self.car_to_boost = HashMap::new();
638 self.car_to_jump = HashMap::new();
639 self.car_to_double_jump = HashMap::new();
640 self.car_to_dodge = HashMap::new();
641 self.actor_state = ActorStateModeler::new();
642 self.boost_pad_events = Vec::new();
643 self.current_frame_boost_pad_events = Vec::new();
644 self.boost_pad_pickup_sequence_times = HashMap::new();
645 self.touch_events = Vec::new();
646 self.current_frame_touch_events = Vec::new();
647 self.dodge_refreshed_events = Vec::new();
648 self.current_frame_dodge_refreshed_events = Vec::new();
649 self.dodge_refreshed_counters = HashMap::new();
650 self.goal_events = Vec::new();
651 self.current_frame_goal_events = Vec::new();
652 self.player_stat_events = Vec::new();
653 self.current_frame_player_stat_events = Vec::new();
654 self.player_stat_counters = HashMap::new();
655 self.demolishes = Vec::new();
656 self.known_demolishes = Vec::new();
657 self.demolish_format = None;
658 self.kickoff_phase_active_last_frame = false;
659 }
660}
661
662#[cfg(test)]
663mod tests {
664 use super::ReplayProcessor;
665
666 #[test]
667 fn rigid_body_normalization_factors_split_at_expected_legacy_boundary() {
668 assert_eq!(
669 ReplayProcessor::rigid_body_location_normalization_factor_for_net_version(None),
670 100.0
671 );
672 assert_eq!(
673 ReplayProcessor::rigid_body_velocity_normalization_factor_for_net_version(None),
674 10.0
675 );
676 assert_eq!(
677 ReplayProcessor::rigid_body_location_normalization_factor_for_net_version(Some(2)),
678 100.0
679 );
680 assert_eq!(
681 ReplayProcessor::rigid_body_velocity_normalization_factor_for_net_version(Some(2)),
682 10.0
683 );
684 assert!(ReplayProcessor::uses_legacy_rigid_body_rotation_for_net_version(Some(2)));
685 assert_eq!(
686 ReplayProcessor::rigid_body_location_normalization_factor_for_net_version(Some(5)),
687 1.0
688 );
689 assert_eq!(
690 ReplayProcessor::rigid_body_velocity_normalization_factor_for_net_version(Some(5)),
691 1.0
692 );
693 assert!(ReplayProcessor::uses_legacy_rigid_body_rotation_for_net_version(Some(5)));
694 assert_eq!(
695 ReplayProcessor::rigid_body_location_normalization_factor_for_net_version(Some(10)),
696 1.0
697 );
698 assert_eq!(
699 ReplayProcessor::rigid_body_velocity_normalization_factor_for_net_version(Some(10)),
700 1.0
701 );
702 assert!(!ReplayProcessor::uses_legacy_rigid_body_rotation_for_net_version(Some(7)));
703 assert!(!ReplayProcessor::uses_legacy_rigid_body_rotation_for_net_version(Some(10)));
704 }
705}