physis/savedata/
chardat.rs

1// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4#![allow(unused_variables)] // shut up a nonsensical error in binrw for checksum
5
6use 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    /// The race of the character.
21    pub race: Race,
22
23    /// The gender of the character.
24    pub gender: Gender,
25
26    /// The age of the character. Normal = 1, Old = 3, Young = 4.
27    pub age: u8,
28
29    /// The height of the character from 0 to 100.
30    pub height: u8,
31
32    /// The character's tribe.
33    pub tribe: Tribe,
34
35    /// The character's selected face.
36    pub face: u8,
37
38    /// The character's selected hair.
39    pub hair: u8,
40
41    /// If hair highlights are enabled for this character.
42    #[br(map = read_bool_from::<u8>)]
43    #[bw(map = write_bool_as::<u8>)]
44    pub enable_highlights: bool,
45
46    /// The character's skin tone.
47    pub skin_tone: u8,
48
49    /// The character's right eye color.
50    pub right_eye_color: u8,
51
52    /// The character's hair color.
53    pub hair_tone: u8,
54
55    /// The color of the hair highlights.
56    pub highlights: u8,
57
58    /// The selected facial features.
59    pub facial_features: u8,
60
61    /// The color of the character's facial features.'
62    pub facial_feature_color: u8,
63
64    /// The character's selected eyebrows.
65    pub eyebrows: u8,
66
67    /// The character's left eye color.
68    pub left_eye_color: u8,
69
70    /// The character's selected eyes.
71    /// If the character has selected "Small Iris" then it adds 128 to this.
72    pub eyes: u8,
73
74    /// The character's selected nose.
75    pub nose: u8,
76
77    /// The character's selected jaw.
78    pub jaw: u8,
79
80    /// The character's selected mouth.
81    pub mouth: u8,
82
83    /// The character's selected pattern.
84    pub lips_tone_fur_pattern: u8,
85
86    /// Depending on the race, it's either the ear size, muscle size, or tail size.
87    pub race_feature_size: u8,
88
89    /// Depending on the race, it's the selected tail or ears.
90    pub race_feature_type: u8,
91
92    /// The size of the character's bust from 0 to 100.
93    pub bust: u8,
94
95    /// The character's choice of face paint.
96    pub face_paint: u8,
97
98    /// The color of the face paint.
99    /// If the character has selected "Light" then it adds 128 to this.
100    pub face_paint_color: u8,
101
102    /// The character's chosen voice.
103    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/// Represents the several options that make up a character data file (DAT) which is used by the game's character creation system to save and load presets.
143#[binrw]
144#[br(little)]
145#[brw(magic = 0x2013FF14u32)]
146#[derive(Debug)]
147pub struct CharacterData {
148    /// The "version" of the game this was created with.
149    /// Always corresponds to the released expansion at the time, e.g. A Realm Reborn character will have a version of 1. A Shadowbringers character will have a version of 4.
150    pub version: u32,
151
152    /// The checksum of the data fields.
153    // TODO: should we re-expose this checksum?
154    #[brw(pad_after = 4)]
155    #[bw(calc = self.calc_checksum())]
156    pub checksum: u32,
157
158    pub customize: CustomizeData,
159
160    /// The timestamp when the preset was created.
161    /// This is a UTC time in seconds since the Unix epoch.
162    #[brw(pad_before = 1)]
163    pub timestamp: u32,
164
165    // TODO: this is terrible, just read until string nul terminator
166    #[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    /// Parses existing character data.
175    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    /// Write existing character data to a buffer.
182    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        // The checksum also considers the timestamp and the comment
206        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        // Feeding it invalid data should not panic
236        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}