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("File is not an RKG")]
17 NotRKGD,
18 #[error("Data length is too short")]
20 DataLengthTooShort,
21 #[error("Invalid MKW-SP footer version")]
23 InvalidFooterVersion,
24 #[error("In Game Time Error: {0}")]
26 InGameTimeError(#[from] InGameTimeError),
27 #[error("Lap split index not semantically valid")]
29 LapSplitIndexError,
30 #[error("Try From Slice Error: {0}")]
32 TryFromSliceError(#[from] std::array::TryFromSliceError),
33}
34
35pub struct SPFooter {
41 raw_data: Vec<u8>,
43 footer_version: u32,
45 possible_sp_versions: Option<Vec<SPVersion>>,
48 track_sha1: [u8; 0x14],
50 exact_finish_time: ExactFinishTime,
52 exact_lap_times: [ExactFinishTime; 11],
54 has_speed_mod: bool,
56 has_ultra_shortcut: bool,
58 has_horizontal_wall_glitch: bool,
60 has_wallride: bool,
62 shroomstrat: Option<[u8; 11]>,
64 is_vanilla_mode_enabled: Option<bool>,
66 has_simplified_controls: Option<bool>,
68 set_in_mirror: Option<bool>,
70 len: u32,
72 lap_count: u8,
74}
75
76impl SPFooter {
77 pub fn new(data: &[u8]) -> Result<Self, SPFooterError> {
95 if data.len() < 0x04 {
96 return Err(SPFooterError::DataLengthTooShort);
97 }
98
99 if data[..0x04] != *b"RKGD" {
100 return Err(SPFooterError::NotRKGD);
101 }
102
103 if data.len() < 0x08 {
104 return Err(SPFooterError::DataLengthTooShort);
105 }
106
107 if data[data.len() - 0x08..data.len() - 0x04] != *b"SPGD" {
108 return Err(SPFooterError::NotSPGD);
109 }
110
111 if data.len() < 0x0C {
112 return Err(SPFooterError::DataLengthTooShort);
113 }
114
115 let footer_len = (u32::from_be_bytes(
116 data[data.len() - 0x0C..data.len() - 0x08]
117 .try_into()
118 .unwrap(),
119 ) + 0x08) as usize;
120
121 if data.len() < footer_len {
122 return Err(SPFooterError::DataLengthTooShort);
123 }
124
125 let lap_count = data[0x10];
126 let laps_data = &data[0x11..0x32];
127
128 let footer_data = &data[data.len() - footer_len - 0x04..data.len() - 0x04];
129
130 let footer_version = u32::from_be_bytes(footer_data[..0x04].try_into().unwrap());
131
132 if footer_version > 5 {
133 return Err(SPFooterError::InvalidFooterVersion);
134 }
135
136 let possible_sp_versions = SPVersion::from(footer_version);
137
138 let mut current_offset = 0x04;
139
140 let track_sha1 = footer_data[current_offset..current_offset + 0x14]
141 .to_owned()
142 .try_into()
143 .unwrap();
144 current_offset += 0x14;
145
146 let mut previous_subtractions = 0i64;
148 let mut exact_lap_times = [ExactFinishTime::default(); 11];
149 let mut in_game_time_offset = 0x00usize;
150 let mut subtraction_ps = 0i64;
151
152 for exact_lap_time in exact_lap_times.iter_mut().take(lap_count as usize) {
153 let mut true_time_subtraction = ((f32::from_be_bytes(
154 footer_data[current_offset..current_offset + 0x04].try_into()?,
155 ) as f64)
156 * 1e+9)
157 .floor() as i64;
158
159 let lap_time = InGameTime::from_byte_handler(
160 &laps_data[in_game_time_offset..=in_game_time_offset + 0x02],
161 )?;
162
163 true_time_subtraction -= previous_subtractions;
166
167 if true_time_subtraction > 1e+9 as i64 {
168 true_time_subtraction -= subtraction_ps;
169 subtraction_ps = if subtraction_ps == 0 { 1e+9 as i64 } else { 0 };
170 }
171 previous_subtractions += true_time_subtraction;
172 *exact_lap_time = ExactFinishTime::new(
173 lap_time.minutes(),
174 lap_time.seconds(),
175 (lap_time.milliseconds() as i64 * 1e+9 as i64 + true_time_subtraction) as u64,
176 );
177 in_game_time_offset += 0x03;
178 current_offset += 0x04;
179 }
180
181 let exact_finish_time = exact_lap_times[..lap_count as usize].iter().copied().sum();
182
183 current_offset += (11 - lap_count as usize) * 0x04;
184
185 let bools = ByteHandler::from(footer_data[current_offset]);
186 let has_speed_mod = bools.read_bool(7);
187 let has_ultra_shortcut = bools.read_bool(6);
188 let has_horizontal_wall_glitch = bools.read_bool(5);
189 let has_wallride = bools.read_bool(4);
190
191 let shroomstrat;
192
193 if footer_version >= 1 {
194 let shroom_data: [u8; 3] = footer_data[current_offset..current_offset + 0x03]
195 .try_into()
196 .unwrap();
197
198 let mut shroom_arr = [0u8; 11];
199 let mut shrooms = [0u8; 3];
200
201 let raw = u32::from_be_bytes([0, shroom_data[0], shroom_data[1], shroom_data[2]]);
202 shrooms[0] = ((raw >> 15) & 0x1F) as u8;
203 shrooms[1] = ((raw >> 10) & 0x1F) as u8;
204 shrooms[2] = ((raw >> 5) & 0x1F) as u8;
205
206 for shroom in shrooms.iter() {
207 if *shroom != 0 {
208 shroom_arr[*shroom as usize - 1] += 1;
209 }
210 }
211 shroomstrat = Some(shroom_arr);
212 } else {
213 shroomstrat = None;
214 }
215
216 current_offset += 0x02;
217
218 let bools = ByteHandler::from(footer_data[current_offset]);
219
220 let is_vanilla_mode_enabled = if footer_version >= 3 {
221 Some(bools.read_bool(4))
222 } else {
223 None
224 };
225
226 let has_simplified_controls = if footer_version >= 4 {
227 Some(bools.read_bool(3))
228 } else {
229 None
230 };
231
232 let set_in_mirror = if footer_version >= 5 {
233 Some(bools.read_bool(2))
234 } else {
235 None
236 };
237
238 Ok(Self {
239 raw_data: footer_data.to_owned(),
240 footer_version,
241 possible_sp_versions,
242 track_sha1,
243 exact_finish_time,
244 exact_lap_times,
245 has_speed_mod,
246 has_ultra_shortcut,
247 has_horizontal_wall_glitch,
248 has_wallride,
249 shroomstrat,
250 is_vanilla_mode_enabled,
251 has_simplified_controls,
252 set_in_mirror,
253 len: footer_len as u32,
254 lap_count,
255 })
256 }
257
258 pub fn raw_data(&self) -> &[u8] {
260 &self.raw_data
261 }
262
263 pub fn footer_version(&self) -> u32 {
265 self.footer_version
266 }
267
268 pub fn possible_sp_versions(&self) -> Option<&Vec<SPVersion>> {
272 self.possible_sp_versions.as_ref()
273 }
274
275 pub fn track_sha1(&self) -> &[u8; 0x14] {
277 &self.track_sha1
278 }
279
280 pub fn exact_finish_time(&self) -> ExactFinishTime {
282 self.exact_finish_time
283 }
284
285 pub fn exact_lap_times(&self) -> &[ExactFinishTime] {
287 &self.exact_lap_times[..self.lap_count as usize]
288 }
289
290 pub fn exact_lap_time(&self, idx: usize) -> Result<ExactFinishTime, SPFooterError> {
297 if idx >= self.lap_count as usize {
298 return Err(SPFooterError::LapSplitIndexError);
299 }
300 Ok(self.exact_lap_times[idx])
301 }
302
303 pub fn has_speed_mod(&self) -> bool {
305 self.has_speed_mod
306 }
307
308 pub fn has_ultra_shortcut(&self) -> bool {
310 self.has_ultra_shortcut
311 }
312
313 pub fn has_horizontal_wall_glitch(&self) -> bool {
315 self.has_horizontal_wall_glitch
316 }
317
318 pub fn has_wallride(&self) -> bool {
320 self.has_wallride
321 }
322
323 pub fn shroomstrat(&self) -> Option<&[u8]> {
327 self.shroomstrat
328 .as_ref()
329 .map(|s| &s[..self.lap_count as usize])
330 }
331
332 pub fn shroomstrat_string(&self) -> Option<String> {
338 if let Some(shroomstrat) = self.shroomstrat() {
339 let mut s = String::new();
340
341 for (idx, lap) in shroomstrat.iter().enumerate() {
342 s += lap.to_string().as_str();
343
344 if idx + 1 < self.lap_count as usize {
345 s += "-";
346 }
347 }
348 Some(s)
349 } else {
350 None
351 }
352 }
353
354 pub fn is_vanilla_mode_enabled(&self) -> Option<bool> {
358 self.is_vanilla_mode_enabled
359 }
360
361 pub fn has_simplified_controls(&self) -> Option<bool> {
365 self.has_simplified_controls
366 }
367
368 pub fn set_in_mirror(&self) -> Option<bool> {
372 self.set_in_mirror
373 }
374
375 pub fn len(&self) -> usize {
377 self.len as usize
378 }
379
380 pub fn is_empty(&self) -> bool {
382 self.len == 0
383 }
384}