rkg_utils/footer/sp_footer/
mod.rs1use crate::{
2 byte_handler::{ByteHandler, FromByteHandler},
3 footer::{ctgp_footer::exact_finish_time::ExactFinishTime, sp_footer::sp_version::SPVersion},
4 header::in_game_time::{InGameTime, InGameTimeError},
5};
6
7pub mod sp_version;
8
9#[derive(thiserror::Error, Debug)]
11pub enum SPFooterError {
12 #[error("Ghost is not SPGD")]
14 NotSPGD,
15 #[error("Invalid MKW-SP footer version")]
17 InvalidFooterVersion,
18 #[error("In Game Time Error: {0}")]
20 InGameTimeError(#[from] InGameTimeError),
21 #[error("Lap split index not semantically valid")]
23 LapSplitIndexError,
24 #[error("Try From Slice Error: {0}")]
26 TryFromSliceError(#[from] std::array::TryFromSliceError),
27}
28
29pub struct SPFooter {
35 raw_data: Vec<u8>,
37 footer_version: u32,
39 possible_sp_versions: Option<Vec<SPVersion>>,
42 track_sha1: [u8; 0x14],
44 exact_finish_time: ExactFinishTime,
46 exact_lap_times: [ExactFinishTime; 11],
48 has_speed_mod: bool,
50 has_ultra_shortcut: bool,
52 has_horizontal_wall_glitch: bool,
54 has_wallride: bool,
56 shroomstrat: Option<[u8; 11]>,
58 is_vanilla_mode_enabled: Option<bool>,
60 has_simplified_controls: Option<bool>,
62 set_in_mirror: Option<bool>,
64 len: u32,
66 lap_count: u8,
68}
69
70impl SPFooter {
71 pub fn new(data: &[u8]) -> Result<Self, SPFooterError> {
89 if data[data.len() - 0x08..data.len() - 0x04] != *b"SPGD" {
90 return Err(SPFooterError::NotSPGD);
91 }
92
93 let footer_len = (u32::from_be_bytes(
94 data[data.len() - 0x0C..data.len() - 0x08]
95 .try_into()
96 .unwrap(),
97 ) + 0x08) as usize;
98
99 let lap_count = data[0x10];
100 let laps_data = &data[0x11..0x32];
101
102 let footer_data = &data[data.len() - footer_len - 0x04..data.len() - 0x04];
103
104 let footer_version = u32::from_be_bytes(footer_data[..0x04].try_into().unwrap());
105
106 if footer_version > 5 {
107 return Err(SPFooterError::InvalidFooterVersion);
108 }
109
110 let possible_sp_versions = SPVersion::from(footer_version);
111
112 let mut current_offset = 0x04;
113
114 let track_sha1 = footer_data[current_offset..current_offset + 0x14]
115 .to_owned()
116 .try_into()
117 .unwrap();
118 current_offset += 0x14;
119
120 let mut previous_subtractions = 0i64;
122 let mut exact_lap_times = [ExactFinishTime::default(); 11];
123 let mut in_game_time_offset = 0x00usize;
124 let mut subtraction_ps = 0i64;
125
126 for exact_lap_time in exact_lap_times.iter_mut().take(lap_count as usize) {
127 let mut true_time_subtraction = ((f32::from_be_bytes(
128 footer_data[current_offset..current_offset + 0x04].try_into()?,
129 ) as f64)
130 * 1e+9)
131 .floor() as i64;
132
133 let lap_time = InGameTime::from_byte_handler(
134 &laps_data[in_game_time_offset..=in_game_time_offset + 0x02],
135 )?;
136
137 true_time_subtraction -= previous_subtractions;
140
141 if true_time_subtraction > 1e+9 as i64 {
142 true_time_subtraction -= subtraction_ps;
143 subtraction_ps = if subtraction_ps == 0 { 1e+9 as i64 } else { 0 };
144 }
145 previous_subtractions += true_time_subtraction;
146 *exact_lap_time = ExactFinishTime::new(
147 lap_time.minutes(),
148 lap_time.seconds(),
149 (lap_time.milliseconds() as i64 * 1e+9 as i64 + true_time_subtraction) as u64,
150 );
151 in_game_time_offset += 0x03;
152 current_offset += 0x04;
153 }
154
155 let exact_finish_time = exact_lap_times[..lap_count as usize].iter().copied().sum();
156
157 current_offset += (11 - lap_count as usize) * 0x04;
158
159 let bools = ByteHandler::from(footer_data[current_offset]);
160 let has_speed_mod = bools.read_bool(7);
161 let has_ultra_shortcut = bools.read_bool(6);
162 let has_horizontal_wall_glitch = bools.read_bool(5);
163 let has_wallride = bools.read_bool(4);
164
165 let shroomstrat;
166
167 if footer_version >= 1 {
168 let shroom_data: [u8; 3] = footer_data[current_offset..current_offset + 0x03]
169 .try_into()
170 .unwrap();
171
172 let mut shroom_arr = [0u8; 11];
173 let mut shrooms = [0u8; 3];
174
175 let raw = u32::from_be_bytes([0, shroom_data[0], shroom_data[1], shroom_data[2]]);
176 shrooms[0] = ((raw >> 15) & 0x1F) as u8;
177 shrooms[1] = ((raw >> 10) & 0x1F) as u8;
178 shrooms[2] = ((raw >> 5) & 0x1F) as u8;
179
180 for shroom in shrooms.iter() {
181 if *shroom != 0 {
182 shroom_arr[*shroom as usize - 1] += 1;
183 }
184 }
185 shroomstrat = Some(shroom_arr);
186 } else {
187 shroomstrat = None;
188 }
189
190 current_offset += 0x02;
191
192 let bools = ByteHandler::from(footer_data[current_offset]);
193
194 let is_vanilla_mode_enabled = if footer_version >= 3 {
195 Some(bools.read_bool(4))
196 } else {
197 None
198 };
199
200 let has_simplified_controls = if footer_version >= 4 {
201 Some(bools.read_bool(3))
202 } else {
203 None
204 };
205
206 let set_in_mirror = if footer_version >= 5 {
207 Some(bools.read_bool(2))
208 } else {
209 None
210 };
211
212 Ok(Self {
213 raw_data: footer_data.to_owned(),
214 footer_version,
215 possible_sp_versions,
216 track_sha1,
217 exact_finish_time,
218 exact_lap_times,
219 has_speed_mod,
220 has_ultra_shortcut,
221 has_horizontal_wall_glitch,
222 has_wallride,
223 shroomstrat,
224 is_vanilla_mode_enabled,
225 has_simplified_controls,
226 set_in_mirror,
227 len: footer_len as u32,
228 lap_count,
229 })
230 }
231
232 pub fn raw_data(&self) -> &[u8] {
234 &self.raw_data
235 }
236
237 pub fn footer_version(&self) -> u32 {
239 self.footer_version
240 }
241
242 pub fn possible_sp_versions(&self) -> Option<&Vec<SPVersion>> {
246 self.possible_sp_versions.as_ref()
247 }
248
249 pub fn track_sha1(&self) -> &[u8; 0x14] {
251 &self.track_sha1
252 }
253
254 pub fn exact_finish_time(&self) -> ExactFinishTime {
256 self.exact_finish_time
257 }
258
259 pub fn exact_lap_times(&self) -> &[ExactFinishTime] {
261 &self.exact_lap_times[..self.lap_count as usize]
262 }
263
264 pub fn exact_lap_time(&self, idx: usize) -> Result<ExactFinishTime, SPFooterError> {
271 if idx >= self.lap_count as usize {
272 return Err(SPFooterError::LapSplitIndexError);
273 }
274 Ok(self.exact_lap_times[idx])
275 }
276
277 pub fn has_speed_mod(&self) -> bool {
279 self.has_speed_mod
280 }
281
282 pub fn has_ultra_shortcut(&self) -> bool {
284 self.has_ultra_shortcut
285 }
286
287 pub fn has_horizontal_wall_glitch(&self) -> bool {
289 self.has_horizontal_wall_glitch
290 }
291
292 pub fn has_wallride(&self) -> bool {
294 self.has_wallride
295 }
296
297 pub fn shroomstrat(&self) -> Option<&[u8]> {
301 self.shroomstrat
302 .as_ref()
303 .map(|s| &s[..self.lap_count as usize])
304 }
305
306 pub fn shroomstrat_string(&self) -> Option<String> {
312 if let Some(shroomstrat) = self.shroomstrat() {
313 let mut s = String::new();
314
315 for (idx, lap) in shroomstrat.iter().enumerate() {
316 s += lap.to_string().as_str();
317
318 if idx + 1 < self.lap_count as usize {
319 s += "-";
320 }
321 }
322 Some(s)
323 } else {
324 None
325 }
326 }
327
328 pub fn is_vanilla_mode_enabled(&self) -> Option<bool> {
332 self.is_vanilla_mode_enabled
333 }
334
335 pub fn has_simplified_controls(&self) -> Option<bool> {
339 self.has_simplified_controls
340 }
341
342 pub fn set_in_mirror(&self) -> Option<bool> {
346 self.set_in_mirror
347 }
348
349 pub fn len(&self) -> usize {
351 self.len as usize
352 }
353
354 pub fn is_empty(&self) -> bool {
356 self.len == 0
357 }
358}