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}