Skip to main content

rkg_utils/header/combo/
mod.rs

1pub mod character;
2pub mod transmission;
3pub mod vehicle;
4pub mod weight_class;
5
6use crate::{
7    byte_handler::{ByteHandler, ByteHandlerError, FromByteHandler},
8    header::combo::{
9        character::Character,
10        transmission::Transmission,
11        vehicle::Vehicle,
12        weight_class::{GetWeightClass, WeightClass},
13    },
14};
15use std::fmt::Display;
16
17/// Represents a valid character and vehicle combination from a Mario Kart Wii RKG ghost file.
18///
19/// A combo is only valid when the character and vehicle share the same [`weight_class::WeightClass`].
20/// Construction via [`Combo::new`] enforces this constraint.
21pub struct Combo {
22    /// The character used in the run.
23    character: Character,
24    /// The vehicle used in the run.
25    vehicle: Vehicle,
26}
27
28impl Combo {
29    pub const fn get_transmission(&self) -> Transmission {
30        self.vehicle.get_transmission()
31    }
32}
33
34/// Formats the combo as `"{character} on {vehicle}"` (e.g. `"Mario on Mach Bike"`).
35impl Display for Combo {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        write!(f, "{} on {}", self.character(), self.vehicle())
38    }
39}
40
41/// Errors that can occur while constructing or deserializing a [`Combo`].
42#[derive(thiserror::Error, Debug)]
43pub enum ComboError {
44    /// The input iterator did not contain enough bytes to extract a combo.
45    #[error("Insufficiently Long Iterator")]
46    InsufficientlyLongIterator,
47    /// The character and vehicle belong to different weight classes.
48    #[error("The combo has incongruent weight classes")]
49    IncongruentWeightClasses,
50    /// The vehicle byte did not map to a known [`Vehicle`] variant.
51    #[error("Invalid Vehicle ID")]
52    InvalidVehicleId,
53    /// The character byte did not map to a known [`Character`] variant.
54    #[error("Invalid Character ID")]
55    InvalidCharacterId,
56    /// The character ID corresponds to a character that cannot appear in ghost files.
57    #[error("Impossible Character ID")]
58    ImpossibleCharacterId,
59    /// A `ByteHandler` operation failed.
60    #[error("ByteHandler Error: {0}")]
61    ByteHandlerError(#[from] ByteHandlerError),
62}
63
64impl Combo {
65    /// Creates a new [`Combo`] from a vehicle and character.
66    ///
67    /// # Errors
68    ///
69    /// Returns [`ComboError::IncongruentWeightClasses`] if the character and vehicle
70    /// do not share the same [`WeightClass`].
71    #[inline(always)]
72    pub fn new(vehicle: Vehicle, character: Character) -> Result<Self, ComboError> {
73        if character.get_weight_class() != vehicle.get_weight_class() {
74            return Err(ComboError::IncongruentWeightClasses);
75        }
76
77        Ok(Self { vehicle, character })
78    }
79
80    /// Returns the character used in the run.
81    pub const fn character(&self) -> Character {
82        self.character
83    }
84
85    /// Returns the vehicle used in the run.
86    pub const fn vehicle(&self) -> Vehicle {
87        self.vehicle
88    }
89}
90
91/// Deserializes a [`Combo`] from a `ByteHandler` containing 2 bytes at header offset `0x08..0x0A`.
92///
93/// The bytes are packed as follows:
94/// ```text
95/// Byte 1: VVVVVVCC
96/// Byte 2: CCCCXXXX
97/// ```
98/// where `V` = vehicle ID bits and `C` = character ID bits.
99impl FromByteHandler for Combo {
100    type Err = ComboError;
101
102    fn from_byte_handler<T>(handler: T) -> Result<Self, Self::Err>
103    where
104        T: TryInto<ByteHandler>,
105        Self::Err: From<T::Error>,
106    {
107        let mut handler = handler.try_into()?;
108
109        handler.shift_right(2); // 1. 00VVVVVV
110        let vehicle = handler.copy_byte(0);
111
112        handler.shift_right(2); // 2. VVCCCCCC
113        let character = handler.copy_byte(1) & 0x3F;
114
115        Self::new(
116            Vehicle::try_from(vehicle).map_err(|_| ComboError::InvalidVehicleId)?,
117            Character::try_from(character).map_err(|_| ComboError::InvalidCharacterId)?,
118        )
119    }
120}
121
122/// Returns the weight class of the combo, which is always equal to both the
123/// character's and vehicle's weight class (enforced at construction time).
124impl GetWeightClass for Combo {
125    fn get_weight_class(&self) -> WeightClass {
126        self.character.get_weight_class()
127    }
128}