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