rkg_utils/footer/ctgp_footer/
mod.rs1use std::fmt::Write;
2use crate::byte_handler::FromByteHandler;
3use crate::footer::ctgp_footer::{
4 category::Category, ctgp_version::CTGPVersion, exact_finish_time::ExactFinishTime,
5};
6use crate::header::in_game_time::InGameTime;
7use crate::{byte_handler::ByteHandler, input_data::yaz1_decompress};
8use crate::{compute_sha1_hex, datetime_from_timestamp, duration_from_ticks};
9use chrono::{TimeDelta, prelude::*};
10
11pub mod category;
12pub mod ctgp_version;
13pub mod exact_finish_time;
14
15#[derive(thiserror::Error, Debug)]
17pub enum CTGPFooterError {
18 #[error("Ghost is not CKGD")]
20 NotCKGD,
21 #[error("Invalid CTGP footer version")]
23 InvalidFooterVersion,
24 #[error("Try From Slice Error: {0}")]
26 TryFromSliceError(#[from] std::array::TryFromSliceError),
27 #[error("Lap split index not semantically valid")]
29 LapSplitIndexError,
30 #[error("Category Error: {0}")]
32 CategoryError(#[from] category::CategoryError),
33 #[error("In Game Time Error: {0}")]
35 InGameTimeError(#[from] crate::header::in_game_time::InGameTimeError),
36 #[error("Parse Int Error: {0}")]
38 ParseIntError(#[from] std::num::ParseIntError),
39}
40
41pub struct CTGPFooter {
47 raw_data: Vec<u8>,
49 security_data: Vec<u8>,
51 track_sha1: [u8; 0x14],
53 ghost_sha1: [u8; 0x14],
55 player_id: u64,
57 exact_finish_time: ExactFinishTime,
59 core_version: CTGPVersion,
61 possible_ctgp_versions: Option<Vec<CTGPVersion>>,
64 lap_split_suspicious_intersections: Option<[bool; 10]>,
67 exact_lap_times: [ExactFinishTime; 10],
69 rtc_race_end: NaiveDateTime,
71 rtc_race_begins: NaiveDateTime,
73 rtc_time_paused: TimeDelta,
75 pause_times: Vec<InGameTime>,
77 my_stuff_enabled: bool,
79 my_stuff_used: bool,
81 usb_gamecube_enabled: bool,
83 final_lap_suspicious_intersection: bool,
85 shroomstrat: [u8; 10],
87 cannoned: bool,
89 went_oob: bool,
91 potential_slowdown: bool,
93 potential_rapidfire: bool,
95 potentially_cheated_ghost: bool,
97 has_mii_data_replaced: bool,
99 has_name_replaced: bool, respawns: bool,
103 category: Category,
105 footer_version: u8,
107 len: usize,
109 lap_count: u8,
111}
112
113impl CTGPFooter {
114 pub fn new(data: &[u8]) -> Result<Self, CTGPFooterError> {
131 if data[data.len() - 0x08..data.len() - 0x04] != *b"CKGD" {
132 return Err(CTGPFooterError::NotCKGD);
133 }
134
135 let footer_version = data[data.len() - 0x0D];
136
137 match footer_version {
138 1 | 2 | 3 | 5 | 6 | 7 => {}
139 _ => {
140 return Err(CTGPFooterError::InvalidFooterVersion);
141 }
142 }
143
144 let len = if footer_version < 7 { 0xD4 } else { 0xE4 };
145
146 let security_data_size = if footer_version < 7 { 0x48 } else { 0x58 };
147
148 let raw_data = Vec::from(&data[data.len() - len..data.len() - 0x04]);
149
150 let header_data = &data[..0x88];
151 let input_data = &data[0x88..data.len() - len];
152 let metadata = &data[data.len() - len..];
153 let mut current_offset = 0usize;
154
155 let security_data = Vec::from(&metadata[..security_data_size]);
156 current_offset += security_data_size;
157
158 let track_sha1 = metadata[current_offset..current_offset + 0x14]
159 .to_owned()
160 .try_into()
161 .unwrap();
162 current_offset += 0x14;
163
164 let ghost_sha1 = compute_sha1_hex(data);
165
166 let player_id =
167 u64::from_be_bytes(metadata[current_offset..current_offset + 0x08].try_into()?);
168 current_offset += 0x08;
169
170 let finish_time = InGameTime::from_byte_handler(&header_data[0x04..0x07])?;
171 let true_time_subtraction =
172 (f32::from_be_bytes(metadata[current_offset..current_offset + 0x04].try_into()?) as f64
173 * 1e+9)
174 .floor() as i64;
175 let exact_finish_time = ExactFinishTime::new(
176 finish_time.minutes(),
177 finish_time.seconds(),
178 (finish_time.milliseconds() as i64 * 1e+9 as i64 + true_time_subtraction) as u64,
179 );
180 current_offset += 0x04;
181
182 let possible_ctgp_versions;
183 let core_version;
184 let mut lap_split_suspicious_intersections = Some([false; 10]);
185
186 if footer_version >= 2 {
187 let version_bytes = &metadata[current_offset..current_offset + 0x04];
188 core_version = CTGPVersion::core_from(version_bytes)?;
189 possible_ctgp_versions = CTGPVersion::from(version_bytes);
190 current_offset += 0x04;
191
192 let laps_handler = ByteHandler::try_from(&metadata[current_offset..current_offset + 2])
193 .expect("ByteHandler try_from() failed");
194
195 if let Some(mut array) = lap_split_suspicious_intersections {
196 for (index, intersection) in array.iter_mut().enumerate() {
197 *intersection = laps_handler.read_bool(index as u8 + 6);
198 }
199 }
200 current_offset -= 0x04;
201 } else {
202 core_version = CTGPVersion::new(1, 3, 134, None);
204 possible_ctgp_versions = Some(Vec::from([CTGPVersion::new(1, 3, 1044, None)]));
206 lap_split_suspicious_intersections = None;
207 }
208
209 current_offset += 0x3C;
210
211 let mut previous_subtractions = 0i64;
213 let mut exact_lap_times = [ExactFinishTime::default(); 10];
214 let lap_count = header_data[0x10];
215 let mut in_game_time_offset = 0x11usize;
216 let mut subtraction_ps = 0i64;
217
218 for exact_lap_time in exact_lap_times.iter_mut().take(lap_count as usize) {
219 let mut true_time_subtraction =
220 ((f32::from_be_bytes(metadata[current_offset..current_offset + 0x04].try_into()?)
221 as f64)
222 * 1e+9)
223 .floor() as i64;
224
225 let lap_time = InGameTime::from_byte_handler(
226 &header_data[in_game_time_offset..in_game_time_offset + 0x03],
227 )?;
228
229 true_time_subtraction -= previous_subtractions;
232
233 if true_time_subtraction > 1e+9 as i64 {
234 true_time_subtraction -= subtraction_ps;
235 subtraction_ps = if subtraction_ps == 0 { 1e+9 as i64 } else { 0 };
236 }
237 previous_subtractions += true_time_subtraction;
238 *exact_lap_time = ExactFinishTime::new(
239 lap_time.minutes(),
240 lap_time.seconds(),
241 (lap_time.milliseconds() as i64 * 1e+9 as i64 + true_time_subtraction) as u64,
242 );
243 in_game_time_offset += 0x03;
244 current_offset -= 0x04;
245 }
246
247 current_offset += 0x04 * (lap_count as usize + 1);
248
249 let rtc_race_end = datetime_from_timestamp(u64::from_be_bytes(
250 metadata[current_offset..current_offset + 0x08].try_into()?,
251 ));
252 current_offset += 0x08;
253
254 let rtc_race_begins = datetime_from_timestamp(u64::from_be_bytes(
255 metadata[current_offset..current_offset + 0x08].try_into()?,
256 ));
257 current_offset += 0x08;
258
259 let rtc_time_paused = duration_from_ticks(u64::from_be_bytes(
260 metadata[current_offset..current_offset + 0x08].try_into()?,
261 ));
262 current_offset += 0x08;
263
264 let mut pause_times = Vec::new();
266 let input_data = if input_data[4..8] == [0x59, 0x61, 0x7A, 0x31] {
267 yaz1_decompress(&input_data[4..]).unwrap()
269 } else {
270 Vec::from(input_data)
271 };
272
273 let face_input_count = u16::from_be_bytes([input_data[0], input_data[1]]);
274
275 let mut current_input_byte = 8;
276 let mut elapsed_frames = 1u32;
277 while current_input_byte < 8 + face_input_count * 2 {
278 let idx = current_input_byte as usize;
279 let input = &input_data[idx..idx + 2];
280
281 if contains_ctgp_pause(input[0]) {
282 let mut pause_timestamp_seconds = (elapsed_frames - 242) as f64 / 59.94;
285 let mut minutes = 0;
286 let mut seconds = 0;
287
288 while pause_timestamp_seconds >= 60.0 {
289 minutes += 1;
290 pause_timestamp_seconds -= 60.0;
291 }
292
293 while pause_timestamp_seconds >= 1.0 {
294 seconds += 1;
295 pause_timestamp_seconds -= 1.0;
296 }
297
298 let milliseconds = (pause_timestamp_seconds * 1000.0) as u16;
299
300 pause_times.push(InGameTime::new(minutes, seconds, milliseconds));
301 }
302
303 elapsed_frames += input[1] as u32;
304 current_input_byte += 2;
305 }
306
307 let bool_handler = ByteHandler::from(metadata[current_offset]);
308 let my_stuff_enabled = bool_handler.read_bool(3);
309 let my_stuff_used = bool_handler.read_bool(2);
310 let usb_gamecube_enabled = bool_handler.read_bool(1);
311 let final_lap_suspicious_intersection = bool_handler.read_bool(0);
312 current_offset += 0x01;
313
314 let mut shroomstrat: [u8; 10] = [0; 10];
315 for _ in 0..3 {
316 let lap = metadata[current_offset];
317 if lap != 0 {
318 shroomstrat[(lap - 1) as usize] += 1;
319 }
320 current_offset += 0x01;
321 }
322
323 let category = Category::try_from(metadata[current_offset + 2], metadata[current_offset])?;
324 current_offset += 0x01;
325 let bool_handler = ByteHandler::from(metadata[current_offset]);
326 let cannoned = bool_handler.read_bool(7);
327 let went_oob = bool_handler.read_bool(6);
328 let potential_slowdown = bool_handler.read_bool(5);
329 let potential_rapidfire = bool_handler.read_bool(4);
330 let potentially_cheated_ghost = bool_handler.read_bool(3);
331 let has_mii_data_replaced = bool_handler.read_bool(2);
332 let has_name_replaced = bool_handler.read_bool(1);
333 let respawns = bool_handler.read_bool(0);
334
335 Ok(Self {
336 raw_data,
337 security_data,
338 track_sha1,
339 ghost_sha1,
340 player_id,
341 exact_finish_time,
342 core_version,
343 possible_ctgp_versions,
344 lap_split_suspicious_intersections,
345 exact_lap_times,
346 rtc_race_end,
347 rtc_race_begins,
348 rtc_time_paused,
349 pause_times,
350 my_stuff_enabled,
351 my_stuff_used,
352 usb_gamecube_enabled,
353 final_lap_suspicious_intersection,
354 shroomstrat,
355 cannoned,
356 went_oob,
357 potential_slowdown,
358 potential_rapidfire,
359 potentially_cheated_ghost,
360 has_mii_data_replaced,
361 has_name_replaced,
362 respawns,
363 category,
364 footer_version,
365 len: len - 0x04,
366 lap_count,
367 })
368 }
369
370 pub fn raw_data(&self) -> &[u8] {
372 &self.raw_data
373 }
374
375 pub fn security_data(&self) -> &[u8] {
377 &self.security_data
378 }
379
380 pub fn track_sha1(&self) -> &[u8] {
382 &self.track_sha1
383 }
384
385 pub fn ghost_sha1(&self) -> &[u8] {
387 &self.ghost_sha1
388 }
389
390 pub(crate) fn set_ghost_sha1(&mut self, ghost_sha1: &[u8]) -> Result<(), CTGPFooterError> {
396 self.ghost_sha1 = ghost_sha1.try_into()?;
397 Ok(())
398 }
399
400 pub fn player_id(&self) -> u64 {
402 self.player_id
403 }
404
405 pub fn exact_finish_time(&self) -> ExactFinishTime {
407 self.exact_finish_time
408 }
409
410 pub fn core_version(&self) -> CTGPVersion {
412 self.core_version
413 }
414
415 pub fn possible_ctgp_versions(&self) -> Option<&Vec<CTGPVersion>> {
419 self.possible_ctgp_versions.as_ref()
420 }
421
422 pub fn lap_split_suspicious_intersections(&self) -> Option<&[bool]> {
426 if let Some(intersections) = &self.lap_split_suspicious_intersections {
427 return Some(&intersections[0..self.lap_count as usize]);
428 }
429 None
430 }
431
432 pub fn exact_lap_times(&self) -> &[ExactFinishTime] {
434 &self.exact_lap_times[0..self.lap_count as usize]
435 }
436
437 pub fn exact_lap_time(&self, idx: usize) -> Result<ExactFinishTime, CTGPFooterError> {
444 if idx >= self.lap_count as usize {
445 return Err(CTGPFooterError::LapSplitIndexError);
446 }
447 Ok(self.exact_lap_times[idx])
448 }
449
450 pub fn rtc_race_end(&self) -> NaiveDateTime {
452 self.rtc_race_end
453 }
454
455 pub fn rtc_race_begins(&self) -> NaiveDateTime {
457 self.rtc_race_begins
458 }
459
460 pub fn rtc_time_paused(&self) -> TimeDelta {
462 self.rtc_time_paused
463 }
464
465 pub fn pause_times(&self) -> &Vec<InGameTime> {
467 &self.pause_times
468 }
469
470 pub fn my_stuff_enabled(&self) -> bool {
472 self.my_stuff_enabled
473 }
474
475 pub fn my_stuff_used(&self) -> bool {
477 self.my_stuff_used
478 }
479
480 pub fn usb_gamecube_enabled(&self) -> bool {
482 self.usb_gamecube_enabled
483 }
484
485 pub fn final_lap_suspicious_intersection(&self) -> bool {
487 self.final_lap_suspicious_intersection
488 }
489
490 pub fn shroomstrat(&self) -> &[u8] {
492 &self.shroomstrat[0..self.lap_count as usize]
493 }
494
495 pub fn shroomstrat_string(&self) -> String {
500 let mut shroomstrat = self.shroomstrat().iter();
501
502 let mut s = shroomstrat.next().unwrap().to_string();
503 for lap_shrooms in shroomstrat {
504 write!(s, "-{lap_shrooms}").unwrap()
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}