1use super::*;
2
3impl<'a> ReplayProcessor<'a> {
4 pub fn find_update_in_direction(
6 &self,
7 current_index: usize,
8 actor_id: &boxcars::ActorId,
9 object_id: &boxcars::ObjectId,
10 direction: SearchDirection,
11 ) -> SubtrActorResult<(boxcars::Attribute, usize)> {
12 let frames = self
13 .replay
14 .network_frames
15 .as_ref()
16 .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::NoNetworkFrames))?;
17 match direction {
18 SearchDirection::Forward => {
19 for index in (current_index + 1)..frames.frames.len() {
20 if let Some(attribute) = frames.frames[index]
21 .updated_actors
22 .iter()
23 .find(|update| {
24 &update.actor_id == actor_id && &update.object_id == object_id
25 })
26 .map(|update| update.attribute.clone())
27 {
28 return Ok((attribute, index));
29 }
30 }
31 }
32 SearchDirection::Backward => {
33 for index in (0..current_index).rev() {
34 if let Some(attribute) = frames.frames[index]
35 .updated_actors
36 .iter()
37 .find(|update| {
38 &update.actor_id == actor_id && &update.object_id == object_id
39 })
40 .map(|update| update.attribute.clone())
41 {
42 return Ok((attribute, index));
43 }
44 }
45 }
46 }
47
48 SubtrActorError::new_result(SubtrActorErrorVariant::NoUpdateAfterFrame {
49 actor_id: *actor_id,
50 object_id: *object_id,
51 frame_index: current_index,
52 })
53 }
54
55 pub fn get_player_id_from_car_id(
57 &self,
58 actor_id: &boxcars::ActorId,
59 ) -> SubtrActorResult<PlayerId> {
60 self.get_player_id_from_actor_id(&self.get_player_actor_id_from_car_actor_id(actor_id)?)
61 }
62
63 pub(crate) fn get_player_id_from_actor_id(
65 &self,
66 actor_id: &boxcars::ActorId,
67 ) -> SubtrActorResult<PlayerId> {
68 for (player_id, player_actor_id) in self.player_to_actor_id.iter() {
69 if actor_id == player_actor_id {
70 return Ok(player_id.clone());
71 }
72 }
73 SubtrActorError::new_result(SubtrActorErrorVariant::NoMatchingPlayerId {
74 actor_id: *actor_id,
75 })
76 }
77
78 fn get_player_actor_id_from_car_actor_id(
79 &self,
80 actor_id: &boxcars::ActorId,
81 ) -> SubtrActorResult<boxcars::ActorId> {
82 self.car_to_player.get(actor_id).copied().ok_or_else(|| {
83 SubtrActorError::new(SubtrActorErrorVariant::NoMatchingPlayerId {
84 actor_id: *actor_id,
85 })
86 })
87 }
88
89 pub(crate) fn demolish_is_known(&self, demo: &DemolishAttribute, frame_index: usize) -> bool {
91 self.known_demolishes
92 .iter()
93 .any(|(existing, existing_frame_index)| {
94 existing == demo
95 && frame_index
96 .checked_sub(*existing_frame_index)
97 .or_else(|| existing_frame_index.checked_sub(frame_index))
98 .unwrap()
99 < MAX_DEMOLISH_KNOWN_FRAMES_PASSED
100 })
101 }
102
103 pub fn get_demolish_format(&self) -> Option<DemolishFormat> {
105 self.demolish_format
106 }
107
108 pub fn current_frame_boost_pad_events(&self) -> &[BoostPadEvent] {
110 &self.current_frame_boost_pad_events
111 }
112
113 pub(crate) fn resolved_boost_pads(&self) -> Vec<ResolvedBoostPad> {
118 self.boost_pad_resolution.resolved_boost_pads()
119 }
120
121 pub fn current_frame_touch_events(&self) -> &[TouchEvent] {
123 &self.current_frame_touch_events
124 }
125
126 pub fn current_frame_dodge_refreshed_events(&self) -> &[DodgeRefreshedEvent] {
128 &self.current_frame_dodge_refreshed_events
129 }
130
131 pub fn current_frame_goal_events(&self) -> &[GoalEvent] {
133 &self.current_frame_goal_events
134 }
135
136 pub fn current_frame_player_stat_events(&self) -> &[PlayerStatEvent] {
138 &self.current_frame_player_stat_events
139 }
140
141 pub fn detect_demolish_format(&self) -> Option<DemolishFormat> {
143 let actors = self.iter_actors_by_type_err(CAR_TYPE).ok()?;
144 for (_actor_id, state) in actors {
145 if get_attribute_errors_expected!(
146 self,
147 &state.attributes,
148 DEMOLISH_EXTENDED_KEY,
149 boxcars::Attribute::DemolishExtended
150 )
151 .is_ok()
152 {
153 return Some(DemolishFormat::Extended);
154 }
155 if get_attribute_errors_expected!(
156 self,
157 &state.attributes,
158 DEMOLISH_GOAL_EXPLOSION_KEY,
159 boxcars::Attribute::DemolishFx
160 )
161 .is_ok()
162 {
163 return Some(DemolishFormat::Fx);
164 }
165 }
166 None
167 }
168
169 pub fn get_active_demos(
171 &self,
172 ) -> SubtrActorResult<impl Iterator<Item = DemolishAttribute> + '_> {
173 let format = self.demolish_format;
174 let actors: Vec<_> = self.iter_actors_by_type_err(CAR_TYPE)?.collect();
175 Ok(actors
176 .into_iter()
177 .filter_map(move |(_actor_id, state)| match format {
178 Some(DemolishFormat::Extended) => get_attribute_errors_expected!(
179 self,
180 &state.attributes,
181 DEMOLISH_EXTENDED_KEY,
182 boxcars::Attribute::DemolishExtended
183 )
184 .ok()
185 .map(|demo| DemolishAttribute::Extended(**demo)),
186 Some(DemolishFormat::Fx) => get_attribute_errors_expected!(
187 self,
188 &state.attributes,
189 DEMOLISH_GOAL_EXPLOSION_KEY,
190 boxcars::Attribute::DemolishFx
191 )
192 .ok()
193 .map(|demo| DemolishAttribute::Fx(**demo)),
194 None => None,
195 }))
196 }
197
198 fn get_frame(&self, frame_index: usize) -> SubtrActorResult<&boxcars::Frame> {
199 self.replay
200 .network_frames
201 .as_ref()
202 .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::NoNetworkFrames))?
203 .frames
204 .get(frame_index)
205 .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::FrameIndexOutOfBounds))
206 }
207
208 pub(crate) fn velocities_applied_rigid_body(
209 &self,
210 rigid_body: &boxcars::RigidBody,
211 rb_frame_index: usize,
212 target_time: f32,
213 ) -> SubtrActorResult<boxcars::RigidBody> {
214 let rb_frame = self.get_frame(rb_frame_index)?;
215 let interpolation_amount = target_time - rb_frame.time;
216 let normalized_rigid_body = self.normalize_rigid_body(rigid_body);
217 Ok(apply_velocities_to_rigid_body(
218 &normalized_rigid_body,
219 interpolation_amount,
220 ))
221 }
222
223 pub fn get_interpolated_actor_rigid_body(
225 &self,
226 actor_id: &boxcars::ActorId,
227 time: f32,
228 close_enough: f32,
229 ) -> SubtrActorResult<boxcars::RigidBody> {
230 let (frame_body, frame_index) = self.get_actor_rigid_body(actor_id)?;
231 let frame_time = self.get_frame(*frame_index)?.time;
232 let time_and_frame_difference = time - frame_time;
233
234 if time_and_frame_difference.abs() <= close_enough.abs() {
235 return Ok(self.normalize_rigid_body(frame_body));
236 }
237
238 let search_direction = if time_and_frame_difference > 0.0 {
239 SearchDirection::Forward
240 } else {
241 SearchDirection::Backward
242 };
243
244 let object_id = self.get_object_id_for_key(RIGID_BODY_STATE_KEY)?;
245
246 let (attribute, found_frame) =
247 self.find_update_in_direction(*frame_index, actor_id, object_id, search_direction)?;
248 let found_time = self.get_frame(found_frame)?.time;
249
250 let found_body = attribute_match!(attribute, boxcars::Attribute::RigidBody)?;
251
252 if (found_time - time).abs() <= close_enough {
253 return Ok(self.normalize_rigid_body(&found_body));
254 }
255
256 let (start_body, start_time, end_body, end_time) = match search_direction {
257 SearchDirection::Forward => (frame_body, frame_time, &found_body, found_time),
258 SearchDirection::Backward => (&found_body, found_time, frame_body, frame_time),
259 };
260 let start_body = self.normalize_rigid_body(start_body);
261 let end_body = self.normalize_rigid_body(end_body);
262
263 get_interpolated_rigid_body(&start_body, start_time, &end_body, end_time, time)
264 }
265
266 pub fn get_object_id_for_key(
268 &self,
269 name: &'static str,
270 ) -> SubtrActorResult<&boxcars::ObjectId> {
271 self.name_to_object_id
272 .get(name)
273 .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::ObjectIdNotFound { name }))
274 }
275
276 pub fn get_actor_ids_by_type(
278 &self,
279 name: &'static str,
280 ) -> SubtrActorResult<&[boxcars::ActorId]> {
281 self.get_object_id_for_key(name)
282 .map(|object_id| self.get_actor_ids_by_object_id(object_id))
283 }
284
285 pub(crate) fn get_actor_ids_by_object_id(
286 &self,
287 object_id: &boxcars::ObjectId,
288 ) -> &[boxcars::ActorId] {
289 self.actor_state
290 .actor_ids_by_type
291 .get(object_id)
292 .map(|v| &v[..])
293 .unwrap_or_else(|| &EMPTY_ACTOR_IDS)
294 }
295
296 pub(crate) fn get_actor_state(
298 &self,
299 actor_id: &boxcars::ActorId,
300 ) -> SubtrActorResult<&ActorState> {
301 self.actor_state.actor_states.get(actor_id).ok_or_else(|| {
302 SubtrActorError::new(SubtrActorErrorVariant::NoStateForActorId {
303 actor_id: *actor_id,
304 })
305 })
306 }
307
308 pub(crate) fn get_actor_state_or_recently_deleted(
310 &self,
311 actor_id: &boxcars::ActorId,
312 ) -> SubtrActorResult<&ActorState> {
313 self.actor_state
314 .actor_states
315 .get(actor_id)
316 .or_else(|| self.actor_state.recently_deleted_actor_states.get(actor_id))
317 .ok_or_else(|| {
318 SubtrActorError::new(SubtrActorErrorVariant::NoStateForActorId {
319 actor_id: *actor_id,
320 })
321 })
322 }
323
324 fn get_actor_object_name(&self, actor_id: &boxcars::ActorId) -> Option<String> {
325 self.actor_state
326 .actor_states
327 .get(actor_id)
328 .and_then(|state| self.object_id_to_name.get(&state.object_id))
329 .cloned()
330 .or_else(|| {
331 usize::try_from(actor_id.0)
332 .ok()
333 .and_then(|object_index| self.replay.objects.get(object_index))
334 .cloned()
335 })
336 }
337
338 fn get_first_attribute_by_object_id(
339 &self,
340 object_id: Option<boxcars::ObjectId>,
341 ) -> Option<&boxcars::Attribute> {
342 let object_id = object_id?;
343 self.actor_state.actor_states.values().find_map(|state| {
344 state
345 .attributes
346 .get(&object_id)
347 .map(|(attribute, _)| attribute)
348 })
349 }
350
351 pub fn get_replicated_game_playlist(&self) -> Option<i32> {
353 match self.get_first_attribute_by_object_id(self.cached_object_ids.replicated_game_playlist)
354 {
355 Some(boxcars::Attribute::Int(playlist_id)) => Some(*playlist_id),
356 _ => None,
357 }
358 }
359
360 pub fn get_match_type_class(&self) -> Option<String> {
362 match self.get_first_attribute_by_object_id(self.cached_object_ids.match_type_class) {
363 Some(boxcars::Attribute::ActiveActor(active_actor)) => {
364 self.get_actor_object_name(&active_actor.actor)
365 }
366 _ => None,
367 }
368 }
369
370 pub fn get_replay_game_type_details(&self) -> ReplayGameTypeDetails {
372 self.game_type_details.clone()
373 }
374
375 pub(crate) fn update_game_type_details(&mut self) {
376 self.game_type_details = self.game_type_details.with_network_signals(
377 self.get_replicated_game_playlist(),
378 self.get_match_type_class(),
379 );
380 }
381
382 pub(crate) fn get_actor_attribute<'b>(
383 &'b self,
384 actor_id: &boxcars::ActorId,
385 property: &'static str,
386 ) -> SubtrActorResult<&'b boxcars::Attribute> {
387 self.get_attribute(&self.get_actor_state(actor_id)?.attributes, property)
388 }
389
390 pub fn get_attribute<'b>(
392 &'b self,
393 map: &'b HashMap<boxcars::ObjectId, (boxcars::Attribute, usize)>,
394 property: &'static str,
395 ) -> SubtrActorResult<&'b boxcars::Attribute> {
396 self.get_attribute_and_updated(map, property).map(|v| &v.0)
397 }
398
399 pub fn get_attribute_and_updated<'b>(
401 &'b self,
402 map: &'b HashMap<boxcars::ObjectId, (boxcars::Attribute, usize)>,
403 property: &'static str,
404 ) -> SubtrActorResult<&'b (boxcars::Attribute, usize)> {
405 let attribute_object_id = self.get_object_id_for_key(property)?;
406 map.get(attribute_object_id).ok_or_else(|| {
407 SubtrActorError::new(SubtrActorErrorVariant::PropertyNotFoundInState { property })
408 })
409 }
410
411 pub(crate) fn find_ball_actor(&self) -> Option<boxcars::ActorId> {
416 self.actor_state
417 .actor_ids_by_type
418 .iter()
419 .filter(|(object_id, _)| {
420 self.object_id_to_name
421 .get(object_id)
422 .is_some_and(|name| name.starts_with(BALL_TYPE_PREFIX))
423 })
424 .flat_map(|(_, actor_ids)| actor_ids.iter().copied())
425 .next()
426 }
427
428 pub fn get_ball_actor_id(&self) -> SubtrActorResult<boxcars::ActorId> {
430 self.ball_actor_id
431 .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::BallActorNotFound))
432 }
433
434 pub fn get_metadata_actor_id(&self) -> SubtrActorResult<boxcars::ActorId> {
436 if let Ok(actor_ids) = self.get_actor_ids_by_type(GAME_TYPE) {
437 if let Some(actor_id) = actor_ids.first() {
438 return Ok(*actor_id);
439 }
440 }
441
442 let metadata_object_ids = [
443 self.cached_object_ids.seconds_remaining,
444 self.cached_object_ids.replicated_state_name,
445 self.cached_object_ids.replicated_game_state_time_remaining,
446 self.cached_object_ids.ball_has_been_hit,
447 ];
448
449 self.actor_state
450 .actor_states
451 .iter()
452 .filter_map(|(actor_id, actor_state)| {
453 let metadata_attribute_count = metadata_object_ids
454 .iter()
455 .flatten()
456 .filter(|object_id| actor_state.attributes.contains_key(object_id))
457 .count();
458 (metadata_attribute_count > 0).then_some((
459 metadata_attribute_count,
460 std::cmp::Reverse(*actor_id),
461 *actor_id,
462 ))
463 })
464 .max()
465 .map(|(_, _, actor_id)| actor_id)
466 .ok_or_else(|| SubtrActorError::new(SubtrActorErrorVariant::NoGameActor))
467 }
468
469 pub fn get_player_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
471 self.player_to_actor_id
472 .get(player_id)
473 .ok_or_else(|| {
474 SubtrActorError::new(SubtrActorErrorVariant::ActorNotFound {
475 name: "ActorId",
476 player_id: player_id.clone(),
477 })
478 })
479 .cloned()
480 }
481
482 pub fn get_car_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
484 self.player_to_car
485 .get(&self.get_player_actor_id(player_id)?)
486 .ok_or_else(|| {
487 SubtrActorError::new(SubtrActorErrorVariant::ActorNotFound {
488 name: "Car",
489 player_id: player_id.clone(),
490 })
491 })
492 .cloned()
493 }
494
495 pub fn get_car_connected_actor_id(
497 &self,
498 player_id: &PlayerId,
499 map: &HashMap<boxcars::ActorId, boxcars::ActorId>,
500 name: &'static str,
501 ) -> SubtrActorResult<boxcars::ActorId> {
502 map.get(&self.get_car_actor_id(player_id)?)
503 .ok_or_else(|| {
504 SubtrActorError::new(SubtrActorErrorVariant::ActorNotFound {
505 name,
506 player_id: player_id.clone(),
507 })
508 })
509 .cloned()
510 }
511
512 pub fn get_boost_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
514 self.get_car_connected_actor_id(player_id, &self.car_to_boost, "Boost")
515 }
516
517 pub fn get_jump_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
519 self.get_car_connected_actor_id(player_id, &self.car_to_jump, "Jump")
520 }
521
522 pub fn get_double_jump_actor_id(
524 &self,
525 player_id: &PlayerId,
526 ) -> SubtrActorResult<boxcars::ActorId> {
527 self.get_car_connected_actor_id(player_id, &self.car_to_double_jump, "Double Jump")
528 }
529
530 pub fn get_dodge_actor_id(&self, player_id: &PlayerId) -> SubtrActorResult<boxcars::ActorId> {
532 self.get_car_connected_actor_id(player_id, &self.car_to_dodge, "Dodge")
533 }
534
535 pub fn get_actor_rigid_body(
537 &self,
538 actor_id: &boxcars::ActorId,
539 ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
540 get_attribute_and_updated!(
541 self,
542 &self.get_actor_state(actor_id)?.attributes,
543 RIGID_BODY_STATE_KEY,
544 boxcars::Attribute::RigidBody
545 )
546 }
547
548 pub fn get_actor_rigid_body_or_recently_deleted(
550 &self,
551 actor_id: &boxcars::ActorId,
552 ) -> SubtrActorResult<(&boxcars::RigidBody, &usize)> {
553 get_attribute_and_updated!(
554 self,
555 &self
556 .get_actor_state_or_recently_deleted(actor_id)?
557 .attributes,
558 RIGID_BODY_STATE_KEY,
559 boxcars::Attribute::RigidBody
560 )
561 }
562
563 pub fn iter_player_ids_in_order(&self) -> impl Iterator<Item = &PlayerId> {
565 self.team_zero.iter().chain(self.team_one.iter())
566 }
567
568 pub fn current_in_game_team_player_counts(&self) -> [usize; 2] {
570 let mut counts = [0, 0];
571 let Ok(player_actor_ids) = self.get_actor_ids_by_type(PLAYER_TYPE) else {
572 return counts;
573 };
574 let mut seen_players = std::collections::HashSet::new();
575
576 for actor_id in player_actor_ids {
577 let Ok(player_id) = self.get_player_id_from_actor_id(actor_id) else {
578 continue;
579 };
580 if !seen_players.insert(player_id) {
581 continue;
582 }
583
584 let Some(team_actor_id) = self.player_to_team.get(actor_id) else {
585 continue;
586 };
587 let Ok(team_state) = self.get_actor_state(team_actor_id) else {
588 continue;
589 };
590 let Some(team_name) = self.object_id_to_name.get(&team_state.object_id) else {
591 continue;
592 };
593
594 match team_name.chars().last() {
595 Some('0') => counts[0] += 1,
596 Some('1') => counts[1] += 1,
597 _ => {}
598 }
599 }
600
601 counts
602 }
603
604 pub fn player_count(&self) -> usize {
606 self.iter_player_ids_in_order().count()
607 }
608
609 pub fn get_player_names(&self) -> HashMap<PlayerId, String> {
611 self.iter_player_ids_in_order()
612 .filter_map(|player_id| {
613 self.get_player_name(player_id)
614 .ok()
615 .map(|name| (player_id.clone(), name))
616 })
617 .collect()
618 }
619
620 pub(crate) fn iter_actors_by_type_err(
622 &self,
623 name: &'static str,
624 ) -> SubtrActorResult<impl Iterator<Item = (&boxcars::ActorId, &ActorState)>> {
625 Ok(self.iter_actors_by_object_id(self.get_object_id_for_key(name)?))
626 }
627
628 pub fn iter_actors_by_type(
630 &self,
631 name: &'static str,
632 ) -> Option<impl Iterator<Item = (&boxcars::ActorId, &ActorState)>> {
633 self.iter_actors_by_type_err(name).ok()
634 }
635
636 pub fn iter_actors_by_object_id<'b>(
638 &'b self,
639 object_id: &'b boxcars::ObjectId,
640 ) -> impl Iterator<Item = (&'b boxcars::ActorId, &'b ActorState)> + 'b {
641 let actor_ids = self
642 .actor_state
643 .actor_ids_by_type
644 .get(object_id)
645 .map(|v| &v[..])
646 .unwrap_or_else(|| &EMPTY_ACTOR_IDS);
647
648 actor_ids
649 .iter()
650 .map(move |id| (id, self.actor_state.actor_states.get(id).unwrap()))
651 }
652}