1#![allow(unused_variables)] use std::io::{BufWriter, Cursor};
7
8use crate::common_file_operations::{read_bool_from, read_string, write_bool_as, write_string};
9use crate::{ByteBuffer, ByteSpan};
10use binrw::binrw;
11use binrw::{BinRead, BinWrite};
12
13use crate::race::{Gender, Race, Tribe};
14
15#[binrw]
16#[br(little)]
17#[repr(C)]
18#[derive(Clone, Debug)]
19pub struct CustomizeData {
20 pub race: Race,
22
23 pub gender: Gender,
25
26 pub age: u8,
28
29 pub height: u8,
31
32 pub tribe: Tribe,
34
35 pub face: u8,
37
38 pub hair: u8,
40
41 #[br(map = read_bool_from::<u8>)]
43 #[bw(map = write_bool_as::<u8>)]
44 pub enable_highlights: bool,
45
46 pub skin_tone: u8,
48
49 pub right_eye_color: u8,
51
52 pub hair_tone: u8,
54
55 pub highlights: u8,
57
58 pub facial_features: u8,
60
61 pub facial_feature_color: u8,
63
64 pub eyebrows: u8,
66
67 pub left_eye_color: u8,
69
70 pub eyes: u8,
73
74 pub nose: u8,
76
77 pub jaw: u8,
79
80 pub mouth: u8,
82
83 pub lips_tone_fur_pattern: u8,
85
86 pub race_feature_size: u8,
88
89 pub race_feature_type: u8,
91
92 pub bust: u8,
94
95 pub face_paint: u8,
97
98 pub face_paint_color: u8,
101
102 pub voice: u8,
104}
105
106impl Default for CustomizeData {
107 fn default() -> Self {
108 Self {
109 race: Race::Hyur,
110 tribe: Tribe::Midlander,
111 gender: Gender::Male,
112 age: 1,
113 height: 50,
114 face: 1,
115 hair: 1,
116 enable_highlights: false,
117 skin_tone: 1,
118 right_eye_color: 1,
119 hair_tone: 1,
120 highlights: 1,
121 facial_features: 0,
122 facial_feature_color: 1,
123 eyebrows: 1,
124 left_eye_color: 1,
125 eyes: 1,
126 nose: 1,
127 jaw: 1,
128 mouth: 1,
129 lips_tone_fur_pattern: 0,
130 race_feature_size: 0,
131 race_feature_type: 0,
132 bust: 0,
133 face_paint: 0,
134 face_paint_color: 1,
135 voice: 1,
136 }
137 }
138}
139
140const MAX_COMMENT_LENGTH: usize = 164;
141
142#[binrw]
144#[br(little)]
145#[brw(magic = 0x2013FF14u32)]
146#[derive(Debug)]
147pub struct CharacterData {
148 pub version: u32,
151
152 #[brw(pad_after = 4)]
155 #[bw(calc = self.calc_checksum())]
156 pub checksum: u32,
157
158 pub customize: CustomizeData,
159
160 #[brw(pad_before = 1)]
163 pub timestamp: u32,
164
165 #[br(count = MAX_COMMENT_LENGTH)]
167 #[bw(pad_size_to = MAX_COMMENT_LENGTH)]
168 #[br(map = read_string)]
169 #[bw(map = write_string)]
170 pub comment: String,
171}
172
173impl CharacterData {
174 pub fn from_existing(buffer: ByteSpan) -> Option<CharacterData> {
176 let mut cursor = Cursor::new(buffer);
177
178 CharacterData::read(&mut cursor).ok()
179 }
180
181 pub fn write_to_buffer(&self) -> Option<ByteBuffer> {
183 let mut buffer = ByteBuffer::new();
184
185 {
186 let cursor = Cursor::new(&mut buffer);
187 let mut writer = BufWriter::new(cursor);
188
189 self.write_le(&mut writer).ok()?;
190 }
191
192 Some(buffer)
193 }
194
195 fn calc_checksum(&self) -> u32 {
196 let mut buffer = ByteBuffer::new();
197
198 {
199 let cursor = Cursor::new(&mut buffer);
200 let mut writer = BufWriter::new(cursor);
201
202 self.customize.write_le(&mut writer).unwrap();
203 }
204
205 buffer.push(0x00);
207 buffer.extend_from_slice(&self.timestamp.to_le_bytes());
208
209 let mut comment = write_string(&self.comment);
210 comment.resize(MAX_COMMENT_LENGTH, 0);
211 buffer.extend_from_slice(&comment);
212
213 let mut checksum: u32 = 0;
214 for (i, byte) in buffer.iter().enumerate() {
215 checksum ^= (*byte as u32) << (i % 24);
216 }
217
218 checksum
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use std::fs::read;
225 use std::path::PathBuf;
226
227 use super::*;
228
229 #[test]
230 fn test_invalid() {
231 let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
232 d.push("resources/tests");
233 d.push("random");
234
235 CharacterData::from_existing(&read(d).unwrap());
237 }
238
239 fn common_setup(name: &str) -> CharacterData {
240 let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
241 d.push("resources/tests/chardat");
242 d.push(name);
243
244 CharacterData::from_existing(&read(d).unwrap()).unwrap()
245 }
246
247 #[test]
248 fn read_arr() {
249 let chardat = common_setup("arr.dat");
250
251 assert_eq!(chardat.version, 1);
252 assert_eq!(chardat.customize.race, Race::Hyur);
253 assert_eq!(chardat.customize.gender, Gender::Male);
254 assert_eq!(chardat.customize.age, 1);
255 assert_eq!(chardat.customize.height, 50);
256 assert_eq!(chardat.customize.tribe, Tribe::Midlander);
257 assert_eq!(chardat.customize.face, 5);
258 assert_eq!(chardat.customize.hair, 1);
259 assert!(!chardat.customize.enable_highlights);
260 assert_eq!(chardat.customize.skin_tone, 2);
261 assert_eq!(chardat.customize.right_eye_color, 37);
262 assert_eq!(chardat.customize.hair_tone, 53);
263 assert_eq!(chardat.customize.highlights, 0);
264 assert_eq!(chardat.customize.facial_features, 2);
265 assert_eq!(chardat.customize.facial_feature_color, 2);
266 assert_eq!(chardat.customize.eyebrows, 0);
267 assert_eq!(chardat.customize.left_eye_color, 37);
268 assert_eq!(chardat.customize.eyes, 0);
269 assert_eq!(chardat.customize.nose, 0);
270 assert_eq!(chardat.customize.jaw, 0);
271 assert_eq!(chardat.customize.mouth, 0);
272 assert_eq!(chardat.customize.lips_tone_fur_pattern, 43);
273 assert_eq!(chardat.customize.race_feature_size, 50);
274 assert_eq!(chardat.customize.race_feature_type, 0);
275 assert_eq!(chardat.customize.bust, 0);
276 assert_eq!(chardat.customize.face_paint_color, 36);
277 assert_eq!(chardat.customize.face_paint, 0);
278 assert_eq!(chardat.customize.voice, 1);
279 assert_eq!(chardat.comment, "Custom Comment Text");
280 }
281
282 #[test]
283 fn read_heavensward() {
284 let chardat = common_setup("heavensward.dat");
285
286 assert_eq!(chardat.version, 2);
287 assert_eq!(chardat.customize.race, Race::AuRa);
288 assert_eq!(chardat.customize.gender, Gender::Female);
289 assert_eq!(chardat.customize.age, 1);
290 assert_eq!(chardat.customize.height, 50);
291 assert_eq!(chardat.customize.tribe, Tribe::Xaela);
292 assert_eq!(chardat.customize.face, 3);
293 assert_eq!(chardat.customize.hair, 5);
294 assert!(!chardat.customize.enable_highlights);
295 assert_eq!(chardat.customize.skin_tone, 160);
296 assert_eq!(chardat.customize.right_eye_color, 91);
297 assert_eq!(chardat.customize.hair_tone, 159);
298 assert_eq!(chardat.customize.highlights, 0);
299 assert_eq!(chardat.customize.facial_features, 127);
300 assert_eq!(chardat.customize.facial_feature_color, 99);
301 assert_eq!(chardat.customize.eyebrows, 0);
302 assert_eq!(chardat.customize.left_eye_color, 91);
303 assert_eq!(chardat.customize.eyes, 0);
304 assert_eq!(chardat.customize.nose, 0);
305 assert_eq!(chardat.customize.jaw, 0);
306 assert_eq!(chardat.customize.mouth, 0);
307 assert_eq!(chardat.customize.lips_tone_fur_pattern, 0);
308 assert_eq!(chardat.customize.race_feature_size, 50);
309 assert_eq!(chardat.customize.race_feature_type, 1);
310 assert_eq!(chardat.customize.bust, 25);
311 assert_eq!(chardat.customize.face_paint_color, 0);
312 assert_eq!(chardat.customize.face_paint, 0);
313 assert_eq!(chardat.customize.voice, 112);
314 assert_eq!(chardat.comment, "Heavensward Comment Text");
315 }
316
317 #[test]
318 fn read_stormblood() {
319 let chardat = common_setup("stormblood.dat");
320
321 assert_eq!(chardat.version, 3);
322 assert_eq!(chardat.customize.race, Race::Lalafell);
323 assert_eq!(chardat.customize.gender, Gender::Male);
324 assert_eq!(chardat.customize.age, 1);
325 assert_eq!(chardat.customize.height, 50);
326 assert_eq!(chardat.customize.tribe, Tribe::Plainsfolk);
327 assert_eq!(chardat.customize.face, 1);
328 assert_eq!(chardat.customize.hair, 8);
329 assert!(!chardat.customize.enable_highlights);
330 assert_eq!(chardat.customize.skin_tone, 25);
331 assert_eq!(chardat.customize.right_eye_color, 11);
332 assert_eq!(chardat.customize.hair_tone, 45);
333 assert_eq!(chardat.customize.highlights, 0);
334 assert_eq!(chardat.customize.facial_features, 0);
335 assert_eq!(chardat.customize.facial_feature_color, 2);
336 assert_eq!(chardat.customize.eyebrows, 0);
337 assert_eq!(chardat.customize.left_eye_color, 11);
338 assert_eq!(chardat.customize.eyes, 0);
339 assert_eq!(chardat.customize.nose, 0);
340 assert_eq!(chardat.customize.jaw, 0);
341 assert_eq!(chardat.customize.mouth, 0);
342 assert_eq!(chardat.customize.lips_tone_fur_pattern, 43);
343 assert_eq!(chardat.customize.race_feature_size, 25);
344 assert_eq!(chardat.customize.race_feature_type, 2);
345 assert_eq!(chardat.customize.bust, 0);
346 assert_eq!(chardat.customize.face_paint_color, 36);
347 assert_eq!(chardat.customize.face_paint, 0);
348 assert_eq!(chardat.customize.voice, 19);
349 assert_eq!(chardat.comment, "Stormblood Comment Text");
350 }
351
352 #[test]
353 fn read_shadowbringers() {
354 let chardat = common_setup("shadowbringers.dat");
355
356 assert_eq!(chardat.version, 4);
357 assert_eq!(chardat.customize.race, Race::Viera);
358 assert_eq!(chardat.customize.gender, Gender::Female);
359 assert_eq!(chardat.customize.age, 1);
360 assert_eq!(chardat.customize.height, 50);
361 assert_eq!(chardat.customize.tribe, Tribe::Rava);
362 assert_eq!(chardat.customize.face, 1);
363 assert_eq!(chardat.customize.hair, 8);
364 assert!(!chardat.customize.enable_highlights);
365 assert_eq!(chardat.customize.skin_tone, 12);
366 assert_eq!(chardat.customize.right_eye_color, 43);
367 assert_eq!(chardat.customize.hair_tone, 53);
368 assert_eq!(chardat.customize.highlights, 0);
369 assert_eq!(chardat.customize.facial_features, 4);
370 assert_eq!(chardat.customize.facial_feature_color, 0);
371 assert_eq!(chardat.customize.eyebrows, 2);
372 assert_eq!(chardat.customize.left_eye_color, 43);
373 assert_eq!(chardat.customize.eyes, 131);
374 assert_eq!(chardat.customize.nose, 2);
375 assert_eq!(chardat.customize.jaw, 1);
376 assert_eq!(chardat.customize.mouth, 131);
377 assert_eq!(chardat.customize.lips_tone_fur_pattern, 171);
378 assert_eq!(chardat.customize.race_feature_size, 50);
379 assert_eq!(chardat.customize.race_feature_type, 2);
380 assert_eq!(chardat.customize.bust, 100);
381 assert_eq!(chardat.customize.face_paint_color, 131);
382 assert_eq!(chardat.customize.face_paint, 3);
383 assert_eq!(chardat.customize.voice, 160);
384 assert_eq!(chardat.comment, "Shadowbringers Comment Text");
385 }
386
387 #[test]
388 fn write_shadowbringers() {
389 let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
390 d.push("resources/tests/chardat");
391 d.push("shadowbringers.dat");
392
393 let chardat_bytes = &read(d).unwrap();
394 let chardat = CharacterData::from_existing(chardat_bytes).unwrap();
395 assert_eq!(*chardat_bytes, chardat.write_to_buffer().unwrap());
396 }
397}