Skip to main content

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}