Skip to main content

rkg_utils/header/mii/
mod.rs

1use chrono::{DateTime, Duration, NaiveDateTime};
2
3use crate::{
4    byte_handler::{ByteHandler, ByteHandlerError, FromByteHandler},
5    header::mii::{
6        birthday::{Birthday, BirthdayError},
7        build::{Build, BuildError},
8        eyebrows::{Eyebrows, EyebrowsError},
9        eyes::{Eyes, EyesError},
10        facial_hair::{FacialHair, FacialHairError},
11        favorite_color::{FavoriteColor, FavoriteColorError},
12        glasses::{Glasses, GlassesError},
13        hair::{Hair, HairError},
14        head::{Head, HeadError},
15        lips::{Lips, LipsError},
16        mii_type::{MiiType, MiiTypeError},
17        mole::{Mole, MoleError},
18        nose::{Nose, NoseError},
19    },
20    write_bits,
21};
22
23use std::io::{Read, Write};
24
25pub mod birthday;
26pub mod build;
27pub mod eyebrows;
28pub mod eyes;
29pub mod facial_hair;
30pub mod favorite_color;
31pub mod glasses;
32pub mod hair;
33pub mod head;
34pub mod lips;
35pub mod mii_type;
36pub mod mole;
37pub mod nose;
38
39/// Errors that can occur while parsing or modifying a [`Mii`].
40#[derive(thiserror::Error, Debug)]
41pub enum MiiError {
42    /// A UTF-16 byte sequence could not be decoded as a valid string.
43    #[error("FromUtf16Error: {0}")]
44    FromUtf16Error(#[from] std::string::FromUtf16Error),
45    /// The input data was not exactly `0x4A` bytes long.
46    #[error("Invalid data length")]
47    InvalidLength,
48    /// A birthday field could not be parsed.
49    #[error("Birthday Error: {0}")]
50    BirthdayError(#[from] BirthdayError),
51    /// A favorite color field could not be parsed.
52    #[error("FavColor Error: {0}")]
53    FavColorError(#[from] FavoriteColorError),
54    /// A build (height/weight) field could not be parsed.
55    #[error("Build Error: {0}")]
56    BuildError(#[from] BuildError),
57    /// A head (shape/skin tone/face features) field could not be parsed.
58    #[error("Head Error: {0}")]
59    HeadError(#[from] HeadError),
60    /// A hair field could not be parsed.
61    #[error("Hair Error: {0}")]
62    HairError(#[from] HairError),
63    /// An eyebrows field could not be parsed.
64    #[error("Eyebrows Error: {0}")]
65    EyebrowsError(#[from] EyebrowsError),
66    /// An eyes field could not be parsed.
67    #[error("Eyes Error: {0}")]
68    EyesError(#[from] EyesError),
69    /// A nose field could not be parsed.
70    #[error("Nose Error: {0}")]
71    NoseError(#[from] NoseError),
72    /// A lips field could not be parsed.
73    #[error("Lips Error: {0}")]
74    LipsError(#[from] LipsError),
75    /// A glasses field could not be parsed.
76    #[error("Glasses Error: {0}")]
77    GlassesError(#[from] GlassesError),
78    /// A facial hair field could not be parsed.
79    #[error("Facial Hair Error: {0}")]
80    FacialHairError(#[from] FacialHairError),
81    /// A mole field could not be parsed.
82    #[error("Mole Error: {0}")]
83    MoleError(#[from] MoleError),
84    /// A Mii type field could not be parsed.
85    #[error("Mii Type Error: {0}")]
86    MiiTypeError(#[from] MiiTypeError),
87    /// A `ByteHandler` operation failed.
88    #[error("ByteHandler Error: {0}")]
89    ByteHandlerError(#[from] ByteHandlerError),
90    /// A file I/O operation failed.
91    #[error("IO Error: {0}")]
92    IOError(#[from] std::io::Error),
93}
94
95/// A fully parsed Mii character, as stored in the Wii's Mii data format.
96///
97/// Holds all customization fields decoded from the 74-byte (`0x4A`) Mii binary
98/// block, along with a copy of the original raw bytes. Any setter method updates
99/// both the parsed field and the corresponding bits in [`raw_data`](Mii::raw_data),
100/// and sets [`is_modified`](Mii::is_modified) to `true`.
101///
102/// The binary layout is documented at <http://wiibrew.org/wiki/Mii_Data#Mii_format>.
103pub struct Mii {
104    /// The raw 74-byte Mii data block, kept in sync with all parsed fields.
105    raw_data: [u8; 0x4A],
106    /// Whether any field has been modified since the Mii was parsed.
107    is_modified: bool,
108    /// Whether the Mii is female (`true`) or male (`false`).
109    is_girl: bool,
110    /// The Mii's birthday.
111    birthday: Birthday,
112    /// The Mii's favorite color, used to tint outfits and UI elements.
113    favorite_color: FavoriteColor,
114    /// Whether this Mii has been marked as a favorite.
115    is_favorite: bool,
116    /// The Mii's display name (up to 10 characters).
117    name: String,
118    /// The Mii's body proportions (height and weight).
119    build: Build,
120    /// Whether this is a normal, foreign, or special Mii.
121    mii_type: MiiType,
122    /// The date and time the Mii was created, derived from the embedded timestamp.
123    creation_date: NaiveDateTime,
124    /// The full 32-bit Mii ID, including the type bits and creation timestamp.
125    mii_id: u32,
126    /// The ID of the Wii console that created this Mii.
127    system_id: u32,
128    /// The Mii's head shape, skin tone, and facial feature overlay.
129    head: Head,
130    /// Whether StreetPass/Mingle is disabled for this Mii.
131    mingle_off: bool,
132    /// Whether this Mii was downloaded from the Mii Channel.
133    downloaded: bool,
134    /// The Mii's hair style, color, and flip setting.
135    hair: Hair,
136    /// The Mii's eyebrow style, color, size, rotation, and position.
137    eyebrows: Eyebrows,
138    /// The Mii's eye style, color, size, rotation, and position.
139    eyes: Eyes,
140    /// The Mii's nose style, size, and vertical position.
141    nose: Nose,
142    /// The Mii's lip style, color, size, and vertical position.
143    lips: Lips,
144    /// The Mii's glasses style, color, size, and vertical position.
145    glasses: Glasses,
146    /// The Mii's beard and mustache style, color, size, and position.
147    facial_hair: FacialHair,
148    /// The Mii's mole visibility, position, and size.
149    mole: Mole,
150    /// The name of the player who created this Mii (up to 10 characters).
151    creator_name: String,
152}
153
154impl Mii {
155    /// Parses a [`Mii`] from a 74-byte (`0x4A`) data block.
156    ///
157    /// Accepts any type that can be converted into `[u8; 0x4A]`, such as a
158    /// `&[u8]` slice or a byte array.
159    ///
160    /// # Errors
161    ///
162    /// Returns [`MiiError::InvalidLength`] if the input cannot be converted to
163    /// exactly `0x4A` bytes. Returns other [`MiiError`] variants if any
164    /// individual field fails to parse.
165    pub fn new(mii_data: impl TryInto<[u8; 0x4A]>) -> Result<Self, MiiError> {
166        let raw_data = mii_data.try_into().map_err(|_| MiiError::InvalidLength)?;
167
168        let bytes = ByteHandler::try_from(&raw_data[0..=1])?;
169        let is_girl = bytes.read_bool(6);
170        let birthday = Birthday::from_byte_handler(bytes)?;
171
172        let favorite_color = FavoriteColor::from_byte_handler(raw_data[1])?;
173        let is_favorite = raw_data[1].is_multiple_of(2);
174
175        let name = utf16be_to_string(&raw_data[0x02..=0x15])?;
176
177        let build = Build::from_byte_handler(&raw_data[0x16..=0x17])?;
178
179        let mii_type = MiiType::try_from(&raw_data[0x18] >> 5)?;
180
181        let mut mii_id = raw_data[0x18..0x1C].to_owned();
182        mii_id[0] &= 0x1F;
183        let creation_date =
184            creation_date_from_timestamp(u32::from_be_bytes(mii_id.try_into().unwrap()));
185
186        let mii_id = ByteHandler::try_from(&raw_data[0x18..=0x1B])?.copy_dword();
187        let system_id = ByteHandler::try_from(&raw_data[0x1C..=0x1F])?.copy_dword();
188
189        let bytes = ByteHandler::try_from(&raw_data[0x20..=0x21])?;
190        let mingle_off = bytes.read_bool(10);
191        let downloaded = bytes.read_bool(8);
192        let head = Head::from_byte_handler(bytes)?;
193        let hair = Hair::from_byte_handler(&raw_data[0x22..=0x23])?;
194        let eyebrows = Eyebrows::from_byte_handler(&raw_data[0x24..=0x27])?;
195        let eyes = Eyes::from_byte_handler(&raw_data[0x28..=0x2B])?;
196        let nose = Nose::from_byte_handler(&raw_data[0x2C..=0x2D])?;
197        let lips = Lips::from_byte_handler(&raw_data[0x2E..=0x2F])?;
198        let glasses = Glasses::from_byte_handler(&raw_data[0x30..=0x31])?;
199        let facial_hair = FacialHair::from_byte_handler(&raw_data[0x32..=0x33])?;
200        let mole = Mole::from_byte_handler(&raw_data[0x34..=0x35])?;
201
202        let creator_name = utf16be_to_string(&raw_data[0x36..=0x49])?;
203
204        let is_modified = false;
205
206        Ok(Self {
207            raw_data,
208            is_modified,
209            is_girl,
210            birthday,
211            favorite_color,
212            is_favorite,
213            name,
214            build,
215            mii_type,
216            creation_date,
217            mii_id,
218            system_id,
219            head,
220            mingle_off,
221            downloaded,
222            hair,
223            eyebrows,
224            eyes,
225            nose,
226            lips,
227            glasses,
228            facial_hair,
229            mole,
230            creator_name,
231        })
232    }
233
234    /// Parses a [`Mii`] from a raw Mii file or an RKG ghost file.
235    ///
236    /// If the file begins with the `RKGD` magic bytes, Mii data is read
237    /// starting at offset `0x3C` within the file. Otherwise the file is
238    /// read from the beginning. In both cases exactly `0x4A` bytes are used.
239    ///
240    /// # Errors
241    ///
242    /// Returns [`MiiError::InvalidLength`] if the file contains fewer than
243    /// `0x4A` usable bytes. Returns [`MiiError::IOError`] for file I/O
244    /// failures, and other [`MiiError`] variants if any field fails to parse.
245    pub fn new_from_file<T: AsRef<std::path::Path>>(path: T) -> Result<Self, MiiError> {
246        let mut data = Vec::with_capacity(0x4A);
247        std::fs::File::open(&path)?.read_exact(&mut data)?;
248
249        if data[..4] == *b"RKGD" {
250            // Mii data starts at offset 0x3C in an RKG file
251            data = Vec::from(&data[0x3C..]);
252        }
253
254        if data.len() < 0x4A {
255            return Err(MiiError::InvalidLength);
256        }
257
258        Self::new(&data[..0x4A])
259    }
260
261    /// Writes the Mii's raw data to a file, creating or truncating it as needed.
262    ///
263    /// # Errors
264    ///
265    /// Returns [`MiiError::IOError`] if the file cannot be created or written.
266    pub fn save_to_file<T: AsRef<std::path::Path>>(&self, path: T) -> Result<(), MiiError> {
267        let mut file = std::fs::File::create(path)?;
268        file.write_all(self.raw_data())?;
269
270        Ok(())
271    }
272
273    /// Returns the raw 74-byte Mii data block.
274    pub fn raw_data(&self) -> &[u8] {
275        &self.raw_data
276    }
277
278    /// Returns a mutable reference to the raw 74-byte Mii data block.
279    pub fn raw_data_mut(&mut self) -> &mut [u8] {
280        &mut self.raw_data
281    }
282
283    /// Returns whether any field has been modified since the Mii was parsed.
284    pub fn is_modified(&self) -> bool {
285        self.is_modified
286    }
287
288    /// Returns whether the Mii is female.
289    pub fn is_girl(&self) -> bool {
290        self.is_girl
291    }
292
293    /// Sets whether the Mii is female and updates the raw data accordingly.
294    pub fn set_is_girl(&mut self, is_girl: bool) {
295        self.is_girl = is_girl;
296        write_bits(self.raw_data_mut(), 0x00, 1, 1, is_girl as u64);
297        self.is_modified = true;
298    }
299
300    /// Returns the Mii's birthday.
301    pub fn birthday(&self) -> Birthday {
302        self.birthday
303    }
304
305    /// Sets the Mii's birthday and updates the raw data accordingly.
306    pub fn set_birthday(&mut self, birthday: Birthday) {
307        // "public fortnite" - fawwe
308
309        self.birthday = birthday;
310
311        let month = birthday.month().unwrap_or(0) as u64;
312        let day = birthday.day().unwrap_or(0) as u64;
313
314        write_bits(self.raw_data_mut(), 0x00, 2, 4, month);
315        write_bits(self.raw_data_mut(), 0x00, 6, 5, day);
316
317        self.is_modified = true;
318    }
319
320    /// Returns the Mii's favorite color.
321    pub fn favorite_color(&self) -> FavoriteColor {
322        self.favorite_color
323    }
324
325    /// Sets the Mii's favorite color and updates the raw data accordingly.
326    pub fn set_favorite_color(&mut self, favorite_color: FavoriteColor) {
327        self.favorite_color = favorite_color;
328        write_bits(
329            self.raw_data_mut(),
330            0x01,
331            3,
332            4,
333            u8::from(favorite_color) as u64,
334        );
335        self.is_modified = true;
336    }
337
338    /// Returns whether this Mii has been marked as a favorite.
339    pub fn is_favorite(&self) -> bool {
340        self.is_favorite
341    }
342
343    /// Sets whether this Mii is marked as a favorite and updates the raw data accordingly.
344    pub fn set_is_favorite(&mut self, is_favorite: bool) {
345        self.is_favorite = is_favorite;
346        write_bits(self.raw_data_mut(), 0x01, 7, 1, is_favorite as u64);
347        self.is_modified = true;
348    }
349
350    /// Returns the Mii's display name.
351    pub fn name(&self) -> &str {
352        &self.name
353    }
354
355    /// Sets the Mii's display name and updates the raw data accordingly.
356    ///
357    /// Names longer than 10 characters are silently ignored.
358    pub fn set_name(&mut self, name: &str) {
359        if name.len() > 10 {
360            return;
361        }
362        self.name = name.to_string();
363        let name_bytes = string_to_utf16be(name);
364        let mut padded = [0u8; 0x14];
365        padded[..name_bytes.len()].copy_from_slice(&name_bytes);
366        self.raw_data_mut()[0x02..0x16].copy_from_slice(&padded);
367        self.is_modified = true;
368    }
369
370    /// Returns the Mii's body proportions (height and weight).
371    pub fn build(&self) -> Build {
372        self.build
373    }
374
375    /// Sets the Mii's body proportions and updates the raw data accordingly.
376    pub fn set_build(&mut self, build: Build) {
377        self.build = build;
378
379        write_bits(self.raw_data_mut(), 0x16, 1, 7, build.height() as u64);
380        write_bits(self.raw_data_mut(), 0x17, 1, 7, build.weight() as u64);
381
382        self.is_modified = true;
383    }
384
385    /// Returns whether this is a normal, foreign, or special Mii.
386    pub fn mii_type(&self) -> MiiType {
387        self.mii_type
388    }
389
390    /// Sets the Mii type and updates the type bits in the raw Mii ID field.
391    ///
392    /// Does nothing if the type is already set to the given value.
393    pub fn set_mii_type(&mut self, mii_type: MiiType) {
394        if self.mii_type() == mii_type {
395            return;
396        }
397        let raw_data = self.raw_data_mut();
398        write_bits(raw_data, 0x18, 0, 3, u8::from(mii_type) as u64);
399        self.mii_id = u32::from_be_bytes(raw_data[0x18..0x1C].try_into().unwrap());
400        self.mii_type = mii_type;
401    }
402
403    /// Returns the date and time the Mii was created.
404    pub fn creation_date(&self) -> NaiveDateTime {
405        self.creation_date
406    }
407
408    /// Sets the Mii's creation date and updates the timestamp bits in the raw Mii ID field.
409    ///
410    /// Note: the valid range of the timestamp has not been fully determined.
411    pub fn set_creation_date(&mut self, creation_date: NaiveDateTime) {
412        // TODO: determine limits for timestamp
413        let raw_data = self.raw_data_mut();
414        write_bits(
415            raw_data,
416            0x18,
417            3,
418            29,
419            timestamp_from_creation_date(creation_date) as u64,
420        );
421        self.mii_id = u32::from_be_bytes(raw_data[0x18..0x1C].try_into().unwrap());
422        self.creation_date = creation_date;
423    }
424
425    /// Returns the full 32-bit Mii ID, which encodes both the type and creation timestamp.
426    pub fn mii_id(&self) -> u32 {
427        self.mii_id
428    }
429
430    /// Returns the ID of the Wii console that created this Mii.
431    pub fn system_id(&self) -> u32 {
432        self.system_id
433    }
434
435    /// Sets the system ID and updates the raw data accordingly.
436    pub fn set_system_id(&mut self, system_id: u32) {
437        self.system_id = system_id;
438        self.raw_data_mut()[0x1C..0x20].copy_from_slice(&system_id.to_be_bytes());
439        self.is_modified = true;
440    }
441
442    /// Returns the Mii's head shape, skin tone, and facial feature overlay.
443    pub fn head(&self) -> Head {
444        self.head
445    }
446
447    /// Sets the Mii's head options and updates the raw data accordingly.
448    pub fn set_head(&mut self, head: Head) {
449        self.head = head;
450
451        write_bits(
452            self.raw_data_mut(),
453            0x20,
454            0,
455            3,
456            u8::from(head.shape()) as u64,
457        );
458        write_bits(
459            self.raw_data_mut(),
460            0x20,
461            3,
462            3,
463            u8::from(head.skin_tone()) as u64,
464        );
465        write_bits(
466            self.raw_data_mut(),
467            0x20,
468            6,
469            4,
470            u8::from(head.face_features()) as u64,
471        );
472
473        self.is_modified = true;
474    }
475
476    /// Returns whether StreetPass/Mingle is enabled for this Mii.
477    pub fn is_mingle_enabled(&self) -> bool {
478        !self.mingle_off
479    }
480
481    /// Sets whether StreetPass/Mingle is enabled for this Mii and updates the raw data accordingly.
482    pub fn set_mingle_enabled(&mut self, enabled: bool) {
483        self.mingle_off = !enabled;
484        write_bits(self.raw_data_mut(), 0x21, 5, 1, !enabled as u64);
485        self.is_modified = true;
486    }
487
488    /// Returns whether this Mii was downloaded from the Mii Channel.
489    pub fn downloaded(&self) -> bool {
490        self.downloaded
491    }
492
493    /// Sets whether this Mii was downloaded from the Mii Channel and updates the raw data accordingly.
494    pub fn set_downloaded(&mut self, downloaded: bool) {
495        self.downloaded = downloaded;
496        write_bits(self.raw_data_mut(), 0x21, 7, 1, downloaded as u64);
497        self.is_modified = true;
498    }
499
500    /// Returns the Mii's hair style, color, and flip setting.
501    pub fn hair(&self) -> Hair {
502        self.hair
503    }
504
505    /// Sets the Mii's hair options and updates the raw data accordingly.
506    pub fn set_hair(&mut self, hair: Hair) {
507        self.hair = hair;
508
509        write_bits(
510            self.raw_data_mut(),
511            0x22,
512            0,
513            7,
514            u8::from(hair.hair_type()) as u64,
515        );
516        write_bits(
517            self.raw_data_mut(),
518            0x22,
519            7,
520            3,
521            u8::from(hair.hair_color()) as u64,
522        );
523        write_bits(self.raw_data_mut(), 0x23, 2, 1, hair.is_flipped() as u64);
524
525        self.is_modified = true;
526    }
527
528    /// Returns the Mii's eyebrow options.
529    pub fn eyebrows(&self) -> Eyebrows {
530        self.eyebrows
531    }
532
533    /// Sets the Mii's eyebrow options and updates the raw data accordingly.
534    pub fn set_eyebrows(&mut self, eyebrows: Eyebrows) {
535        self.eyebrows = eyebrows;
536
537        write_bits(
538            self.raw_data_mut(),
539            0x24,
540            0,
541            5,
542            u8::from(eyebrows.eyebrow_type()) as u64,
543        );
544        write_bits(self.raw_data_mut(), 0x24, 5, 5, eyebrows.rotation() as u64);
545        write_bits(
546            self.raw_data_mut(),
547            0x26,
548            0,
549            3,
550            u8::from(eyebrows.eyebrow_color()) as u64,
551        );
552        write_bits(self.raw_data_mut(), 0x26, 3, 4, eyebrows.size() as u64);
553        write_bits(self.raw_data_mut(), 0x26, 7, 5, eyebrows.y() as u64);
554        write_bits(self.raw_data_mut(), 0x27, 4, 4, eyebrows.x() as u64);
555
556        self.is_modified = true;
557    }
558
559    /// Returns the Mii's eye options.
560    pub fn eyes(&self) -> Eyes {
561        self.eyes
562    }
563
564    /// Sets the Mii's eye options and updates the raw data accordingly.
565    pub fn set_eyes(&mut self, eyes: Eyes) {
566        self.eyes = eyes;
567
568        write_bits(
569            self.raw_data_mut(),
570            0x28,
571            0,
572            6,
573            u8::from(eyes.eye_type()) as u64,
574        );
575        write_bits(self.raw_data_mut(), 0x28, 6, 5, eyes.rotation() as u64);
576        write_bits(self.raw_data_mut(), 0x29, 3, 5, eyes.y() as u64);
577        write_bits(
578            self.raw_data_mut(),
579            0x2A,
580            0,
581            3,
582            u8::from(eyes.eye_color()) as u64,
583        );
584        write_bits(self.raw_data_mut(), 0x2A, 3, 4, eyes.size() as u64);
585        write_bits(self.raw_data_mut(), 0x2A, 7, 4, eyes.x() as u64);
586
587        self.is_modified = true;
588    }
589
590    /// Returns the Mii's nose options.
591    pub fn nose(&self) -> Nose {
592        self.nose
593    }
594
595    /// Sets the Mii's nose options and updates the raw data accordingly.
596    pub fn set_nose(&mut self, nose: Nose) {
597        self.nose = nose;
598
599        write_bits(
600            self.raw_data_mut(),
601            0x2C,
602            0,
603            4,
604            u8::from(nose.nose_type()) as u64,
605        );
606        write_bits(self.raw_data_mut(), 0x2C, 4, 4, nose.size() as u64);
607        write_bits(self.raw_data_mut(), 0x2D, 0, 5, nose.y() as u64);
608
609        self.is_modified = true;
610    }
611
612    /// Returns the Mii's lip options.
613    pub fn lips(&self) -> Lips {
614        self.lips
615    }
616
617    /// Sets the Mii's lip options and updates the raw data accordingly.
618    pub fn set_lips(&mut self, lips: Lips) {
619        self.lips = lips;
620
621        write_bits(
622            self.raw_data_mut(),
623            0x2E,
624            0,
625            5,
626            u8::from(lips.lips_type()) as u64,
627        );
628        write_bits(
629            self.raw_data_mut(),
630            0x2E,
631            5,
632            2,
633            u8::from(lips.lips_color()) as u64,
634        );
635        write_bits(self.raw_data_mut(), 0x2E, 7, 4, lips.size() as u64);
636        write_bits(self.raw_data_mut(), 0x2F, 3, 5, lips.y() as u64);
637
638        self.is_modified = true;
639    }
640
641    /// Returns the Mii's glasses options.
642    pub fn glasses(&self) -> Glasses {
643        self.glasses
644    }
645
646    /// Sets the Mii's glasses options and updates the raw data accordingly.
647    pub fn set_glasses(&mut self, glasses: Glasses) {
648        self.glasses = glasses;
649
650        write_bits(
651            self.raw_data_mut(),
652            0x30,
653            0,
654            4,
655            u8::from(glasses.glasses_type()) as u64,
656        );
657        write_bits(
658            self.raw_data_mut(),
659            0x30,
660            4,
661            3,
662            u8::from(glasses.glasses_color()) as u64,
663        );
664        write_bits(self.raw_data_mut(), 0x30, 7, 4, glasses.size() as u64);
665        write_bits(self.raw_data_mut(), 0x31, 3, 5, glasses.y() as u64);
666
667        self.is_modified = true;
668    }
669
670    /// Returns the Mii's facial hair options.
671    pub fn facial_hair(&self) -> FacialHair {
672        self.facial_hair
673    }
674
675    /// Sets the Mii's facial hair options and updates the raw data accordingly.
676    pub fn set_facial_hair(&mut self, facial_hair: FacialHair) {
677        self.facial_hair = facial_hair;
678
679        write_bits(
680            self.raw_data_mut(),
681            0x32,
682            0,
683            2,
684            u8::from(facial_hair.mustache_type()) as u64,
685        );
686        write_bits(
687            self.raw_data_mut(),
688            0x32,
689            2,
690            2,
691            u8::from(facial_hair.beard_type()) as u64,
692        );
693        write_bits(
694            self.raw_data_mut(),
695            0x32,
696            4,
697            3,
698            u8::from(facial_hair.color()) as u64,
699        );
700        write_bits(
701            self.raw_data_mut(),
702            0x32,
703            7,
704            4,
705            facial_hair.mustache_size() as u64,
706        );
707        write_bits(
708            self.raw_data_mut(),
709            0x33,
710            3,
711            5,
712            facial_hair.mustache_y() as u64,
713        );
714
715        self.is_modified = true;
716    }
717
718    /// Returns the Mii's mole options.
719    pub fn mole(&self) -> Mole {
720        self.mole
721    }
722
723    /// Sets the Mii's mole options and updates the raw data accordingly.
724    pub fn set_mole(&mut self, mole: Mole) {
725        self.mole = mole;
726
727        write_bits(self.raw_data_mut(), 0x34, 0, 1, mole.has_mole() as u64);
728        write_bits(self.raw_data_mut(), 0x34, 1, 4, mole.size() as u64);
729        write_bits(self.raw_data_mut(), 0x34, 5, 5, mole.y() as u64);
730        write_bits(self.raw_data_mut(), 0x35, 2, 5, mole.x() as u64);
731
732        self.is_modified = true;
733    }
734
735    /// Returns the name of the player who created this Mii.
736    pub fn creator_name(&self) -> &str {
737        &self.creator_name
738    }
739
740    /// Sets the creator name and updates the raw data accordingly.
741    ///
742    /// Names longer than 10 characters are silently ignored.
743    pub fn set_creator_name(&mut self, creator_name: &str) {
744        if creator_name.len() > 10 {
745            return;
746        }
747        self.creator_name = creator_name.to_string();
748
749        let creator_name_bytes = string_to_utf16be(creator_name);
750
751        let mut padded = [0u8; 0x14];
752        padded[..creator_name_bytes.len()].copy_from_slice(&creator_name_bytes);
753        self.raw_data_mut()[0x36..0x4A].copy_from_slice(&padded);
754
755        self.is_modified = true;
756    }
757}
758
759/// Decodes a big-endian UTF-16 byte slice into a [`String`], stopping at the
760/// first null character (`U+0000`).
761///
762/// # Errors
763///
764/// Returns a [`std::string::FromUtf16Error`] if the byte sequence contains
765/// invalid UTF-16 code units.
766fn utf16be_to_string(bytes: &[u8]) -> Result<String, std::string::FromUtf16Error> {
767    let utf16: Vec<u16> = bytes
768        .chunks_exact(2)
769        .map(|c| u16::from_be_bytes([c[0], c[1]]))
770        .take_while(|&u| u != 0)
771        .collect();
772
773    String::from_utf16(&utf16)
774}
775
776/// Encodes a UTF-8 string as a big-endian UTF-16 byte sequence.
777///
778/// The result is not null-terminated; callers are responsible for any padding.
779fn string_to_utf16be(string: &str) -> Vec<u8> {
780    string
781        .encode_utf16()
782        .collect::<Vec<u16>>()
783        .iter()
784        .flat_map(|&u| u.to_be_bytes())
785        .collect()
786}
787
788/// Converts a raw 29-bit Mii creation timestamp into a [`NaiveDateTime`].
789///
790/// The timestamp counts ticks at a rate of 4 ticks per second (i.e. each tick
791/// is 0.25 seconds), relative to the Mii epoch of 2006-01-01 00:00:00 UTC.
792fn creation_date_from_timestamp(value: u32) -> NaiveDateTime {
793    let clock_rate = 0.25; // 3 second ticks
794    let epoch_shift = 1_136_073_600; // Shifts epoch from 1970-01-01 to 2006-01-01 (which is what Miis use)
795    let total_seconds = (value as f64 / clock_rate).floor() as i64;
796
797    let duration = Duration::seconds(total_seconds);
798    let epoch = DateTime::from_timestamp(epoch_shift, 0).unwrap();
799
800    epoch.naive_utc() + duration
801}
802
803/// Converts a [`NaiveDateTime`] into a raw 29-bit Mii creation timestamp.
804///
805/// The timestamp counts ticks at a rate of 4 ticks per second (i.e. each tick
806/// is 0.25 seconds), relative to the Mii epoch of 2006-01-01 00:00:00 UTC.
807fn timestamp_from_creation_date(date: NaiveDateTime) -> u32 {
808    let clock_rate = 0.25; // 3 second ticks
809    let epoch_shift = 1_136_073_600; // Shifts epoch from 1970-01-01 to 2006-01-01 (which is what Miis use)
810    let epoch = DateTime::from_timestamp(epoch_shift, 0).unwrap();
811
812    let duration = date.signed_duration_since(epoch.naive_utc());
813    let total_seconds = duration.num_seconds();
814
815    (total_seconds as f64 * clock_rate).floor() as u32
816}