Skip to main content

rkg_utils/header/
mod.rs

1use crate::{
2    byte_handler::{ByteHandler, ByteHandlerError, FromByteHandler},
3    crc::crc16,
4    header::{
5        combo::{Combo, ComboError, transmission::Transmission},
6        controller::{Controller, ControllerError},
7        date::{Date, DateError},
8        ghost_type::{GhostType, GhostTypeError},
9        in_game_time::{InGameTime, InGameTimeError},
10        location::{
11            Location,
12            constants::{Country, CountryError, SubregionError, Version},
13        },
14        mii::{Mii, MiiError},
15        slot_id::{SlotId, SlotIdError},
16        transmission_mod::{TransmissionMod, TransmissionModError},
17    },
18    write_bits,
19};
20
21use std::io::Read;
22
23pub mod combo;
24pub mod controller;
25pub mod date;
26pub mod ghost_type;
27pub mod in_game_time;
28pub mod location;
29pub mod mii;
30pub mod slot_id;
31pub mod transmission_mod;
32
33/// Errors that can occur while parsing or modifying a [`Header`].
34#[derive(thiserror::Error, Debug)]
35pub enum HeaderError {
36    /// The file does not begin with the `RKGD` magic bytes.
37    #[error("File is not RKGD")]
38    NotRKGD,
39    /// The input slice is not exactly `0x88` bytes long.
40    #[error("Data passed is not correct size (0x88)")]
41    NotCorrectSize,
42    /// A friend ghost number was specified outside the valid range of 1–30.
43    #[error("Friend ghost number out of range (1-30)")]
44    FriendNumberOutOfRange,
45    /// A lap split idx was out of bounds for the recorded lap count.
46    #[error("Lap split idx not semantically valid")]
47    LapSplitIndexError,
48    /// A finish or lap time field could not be parsed.
49    #[error("In Game Time Error: {0}")]
50    InGameTimeError(#[from] InGameTimeError),
51    /// The slot ID field could not be parsed.
52    #[error("Slot ID Error: {0}")]
53    SlotIdError(#[from] SlotIdError),
54    /// The character/vehicle combo field could not be parsed.
55    #[error("Combo Error: {0}")]
56    ComboError(#[from] ComboError),
57    /// The date field could not be parsed.
58    #[error("Date Error: {0}")]
59    DateError(#[from] DateError),
60    /// The controller field could not be parsed.
61    #[error("Controller Error: {0}")]
62    ControllerError(#[from] ControllerError),
63    /// The transmission mod field could not be parsed.
64    #[error("Transmission Mod Error: {0}")]
65    TransmissionModError(#[from] TransmissionModError),
66    /// The ghost type field could not be parsed.
67    #[error("Ghost Type Error: {0}")]
68    GhostTypeError(#[from] GhostTypeError),
69    /// The embedded Mii data could not be parsed.
70    #[error("Mii Error: {0}")]
71    MiiError(#[from] MiiError),
72    /// A file I/O operation failed.
73    #[error("Io Error: {0}")]
74    IoError(#[from] std::io::Error),
75    /// The country code could not be parsed.
76    #[error("Country Error: {0}")]
77    CountryError(#[from] CountryError),
78    /// The subregion code could not be parsed.
79    #[error("Subregion Error: {0}")]
80    SubregionError(#[from] SubregionError),
81    /// A `ByteHandler` operation failed.
82    #[error("ByteHandler Error: {0}")]
83    ByteHandlerError(#[from] ByteHandlerError),
84}
85
86/// The parsed 136-byte (`0x88`) header of a Mario Kart Wii RKG ghost file.
87///
88/// Holds all metadata decoded from the RKGD file header, along with a copy of
89/// the raw bytes kept in sync with every setter. The layout is documented at
90/// <https://wiki.tockdom.com/wiki/RKG_(File_Format)#File_Header>.
91pub struct Header {
92    /// The raw 136-byte header block, kept in sync with all parsed fields.
93    raw_data: [u8; 0x88],
94    /// The ghost's recorded finish time.
95    finish_time: InGameTime,
96    /// The course slot the ghost was recorded on.
97    slot_id: SlotId,
98    /// The character and vehicle used.
99    combo: Combo,
100    /// The calendar date the ghost was set.
101    date_set: Date,
102    /// The input controller used to record the ghost.
103    controller: Controller,
104    /// Whether the ghost's input data is Yaz compressed.
105    is_compressed: bool,
106    /// The Retro Rewind transmission override active when the ghost was recorded.
107    transmission_mod: TransmissionMod,
108    /// The storage slot or origin category of the ghost.
109    ghost_type: GhostType,
110    /// Whether automatic drift (as opposed to manual) was used.
111    is_automatic_drift: bool,
112    /// The byte length of the input data after decompression.
113    decompressed_input_data_length: u16,
114    /// The number of laps recorded in the ghost.
115    lap_count: u8,
116    /// Per-lap split times; only the first [`lap_count`](Header::lap_count) entries are valid.
117    lap_split_times: [InGameTime; 11],
118    /// The player's geographic location at the time the ghost was set.
119    location: Location,
120    /// The Mii character embedded in the ghost header.
121    mii: Mii,
122    /// The CRC-16 checksum of the embedded Mii data (`0x3C`–`0x85` inclusive).
123    mii_crc16: u16,
124}
125
126impl Header {
127    /// Parses a [`Header`] from an RKG file at the given path.
128    ///
129    /// Only the first `0x88` bytes of the file are read.
130    ///
131    /// # Errors
132    ///
133    /// Returns [`HeaderError::IoError`] if the file cannot be opened or read,
134    /// and other [`HeaderError`] variants if any field fails to parse.
135    pub fn new_from_path<P: AsRef<std::path::Path>>(p: P) -> Result<Self, HeaderError> {
136        let mut rkg_data = [0u8; 0x88];
137        std::fs::File::open(p)?.read_exact(&mut rkg_data)?;
138        Self::new(&rkg_data)
139    }
140
141    /// Parses a [`Header`] from a 136-byte (`0x88`) slice.
142    ///
143    /// # Errors
144    ///
145    /// Returns [`HeaderError::NotCorrectSize`] if `header_data` is not exactly
146    /// `0x88` bytes long. Returns [`HeaderError::NotRKGD`] if the first four
147    /// bytes are not the `RKGD` magic. Returns other [`HeaderError`] variants
148    /// if any individual field fails to parse.
149    pub fn new(header_data: &[u8]) -> Result<Self, HeaderError> {
150        if header_data.len() != 0x88 {
151            return Err(HeaderError::NotCorrectSize);
152        }
153        if header_data[0..4] != [0x52, 0x4B, 0x47, 0x44] {
154            return Err(HeaderError::NotRKGD);
155        }
156
157        let finish_time = InGameTime::from_byte_handler(&header_data[4..7])?;
158        let slot_id = SlotId::from_byte_handler(header_data[7])?;
159        let combo = Combo::from_byte_handler(&header_data[0x08..0x0A])?;
160        let date_set = Date::from_byte_handler(&header_data[0x09..=0x0B])?;
161        let controller = Controller::from_byte_handler(header_data[0x0B])?;
162        let is_compressed = ByteHandler::from(header_data[0x0C]).read_bool(3);
163        let transmission_mod = TransmissionMod::from_byte_handler(header_data[0x0C])?;
164        let ghost_type = GhostType::from_byte_handler(&header_data[0x0C..=0x0D])?;
165        let is_automatic_drift = ByteHandler::from(header_data[0x0D]).read_bool(1);
166        let decompressed_input_data_length =
167            ByteHandler::try_from(&header_data[0x0E..=0x0F])?.copy_word(0);
168
169        let lap_count = header_data[0x10];
170        let mut lap_split_times = [InGameTime::default(); 11];
171        for idx in 0..lap_count {
172            let start = (0x11 + idx * 3) as usize;
173            lap_split_times[idx as usize] =
174                InGameTime::from_byte_handler(&header_data[start..start + 3])?;
175        }
176
177        let codes = ByteHandler::try_from(&header_data[0x34..=0x37]).unwrap();
178
179        let location = Location::find(codes.copy_byte(0), codes.copy_byte(1), Some(Version::ER12))
180            .unwrap_or_default();
181
182        let mii = Mii::new(&header_data[0x3C..0x3C + 0x4A])?;
183
184        let mii_crc16 = ByteHandler::try_from(&header_data[0x86..=0x87])?.copy_word(0);
185
186        Ok(Self {
187            raw_data: header_data.try_into().unwrap(),
188            finish_time,
189            slot_id,
190            combo,
191            date_set,
192            controller,
193            is_compressed,
194            transmission_mod,
195            ghost_type,
196            is_automatic_drift,
197            decompressed_input_data_length,
198            lap_count,
199            lap_split_times,
200            location,
201            mii,
202            mii_crc16,
203        })
204    }
205
206    /// Returns `true` if the stored Mii CRC-16 matches a computed
207    /// checksum of the Mii bytes at offsets `0x3C`–`0x85`.
208    pub fn verify_mii_crc16(&self) -> bool {
209        crc16(&self.raw_data[0x3C..0x86]) == self.mii_crc16()
210    }
211
212    /// Recomputes the Mii CRC-16 from the current raw header bytes and writes
213    /// the updated value to both the parsed field and the raw buffer.
214    pub fn fix_mii_crc16(&mut self) {
215        self.mii_crc16 = crc16(&self.raw_data[0x3C..0x86]);
216        self.raw_data[0x86..0x88].copy_from_slice(&self.mii_crc16.to_be_bytes());
217    }
218
219    /// Returns the raw 136-byte header block.
220    pub fn raw_data(&self) -> &[u8; 0x88] {
221        &self.raw_data
222    }
223
224    /// Returns a mutable reference to the raw 136-byte header block.
225    pub fn raw_data_mut(&mut self) -> &mut [u8; 0x88] {
226        &mut self.raw_data
227    }
228
229    /// Returns the ghost's recorded finish time.
230    pub fn finish_time(&self) -> &InGameTime {
231        &self.finish_time
232    }
233
234    /// Sets the finish time and updates the raw data accordingly.
235    pub fn set_finish_time(&mut self, finish_time: InGameTime) {
236        self.finish_time = finish_time;
237        write_in_game_time(self.raw_data_mut(), 0x04, 0, &finish_time);
238    }
239
240    /// Returns the course slot the ghost was recorded on.
241    pub fn slot_id(&self) -> SlotId {
242        self.slot_id
243    }
244
245    /// Sets the course slot and updates the raw data accordingly.
246    pub fn set_slot_id(&mut self, slot_id: SlotId) {
247        self.slot_id = slot_id;
248        write_bits(self.raw_data_mut(), 0x07, 0, 6, u8::from(slot_id) as u64);
249    }
250
251    /// Returns the character and vehicle combo used in the ghost.
252    pub fn combo(&self) -> &Combo {
253        &self.combo
254    }
255
256    /// Sets the character/vehicle combo and updates the raw data accordingly.
257    pub fn set_combo(&mut self, combo: Combo) {
258        write_bits(
259            self.raw_data_mut(),
260            0x08,
261            0,
262            6,
263            u8::from(combo.vehicle()) as u64,
264        );
265        write_bits(
266            self.raw_data_mut(),
267            0x08,
268            6,
269            6,
270            u8::from(combo.character()) as u64,
271        );
272
273        self.combo = combo;
274    }
275
276    /// Returns the date the ghost was set.
277    pub fn date_set(&self) -> &Date {
278        &self.date_set
279    }
280
281    /// Sets the ghost's date and updates the raw data accordingly.
282    pub fn set_date_set(&mut self, date_set: Date) {
283        write_bits(
284            self.raw_data_mut(),
285            0x09,
286            4,
287            7,
288            (date_set.year() - 2000) as u64,
289        );
290        write_bits(self.raw_data_mut(), 0x0A, 3, 4, date_set.month() as u64);
291        write_bits(self.raw_data_mut(), 0x0A, 7, 5, date_set.day() as u64);
292
293        self.date_set = date_set;
294    }
295
296    /// Returns the input controller used to record the ghost.
297    pub fn controller(&self) -> Controller {
298        self.controller
299    }
300
301    /// Sets the controller and updates the raw data accordingly.
302    pub fn set_controller(&mut self, controller: Controller) {
303        self.controller = controller;
304        write_bits(self.raw_data_mut(), 0x0B, 4, 4, u8::from(controller) as u64);
305    }
306
307    /// Returns whether the ghost's input data is Yaz1 compressed.
308    pub fn is_compressed(&self) -> bool {
309        self.is_compressed
310    }
311
312    /// Sets the compression flag and updates the raw data accordingly.
313    ///
314    /// This is `pub(crate)` because compression state should be managed by the
315    /// RKG file layer, not set directly by callers.
316    pub(crate) fn set_compressed(&mut self, is_compressed: bool) {
317        self.is_compressed = is_compressed;
318        write_bits(self.raw_data_mut(), 0x0C, 4, 1, is_compressed as u64);
319    }
320
321    /// Returns the Retro Rewind (Pulsar) transmission override active for this ghost.
322    pub fn transmission_mod(&self) -> TransmissionMod {
323        self.transmission_mod
324    }
325
326    /// Sets the transmission mod and updates the raw data accordingly.
327    pub fn set_transmission_mod(&mut self, transmission_mod: TransmissionMod) {
328        self.transmission_mod = transmission_mod;
329        write_bits(
330            self.raw_data_mut(),
331            0x0C,
332            5,
333            2,
334            u8::from(transmission_mod) as u64,
335        );
336    }
337
338    /// Returns the ghost type of this ghost.
339    pub fn ghost_type(&self) -> GhostType {
340        self.ghost_type
341    }
342
343    /// Sets the ghost type and updates the raw data accordingly.
344    pub fn set_ghost_type(&mut self, ghost_type: GhostType) {
345        self.ghost_type = ghost_type;
346        write_bits(self.raw_data_mut(), 0x0C, 7, 7, u8::from(ghost_type) as u64);
347    }
348
349    /// Returns whether automatic drift was used during the recorded run.
350    pub fn is_automatic_drift(&self) -> bool {
351        self.is_automatic_drift
352    }
353
354    /// Sets the automatic drift flag and updates the raw data accordingly.
355    pub fn set_automatic_drift(&mut self, is_automatic_drift: bool) {
356        self.is_automatic_drift = is_automatic_drift;
357        write_bits(self.raw_data_mut(), 0x0D, 6, 1, is_automatic_drift as u64);
358    }
359
360    /// Returns the byte length of the input data block after decompression.
361    pub fn decompressed_input_data_length(&self) -> u16 {
362        self.decompressed_input_data_length
363    }
364
365    /// Returns the number of laps recorded in this ghost.
366    pub fn lap_count(&self) -> u8 {
367        self.lap_count
368    }
369
370    /// Returns a slice of the valid lap split times (length equal to [`lap_count`](Header::lap_count)).
371    pub fn lap_split_times(&self) -> &[InGameTime] {
372        &self.lap_split_times[0..self.lap_count as usize]
373    }
374
375    /// Returns the lap split time at the given zero-based idx.
376    ///
377    /// # Errors
378    ///
379    /// Returns [`HeaderError::LapSplitIndexError`] if `idx` is greater than or
380    /// equal to [`lap_count`](Header::lap_count).
381    pub fn lap_split_time(&self, idx: usize) -> Result<InGameTime, HeaderError> {
382        if idx >= self.lap_count as usize {
383            return Err(HeaderError::LapSplitIndexError);
384        }
385        Ok(self.lap_split_times[idx])
386    }
387
388    /// Sets the lap split time at the given zero-based idx and updates the raw data accordingly.
389    ///
390    /// Does nothing if `idx` is greater than or equal to [`lap_count`](Header::lap_count).
391    pub fn set_lap_split_time(&mut self, idx: usize, lap_split_time: InGameTime) {
392        if idx >= self.lap_count as usize {
393            return;
394        }
395        self.lap_split_times[idx] = lap_split_time;
396
397        write_bits(
398            self.raw_data_mut(),
399            0x11 + idx * 0x03,
400            0,
401            7,
402            lap_split_time.minutes() as u64,
403        );
404        write_bits(
405            self.raw_data_mut(),
406            0x11 + idx * 0x03,
407            7,
408            7,
409            lap_split_time.seconds() as u64,
410        );
411        write_bits(
412            self.raw_data_mut(),
413            0x12 + idx * 0x03,
414            6,
415            10,
416            lap_split_time.milliseconds() as u64,
417        );
418    }
419
420    /// Returns the player's geographic location when the ghost was set.
421    pub fn location(&self) -> &Location {
422        &self.location
423    }
424
425    /// Sets the player's location and updates the raw data accordingly.
426    ///
427    /// When the country is [`Country::NotSet`], the subregion byte is written
428    /// as `0xFF` (Not Set).
429    pub fn set_location(&mut self, location: Location) {
430        write_bits(
431            self.raw_data_mut(),
432            0x34,
433            0,
434            8,
435            u8::from(location.country()) as u64,
436        );
437
438        let subregion_id = if location.country() != Country::NotSet {
439            u8::from(location.subregion()) as u64
440        } else {
441            0xFF
442        };
443
444        write_bits(self.raw_data_mut(), 0x35, 0, 8, subregion_id);
445
446        self.location = location;
447    }
448
449    /// Returns a reference to the Mii embedded in the ghost header.
450    pub fn mii(&self) -> &Mii {
451        &self.mii
452    }
453
454    /// Returns a mutable reference to the Mii embedded in the ghost header.
455    pub fn mii_mut(&mut self) -> &mut Mii {
456        &mut self.mii
457    }
458
459    /// Replaces the embedded Mii, updates the raw header bytes at `0x3C`–`0x85`,
460    /// and recomputes the Mii CRC-16.
461    pub fn set_mii(&mut self, mii: Mii) {
462        self.mii_crc16 = crc16(mii.raw_data());
463        self.raw_data_mut()[0x3C..0x86].copy_from_slice(mii.raw_data());
464        self.mii = mii;
465    }
466
467    /// Returns the CRC-16 checksum of the embedded Mii data as stored in the header.
468    pub fn mii_crc16(&self) -> u16 {
469        self.mii_crc16
470    }
471
472    /// Returns the transmission of the combo adjusted depending on transmission mod.
473    pub const fn transmission_adjusted(&self) -> Transmission {
474        match self.transmission_mod {
475            TransmissionMod::Vanilla => self.combo.get_transmission(),
476            TransmissionMod::AllInside => Transmission::Inside,
477            TransmissionMod::AllOutside => Transmission::Outside,
478            TransmissionMod::AllBikeInside if self.combo.vehicle().is_bike() => {
479                Transmission::Inside
480            }
481            TransmissionMod::AllBikeInside => Transmission::Outside,
482        }
483    }
484}
485
486/// Writes a packed [`InGameTime`] value (7 minutes + 7 seconds + 10 milliseconds bits)
487/// into `buf` starting at the given byte and bit offset.
488fn write_in_game_time(
489    buf: &mut [u8],
490    byte_offset: usize,
491    bit_offset: usize,
492    in_game_time: &InGameTime,
493) {
494    let mut bit_offset = bit_offset + byte_offset * 8;
495
496    write_bits(
497        buf,
498        bit_offset / 8,
499        bit_offset % 8,
500        7,
501        in_game_time.minutes() as u64,
502    );
503
504    bit_offset += 7;
505
506    write_bits(
507        buf,
508        bit_offset / 8,
509        bit_offset % 8,
510        7,
511        in_game_time.seconds() as u64,
512    );
513
514    bit_offset += 7;
515
516    write_bits(
517        buf,
518        bit_offset / 8,
519        bit_offset % 8,
520        10,
521        in_game_time.milliseconds() as u64,
522    );
523}