physis/
chardat.rs

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