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