rkg_utils/header/in_game_time.rs
1use std::fmt::Display;
2
3use crate::byte_handler::{ByteHandler, ByteHandlerError, FromByteHandler};
4
5/// Errors that can occur while deserializing an [`InGameTime`].
6#[derive(thiserror::Error, Debug)]
7pub enum InGameTimeError {
8 /// The input iterator did not contain enough bytes to extract a time value.
9 #[error("Insufficiently Long Iterator")]
10 InsufficientlyLongIterator,
11 /// A `ByteHandler` operation failed.
12 #[error("ByteHandler Error: {0}")]
13 ByteHandlerError(#[from] ByteHandlerError),
14}
15
16/// A lap or finish time recorded in a Mario Kart Wii ghost file, expressed as
17/// minutes, seconds, and milliseconds.
18///
19/// Not all combinations of fields are semantically meaningful; use
20/// [`is_technically_valid`](InGameTime::is_technically_valid) to check whether
21/// the time falls within the bounds expected during normal gameplay.
22///
23/// [`InGameTime`] values can be added together and summed via the standard
24/// [`Add`](std::ops::Add) and [`Sum`](std::iter::Sum) traits, which convert
25/// through total milliseconds to avoid per-field overflow.
26#[derive(Default, Clone, Copy)]
27pub struct InGameTime {
28 /// Minutes component (0–99 in normal play).
29 minutes: u8,
30 /// Seconds component (0–59 in normal play).
31 seconds: u8,
32 /// Milliseconds component (0–999 in normal play).
33 milliseconds: u16,
34}
35
36impl InGameTime {
37 /// Creates a new [`InGameTime`] from raw minutes, seconds, and milliseconds.
38 ///
39 /// No range validation is performed; all combinations are accepted.
40 #[inline(always)]
41 pub fn new(minutes: u8, seconds: u8, milliseconds: u16) -> Self {
42 Self {
43 minutes,
44 seconds,
45 milliseconds,
46 }
47 }
48
49 /// Returns the minutes component of the time.
50 pub fn minutes(self) -> u8 {
51 self.minutes
52 }
53
54 /// Returns the seconds component of the time.
55 pub fn seconds(self) -> u8 {
56 self.seconds
57 }
58
59 /// Returns the milliseconds component of the time.
60 pub fn milliseconds(self) -> u16 {
61 self.milliseconds
62 }
63
64 /// Returns `true` if the time falls within possible in-game bounds.
65 ///
66 /// A time is considered technically valid when minutes ≤ 99, seconds ≤ 59,
67 /// and milliseconds ≤ 999. Times outside these bounds can appear in ghost
68 /// files but would not be achievable under standard race conditions.
69 pub fn is_technically_valid(self) -> bool {
70 self.minutes > 99 || self.seconds > 59 || self.milliseconds > 999
71 }
72
73 /// Converts the time to a total number of milliseconds.
74 pub fn igt_to_millis(self) -> u32 {
75 (self.milliseconds as u32) + (self.seconds as u32) * 1000 + (self.minutes as u32) * 60000
76 }
77}
78
79/// Formats the time as `MM:SS.mmm` (e.g. `"01:23.456"`).
80impl Display for InGameTime {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 write!(
83 f,
84 "{:02}:{:02}.{:03}",
85 self.minutes, self.seconds, self.milliseconds
86 )
87 }
88}
89
90/// Deserializes an [`InGameTime`] from 3 packed bytes.
91///
92/// The bits are laid out as follows, where `M` = minutes, `S` = seconds,
93/// and `C` = milliseconds:
94/// ```text
95/// Byte 1: MMMMMMMMS
96/// Byte 2: SSSSSSCC
97/// Byte 3: CCCCCCCC
98/// ```
99/// Minutes occupy 7 bits, seconds 7 bits, and milliseconds 10 bits.
100impl FromByteHandler for InGameTime {
101 type Err = InGameTimeError;
102
103 fn from_byte_handler<T>(handler: T) -> Result<Self, Self::Err>
104 where
105 T: TryInto<ByteHandler>,
106 Self::Err: From<T::Error>,
107 {
108 let mut handler = handler.try_into()?;
109
110 handler.shift_right(1);
111 let minutes = handler.copy_byte(0);
112 let seconds = handler.copy_byte(1) >> 1;
113 handler.shift_left(9);
114
115 Ok(Self {
116 minutes,
117 seconds,
118 milliseconds: handler.copy_word(0) & 0x3FF,
119 })
120 }
121}
122
123/// Adds two [`InGameTime`] values by converting to total milliseconds and back.
124impl std::ops::Add for InGameTime {
125 type Output = InGameTime;
126
127 fn add(self, rhs: InGameTime) -> InGameTime {
128 let total_millis = self.igt_to_millis() + rhs.igt_to_millis();
129
130 let milliseconds = (total_millis % 1000) as u16;
131 let total_seconds = total_millis / 1000;
132 let seconds = (total_seconds % 60) as u8;
133 let minutes = (total_seconds / 60) as u8;
134
135 InGameTime::new(minutes, seconds, milliseconds)
136 }
137}
138
139/// Sums an iterator of [`InGameTime`] values, starting from [`InGameTime::default`] (zero).
140impl std::iter::Sum for InGameTime {
141 fn sum<I: Iterator<Item = InGameTime>>(iter: I) -> Self {
142 iter.fold(InGameTime::default(), |a, b| a + b)
143 }
144}