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