1use crate::byte_handler::FromByteHandler;
2use crate::ctgp_footer::exact_finish_time::ExactFinishTime;
3use crate::ctgp_footer::{category::Category, ctgp_version::CTGPVersion};
4use crate::header::in_game_time::InGameTime;
5use crate::{byte_handler::ByteHandler, input_data::yaz1_decompress};
6use crate::{compute_sha1_hex, datetime_from_timestamp, duration_from_ticks};
7use chrono::{TimeDelta, prelude::*};
8
9pub mod category;
10pub mod ctgp_version;
11pub mod exact_finish_time;
12
13#[derive(thiserror::Error, Debug)]
15pub enum CTGPFooterError {
16 #[error("Ghost is not CKGD")]
18 NotCKGD,
19 #[error("Invalid CTGP footer version")]
21 InvalidFooterVersion,
22 #[error("Try From Slice Error: {0}")]
24 TryFromSliceError(#[from] std::array::TryFromSliceError),
25 #[error("Lap split index not semantically valid")]
27 LapSplitIndexError,
28 #[error("Category Error: {0}")]
30 CategoryError(#[from] category::CategoryError),
31 #[error("In Game Time Error: {0}")]
33 InGameTimeError(#[from] crate::header::in_game_time::InGameTimeError),
34 #[error("Parse Int Error: {0}")]
36 ParseIntError(#[from] std::num::ParseIntError),
37}
38
39pub struct CTGPFooter {
45 raw_data: Vec<u8>,
47 security_data: Vec<u8>,
49 track_sha1: [u8; 0x14],
51 ghost_sha1: [u8; 0x14],
53 player_id: u64,
55 exact_finish_time: ExactFinishTime,
57 core_version: CTGPVersion,
59 possible_ctgp_versions: Option<Vec<CTGPVersion>>,
62 lap_split_suspicious_intersections: Option<[bool; 10]>,
65 exact_lap_times: [ExactFinishTime; 10],
67 rtc_race_end: NaiveDateTime,
69 rtc_race_begins: NaiveDateTime,
71 rtc_time_paused: TimeDelta,
73 pause_times: Vec<InGameTime>,
75 my_stuff_enabled: bool,
77 my_stuff_used: bool,
79 usb_gamecube_enabled: bool,
81 final_lap_suspicious_intersection: bool,
83 shroomstrat: [u8; 10],
85 cannoned: bool,
87 went_oob: bool,
89 potential_slowdown: bool,
91 potential_rapidfire: bool,
93 potentially_cheated_ghost: bool,
95 has_mii_data_replaced: bool,
97 has_name_replaced: bool, respawns: bool,
101 category: Category,
103 footer_version: u8,
105 len: usize,
107 lap_count: u8,
109}
110
111impl CTGPFooter {
112 pub fn new(data: &[u8]) -> Result<Self, CTGPFooterError> {
129 if data[data.len() - 0x08..data.len() - 0x04] != *b"CKGD" {
130 return Err(CTGPFooterError::NotCKGD);
131 }
132
133 let footer_version = data[data.len() - 0x0D];
134
135 match footer_version {
136 1 | 2 | 3 | 5 | 6 | 7 => {}
137 _ => {
138 return Err(CTGPFooterError::InvalidFooterVersion);
139 }
140 }
141
142 let len = if footer_version < 7 { 0xD4 } else { 0xE4 };
143
144 let security_data_size = if footer_version < 7 { 0x48 } else { 0x58 };
145
146 let raw_data = Vec::from(&data[data.len() - len..data.len() - 0x04]);
147
148 let header_data = &data[..0x88];
149 let input_data = &data[0x88..data.len() - len];
150 let metadata = &data[data.len() - len..];
151 let mut current_offset = 0usize;
152
153 let security_data = Vec::from(&metadata[..security_data_size]);
154 current_offset += security_data_size;
155
156 let track_sha1 = metadata[current_offset..current_offset + 0x14]
157 .to_owned()
158 .try_into()
159 .unwrap();
160 current_offset += 0x14;
161
162 let ghost_sha1 = compute_sha1_hex(data);
163
164 let player_id =
165 u64::from_be_bytes(metadata[current_offset..current_offset + 0x08].try_into()?);
166 current_offset += 0x08;
167
168 let finish_time = InGameTime::from_byte_handler(&header_data[0x04..0x07])?;
169 let true_time_subtraction =
170 (f32::from_be_bytes(metadata[current_offset..current_offset + 0x04].try_into()?) as f64
171 * 1e+9)
172 .floor() as i64;
173 let exact_finish_time = ExactFinishTime::new(
174 finish_time.minutes(),
175 finish_time.seconds(),
176 (finish_time.milliseconds() as i64 * 1e+9 as i64 + true_time_subtraction) as u64,
177 );
178 current_offset += 0x04;
179
180 let possible_ctgp_versions;
181 let core_version;
182 let mut lap_split_suspicious_intersections = Some([false; 10]);
183
184 if footer_version >= 2 {
185 let version_bytes = &metadata[current_offset..current_offset + 0x04];
186 core_version = CTGPVersion::core_from(version_bytes)?;
187 possible_ctgp_versions = CTGPVersion::from(version_bytes);
188 current_offset += 0x04;
189
190 let laps_handler = ByteHandler::try_from(&metadata[current_offset..current_offset + 2])
191 .expect("ByteHandler try_from() failed");
192
193 if let Some(mut array) = lap_split_suspicious_intersections {
194 for (index, intersection) in array.iter_mut().enumerate() {
195 *intersection = laps_handler.read_bool(index as u8 + 6);
196 }
197 }
198 current_offset -= 0x04;
199 } else {
200 core_version = CTGPVersion::new(1, 3, 134, None);
202 possible_ctgp_versions = Some(Vec::from([CTGPVersion::new(1, 3, 1044, None)]));
204 lap_split_suspicious_intersections = None;
205 }
206
207 current_offset += 0x3C;
208
209 let mut previous_subtractions = 0i64;
211 let mut exact_lap_times = [ExactFinishTime::default(); 10];
212 let lap_count = header_data[0x10];
213 let mut in_game_time_offset = 0x11usize;
214 let mut subtraction_ps = 0i64;
215
216 for exact_lap_time in exact_lap_times.iter_mut().take(lap_count as usize) {
217 let mut true_time_subtraction =
218 ((f32::from_be_bytes(metadata[current_offset..current_offset + 0x04].try_into()?)
219 as f64)
220 * 1e+9)
221 .floor() as i64;
222
223 let lap_time = InGameTime::from_byte_handler(
224 &header_data[in_game_time_offset..in_game_time_offset + 0x03],
225 )?;
226
227 true_time_subtraction -= previous_subtractions;
230
231 if true_time_subtraction > 1e+9 as i64 {
232 true_time_subtraction -= subtraction_ps;
233 subtraction_ps = if subtraction_ps == 0 { 1e+9 as i64 } else { 0 };
234 }
235 previous_subtractions += true_time_subtraction;
236 *exact_lap_time = ExactFinishTime::new(
237 lap_time.minutes(),
238 lap_time.seconds(),
239 (lap_time.milliseconds() as i64 * 1e+9 as i64 + true_time_subtraction) as u64,
240 );
241 in_game_time_offset += 0x03;
242 current_offset -= 0x04;
243 }
244
245 current_offset += 0x04 * (lap_count as usize + 1);
246
247 let rtc_race_end = datetime_from_timestamp(u64::from_be_bytes(
248 metadata[current_offset..current_offset + 0x08].try_into()?,
249 ));
250 current_offset += 0x08;
251
252 let rtc_race_begins = datetime_from_timestamp(u64::from_be_bytes(
253 metadata[current_offset..current_offset + 0x08].try_into()?,
254 ));
255 current_offset += 0x08;
256
257 let rtc_time_paused = duration_from_ticks(u64::from_be_bytes(
258 metadata[current_offset..current_offset + 0x08].try_into()?,
259 ));
260 current_offset += 0x08;
261
262 let mut pause_times = Vec::new();
264 let input_data = if input_data[4..8] == [0x59, 0x61, 0x7A, 0x31] {
265 yaz1_decompress(&input_data[4..]).unwrap()
267 } else {
268 Vec::from(input_data)
269 };
270
271 let face_input_count = u16::from_be_bytes([input_data[0], input_data[1]]);
272
273 let mut current_input_byte = 8;
274 let mut elapsed_frames = 1u32;
275 while current_input_byte < 8 + face_input_count * 2 {
276 let idx = current_input_byte as usize;
277 let input = &input_data[idx..idx + 2];
278
279 if contains_ctgp_pause(input[0]) {
280 let mut pause_timestamp_seconds = (elapsed_frames - 242) as f64 / 59.94;
283 let mut minutes = 0;
284 let mut seconds = 0;
285
286 while pause_timestamp_seconds >= 60.0 {
287 minutes += 1;
288 pause_timestamp_seconds -= 60.0;
289 }
290
291 while pause_timestamp_seconds >= 1.0 {
292 seconds += 1;
293 pause_timestamp_seconds -= 1.0;
294 }
295
296 let milliseconds = (pause_timestamp_seconds * 1000.0) as u16;
297
298 pause_times.push(InGameTime::new(minutes, seconds, milliseconds));
299 }
300
301 elapsed_frames += input[1] as u32;
302 current_input_byte += 2;
303 }
304
305 let bool_handler = ByteHandler::from(metadata[current_offset]);
306 let my_stuff_enabled = bool_handler.read_bool(3);
307 let my_stuff_used = bool_handler.read_bool(2);
308 let usb_gamecube_enabled = bool_handler.read_bool(1);
309 let final_lap_suspicious_intersection = bool_handler.read_bool(0);
310 current_offset += 0x01;
311
312 let mut shroomstrat: [u8; 10] = [0; 10];
313 for _ in 0..3 {
314 let lap = metadata[current_offset];
315 if lap != 0 {
316 shroomstrat[(lap - 1) as usize] += 1;
317 }
318 current_offset += 0x01;
319 }
320
321 let category = Category::try_from(metadata[current_offset + 2], metadata[current_offset])?;
322 current_offset += 0x01;
323 let bool_handler = ByteHandler::from(metadata[current_offset]);
324 let cannoned = bool_handler.read_bool(7);
325 let went_oob = bool_handler.read_bool(6);
326 let potential_slowdown = bool_handler.read_bool(5);
327 let potential_rapidfire = bool_handler.read_bool(4);
328 let potentially_cheated_ghost = bool_handler.read_bool(3);
329 let has_mii_data_replaced = bool_handler.read_bool(2);
330 let has_name_replaced = bool_handler.read_bool(1);
331 let respawns = bool_handler.read_bool(0);
332
333 Ok(Self {
334 raw_data,
335 security_data,
336 track_sha1,
337 ghost_sha1,
338 player_id,
339 exact_finish_time,
340 core_version,
341 possible_ctgp_versions,
342 lap_split_suspicious_intersections,
343 exact_lap_times,
344 rtc_race_end,
345 rtc_race_begins,
346 rtc_time_paused,
347 pause_times,
348 my_stuff_enabled,
349 my_stuff_used,
350 usb_gamecube_enabled,
351 final_lap_suspicious_intersection,
352 shroomstrat,
353 cannoned,
354 went_oob,
355 potential_slowdown,
356 potential_rapidfire,
357 potentially_cheated_ghost,
358 has_mii_data_replaced,
359 has_name_replaced,
360 respawns,
361 category,
362 footer_version,
363 len: len - 0x04,
364 lap_count,
365 })
366 }
367
368 pub fn raw_data(&self) -> &[u8] {
370 &self.raw_data
371 }
372
373 pub fn security_data(&self) -> &[u8] {
375 &self.security_data
376 }
377
378 pub fn track_sha1(&self) -> &[u8] {
380 &self.track_sha1
381 }
382
383 pub fn ghost_sha1(&self) -> &[u8] {
385 &self.ghost_sha1
386 }
387
388 pub(crate) fn set_ghost_sha1(&mut self, ghost_sha1: &[u8]) -> Result<(), CTGPFooterError> {
394 self.ghost_sha1 = ghost_sha1.try_into()?;
395 Ok(())
396 }
397
398 pub fn player_id(&self) -> u64 {
400 self.player_id
401 }
402
403 pub fn exact_finish_time(&self) -> ExactFinishTime {
405 self.exact_finish_time
406 }
407
408 pub fn core_version(&self) -> CTGPVersion {
410 self.core_version
411 }
412
413 pub fn possible_ctgp_versions(&self) -> Option<&Vec<CTGPVersion>> {
417 self.possible_ctgp_versions.as_ref()
418 }
419
420 pub fn lap_split_suspicious_intersections(&self) -> Option<&[bool]> {
424 if let Some(intersections) = &self.lap_split_suspicious_intersections {
425 return Some(&intersections[0..self.lap_count as usize]);
426 }
427 None
428 }
429
430 pub fn exact_lap_times(&self) -> &[ExactFinishTime] {
432 &self.exact_lap_times[0..self.lap_count as usize]
433 }
434
435 pub fn exact_lap_time(&self, idx: usize) -> Result<ExactFinishTime, CTGPFooterError> {
442 if idx >= self.lap_count as usize {
443 return Err(CTGPFooterError::LapSplitIndexError);
444 }
445 Ok(self.exact_lap_times[idx])
446 }
447
448 pub fn rtc_race_end(&self) -> NaiveDateTime {
450 self.rtc_race_end
451 }
452
453 pub fn rtc_race_begins(&self) -> NaiveDateTime {
455 self.rtc_race_begins
456 }
457
458 pub fn rtc_time_paused(&self) -> TimeDelta {
460 self.rtc_time_paused
461 }
462
463 pub fn pause_times(&self) -> &Vec<InGameTime> {
465 &self.pause_times
466 }
467
468 pub fn my_stuff_enabled(&self) -> bool {
470 self.my_stuff_enabled
471 }
472
473 pub fn my_stuff_used(&self) -> bool {
475 self.my_stuff_used
476 }
477
478 pub fn usb_gamecube_enabled(&self) -> bool {
480 self.usb_gamecube_enabled
481 }
482
483 pub fn final_lap_suspicious_intersection(&self) -> bool {
485 self.final_lap_suspicious_intersection
486 }
487
488 pub fn shroomstrat(&self) -> &[u8] {
490 &self.shroomstrat[0..self.lap_count as usize]
491 }
492
493 pub fn shroomstrat_string(&self) -> String {
498 let mut s = String::new();
499
500 for (idx, lap) in self.shroomstrat().iter().enumerate() {
501 s += lap.to_string().as_str();
502
503 if idx + 1 < self.lap_count as usize {
504 s += "-";
505 }
506 }
507 s
508 }
509
510 pub fn cannoned(&self) -> bool {
512 self.cannoned
513 }
514
515 pub fn went_oob(&self) -> bool {
517 self.went_oob
518 }
519
520 pub fn potential_slowdown(&self) -> bool {
522 self.potential_slowdown
523 }
524
525 pub fn potential_rapidfire(&self) -> bool {
527 self.potential_rapidfire
528 }
529
530 pub fn potentially_cheated_ghost(&self) -> bool {
532 self.potentially_cheated_ghost
533 }
534
535 pub fn has_mii_data_replaced(&self) -> bool {
537 self.has_mii_data_replaced
538 }
539
540 pub fn has_name_replaced(&self) -> bool {
542 self.has_name_replaced
543 }
544
545 pub fn respawns(&self) -> bool {
547 self.respawns
548 }
549
550 pub fn category(&self) -> Category {
552 self.category
553 }
554
555 pub fn footer_version(&self) -> u8 {
557 self.footer_version
558 }
559
560 pub fn len(&self) -> usize {
562 self.len
563 }
564
565 pub fn is_empty(&self) -> bool {
567 self.len == 0
568 }
569}
570
571fn contains_ctgp_pause(buttons: u8) -> bool {
575 buttons & 0x40 != 0
576}