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("Data length is too short")]
23 DataLengthTooShort,
24 #[error("File is not an RKG file")]
26 NotRKGD,
27 #[error("Invalid CTGP footer version")]
29 InvalidFooterVersion,
30 #[error("Try From Slice Error: {0}")]
32 TryFromSliceError(#[from] std::array::TryFromSliceError),
33 #[error("Lap split index not semantically valid")]
35 LapSplitIndexError,
36 #[error("Category Error: {0}")]
38 CategoryError(#[from] category::CategoryError),
39 #[error("In Game Time Error: {0}")]
41 InGameTimeError(#[from] crate::header::in_game_time::InGameTimeError),
42 #[error("Parse Int Error: {0}")]
44 ParseIntError(#[from] std::num::ParseIntError),
45}
46
47pub struct CTGPFooter {
53 raw_data: Vec<u8>,
55 security_data: Vec<u8>,
57 track_sha1: [u8; 0x14],
59 ghost_sha1: [u8; 0x14],
61 player_id: u64,
63 exact_finish_time: ExactFinishTime,
65 core_version: CTGPVersion,
67 possible_ctgp_versions: Option<Vec<CTGPVersion>>,
70 lap_split_suspicious_intersections: Option<[bool; 10]>,
73 exact_lap_times: [ExactFinishTime; 10],
75 rtc_race_end: NaiveDateTime,
77 rtc_race_begins: NaiveDateTime,
79 rtc_time_paused: TimeDelta,
81 pause_times: Vec<InGameTime>,
83 my_stuff_enabled: bool,
85 my_stuff_used: bool,
87 usb_gamecube_enabled: bool,
89 final_lap_suspicious_intersection: bool,
91 shroomstrat: [u8; 10],
93 cannoned: bool,
95 went_oob: bool,
97 potential_slowdown: bool,
99 potential_rapidfire: bool,
101 potentially_cheated_ghost: bool,
103 has_mii_data_replaced: bool,
105 has_name_replaced: bool, respawns: bool,
109 category: Category,
111 footer_version: u8,
113 len: usize,
115 lap_count: u8,
117}
118
119impl CTGPFooter {
120 pub fn new(data: &[u8]) -> Result<Self, CTGPFooterError> {
137 if data.len() < 0x04 {
138 return Err(CTGPFooterError::DataLengthTooShort);
139 }
140
141 if data[..0x04] != *b"RKGD" {
142 return Err(CTGPFooterError::NotRKGD);
143 }
144
145 if data.len() < 0x08 {
146 return Err(CTGPFooterError::DataLengthTooShort);
147 }
148
149 if data[data.len() - 0x08..data.len() - 0x04] != *b"CKGD" {
150 return Err(CTGPFooterError::NotCKGD);
151 }
152
153 let footer_version = data[data.len() - 0x0D];
154
155 match footer_version {
156 1 | 2 | 3 | 5 | 6 | 7 => {}
157 _ => {
158 return Err(CTGPFooterError::InvalidFooterVersion);
159 }
160 }
161
162 let len = if footer_version < 7 { 0xD4 } else { 0xE4 };
163
164 if data.len() < len {
165 return Err(CTGPFooterError::DataLengthTooShort);
166 }
167
168 let security_data_size = if footer_version < 7 { 0x48 } else { 0x58 };
169
170 let raw_data = Vec::from(&data[data.len() - len..data.len() - 0x04]);
171
172 let header_data = &data[..0x88];
173 let input_data = &data[0x88..data.len() - len];
174 let metadata = &data[data.len() - len..];
175 let mut current_offset = 0usize;
176
177 let security_data = Vec::from(&metadata[..security_data_size]);
178 current_offset += security_data_size;
179
180 let track_sha1 = metadata[current_offset..current_offset + 0x14]
181 .to_owned()
182 .try_into()
183 .unwrap();
184 current_offset += 0x14;
185
186 let ghost_sha1 = compute_sha1_hex(data);
187
188 let player_id =
189 u64::from_be_bytes(metadata[current_offset..current_offset + 0x08].try_into()?);
190 current_offset += 0x08;
191
192 let finish_time = InGameTime::from_byte_handler(&header_data[0x04..0x07])?;
193 let true_time_subtraction =
194 (f32::from_be_bytes(metadata[current_offset..current_offset + 0x04].try_into()?) as f64
195 * 1e+9)
196 .floor() as i64;
197 let exact_finish_time = ExactFinishTime::new(
198 finish_time.minutes(),
199 finish_time.seconds(),
200 (finish_time.milliseconds() as i64 * 1e+9 as i64 + true_time_subtraction) as u64,
201 );
202 current_offset += 0x04;
203
204 let possible_ctgp_versions;
205 let core_version;
206 let mut lap_split_suspicious_intersections = Some([false; 10]);
207
208 if footer_version >= 2 {
209 let version_bytes = &metadata[current_offset..current_offset + 0x04];
210 core_version = CTGPVersion::core_from(version_bytes)?;
211 possible_ctgp_versions = CTGPVersion::from(version_bytes);
212 current_offset += 0x04;
213
214 let laps_handler = ByteHandler::try_from(&metadata[current_offset..current_offset + 2])
215 .expect("ByteHandler try_from() failed");
216
217 if let Some(mut array) = lap_split_suspicious_intersections {
218 for (index, intersection) in array.iter_mut().enumerate() {
219 *intersection = laps_handler.read_bool(index as u8 + 6);
220 }
221 }
222 current_offset -= 0x04;
223 } else {
224 core_version = CTGPVersion::new(1, 3, 134, None);
226 possible_ctgp_versions = Some(Vec::from([CTGPVersion::new(1, 3, 1044, None)]));
228 lap_split_suspicious_intersections = None;
229 }
230
231 current_offset += 0x3C;
232
233 let mut previous_subtractions = 0i64;
235 let mut exact_lap_times = [ExactFinishTime::default(); 10];
236 let lap_count = header_data[0x10];
237 let mut in_game_time_offset = 0x11usize;
238 let mut subtraction_ps = 0i64;
239
240 for exact_lap_time in exact_lap_times.iter_mut().take(lap_count as usize) {
241 let mut true_time_subtraction =
242 ((f32::from_be_bytes(metadata[current_offset..current_offset + 0x04].try_into()?)
243 as f64)
244 * 1e+9)
245 .floor() as i64;
246
247 let lap_time = InGameTime::from_byte_handler(
248 &header_data[in_game_time_offset..in_game_time_offset + 0x03],
249 )?;
250
251 true_time_subtraction -= previous_subtractions;
254
255 if true_time_subtraction > 1e+9 as i64 {
256 true_time_subtraction -= subtraction_ps;
257 subtraction_ps = if subtraction_ps == 0 { 1e+9 as i64 } else { 0 };
258 }
259 previous_subtractions += true_time_subtraction;
260 *exact_lap_time = ExactFinishTime::new(
261 lap_time.minutes(),
262 lap_time.seconds(),
263 (lap_time.milliseconds() as i64 * 1e+9 as i64 + true_time_subtraction) as u64,
264 );
265 in_game_time_offset += 0x03;
266 current_offset -= 0x04;
267 }
268
269 current_offset += 0x04 * (lap_count as usize + 1);
270
271 let rtc_race_end = datetime_from_timestamp(u64::from_be_bytes(
272 metadata[current_offset..current_offset + 0x08].try_into()?,
273 ));
274 current_offset += 0x08;
275
276 let rtc_race_begins = datetime_from_timestamp(u64::from_be_bytes(
277 metadata[current_offset..current_offset + 0x08].try_into()?,
278 ));
279 current_offset += 0x08;
280
281 let rtc_time_paused = duration_from_ticks(u64::from_be_bytes(
282 metadata[current_offset..current_offset + 0x08].try_into()?,
283 ));
284 current_offset += 0x08;
285
286 let mut pause_times = Vec::new();
288 let input_data = if input_data[4..8] == [0x59, 0x61, 0x7A, 0x31] {
289 yaz1_decompress(&input_data[4..]).unwrap()
291 } else {
292 Vec::from(input_data)
293 };
294
295 let face_input_count = u16::from_be_bytes([input_data[0], input_data[1]]);
296
297 let mut current_input_byte = 8;
298 let mut elapsed_frames = 1u32;
299 while current_input_byte < 8 + face_input_count * 2 {
300 let idx = current_input_byte as usize;
301 let input = &input_data[idx..idx + 2];
302
303 if contains_ctgp_pause(input[0]) {
304 let mut pause_timestamp_seconds = (elapsed_frames - 242) as f64 / 59.94;
307 let mut minutes = 0;
308 let mut seconds = 0;
309
310 while pause_timestamp_seconds >= 60.0 {
311 minutes += 1;
312 pause_timestamp_seconds -= 60.0;
313 }
314
315 while pause_timestamp_seconds >= 1.0 {
316 seconds += 1;
317 pause_timestamp_seconds -= 1.0;
318 }
319
320 let milliseconds = (pause_timestamp_seconds * 1000.0) as u16;
321
322 pause_times.push(InGameTime::new(minutes, seconds, milliseconds));
323 }
324
325 elapsed_frames += input[1] as u32;
326 current_input_byte += 2;
327 }
328
329 let bool_handler = ByteHandler::from(metadata[current_offset]);
330 let my_stuff_enabled = bool_handler.read_bool(3);
331 let my_stuff_used = bool_handler.read_bool(2);
332 let usb_gamecube_enabled = bool_handler.read_bool(1);
333 let final_lap_suspicious_intersection = bool_handler.read_bool(0);
334 current_offset += 0x01;
335
336 let mut shroomstrat: [u8; 10] = [0; 10];
337 for _ in 0..3 {
338 let lap = metadata[current_offset];
339 if lap != 0 {
340 shroomstrat[(lap - 1) as usize] += 1;
341 }
342 current_offset += 0x01;
343 }
344
345 let category = Category::try_from(metadata[current_offset + 2], metadata[current_offset])?;
346 current_offset += 0x01;
347 let bool_handler = ByteHandler::from(metadata[current_offset]);
348 let cannoned = bool_handler.read_bool(7);
349 let went_oob = bool_handler.read_bool(6);
350 let potential_slowdown = bool_handler.read_bool(5);
351 let potential_rapidfire = bool_handler.read_bool(4);
352 let potentially_cheated_ghost = bool_handler.read_bool(3);
353 let has_mii_data_replaced = bool_handler.read_bool(2);
354 let has_name_replaced = bool_handler.read_bool(1);
355 let respawns = bool_handler.read_bool(0);
356
357 Ok(Self {
358 raw_data,
359 security_data,
360 track_sha1,
361 ghost_sha1,
362 player_id,
363 exact_finish_time,
364 core_version,
365 possible_ctgp_versions,
366 lap_split_suspicious_intersections,
367 exact_lap_times,
368 rtc_race_end,
369 rtc_race_begins,
370 rtc_time_paused,
371 pause_times,
372 my_stuff_enabled,
373 my_stuff_used,
374 usb_gamecube_enabled,
375 final_lap_suspicious_intersection,
376 shroomstrat,
377 cannoned,
378 went_oob,
379 potential_slowdown,
380 potential_rapidfire,
381 potentially_cheated_ghost,
382 has_mii_data_replaced,
383 has_name_replaced,
384 respawns,
385 category,
386 footer_version,
387 len: len - 0x04,
388 lap_count,
389 })
390 }
391
392 pub fn raw_data(&self) -> &[u8] {
394 &self.raw_data
395 }
396
397 pub fn security_data(&self) -> &[u8] {
399 &self.security_data
400 }
401
402 pub fn track_sha1(&self) -> &[u8] {
404 &self.track_sha1
405 }
406
407 pub fn ghost_sha1(&self) -> &[u8] {
409 &self.ghost_sha1
410 }
411
412 pub(crate) fn set_ghost_sha1(&mut self, ghost_sha1: &[u8]) -> Result<(), CTGPFooterError> {
418 self.ghost_sha1 = ghost_sha1.try_into()?;
419 Ok(())
420 }
421
422 pub fn player_id(&self) -> u64 {
424 self.player_id
425 }
426
427 pub fn exact_finish_time(&self) -> ExactFinishTime {
429 self.exact_finish_time
430 }
431
432 pub fn core_version(&self) -> CTGPVersion {
434 self.core_version
435 }
436
437 pub fn possible_ctgp_versions(&self) -> Option<&Vec<CTGPVersion>> {
441 self.possible_ctgp_versions.as_ref()
442 }
443
444 pub fn lap_split_suspicious_intersections(&self) -> Option<&[bool]> {
448 if let Some(intersections) = &self.lap_split_suspicious_intersections {
449 return Some(&intersections[0..self.lap_count as usize]);
450 }
451 None
452 }
453
454 pub fn exact_lap_times(&self) -> &[ExactFinishTime] {
456 &self.exact_lap_times[0..self.lap_count as usize]
457 }
458
459 pub fn exact_lap_time(&self, idx: usize) -> Result<ExactFinishTime, CTGPFooterError> {
466 if idx >= self.lap_count as usize {
467 return Err(CTGPFooterError::LapSplitIndexError);
468 }
469 Ok(self.exact_lap_times[idx])
470 }
471
472 pub fn rtc_race_end(&self) -> NaiveDateTime {
474 self.rtc_race_end
475 }
476
477 pub fn rtc_race_begins(&self) -> NaiveDateTime {
479 self.rtc_race_begins
480 }
481
482 pub fn rtc_time_paused(&self) -> TimeDelta {
484 self.rtc_time_paused
485 }
486
487 pub fn pause_times(&self) -> &Vec<InGameTime> {
489 &self.pause_times
490 }
491
492 pub fn my_stuff_enabled(&self) -> bool {
494 self.my_stuff_enabled
495 }
496
497 pub fn my_stuff_used(&self) -> bool {
499 self.my_stuff_used
500 }
501
502 pub fn usb_gamecube_enabled(&self) -> bool {
504 self.usb_gamecube_enabled
505 }
506
507 pub fn final_lap_suspicious_intersection(&self) -> bool {
509 self.final_lap_suspicious_intersection
510 }
511
512 pub fn shroomstrat(&self) -> &[u8] {
514 &self.shroomstrat[0..self.lap_count as usize]
515 }
516
517 pub fn shroomstrat_string(&self) -> String {
522 let mut shroomstrat = self.shroomstrat().iter();
523
524 let mut s = shroomstrat.next().unwrap().to_string();
525 for lap_shrooms in shroomstrat {
526 write!(s, "-{lap_shrooms}").unwrap()
527 }
528
529 s
530 }
531
532 pub fn cannoned(&self) -> bool {
534 self.cannoned
535 }
536
537 pub fn went_oob(&self) -> bool {
539 self.went_oob
540 }
541
542 pub fn potential_slowdown(&self) -> bool {
544 self.potential_slowdown
545 }
546
547 pub fn potential_rapidfire(&self) -> bool {
549 self.potential_rapidfire
550 }
551
552 pub fn potentially_cheated_ghost(&self) -> bool {
554 self.potentially_cheated_ghost
555 }
556
557 pub fn has_mii_data_replaced(&self) -> bool {
559 self.has_mii_data_replaced
560 }
561
562 pub fn has_name_replaced(&self) -> bool {
564 self.has_name_replaced
565 }
566
567 pub fn respawns(&self) -> bool {
569 self.respawns
570 }
571
572 pub fn category(&self) -> Category {
574 self.category
575 }
576
577 pub fn footer_version(&self) -> u8 {
579 self.footer_version
580 }
581
582 pub fn len(&self) -> usize {
584 self.len
585 }
586
587 pub fn is_empty(&self) -> bool {
589 self.len == 0
590 }
591}
592
593fn contains_ctgp_pause(buttons: u8) -> bool {
597 buttons & 0x40 != 0
598}