Skip to main content

subtr_actor/processor/
queries.rs

1use super::*;
2
3impl<'a> ReplayProcessor<'a> {
4    /// Searches forward or backward for the next update of a specific actor property.
5    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    /// Resolves a car actor id back to the owning player id.
56    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    /// Resolves a player-controller actor id back to the owning player id.
64    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    /// Returns whether a demolish has already been recorded within the dedupe window.
90    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    /// Returns the demolish attribute encoding currently used by the replay, if known.
104    pub fn get_demolish_format(&self) -> Option<DemolishFormat> {
105        self.demolish_format
106    }
107
108    /// Returns the boost-pad events detected while processing the current frame.
109    pub fn current_frame_boost_pad_events(&self) -> &[BoostPadEvent] {
110        &self.current_frame_boost_pad_events
111    }
112
113    /// Returns the standard Soccar boost pad layout annotated with replay pad ids when known.
114    ///
115    /// This is incremental reconstruction state and should only be materialized by
116    /// final replay-data assembly after the processor has completed its replay pass.
117    pub(crate) fn resolved_boost_pads(&self) -> Vec<ResolvedBoostPad> {
118        self.boost_pad_resolution.resolved_boost_pads()
119    }
120
121    /// Returns the touch events detected while processing the current frame.
122    pub fn current_frame_touch_events(&self) -> &[TouchEvent] {
123        &self.current_frame_touch_events
124    }
125
126    /// Returns the dodge-refresh events detected while processing the current frame.
127    pub fn current_frame_dodge_refreshed_events(&self) -> &[DodgeRefreshedEvent] {
128        &self.current_frame_dodge_refreshed_events
129    }
130
131    /// Returns the goal events detected while processing the current frame.
132    pub fn current_frame_goal_events(&self) -> &[GoalEvent] {
133        &self.current_frame_goal_events
134    }
135
136    /// Returns the player stat events detected while processing the current frame.
137    pub fn current_frame_player_stat_events(&self) -> &[PlayerStatEvent] {
138        &self.current_frame_player_stat_events
139    }
140
141    /// Inspects current actor state to infer which demolish attribute format is present.
142    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    /// Returns an iterator over currently active demolish attributes in actor state.
170    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    /// Interpolates an arbitrary actor rigid body to the requested replay time.
224    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    /// Looks up the object id associated with a replay property name.
267    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    /// Returns the actor ids currently associated with a named object type.
277    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    /// Returns the current modeled state for an actor id.
297    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    /// Returns current or recently deleted modeled state for an actor id.
309    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    /// Returns the replicated Rocket League playlist id, when present in network data.
352    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    /// Returns the resolved match-type class object name, when present in network data.
361    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    /// Returns the best known normalized game-type metadata.
371    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    /// Reads a property from an actor or derived-attribute map by property name.
391    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    /// Reads a property and the frame index when it was last updated.
400    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    /// Scans the actor graph for the first actor whose archetype lives under
412    /// `Archetypes.Ball.`. Matching the prefix rather than an explicit whitelist
413    /// means ball archetypes introduced by new or limited-time game modes are
414    /// recognized automatically.
415    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    /// Returns the tracked actor id for the replay ball.
429    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    /// Returns the main game metadata actor id.
435    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    /// Returns the actor id associated with a player id.
470    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    /// Returns the car actor id currently associated with a player.
483    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    /// Resolves a player to a connected component actor through the supplied mapping.
496    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    /// Returns the player's boost component actor id.
513    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    /// Returns the player's jump component actor id.
518    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    /// Returns the player's double-jump component actor id.
523    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    /// Returns the player's dodge component actor id.
531    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    /// Returns an actor's rigid body together with the frame index of its last update.
536    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    /// Like [`Self::get_actor_rigid_body`], but falls back to recently deleted actor state.
549    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    /// Iterates over players in the stable team-zero, then team-one ordering.
564    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    /// Counts currently in-game players per team from live actor state.
569    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    /// Returns the number of players in the stored replay ordering.
605    pub fn player_count(&self) -> usize {
606        self.iter_player_ids_in_order().count()
607    }
608
609    /// Returns a map from player ids to their resolved display names.
610    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    /// Iterates over actors of a named object type, returning an error if the type is unknown.
621    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    /// Iterates over actors of a named object type, if that type exists in the replay.
629    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    /// Iterates over actors for a concrete object id.
637    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}