rkg_utils/footer/ctgp_footer/
mod.rs1use crate::byte_handler::FromByteHandler;
2use crate::footer::ctgp_footer::{
3 category::Category, ctgp_version::CTGPVersion, exact_finish_time::ExactFinishTime,
4};
5use crate::header::in_game_time::InGameTime;
6use crate::{byte_handler::ByteHandler, input_data::yaz1_decompress};
7use crate::{compute_sha1_hex, datetime_from_timestamp, duration_from_ticks};
8use chrono::{TimeDelta, prelude::*};
9
10pub mod category;
11pub mod ctgp_version;
12pub mod exact_finish_time;
13
14#[derive(thiserror::Error, Debug)]
16pub enum CTGPFooterError {
17 #[error("Ghost is not CKGD")]
19 NotCKGD,
20 #[error("Invalid CTGP footer version")]
22 InvalidFooterVersion,
23 #[error("Try From Slice Error: {0}")]
25 TryFromSliceError(#[from] std::array::TryFromSliceError),
26 #[error("Lap split index not semantically valid")]
28 LapSplitIndexError,
29 #[error("Category Error: {0}")]
31 CategoryError(#[from] category::CategoryError),
32 #[error("In Game Time Error: {0}")]
34 InGameTimeError(#[from] crate::header::in_game_time::InGameTimeError),
35 #[error("Parse Int Error: {0}")]
37 ParseIntError(#[from] std::num::ParseIntError),
38}
39
40pub struct CTGPFooter {
46 raw_data: Vec<u8>,
48 security_data: Vec<u8>,
50 track_sha1: [u8; 0x14],
52 ghost_sha1: [u8; 0x14],
54 player_id: u64,
56 exact_finish_time: ExactFinishTime,
58 core_version: CTGPVersion,
60 possible_ctgp_versions: Option<Vec<CTGPVersion>>,
63 lap_split_suspicious_intersections: Option<[bool; 10]>,
66 exact_lap_times: [ExactFinishTime; 10],
68 rtc_race_end: NaiveDateTime,
70 rtc_race_begins: NaiveDateTime,
72 rtc_time_paused: TimeDelta,
74 pause_times: Vec<InGameTime>,
76 my_stuff_enabled: bool,
78 my_stuff_used: bool,
80 usb_gamecube_enabled: bool,
82 final_lap_suspicious_intersection: bool,
84 shroomstrat: [u8; 10],
86 cannoned: bool,
88 went_oob: bool,
90 potential_slowdown: bool,
92 potential_rapidfire: bool,
94 potentially_cheated_ghost: bool,
96 has_mii_data_replaced: bool,
98 has_name_replaced: bool, respawns: bool,
102 category: Category,
104 footer_version: u8,
106 len: usize,
108 lap_count: u8,
110}
111
112impl CTGPFooter {
113 pub fn new(data: &[u8]) -> Result<Self, CTGPFooterError> {
130 if data[data.len() - 0x08..data.len() - 0x04] != *b"CKGD" {
131 return Err(CTGPFooterError::NotCKGD);
132 }
133
134 let footer_version = data[data.len() - 0x0D];
135
136 match footer_version {
137 1 | 2 | 3 | 5 | 6 | 7 => {}
138 _ => {
139 return Err(CTGPFooterError::InvalidFooterVersion);
140 }
141 }
142
143 let len = if footer_version < 7 { 0xD4 } else { 0xE4 };
144
145 let security_data_size = if footer_version < 7 { 0x48 } else { 0x58 };
146
147 let raw_data = Vec::from(&data[data.len() - len..data.len() - 0x04]);
148
149 let header_data = &data[..0x88];
150 let input_data = &data[0x88..data.len() - len];
151 let metadata = &data[data.len() - len..];
152 let mut current_offset = 0usize;
153
154 let security_data = Vec::from(&metadata[..security_data_size]);
155 current_offset += security_data_size;
156
157 let track_sha1 = metadata[current_offset..current_offset + 0x14]
158 .to_owned()
159 .try_into()
160 .unwrap();
161 current_offset += 0x14;
162
163 let ghost_sha1 = compute_sha1_hex(data);
164
165 let player_id =
166 u64::from_be_bytes(metadata[current_offset..current_offset + 0x08].try_into()?);
167 current_offset += 0x08;
168
169 let finish_time = InGameTime::from_byte_handler(&header_data[0x04..0x07])?;
170 let true_time_subtraction =
171 (f32::from_be_bytes(metadata[current_offset..current_offset + 0x04].try_into()?) as f64
172 * 1e+9)
173 .floor() as i64;
174 let exact_finish_time = ExactFinishTime::new(
175 finish_time.minutes(),
176 finish_time.seconds(),
177 (finish_time.milliseconds() as i64 * 1e+9 as i64 + true_time_subtraction) as u64,
178 );
179 current_offset += 0x04;
180
181 let possible_ctgp_versions;
182 let core_version;
183 let mut lap_split_suspicious_intersections = Some([false; 10]);
184
185 if footer_version >= 2 {
186 let version_bytes = &metadata[current_offset..current_offset + 0x04];
187 core_version = CTGPVersion::core_from(version_bytes)?;
188 possible_ctgp_versions = CTGPVersion::from(version_bytes);
189 current_offset += 0x04;
190
191 let laps_handler = ByteHandler::try_from(&metadata[current_offset..current_offset + 2])
192 .expect("ByteHandler try_from() failed");
193
194 if let Some(mut array) = lap_split_suspicious_intersections {
195 for (index, intersection) in array.iter_mut().enumerate() {
196 *intersection = laps_handler.read_bool(index as u8 + 6);
197 }
198 }
199 current_offset -= 0x04;
200 } else {
201 core_version = CTGPVersion::new(1, 3, 134, None);
203 possible_ctgp_versions = Some(Vec::from([CTGPVersion::new(1, 3, 1044, None)]));
205 lap_split_suspicious_intersections = None;
206 }
207
208 current_offset += 0x3C;
209
210 let mut previous_subtractions = 0i64;
212 let mut exact_lap_times = [ExactFinishTime::default(); 10];
213 let lap_count = header_data[0x10];
214 let mut in_game_time_offset = 0x11usize;
215 let mut subtraction_ps = 0i64;
216
217 for exact_lap_time in exact_lap_times.iter_mut().take(lap_count as usize) {
218 let mut true_time_subtraction =
219 ((f32::from_be_bytes(metadata[current_offset..current_offset + 0x04].try_into()?)
220 as f64)
221 * 1e+9)
222 .floor() as i64;
223
224 let lap_time = InGameTime::from_byte_handler(
225 &header_data[in_game_time_offset..in_game_time_offset + 0x03],
226 )?;
227
228 true_time_subtraction -= previous_subtractions;
231
232 if true_time_subtraction > 1e+9 as i64 {
233 true_time_subtraction -= subtraction_ps;
234 subtraction_ps = if subtraction_ps == 0 { 1e+9 as i64 } else { 0 };
235 }
236 previous_subtractions += true_time_subtraction;
237 *exact_lap_time = ExactFinishTime::new(
238 lap_time.minutes(),
239 lap_time.seconds(),
240 (lap_time.milliseconds() as i64 * 1e+9 as i64 + true_time_subtraction) as u64,
241 );
242 in_game_time_offset += 0x03;
243 current_offset -= 0x04;
244 }
245
246 current_offset += 0x04 * (lap_count as usize + 1);
247
248 let rtc_race_end = datetime_from_timestamp(u64::from_be_bytes(
249 metadata[current_offset..current_offset + 0x08].try_into()?,
250 ));
251 current_offset += 0x08;
252
253 let rtc_race_begins = datetime_from_timestamp(u64::from_be_bytes(
254 metadata[current_offset..current_offset + 0x08].try_into()?,
255 ));
256 current_offset += 0x08;
257
258 let rtc_time_paused = duration_from_ticks(u64::from_be_bytes(
259 metadata[current_offset..current_offset + 0x08].try_into()?,
260 ));
261 current_offset += 0x08;
262
263 let mut pause_times = Vec::new();
265 let input_data = if input_data[4..8] == [0x59, 0x61, 0x7A, 0x31] {
266 yaz1_decompress(&input_data[4..]).unwrap()
268 } else {
269 Vec::from(input_data)
270 };
271
272 let face_input_count = u16::from_be_bytes([input_data[0], input_data[1]]);
273
274 let mut current_input_byte = 8;
275 let mut elapsed_frames = 1u32;
276 while current_input_byte < 8 + face_input_count * 2 {
277 let idx = current_input_byte as usize;
278 let input = &input_data[idx..idx + 2];
279
280 if contains_ctgp_pause(input[0]) {
281 let mut pause_timestamp_seconds = (elapsed_frames - 242) as f64 / 59.94;
284 let mut minutes = 0;
285 let mut seconds = 0;
286
287 while pause_timestamp_seconds >= 60.0 {
288 minutes += 1;
289 pause_timestamp_seconds -= 60.0;
290 }
291
292 while pause_timestamp_seconds >= 1.0 {
293 seconds += 1;
294 pause_timestamp_seconds -= 1.0;
295 }
296
297 let milliseconds = (pause_timestamp_seconds * 1000.0) as u16;
298
299 pause_times.push(InGameTime::new(minutes, seconds, milliseconds));
300 }
301
302 elapsed_frames += input[1] as u32;
303 current_input_byte += 2;
304 }
305
306 let bool_handler = ByteHandler::from(metadata[current_offset]);
307 let my_stuff_enabled = bool_handler.read_bool(3);
308 let my_stuff_used = bool_handler.read_bool(2);
309 let usb_gamecube_enabled = bool_handler.read_bool(1);
310 let final_lap_suspicious_intersection = bool_handler.read_bool(0);
311 current_offset += 0x01;
312
313 let mut shroomstrat: [u8; 10] = [0; 10];
314 for _ in 0..3 {
315 let lap = metadata[current_offset];
316 if lap != 0 {
317 shroomstrat[(lap - 1) as usize] += 1;
318 }
319 current_offset += 0x01;
320 }
321
322 let category = Category::try_from(metadata[current_offset + 2], metadata[current_offset])?;
323 current_offset += 0x01;
324 let bool_handler = ByteHandler::from(metadata[current_offset]);
325 let cannoned = bool_handler.read_bool(7);
326 let went_oob = bool_handler.read_bool(6);
327 let potential_slowdown = bool_handler.read_bool(5);
328 let potential_rapidfire = bool_handler.read_bool(4);
329 let potentially_cheated_ghost = bool_handler.read_bool(3);
330 let has_mii_data_replaced = bool_handler.read_bool(2);
331 let has_name_replaced = bool_handler.read_bool(1);
332 let respawns = bool_handler.read_bool(0);
333
334 Ok(Self {
335 raw_data,
336 security_data,
337 track_sha1,
338 ghost_sha1,
339 player_id,
340 exact_finish_time,
341 core_version,
342 possible_ctgp_versions,
343 lap_split_suspicious_intersections,
344 exact_lap_times,
345 rtc_race_end,
346 rtc_race_begins,
347 rtc_time_paused,
348 pause_times,
349 my_stuff_enabled,
350 my_stuff_used,
351 usb_gamecube_enabled,
352 final_lap_suspicious_intersection,
353 shroomstrat,
354 cannoned,
355 went_oob,
356 potential_slowdown,
357 potential_rapidfire,
358 potentially_cheated_ghost,
359 has_mii_data_replaced,
360 has_name_replaced,
361 respawns,
362 category,
363 footer_version,
364 len: len - 0x04,
365 lap_count,
366 })
367 }
368
369 pub fn raw_data(&self) -> &[u8] {
371 &self.raw_data
372 }
373
374 pub fn security_data(&self) -> &[u8] {
376 &self.security_data
377 }
378
379 pub fn track_sha1(&self) -> &[u8] {
381 &self.track_sha1
382 }
383
384 pub fn ghost_sha1(&self) -> &[u8] {
386 &self.ghost_sha1
387 }
388
389 pub(crate) fn set_ghost_sha1(&mut self, ghost_sha1: &[u8]) -> Result<(), CTGPFooterError> {
395 self.ghost_sha1 = ghost_sha1.try_into()?;
396 Ok(())
397 }
398
399 pub fn player_id(&self) -> u64 {
401 self.player_id
402 }
403
404 pub fn exact_finish_time(&self) -> ExactFinishTime {
406 self.exact_finish_time
407 }
408
409 pub fn core_version(&self) -> CTGPVersion {
411 self.core_version
412 }
413
414 pub fn possible_ctgp_versions(&self) -> Option<&Vec<CTGPVersion>> {
418 self.possible_ctgp_versions.as_ref()
419 }
420
421 pub fn lap_split_suspicious_intersections(&self) -> Option<&[bool]> {
425 if let Some(intersections) = &self.lap_split_suspicious_intersections {
426 return Some(&intersections[0..self.lap_count as usize]);
427 }
428 None
429 }
430
431 pub fn exact_lap_times(&self) -> &[ExactFinishTime] {
433 &self.exact_lap_times[0..self.lap_count as usize]
434 }
435
436 pub fn exact_lap_time(&self, idx: usize) -> Result<ExactFinishTime, CTGPFooterError> {
443 if idx >= self.lap_count as usize {
444 return Err(CTGPFooterError::LapSplitIndexError);
445 }
446 Ok(self.exact_lap_times[idx])
447 }
448
449 pub fn rtc_race_end(&self) -> NaiveDateTime {
451 self.rtc_race_end
452 }
453
454 pub fn rtc_race_begins(&self) -> NaiveDateTime {
456 self.rtc_race_begins
457 }
458
459 pub fn rtc_time_paused(&self) -> TimeDelta {
461 self.rtc_time_paused
462 }
463
464 pub fn pause_times(&self) -> &Vec<InGameTime> {
466 &self.pause_times
467 }
468
469 pub fn my_stuff_enabled(&self) -> bool {
471 self.my_stuff_enabled
472 }
473
474 pub fn my_stuff_used(&self) -> bool {
476 self.my_stuff_used
477 }
478
479 pub fn usb_gamecube_enabled(&self) -> bool {
481 self.usb_gamecube_enabled
482 }
483
484 pub fn final_lap_suspicious_intersection(&self) -> bool {
486 self.final_lap_suspicious_intersection
487 }
488
489 pub fn shroomstrat(&self) -> &[u8] {
491 &self.shroomstrat[0..self.lap_count as usize]
492 }
493
494 pub fn shroomstrat_string(&self) -> String {
499 let mut s = String::new();
500
501 for (idx, lap) in self.shroomstrat().iter().enumerate() {
502 s += lap.to_string().as_str();
503
504 if idx + 1 < self.lap_count as usize {
505 s += "-";
506 }
507 }
508 s
509 }
510
511 pub fn cannoned(&self) -> bool {
513 self.cannoned
514 }
515
516 pub fn went_oob(&self) -> bool {
518 self.went_oob
519 }
520
521 pub fn potential_slowdown(&self) -> bool {
523 self.potential_slowdown
524 }
525
526 pub fn potential_rapidfire(&self) -> bool {
528 self.potential_rapidfire
529 }
530
531 pub fn potentially_cheated_ghost(&self) -> bool {
533 self.potentially_cheated_ghost
534 }
535
536 pub fn has_mii_data_replaced(&self) -> bool {
538 self.has_mii_data_replaced
539 }
540
541 pub fn has_name_replaced(&self) -> bool {
543 self.has_name_replaced
544 }
545
546 pub fn respawns(&self) -> bool {
548 self.respawns
549 }
550
551 pub fn category(&self) -> Category {
553 self.category
554 }
555
556 pub fn footer_version(&self) -> u8 {
558 self.footer_version
559 }
560
561 pub fn len(&self) -> usize {
563 self.len
564 }
565
566 pub fn is_empty(&self) -> bool {
568 self.len == 0
569 }
570}
571
572fn contains_ctgp_pause(buttons: u8) -> bool {
576 buttons & 0x40 != 0
577}