+1
Cargo.lock
+1
Cargo.lock
+1
crates/vee_parse/Cargo.toml
+1
crates/vee_parse/Cargo.toml
+135
-3
crates/vee_parse/src/ctr.rs
+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
+7
crates/vee_parse/src/error.rs
+47
-5
crates/vee_parse/src/generic.rs
+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
+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
+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
+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)]