Mii rendering and parsing library

Add and Implement AsGenericChar where possible right now

Changed files
+366 -26
crates
+1
Cargo.lock
··· 2904 2904 "binrw", 2905 2905 "paste", 2906 2906 "svgbobdoc", 2907 + "thiserror 2.0.12", 2907 2908 "vee_parse_macros", 2908 2909 ] 2909 2910
+1
crates/vee_parse/Cargo.toml
··· 17 17 paste = "1" 18 18 vee_parse_macros = { path = "../vee_parse_macros", version = "0.2.0" } 19 19 svgbobdoc = { version = "0.3", features = ["enable"] } 20 + thiserror = "2" 20 21 21 22 [lints] 22 23 workspace = true
+135 -3
crates/vee_parse/src/ctr.rs
··· 1 - use crate::FixedLengthWideString; 1 + use crate::error::CharConversionError; 2 + use crate::generic::{ 3 + AsGenericChar, Beard, Body, CreationData, CtrCreationData, Eye, Eyebrow, Faceline, 4 + FavoriteColor, Gender, GenericColor, Glass, Hair, MetaData, Mole, Mouth, Mustache, Nose, 5 + Position, PositionY, Rotation, Scale, ScaleX, ScaleY, UniformScale, 6 + }; 7 + use crate::nx::NxColor; 8 + use crate::seal::Sealant; 9 + use crate::{FixedLengthWideString, GenericChar, u8_to_bool}; 2 10 use bilge::prelude::*; 3 11 use binrw::{BinRead, BinWrite, binrw}; 4 12 use vee_parse_macros::bitfield; ··· 105 113 pub struct BeardField { 106 114 pub beard_type: u3, 107 115 pub beard_color: u3, 108 - pub beard_scale: u4, 109 - pub beard_y: u5, 116 + pub mustache_scale: u4, 117 + pub mustache_y: u5, 110 118 pub padding_7: u1, 111 119 } 112 120 ··· 133 141 pub data: [u8; 8], 134 142 } 135 143 144 + // FFLiCreateID 136 145 #[binrw] 137 146 #[derive(Debug)] 138 147 pub struct CtrCreateId { ··· 172 181 pub padding: u16, 173 182 pub crc: u16, 174 183 } 184 + 185 + fn cafe_color_generic(col: u8) -> GenericColor { 186 + GenericColor::CafeTable(col.into()) 187 + } 188 + 189 + impl Sealant for CtrStoreData {} 190 + 191 + impl AsGenericChar for CtrStoreData { 192 + fn as_generic(&self) -> Result<GenericChar, CharConversionError> { 193 + let char = GenericChar { 194 + name: self.name.to_string(), 195 + 196 + creation_data: CreationData::Ctr(CtrCreationData {}), 197 + body: Body { 198 + gender: Gender::from_u8(self.personal_info_2.gender().as_u8())?, 199 + height: self.height, 200 + build: self.build, 201 + }, 202 + faceline: Faceline { 203 + ty: self.face.face_type().as_u8(), 204 + color: cafe_color_generic(self.face.face_color().as_u8()), 205 + wrinkle_ty: self.face.face_texture().as_u8(), 206 + makeup_ty: self.face.face_makeup().as_u8(), 207 + }, 208 + hair: Hair { 209 + ty: self.hair.hair_type(), 210 + color: cafe_color_generic(self.hair.hair_color().as_u8()), 211 + flip: u8_to_bool(self.hair.hair_flip().as_u8(), "hair::flip".to_string())?, 212 + }, 213 + eye: Eye { 214 + ty: self.eye.eye_type().as_u8(), 215 + color: cafe_color_generic(self.eye.eye_color().as_u8()), 216 + pos: Position { 217 + x: self.eye_position.eye_x().as_u8(), 218 + y: self.eye_position.eye_y().as_u8(), 219 + }, 220 + scale: ScaleY { 221 + h: self.eye.eye_scale().as_u8(), 222 + }, 223 + rotation: Rotation { 224 + ang: self.eye_position.eye_rotate().as_u8(), 225 + }, 226 + }, 227 + eyebrow: Eyebrow { 228 + ty: self.eyebrow.eyebrow_type().as_u8(), 229 + color: cafe_color_generic(self.eyebrow.eyebrow_color().as_u8()), 230 + pos: Position { 231 + x: self.eyebrow_position.eyebrow_x().as_u8(), 232 + y: self.eyebrow_position.eyebrow_y().as_u8(), 233 + }, 234 + scale: Scale { 235 + w: self.eyebrow.eyebrow_scale().as_u8(), 236 + h: self.eyebrow.eyebrow_aspect().as_u8(), 237 + }, 238 + rotation: Rotation { 239 + ang: self.eyebrow_position.eyebrow_rotate().as_u8(), 240 + }, 241 + }, 242 + nose: Nose { 243 + ty: self.nose.nose_type().as_u8(), 244 + pos: PositionY { 245 + y: self.nose.nose_y().as_u8(), 246 + }, 247 + scale: UniformScale { 248 + amount: self.nose.nose_scale().as_u8(), 249 + }, 250 + }, 251 + mouth: Mouth { 252 + ty: self.mouth.mouth_type().as_u8(), 253 + color: cafe_color_generic(self.mouth.mouth_color().as_u8()), 254 + pos: PositionY { 255 + y: self.mouth_position.mouth_y().as_u8(), 256 + }, 257 + scale: Scale { 258 + w: self.mouth.mouth_scale().as_u8(), 259 + h: self.mouth.mouth_aspect().as_u8(), 260 + }, 261 + }, 262 + beard: Beard { 263 + ty: self.beard.beard_type().as_u8(), 264 + color: cafe_color_generic(self.beard.beard_color().as_u8()), 265 + }, 266 + mustache: Mustache { 267 + ty: self.mouth_position.mustache_type().as_u8(), 268 + pos: PositionY { 269 + y: self.beard.mustache_y().as_u8(), 270 + }, 271 + scale: ScaleX { 272 + w: self.beard.mustache_scale().as_u8(), 273 + }, 274 + }, 275 + glass: Glass { 276 + ty: self.glass.glass_type().as_u8(), 277 + color: cafe_color_generic(self.glass.glass_color().as_u8()), 278 + pos: PositionY { 279 + y: self.glass.glass_y().as_u8(), 280 + }, 281 + scale: ScaleX { 282 + w: self.glass.glass_scale().as_u8(), 283 + }, 284 + }, 285 + mole: Mole { 286 + ty: self.mole.mole_type().as_u8(), 287 + pos: Position { 288 + x: self.mole.mole_x().as_u8(), 289 + y: self.mole.mole_y().as_u8(), 290 + }, 291 + scale: ScaleX { 292 + w: self.mole.mole_scale().as_u8(), 293 + }, 294 + }, 295 + meta_data: MetaData { 296 + special: { 297 + println!("Warn: Special flag is NOT being read. Placeholder value used."); 298 + false 299 + }, 300 + favorite_color: FavoriteColor(self.personal_info_2.favorite_color().as_usize()), 301 + }, 302 + }; 303 + 304 + Ok(char) 305 + } 306 + }
+7
crates/vee_parse/src/error.rs
··· 1 + use thiserror::Error; 2 + 3 + #[derive(Error, Debug)] 4 + pub enum CharConversionError { 5 + #[error("Field `{0}` is out of bounds.")] 6 + FieldOob(String), 7 + }
+47 -5
crates/vee_parse/src/generic.rs
··· 1 + use crate::{error::CharConversionError, nx::UuidVer4, seal::Sealant}; 2 + 1 3 pub struct Position { 2 4 pub x: u8, 3 5 pub y: u8, ··· 21 23 /// 'Aspect'. 22 24 pub struct ScaleY { 23 25 pub h: u8, 26 + } 27 + 28 + /// Scale and aspect, at the same time. 29 + pub struct UniformScale { 30 + pub amount: u8, 24 31 } 25 32 26 33 /// The actual angle is somewhat dependant on what the shape is. ··· 36 43 } 37 44 38 45 /// Just so happens to be the same on every platform. Cool! 39 - pub struct FavoriteColor(usize); 46 + pub struct FavoriteColor(pub usize); 40 47 41 48 pub struct Eye { 42 49 pub ty: u8, ··· 55 62 pub struct Nose { 56 63 pub ty: u8, 57 64 pub pos: PositionY, 58 - pub scale: ScaleX, 65 + pub scale: UniformScale, 59 66 } 60 67 pub struct Mouth { 61 68 pub ty: u8, ··· 85 92 86 93 pub struct Mustache { 87 94 pub ty: u8, 88 - pub color: GenericColor, 89 - pub scale: ScaleY, 95 + pub pos: PositionY, 96 + pub scale: ScaleX, 90 97 } 91 98 92 99 pub struct Glass { ··· 106 113 Male, 107 114 Female, 108 115 } 116 + impl Gender { 117 + pub fn from_bool(b: bool) -> Gender { 118 + if b { Gender::Female } else { Gender::Male } 119 + } 120 + pub fn from_u8(u: u8) -> Result<Gender, CharConversionError> { 121 + match u { 122 + 0_u8 => Ok(Gender::Male), 123 + 1_u8 => Ok(Gender::Female), 124 + _ => Err(CharConversionError::FieldOob("gender".to_string())), 125 + } 126 + } 127 + } 109 128 110 129 /// The body shape of the Char. 111 130 pub struct Body { ··· 119 138 pub favorite_color: FavoriteColor, 120 139 } 121 140 141 + pub struct RvlCreationData {/* todo */} 122 142 pub struct CtrCreationData {/* todo */} 123 - pub struct NxCreationData {/* todo */} 143 + pub struct NxCreationData { 144 + pub create_info: UuidVer4, 145 + pub font_region: u8, 146 + pub region_move: u8, 147 + } 124 148 125 149 pub enum CreationData { 150 + None, 151 + Rvl(RvlCreationData), 126 152 Ctr(CtrCreationData), 127 153 Nx(NxCreationData), 128 154 } ··· 130 156 /// Generic `Char` information. 131 157 /// Names here are based on names in target-specific structs, but not representative. 132 158 pub struct GenericChar { 159 + /// Sometimes called "nickname" 133 160 pub name: String, 134 161 135 162 pub meta_data: MetaData, ··· 146 173 pub glass: Glass, 147 174 pub mole: Mole, 148 175 } 176 + 177 + /// This trait is sealed (you can't implement this on your own items.) 178 + pub trait AsGenericChar: Sealant { 179 + /// Convert this representation into [GenericChar] 180 + fn as_generic(&self) -> Result<GenericChar, CharConversionError>; 181 + } 182 + 183 + /// This trait is sealed (you can't implement this on your own items.) 184 + pub trait FromGenericChar: Sealant { 185 + /// Hopefully the type you are implementing on. 186 + type Output; 187 + 188 + /// Make this char representation from [GenericChar] 189 + fn from_generic(char: GenericChar) -> Self::Output; 190 + }
+27 -14
crates/vee_parse/src/lib.rs
··· 12 12 //! 13 13 //! Supported by this library: 14 14 //! 15 - //! | .. | Ntr[^gen1] | Rvl[^gen1] | Ctr/Cafe | Nx | WebStudio | 15 + //! | ... | Ntr[^gen1] | Rvl[^gen1] | Ctr/Cafe | Nx | WebStudio | 16 16 //! |-------------|----------------------|------------------|-------------|----------------|-----------| 17 - //! | `CharInfo` | ❌ | ❌ | ❌ | [`.charinfo`](NxCharInfo) | .. | 18 - //! | `StoreData` | [`.nsd`](NtrStoreData) | [`.rsd`](RvlStoreData) | [`.ffsd`](CtrStoreData)[^ff] | ❌ | .. | 19 - //! | `CoreData` | [`.ncd`](NtrCharData) | [`.rcd`](RvlCharData) | ❌ | ❌ | .. | 20 - //! | In-memory | .. | .. | .. | .. | 🏗️[^mnms] | 17 + //! | `CharInfo` | ❌ | ❌ | ❌ | [`.charinfo`](NxCharInfo) | [`.mnms`](StudioCharInfo)[^mnms]<sup>🏗</sup> | 18 + //! | `StoreData` | [`.nsd`](NtrStoreData) | [`.rsd`](RvlStoreData) | [`.ffsd`](CtrStoreData)[^ff] | ❌ | ❌ | 19 + //! | `CoreData` | [`.ncd`](NtrCharData) | [`.rcd`](RvlCharData) | ❌ | ❌ | ❌ | 21 20 //! [^gen1]: These formats are the same, apart from Ntr being little-endian and Rvl being big-endian. 22 - //! [^ff]: The official format is Ca**f**e **F**ace **S**tore **D**ata, probably due to CFSD being taken by Ctr <sup>[src](https://github.com/HEYimHeroic/MiiDataFiles)</sup>. 23 - //! [^mnms]: Stored in the browser's `localStorage`. Often shared as a base64 string, or sometimes saved with the `.mnms` extension. 21 + //! [^ff]: The official format is Ca**f**e **F**ace **S**tore **D**ata, probably due to CFSD being taken by Ctr. 22 + //! [^mnms]: You could tenuously call this a `CharInfo`. Stored in the browser's `localStorage`. Often shared as a base64 string, or stored with the unofficial `.mnms` extension. 24 23 //! 25 24 //! # Conversion 26 25 //! ··· 53 52 /// ┌──┴────────┐ │ │ │ │ 54 53 /// │ ├────────►└───────────────┘ └──────────────┘ 55 54 /// │ RSD,RCD │ ▲ 56 - /// │ │ │ ┌────────────────┐ 57 - /// └───────────┘ │ │ │ 58 - /// └────────────►│ WsLocalStore │ 59 - /// │ │ 60 - /// └────────────────┘ 55 + /// │ │ │ ┌──────────────────┐ 56 + /// └───────────┘ │ │ │ 57 + /// └───────────►│ StudioCharInfo │ 58 + /// │ │ 59 + /// └──────────────────┘ 61 60 /// ``` 62 61 /// </center> 63 62 )] ··· 83 82 //! ``` 84 83 85 84 pub mod ctr; 85 + pub mod error; 86 86 pub mod generic; 87 87 pub mod nx; 88 88 pub mod rvl_ntr; 89 89 90 - pub use binrw::{BinRead, NullWideString, binrw}; 90 + use crate::error::CharConversionError; 91 + pub use binrw::{binrw, BinRead, NullWideString}; 91 92 pub use ctr::CtrStoreData; 92 93 pub use generic::GenericChar; 93 94 pub use nx::NxCharInfo; ··· 96 97 pub use rvl_ntr::RvlCharData; 97 98 pub use rvl_ntr::RvlStoreData; 98 99 100 + fn u8_to_bool(int: u8, field: String) -> Result<bool, CharConversionError> { 101 + match int { 102 + 0 => Ok(false), 103 + 1 => Ok(true), 104 + _ => Err(CharConversionError::FieldOob(field)), 105 + } 106 + } 107 + 108 + pub(crate) mod seal { 109 + pub trait Sealant {} 110 + } 111 + 99 112 /// A UTF-16 String with a fixed length and non-enforced null termination. 100 113 /// The string is allowed to reach the maximum length without a null terminator, 101 114 /// and any nulls are stripped. ··· 129 142 130 143 #[cfg(test)] 131 144 mod tests { 132 - use crate::{CtrStoreData, NxCharInfo, RvlCharData, rvl_ntr::FavoriteColor}; 145 + use crate::{rvl_ntr::FavoriteColor, CtrStoreData, NxCharInfo, RvlCharData}; 133 146 use binrw::BinRead; 134 147 use std::{error::Error, fs::File}; 135 148
+120 -3
crates/vee_parse/src/nx.rs
··· 1 - use binrw::NullWideString; 1 + use crate::{ 2 + FixedLengthWideString, GenericChar, 3 + error::CharConversionError, 4 + generic::{ 5 + AsGenericChar, Beard, Body, CreationData, Eye, Eyebrow, Faceline, FavoriteColor, Gender, 6 + GenericColor, Glass, Hair, MetaData, Mole, Mouth, Mustache, Nose, NxCreationData, Position, 7 + PositionY, Rotation, Scale, ScaleX, ScaleY, UniformScale, 8 + }, 9 + seal::Sealant, 10 + u8_to_bool, 11 + }; 2 12 use binrw::binrw; 3 - 4 - use crate::FixedLengthWideString; 5 13 6 14 /// Wrapper for nn::mii color index. 7 15 #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default)] ··· 80 88 pub mole_y: u8, 81 89 pub reserved: u8, /* always zero */ 82 90 } 91 + 92 + fn nx_color_generic(col: NxColor) -> GenericColor { 93 + GenericColor::NxTable(col.0.into()) 94 + } 95 + 96 + impl Sealant for NxCharInfo {} 97 + 98 + impl AsGenericChar for NxCharInfo { 99 + fn as_generic(&self) -> Result<GenericChar, CharConversionError> { 100 + Ok(GenericChar { 101 + name: self.nickname.to_string(), 102 + body: Body { 103 + gender: Gender::from_u8(self.gender)?, 104 + height: self.height, 105 + build: self.build, 106 + }, 107 + faceline: Faceline { 108 + ty: self.faceline_type, 109 + color: nx_color_generic(self.faceline_color), 110 + wrinkle_ty: self.faceline_wrinkle, 111 + makeup_ty: self.faceline_make, 112 + }, 113 + hair: Hair { 114 + ty: self.hair_type, 115 + color: nx_color_generic(self.hair_color), 116 + flip: u8_to_bool(self.hair_flip, "hair::flip".to_string())?, 117 + }, 118 + eye: Eye { 119 + ty: self.eye_type, 120 + color: nx_color_generic(self.eye_color), 121 + pos: Position { 122 + x: self.eye_x, 123 + y: self.eye_y, 124 + }, 125 + scale: ScaleY { h: self.eye_aspect }, 126 + rotation: Rotation { 127 + ang: self.eye_rotate, 128 + }, 129 + }, 130 + eyebrow: Eyebrow { 131 + ty: self.eyebrow_type, 132 + color: nx_color_generic(self.eyebrow_color), 133 + pos: Position { 134 + x: self.eyebrow_x, 135 + y: self.eyebrow_y, 136 + }, 137 + scale: Scale { 138 + w: self.eye_scale, 139 + h: self.eye_aspect, 140 + }, 141 + rotation: Rotation { 142 + ang: self.eye_rotate, 143 + }, 144 + }, 145 + nose: Nose { 146 + ty: self.nose_type, 147 + pos: PositionY { y: self.nose_y }, 148 + scale: UniformScale { 149 + amount: self.nose_scale, 150 + }, 151 + }, 152 + mouth: Mouth { 153 + ty: self.mouth_type, 154 + color: nx_color_generic(self.mouth_color), 155 + pos: PositionY { y: self.mouth_y }, 156 + scale: Scale { 157 + w: self.mouth_scale, 158 + h: self.mouth_aspect, 159 + }, 160 + }, 161 + beard: Beard { 162 + ty: self.beard_type, 163 + color: nx_color_generic(self.beard_color), 164 + }, 165 + mustache: Mustache { 166 + ty: self.mustache_y, 167 + pos: PositionY { y: self.mustache_y }, 168 + scale: ScaleX { 169 + w: self.mustache_scale, 170 + }, 171 + }, 172 + glass: Glass { 173 + ty: self.glass_type, 174 + color: nx_color_generic(self.glass_color), 175 + pos: PositionY { y: self.glass_y }, 176 + scale: ScaleX { 177 + w: self.glass_scale, 178 + }, 179 + }, 180 + mole: Mole { 181 + ty: self.mole_type, 182 + pos: Position { 183 + x: self.mole_x, 184 + y: self.mole_y, 185 + }, 186 + scale: ScaleX { w: self.mole_scale }, 187 + }, 188 + meta_data: MetaData { 189 + special: u8_to_bool(self.is_special, "meta_data::special".to_string())?, 190 + favorite_color: FavoriteColor(self.favorite_color.0.into()), 191 + }, 192 + creation_data: CreationData::Nx(NxCreationData { 193 + create_info: self.create_info, 194 + font_region: self.font_region, 195 + region_move: self.region_move, 196 + }), 197 + }) 198 + } 199 + }
+28 -1
crates/vee_parse/src/rvl_ntr.rs
··· 1 - use crate::FixedLengthWideString; 1 + use crate::{ 2 + FixedLengthWideString, GenericChar, 3 + generic::{ 4 + AsGenericChar, Beard, Body, CreationData, Eye, Eyebrow, Faceline, Glass, Hair, MetaData, 5 + Mole, Mouth, Mustache, Nose, PositionY, Rotation, Scale, UniformScale, 6 + }, 7 + seal::Sealant, 8 + }; 2 9 use bilge::prelude::*; 3 10 use binrw::{BinRead, BinWrite, binrw}; 4 11 use paste::paste; ··· 42 49 Black = 11, 43 50 #[fallback] 44 51 Invalid(u4), 52 + } 53 + 54 + impl FavoriteColor { 55 + fn as_u8(&self) -> u8 { 56 + match self { 57 + FavoriteColor::Red => 0, 58 + FavoriteColor::Orange => 1, 59 + FavoriteColor::Yellow => 2, 60 + FavoriteColor::YellowGreen => 3, 61 + FavoriteColor::Green => 4, 62 + FavoriteColor::Blue => 5, 63 + FavoriteColor::SkyBlue => 6, 64 + FavoriteColor::Pink => 7, 65 + FavoriteColor::Purple => 8, 66 + FavoriteColor::Brown => 9, 67 + FavoriteColor::White => 10, 68 + FavoriteColor::Black => 11, 69 + FavoriteColor::Invalid(n) => n.as_u8(), 70 + } 71 + } 45 72 } 46 73 47 74 #[bitsize(1)]