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    /// Creates a new, valid [`InGameTime`] from milliseconds.
50    pub fn from_milliseconds(milliseconds: u32) -> Self {
51        let millis = (milliseconds % 1000) as u16;
52        let seconds = ((milliseconds / 1000) % 60) as u8;
53        let minutes = ((milliseconds / 60000) % 60) as u8;
54
55        Self {
56            minutes,
57            seconds,
58            milliseconds: millis,
59        }
60    }
61
62    /// Returns the minutes component of the time.
63    pub fn minutes(self) -> u8 {
64        self.minutes
65    }
66
67    /// Returns the seconds component of the time.
68    pub fn seconds(self) -> u8 {
69        self.seconds
70    }
71
72    /// Returns the milliseconds component of the time.
73    pub fn milliseconds(self) -> u16 {
74        self.milliseconds
75    }
76
77    /// Returns `true` if the time falls within possible in-game bounds.
78    ///
79    /// A time is considered technically valid when minutes ≤ 99, seconds ≤ 59,
80    /// and milliseconds ≤ 999. Times outside these bounds can appear in ghost
81    /// files but would not be achievable under standard race conditions.
82    pub fn is_technically_valid(self) -> bool {
83        self.minutes > 99 || self.seconds > 59 || self.milliseconds > 999
84    }
85
86    /// Converts the time to a total number of milliseconds.
87    pub fn igt_to_millis(self) -> u32 {
88        (self.milliseconds as u32) + (self.seconds as u32) * 1000 + (self.minutes as u32) * 60000
89    }
90}
91
92/// Formats the time as `MM:SS.mmm` (e.g. `"01:23.456"`).
93impl Display for InGameTime {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        write!(
96            f,
97            "{:02}:{:02}.{:03}",
98            self.minutes, self.seconds, self.milliseconds
99        )
100    }
101}
102
103/// Deserializes an [`InGameTime`] from 3 packed bytes.
104///
105/// The bits are laid out as follows, where `M` = minutes, `S` = seconds,
106/// and `C` = milliseconds:
107/// ```text
108/// Byte 1: MMMMMMMMS
109/// Byte 2: SSSSSSCC
110/// Byte 3: CCCCCCCC
111/// ```
112/// Minutes occupy 7 bits, seconds 7 bits, and milliseconds 10 bits.
113impl FromByteHandler for InGameTime {
114    type Err = InGameTimeError;
115
116    fn from_byte_handler<T>(handler: T) -> Result<Self, Self::Err>
117    where
118        T: TryInto<ByteHandler>,
119        Self::Err: From<T::Error>,
120    {
121        let mut handler = handler.try_into()?;
122
123        handler.shift_right(1);
124        let minutes = handler.copy_byte(0);
125        let seconds = handler.copy_byte(1) >> 1;
126        handler.shift_left(9);
127
128        Ok(Self {
129            minutes,
130            seconds,
131            milliseconds: handler.copy_word(0) & 0x3FF,
132        })
133    }
134}
135
136/// Adds two [`InGameTime`] values by converting to total milliseconds and back.
137impl std::ops::Add for InGameTime {
138    type Output = InGameTime;
139
140    fn add(self, rhs: InGameTime) -> InGameTime {
141        let total_millis = self.igt_to_millis() + rhs.igt_to_millis();
142
143        let milliseconds = (total_millis % 1000) as u16;
144        let total_seconds = total_millis / 1000;
145        let seconds = (total_seconds % 60) as u8;
146        let minutes = (total_seconds / 60) as u8;
147
148        InGameTime::new(minutes, seconds, milliseconds)
149    }
150}
151
152/// Sums an iterator of [`InGameTime`] values, starting from [`InGameTime::default`] (zero).
153impl std::iter::Sum for InGameTime {
154    fn sum<I: Iterator<Item = InGameTime>>(iter: I) -> Self {
155        iter.fold(InGameTime::default(), |a, b| a + b)
156    }
157}