Skip to main content

subtr_actor/processor/
bootstrap.rs

1use super::*;
2
3impl<'a> ReplayProcessor<'a> {
4    /// Attempts to seed player ordering from replay headers before falling back to frames.
5    pub(crate) fn set_player_order_from_headers(&mut self) -> SubtrActorResult<()> {
6        let _player_stats = self
7            .replay
8            .properties
9            .iter()
10            .find(|(key, _)| key == "PlayerStats")
11            .ok_or_else(|| {
12                SubtrActorError::new(SubtrActorErrorVariant::PlayerStatsHeaderNotFound)
13            })?;
14        // XXX: implementation incomplete
15        SubtrActorError::new_result(SubtrActorErrorVariant::PlayerStatsHeaderNotFound)
16    }
17
18    /// Processes the replay until it has gathered enough information to map
19    /// players to their actor IDs.
20    ///
21    /// This function is designed to ensure that each player that participated
22    /// in the game is associated with a corresponding actor ID. It runs the
23    /// processing operation for approximately the first 10 seconds of the
24    /// replay (10 * 30 frames), as this time span is generally sufficient to
25    /// identify all players.
26    ///
27    /// Note that this function is particularly necessary because the headers of
28    /// replays sometimes omit some players.
29    ///
30    /// # Errors
31    ///
32    /// If any error other than `FinishProcessingEarly` occurs during the
33    /// processing operation, it is propagated up by this function.
34    pub fn process_long_enough_to_get_actor_ids(&mut self) -> SubtrActorResult<()> {
35        let mut handler = |_p: &ReplayProcessor, _f: &boxcars::Frame, n: usize, _current_time| {
36            // XXX: 10 seconds should be enough to find everyone, right?
37            if n > 10 * 30 {
38                SubtrActorError::new_result(SubtrActorErrorVariant::FinishProcessingEarly)
39            } else {
40                Ok(TimeAdvance::NextFrame)
41            }
42        };
43        let process_result = self.process(&mut handler);
44        if let Some(SubtrActorErrorVariant::FinishProcessingEarly) =
45            process_result.as_ref().err().map(|e| e.variant.clone())
46        {
47            Ok(())
48        } else {
49            process_result
50        }
51    }
52
53    /// Rebuilds team ordering by sampling early replay frames for player/team links.
54    pub(crate) fn set_player_order_from_frames(&mut self) -> SubtrActorResult<()> {
55        self.process_long_enough_to_get_actor_ids()?;
56        let player_to_team_0: HashMap<PlayerId, bool> = self
57            .player_to_actor_id
58            .keys()
59            .filter_map(|player_id| {
60                self.get_player_is_team_0(player_id)
61                    .ok()
62                    .map(|is_team_0| (player_id.clone(), is_team_0))
63            })
64            .collect();
65
66        let (team_zero, team_one): (Vec<_>, Vec<_>) = player_to_team_0
67            .keys()
68            .cloned()
69            // The unwrap here is fine because we know the get will succeed
70            .partition(|player_id| *player_to_team_0.get(player_id).unwrap());
71
72        self.team_zero = team_zero;
73        self.team_one = team_one;
74
75        self.team_zero
76            .sort_by(|a, b| format!("{a:?}").cmp(&format!("{b:?}")));
77        self.team_one
78            .sort_by(|a, b| format!("{a:?}").cmp(&format!("{b:?}")));
79
80        self.reset();
81        Ok(())
82    }
83
84    /// Verifies that the discovered in-replay players match the stored player ordering.
85    pub fn check_player_id_set(&self) -> SubtrActorResult<()> {
86        let known_players =
87            std::collections::HashSet::<_>::from_iter(self.player_to_actor_id.keys());
88        let original_players =
89            std::collections::HashSet::<_>::from_iter(self.iter_player_ids_in_order());
90
91        if original_players != known_players {
92            SubtrActorError::new_result(SubtrActorErrorVariant::InconsistentPlayerSet {
93                found: known_players.into_iter().cloned().collect(),
94                original: original_players.into_iter().cloned().collect(),
95            })
96        } else {
97            Ok(())
98        }
99    }
100
101    /// Processes the replay enough to get the actor IDs and then retrieves the replay metadata.
102    ///
103    /// This method is a convenience function that combines the functionalities
104    /// of
105    /// [`process_long_enough_to_get_actor_ids`](Self::process_long_enough_to_get_actor_ids)
106    /// and [`get_replay_meta`](Self::get_replay_meta) into a single operation.
107    /// It's meant to be used when you don't necessarily want to process the
108    /// whole replay and need only the replay's metadata.
109    pub fn process_and_get_replay_meta(&mut self) -> SubtrActorResult<ReplayMeta> {
110        if self.player_to_actor_id.is_empty() {
111            self.process_long_enough_to_get_actor_ids()?;
112        }
113        self.get_replay_meta()
114    }
115
116    /// Retrieves the replay metadata.
117    ///
118    /// This function collects information about each player in the replay and
119    /// groups them by team. For each player, it gets the player's name and
120    /// statistics. All this information is then wrapped into a [`ReplayMeta`]
121    /// object along with the properties from the replay.
122    pub fn get_replay_meta(&self) -> SubtrActorResult<ReplayMeta> {
123        let empty_player_stats = Vec::new();
124        let player_stats = if let Some((_, boxcars::HeaderProp::Array(per_player))) = self
125            .replay
126            .properties
127            .iter()
128            .find(|(key, _)| key == "PlayerStats")
129        {
130            per_player
131        } else {
132            &empty_player_stats
133        };
134        let known_count = self.iter_player_ids_in_order().count();
135        if player_stats.len() != known_count {
136            log::warn!(
137                "Replay does not have player stats for all players. encountered {:?} {:?}",
138                known_count,
139                player_stats.len()
140            )
141        }
142        let get_player_info = |player_id| {
143            let fallback_name = String::new();
144            let stats = self
145                .get_player_name(player_id)
146                .ok()
147                .and_then(|name| find_player_stats(player_id, &name, player_stats).ok())
148                .or_else(|| find_player_stats(player_id, &fallback_name, player_stats).ok());
149            let name = self
150                .get_player_name(player_id)
151                .ok()
152                .or_else(|| {
153                    stats.as_ref().and_then(|stats| {
154                        stats.get("Name").and_then(|prop| match prop {
155                            boxcars::HeaderProp::Str(name) => Some(name.clone()),
156                            _ => None,
157                        })
158                    })
159                })
160                .unwrap_or_else(|| format!("{player_id:?}"));
161            Ok(PlayerInfo {
162                name,
163                stats,
164                remote_id: player_id.clone(),
165            })
166        };
167        let team_zero: SubtrActorResult<Vec<PlayerInfo>> =
168            self.team_zero.iter().map(get_player_info).collect();
169        let team_one: SubtrActorResult<Vec<PlayerInfo>> =
170            self.team_one.iter().map(get_player_info).collect();
171        Ok(ReplayMeta {
172            team_zero: team_zero?,
173            team_one: team_one?,
174            all_headers: self.replay.properties.clone(),
175        })
176    }
177}