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 mut location = Location::find(codes.copy_byte(0), codes.copy_byte(1), Some(Version::ER12));
180
181        if location.is_none() {
182            location = Location::find(codes.copy_byte(0), codes.copy_byte(1), Some(Version::ER11));
183            
184            if location.is_none() {
185                location = Location::find(codes.copy_byte(0), codes.copy_byte(1), Some(Version::ER10));
186
187                if location.is_none() {
188                    location = Location::find(codes.copy_byte(0), codes.copy_byte(1), Some(Version::Vanilla));
189                }
190            }
191        }
192        
193        let location = location.unwrap_or_default();
194
195        let mii = Mii::new(&header_data[0x3C..0x3C + 0x4A])?;
196
197        let mii_crc16 = ByteHandler::try_from(&header_data[0x86..=0x87])?.copy_word(0);
198
199        Ok(Self {
200            raw_data: header_data.try_into().unwrap(),
201            finish_time,
202            slot_id,
203            combo,
204            date_set,
205            controller,
206            is_compressed,
207            transmission_mod,
208            ghost_type,
209            is_automatic_drift,
210            decompressed_input_data_length,
211            lap_count,
212            lap_split_times,
213            location,
214            mii,
215            mii_crc16,
216        })
217    }
218
219    /// Returns `true` if the stored Mii CRC-16 matches a computed
220    /// checksum of the Mii bytes at offsets `0x3C`–`0x85`.
221    pub fn verify_mii_crc16(&self) -> bool {
222        crc16(&self.raw_data[0x3C..0x86]) == self.mii_crc16()
223    }
224
225    /// Recomputes the Mii CRC-16 from the current raw header bytes and writes
226    /// the updated value to both the parsed field and the raw buffer.
227    pub fn fix_mii_crc16(&mut self) {
228        self.mii_crc16 = crc16(&self.raw_data[0x3C..0x86]);
229        self.raw_data[0x86..0x88].copy_from_slice(&self.mii_crc16.to_be_bytes());
230    }
231
232    /// Returns the raw 136-byte header block.
233    pub fn raw_data(&self) -> &[u8; 0x88] {
234        &self.raw_data
235    }
236
237    /// Returns a mutable reference to the raw 136-byte header block.
238    pub fn raw_data_mut(&mut self) -> &mut [u8; 0x88] {
239        &mut self.raw_data
240    }
241
242    /// Returns the ghost's recorded finish time.
243    pub fn finish_time(&self) -> &InGameTime {
244        &self.finish_time
245    }
246
247    /// Sets the finish time and updates the raw data accordingly.
248    pub fn set_finish_time(&mut self, finish_time: InGameTime) {
249        self.finish_time = finish_time;
250        write_in_game_time(self.raw_data_mut(), 0x04, 0, &finish_time);
251    }
252
253    /// Returns the course slot the ghost was recorded on.
254    pub fn slot_id(&self) -> SlotId {
255        self.slot_id
256    }
257
258    /// Sets the course slot and updates the raw data accordingly.
259    pub fn set_slot_id(&mut self, slot_id: SlotId) {
260        self.slot_id = slot_id;
261        write_bits(self.raw_data_mut(), 0x07, 0, 6, u8::from(slot_id) as u64);
262    }
263
264    /// Returns the character and vehicle combo used in the ghost.
265    pub fn combo(&self) -> &Combo {
266        &self.combo
267    }
268
269    /// Sets the character/vehicle combo and updates the raw data accordingly.
270    pub fn set_combo(&mut self, combo: Combo) {
271        write_bits(
272            self.raw_data_mut(),
273            0x08,
274            0,
275            6,
276            u8::from(combo.vehicle()) as u64,
277        );
278        write_bits(
279            self.raw_data_mut(),
280            0x08,
281            6,
282            6,
283            u8::from(combo.character()) as u64,
284        );
285
286        self.combo = combo;
287    }
288
289    /// Returns the date the ghost was set.
290    pub fn date_set(&self) -> &Date {
291        &self.date_set
292    }
293
294    /// Sets the ghost's date and updates the raw data accordingly.
295    pub fn set_date_set(&mut self, date_set: Date) {
296        write_bits(
297            self.raw_data_mut(),
298            0x09,
299            4,
300            7,
301            (date_set.year() - 2000) as u64,
302        );
303        write_bits(self.raw_data_mut(), 0x0A, 3, 4, date_set.month() as u64);
304        write_bits(self.raw_data_mut(), 0x0A, 7, 5, date_set.day() as u64);
305
306        self.date_set = date_set;
307    }
308
309    /// Returns the input controller used to record the ghost.
310    pub fn controller(&self) -> Controller {
311        self.controller
312    }
313
314    /// Sets the controller and updates the raw data accordingly.
315    pub fn set_controller(&mut self, controller: Controller) {
316        self.controller = controller;
317        write_bits(self.raw_data_mut(), 0x0B, 4, 4, u8::from(controller) as u64);
318    }
319
320    /// Returns whether the ghost's input data is Yaz1 compressed.
321    pub fn is_compressed(&self) -> bool {
322        self.is_compressed
323    }
324
325    /// Sets the compression flag and updates the raw data accordingly.
326    ///
327    /// This is `pub(crate)` because compression state should be managed by the
328    /// RKG file layer, not set directly by callers.
329    pub(crate) fn set_compressed(&mut self, is_compressed: bool) {
330        self.is_compressed = is_compressed;
331        write_bits(self.raw_data_mut(), 0x0C, 4, 1, is_compressed as u64);
332    }
333
334    /// Returns the Retro Rewind (Pulsar) transmission override active for this ghost.
335    pub fn transmission_mod(&self) -> TransmissionMod {
336        self.transmission_mod
337    }
338
339    /// Sets the transmission mod and updates the raw data accordingly.
340    pub fn set_transmission_mod(&mut self, transmission_mod: TransmissionMod) {
341        self.transmission_mod = transmission_mod;
342        write_bits(
343            self.raw_data_mut(),
344            0x0C,
345            5,
346            2,
347            u8::from(transmission_mod) as u64,
348        );
349    }
350
351    /// Returns the ghost type of this ghost.
352    pub fn ghost_type(&self) -> GhostType {
353        self.ghost_type
354    }
355
356    /// Sets the ghost type and updates the raw data accordingly.
357    pub fn set_ghost_type(&mut self, ghost_type: GhostType) {
358        self.ghost_type = ghost_type;
359        write_bits(self.raw_data_mut(), 0x0C, 7, 7, u8::from(ghost_type) as u64);
360    }
361
362    /// Returns whether automatic drift was used during the recorded run.
363    pub fn is_automatic_drift(&self) -> bool {
364        self.is_automatic_drift
365    }
366
367    /// Sets the automatic drift flag and updates the raw data accordingly.
368    pub fn set_automatic_drift(&mut self, is_automatic_drift: bool) {
369        self.is_automatic_drift = is_automatic_drift;
370        write_bits(self.raw_data_mut(), 0x0D, 6, 1, is_automatic_drift as u64);
371    }
372
373    /// Returns the byte length of the input data block after decompression.
374    pub fn decompressed_input_data_length(&self) -> u16 {
375        self.decompressed_input_data_length
376    }
377
378    /// Returns the number of laps recorded in this ghost.
379    pub fn lap_count(&self) -> u8 {
380        self.lap_count
381    }
382
383    /// Returns a slice of the valid lap split times (length equal to [`lap_count`](Header::lap_count)).
384    pub fn lap_split_times(&self) -> &[InGameTime] {
385        &self.lap_split_times[0..self.lap_count as usize]
386    }
387
388    /// Returns the lap split time at the given zero-based idx.
389    ///
390    /// # Errors
391    ///
392    /// Returns [`HeaderError::LapSplitIndexError`] if `idx` is greater than or
393    /// equal to [`lap_count`](Header::lap_count).
394    pub fn lap_split_time(&self, idx: usize) -> Result<InGameTime, HeaderError> {
395        if idx >= self.lap_count as usize {
396            return Err(HeaderError::LapSplitIndexError);
397        }
398        Ok(self.lap_split_times[idx])
399    }
400
401    /// Sets the lap split time at the given zero-based idx and updates the raw data accordingly.
402    ///
403    /// Does nothing if `idx` is greater than or equal to [`lap_count`](Header::lap_count).
404    pub fn set_lap_split_time(&mut self, idx: usize, lap_split_time: InGameTime) {
405        if idx >= self.lap_count as usize {
406            return;
407        }
408        self.lap_split_times[idx] = lap_split_time;
409
410        write_bits(
411            self.raw_data_mut(),
412            0x11 + idx * 0x03,
413            0,
414            7,
415            lap_split_time.minutes() as u64,
416        );
417        write_bits(
418            self.raw_data_mut(),
419            0x11 + idx * 0x03,
420            7,
421            7,
422            lap_split_time.seconds() as u64,
423        );
424        write_bits(
425            self.raw_data_mut(),
426            0x12 + idx * 0x03,
427            6,
428            10,
429            lap_split_time.milliseconds() as u64,
430        );
431    }
432
433    /// Returns the player's geographic location when the ghost was set.
434    pub fn location(&self) -> &Location {
435        &self.location
436    }
437
438    /// Sets the player's location and updates the raw data accordingly.
439    ///
440    /// When the country is [`Country::NotSet`], the subregion byte is written
441    /// as `0xFF` (Not Set).
442    pub fn set_location(&mut self, location: Location) {
443        write_bits(
444            self.raw_data_mut(),
445            0x34,
446            0,
447            8,
448            u8::from(location.country()) as u64,
449        );
450
451        let subregion_id = if location.country() != Country::NotSet {
452            u8::from(location.subregion()) as u64
453        } else {
454            0xFF
455        };
456
457        write_bits(self.raw_data_mut(), 0x35, 0, 8, subregion_id);
458
459        self.location = location;
460    }
461
462    /// Returns a reference to the Mii embedded in the ghost header.
463    pub fn mii(&self) -> &Mii {
464        &self.mii
465    }
466
467    /// Returns a mutable reference to the Mii embedded in the ghost header.
468    pub fn mii_mut(&mut self) -> &mut Mii {
469        &mut self.mii
470    }
471
472    /// Replaces the embedded Mii, updates the raw header bytes at `0x3C`–`0x85`,
473    /// and recomputes the Mii CRC-16.
474    pub fn set_mii(&mut self, mii: Mii) {
475        self.mii_crc16 = crc16(mii.raw_data());
476        self.raw_data_mut()[0x3C..0x86].copy_from_slice(mii.raw_data());
477        self.mii = mii;
478    }
479
480    /// Returns the CRC-16 checksum of the embedded Mii data as stored in the header.
481    pub fn mii_crc16(&self) -> u16 {
482        self.mii_crc16
483    }
484
485    /// Returns the transmission of the combo adjusted depending on transmission mod.
486    pub const fn transmission_adjusted(&self) -> Transmission {
487        match self.transmission_mod {
488            TransmissionMod::Vanilla => self.combo.get_transmission(),
489            TransmissionMod::AllInside => Transmission::Inside,
490            TransmissionMod::AllOutside => Transmission::Outside,
491            TransmissionMod::AllBikeInside if self.combo.vehicle().is_bike() => {
492                Transmission::Inside
493            }
494            TransmissionMod::AllBikeInside => Transmission::Outside,
495        }
496    }
497}
498
499/// Writes a packed [`InGameTime`] value (7 minutes + 7 seconds + 10 milliseconds bits)
500/// into `buf` starting at the given byte and bit offset.
501fn write_in_game_time(
502    buf: &mut [u8],
503    byte_offset: usize,
504    bit_offset: usize,
505    in_game_time: &InGameTime,
506) {
507    let mut bit_offset = bit_offset + byte_offset * 8;
508
509    write_bits(
510        buf,
511        bit_offset / 8,
512        bit_offset % 8,
513        7,
514        in_game_time.minutes() as u64,
515    );
516
517    bit_offset += 7;
518
519    write_bits(
520        buf,
521        bit_offset / 8,
522        bit_offset % 8,
523        7,
524        in_game_time.seconds() as u64,
525    );
526
527    bit_offset += 7;
528
529    write_bits(
530        buf,
531        bit_offset / 8,
532        bit_offset % 8,
533        10,
534        in_game_time.milliseconds() as u64,
535    );
536}