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