From f89eed5dc60e0c87d1e573b79e677a890abe3177 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sat, 8 Mar 2025 13:12:08 -0500 Subject: [PATCH] Calculate the correct checksum when writing character saved data --- src/chardat.rs | 294 +++++++++++++++++++++++++++---------------------- 1 file changed, 163 insertions(+), 131 deletions(-) diff --git a/src/chardat.rs b/src/chardat.rs index 339bde8..979d2d5 100644 --- a/src/chardat.rs +++ b/src/chardat.rs @@ -95,29 +95,19 @@ fn convert_subrace_dat(subrace: &Subrace) -> u8 { } } -/// 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. #[binrw] #[br(little)] #[repr(C)] -#[brw(magic = 0x2013FF14u32)] #[derive(Debug)] -pub struct CharacterData { - /// The "version" of the game this was created with. - /// 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. - pub version: u32, - - /// The checksum of the data fields. - #[brw(pad_after = 4)] - pub checksum: u32, - +pub struct CustomizeData { /// The race of the character. - #[br(map = convert_dat_race )] - #[bw(map = convert_race_dat )] + #[br(map = convert_dat_race)] + #[bw(map = convert_race_dat)] pub race: Race, /// The gender of the character. - #[br(map = convert_dat_gender )] - #[bw(map = convert_gender_dat )] + #[br(map = convert_dat_gender)] + #[bw(map = convert_gender_dat)] pub gender: Gender, /// The age of the character. Normal = 1, Old = 3, Young = 4. @@ -127,8 +117,8 @@ pub struct CharacterData { pub height: u8, /// The character's subrace. - #[br(map = convert_dat_subrace )] - #[bw(map = convert_subrace_dat )] + #[br(map = convert_dat_subrace)] + #[bw(map = convert_subrace_dat)] pub subrace: Subrace, /// The character's selected face. @@ -183,10 +173,10 @@ pub struct CharacterData { pub lips_tone_fur_pattern: u8, /// Depending on the race, it's either the ear size, muscle size, or tail size. - pub ear_muscle_tail_size: u8, + pub race_feature_size: u8, /// Depending on the race, it's the selected tail or ears. - pub tail_ears_type: u8, + pub race_feature_type: u8, /// The size of the character's bust from 0 to 100. pub bust: u8, @@ -200,6 +190,26 @@ pub struct CharacterData { /// The character's chosen voice. pub voice: u8, +} + +/// 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. +#[binrw] +#[br(little)] +#[repr(C)] +#[brw(magic = 0x2013FF14u32)] +#[derive(Debug)] +pub struct CharacterData { + /// The "version" of the game this was created with. + /// 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. + pub version: u32, + + /// The checksum of the data fields. + // TODO: should we re-expose this checksum? + #[brw(pad_after = 4)] + #[bw(calc = self.calc_checksum())] + pub checksum: u32, + + pub customize: CustomizeData, /// The timestamp when the preset was created. /// This is a UTC time in seconds since the Unix epoch. @@ -235,6 +245,32 @@ impl CharacterData { Some(buffer) } + + fn calc_checksum(&self) -> u32 { + let mut buffer = ByteBuffer::new(); + + { + let cursor = Cursor::new(&mut buffer); + let mut writer = BufWriter::new(cursor); + + self.customize.write_le(&mut writer).unwrap(); + } + + // The checksum also considers the timestamp and the comment + buffer.push(0x00); + buffer.extend_from_slice(&self.timestamp.to_le_bytes()); + + let mut comment = write_string(&self.comment); + comment.resize(164, 0); + buffer.extend_from_slice(&comment); + + let mut checksum: u32 = 0; + for (i, byte) in buffer.iter().enumerate() { + checksum ^= (*byte as u32) << (i % 24); + } + + checksum + } } #[cfg(test)] @@ -267,34 +303,33 @@ mod tests { let chardat = common_setup("arr.dat"); assert_eq!(chardat.version, 1); - assert_eq!(chardat.checksum, 747334988); - assert_eq!(chardat.race, Race::Hyur); - assert_eq!(chardat.gender, Gender::Male); - assert_eq!(chardat.age, 1); - assert_eq!(chardat.height, 50); - assert_eq!(chardat.subrace, Subrace::Midlander); - assert_eq!(chardat.face, 5); - assert_eq!(chardat.hair, 1); - assert_eq!(chardat.enable_highlights, false); - assert_eq!(chardat.skin_tone, 2); - assert_eq!(chardat.right_eye_color, 37); - assert_eq!(chardat.hair_tone, 53); - assert_eq!(chardat.highlights, 0); - assert_eq!(chardat.facial_features, 2); - assert_eq!(chardat.facial_feature_color, 2); - assert_eq!(chardat.eyebrows, 0); - assert_eq!(chardat.left_eye_color, 37); - assert_eq!(chardat.eyes, 0); - assert_eq!(chardat.nose, 0); - assert_eq!(chardat.jaw, 0); - assert_eq!(chardat.mouth, 0); - assert_eq!(chardat.lips_tone_fur_pattern, 43); - assert_eq!(chardat.ear_muscle_tail_size, 50); - assert_eq!(chardat.tail_ears_type, 0); - assert_eq!(chardat.bust, 0); - assert_eq!(chardat.face_paint_color, 36); - assert_eq!(chardat.face_paint, 0); - assert_eq!(chardat.voice, 1); + assert_eq!(chardat.customize.race, Race::Hyur); + assert_eq!(chardat.customize.gender, Gender::Male); + assert_eq!(chardat.customize.age, 1); + assert_eq!(chardat.customize.height, 50); + assert_eq!(chardat.customize.subrace, Subrace::Midlander); + assert_eq!(chardat.customize.face, 5); + assert_eq!(chardat.customize.hair, 1); + assert_eq!(chardat.customize.enable_highlights, false); + assert_eq!(chardat.customize.skin_tone, 2); + assert_eq!(chardat.customize.right_eye_color, 37); + assert_eq!(chardat.customize.hair_tone, 53); + assert_eq!(chardat.customize.highlights, 0); + assert_eq!(chardat.customize.facial_features, 2); + assert_eq!(chardat.customize.facial_feature_color, 2); + assert_eq!(chardat.customize.eyebrows, 0); + assert_eq!(chardat.customize.left_eye_color, 37); + assert_eq!(chardat.customize.eyes, 0); + assert_eq!(chardat.customize.nose, 0); + assert_eq!(chardat.customize.jaw, 0); + assert_eq!(chardat.customize.mouth, 0); + assert_eq!(chardat.customize.lips_tone_fur_pattern, 43); + assert_eq!(chardat.customize.race_feature_size, 50); + assert_eq!(chardat.customize.race_feature_type, 0); + assert_eq!(chardat.customize.bust, 0); + assert_eq!(chardat.customize.face_paint_color, 36); + assert_eq!(chardat.customize.face_paint, 0); + assert_eq!(chardat.customize.voice, 1); assert_eq!(chardat.comment, "Custom Comment Text"); } @@ -303,34 +338,33 @@ mod tests { let chardat = common_setup("heavensward.dat"); assert_eq!(chardat.version, 2); - assert_eq!(chardat.checksum, 781137881); - assert_eq!(chardat.race, Race::AuRa); - assert_eq!(chardat.gender, Gender::Female); - assert_eq!(chardat.age, 1); - assert_eq!(chardat.height, 50); - assert_eq!(chardat.subrace, Subrace::Xaela); - assert_eq!(chardat.face, 3); - assert_eq!(chardat.hair, 5); - assert_eq!(chardat.enable_highlights, false); - assert_eq!(chardat.skin_tone, 160); - assert_eq!(chardat.right_eye_color, 91); - assert_eq!(chardat.hair_tone, 159); - assert_eq!(chardat.highlights, 0); - assert_eq!(chardat.facial_features, 127); - assert_eq!(chardat.facial_feature_color, 99); - assert_eq!(chardat.eyebrows, 0); - assert_eq!(chardat.left_eye_color, 91); - assert_eq!(chardat.eyes, 0); - assert_eq!(chardat.nose, 0); - assert_eq!(chardat.jaw, 0); - assert_eq!(chardat.mouth, 0); - assert_eq!(chardat.lips_tone_fur_pattern, 0); - assert_eq!(chardat.ear_muscle_tail_size, 50); - assert_eq!(chardat.tail_ears_type, 1); - assert_eq!(chardat.bust, 25); - assert_eq!(chardat.face_paint_color, 0); - assert_eq!(chardat.face_paint, 0); - assert_eq!(chardat.voice, 112); + assert_eq!(chardat.customize.race, Race::AuRa); + assert_eq!(chardat.customize.gender, Gender::Female); + assert_eq!(chardat.customize.age, 1); + assert_eq!(chardat.customize.height, 50); + assert_eq!(chardat.customize.subrace, Subrace::Xaela); + assert_eq!(chardat.customize.face, 3); + assert_eq!(chardat.customize.hair, 5); + assert_eq!(chardat.customize.enable_highlights, false); + assert_eq!(chardat.customize.skin_tone, 160); + assert_eq!(chardat.customize.right_eye_color, 91); + assert_eq!(chardat.customize.hair_tone, 159); + assert_eq!(chardat.customize.highlights, 0); + assert_eq!(chardat.customize.facial_features, 127); + assert_eq!(chardat.customize.facial_feature_color, 99); + assert_eq!(chardat.customize.eyebrows, 0); + assert_eq!(chardat.customize.left_eye_color, 91); + assert_eq!(chardat.customize.eyes, 0); + assert_eq!(chardat.customize.nose, 0); + assert_eq!(chardat.customize.jaw, 0); + assert_eq!(chardat.customize.mouth, 0); + assert_eq!(chardat.customize.lips_tone_fur_pattern, 0); + assert_eq!(chardat.customize.race_feature_size, 50); + assert_eq!(chardat.customize.race_feature_type, 1); + assert_eq!(chardat.customize.bust, 25); + assert_eq!(chardat.customize.face_paint_color, 0); + assert_eq!(chardat.customize.face_paint, 0); + assert_eq!(chardat.customize.voice, 112); assert_eq!(chardat.comment, "Heavensward Comment Text"); } @@ -339,34 +373,33 @@ mod tests { let chardat = common_setup("stormblood.dat"); assert_eq!(chardat.version, 3); - assert_eq!(chardat.checksum, 564060181); - assert_eq!(chardat.race, Race::Lalafell); - assert_eq!(chardat.gender, Gender::Male); - assert_eq!(chardat.age, 1); - assert_eq!(chardat.height, 50); - assert_eq!(chardat.subrace, Subrace::Plainsfolk); - assert_eq!(chardat.face, 1); - assert_eq!(chardat.hair, 8); - assert_eq!(chardat.enable_highlights, false); - assert_eq!(chardat.skin_tone, 25); - assert_eq!(chardat.right_eye_color, 11); - assert_eq!(chardat.hair_tone, 45); - assert_eq!(chardat.highlights, 0); - assert_eq!(chardat.facial_features, 0); - assert_eq!(chardat.facial_feature_color, 2); - assert_eq!(chardat.eyebrows, 0); - assert_eq!(chardat.left_eye_color, 11); - assert_eq!(chardat.eyes, 0); - assert_eq!(chardat.nose, 0); - assert_eq!(chardat.jaw, 0); - assert_eq!(chardat.mouth, 0); - assert_eq!(chardat.lips_tone_fur_pattern, 43); - assert_eq!(chardat.ear_muscle_tail_size, 25); - assert_eq!(chardat.tail_ears_type, 2); - assert_eq!(chardat.bust, 0); - assert_eq!(chardat.face_paint_color, 36); - assert_eq!(chardat.face_paint, 0); - assert_eq!(chardat.voice, 19); + assert_eq!(chardat.customize.race, Race::Lalafell); + assert_eq!(chardat.customize.gender, Gender::Male); + assert_eq!(chardat.customize.age, 1); + assert_eq!(chardat.customize.height, 50); + assert_eq!(chardat.customize.subrace, Subrace::Plainsfolk); + assert_eq!(chardat.customize.face, 1); + assert_eq!(chardat.customize.hair, 8); + assert_eq!(chardat.customize.enable_highlights, false); + assert_eq!(chardat.customize.skin_tone, 25); + assert_eq!(chardat.customize.right_eye_color, 11); + assert_eq!(chardat.customize.hair_tone, 45); + assert_eq!(chardat.customize.highlights, 0); + assert_eq!(chardat.customize.facial_features, 0); + assert_eq!(chardat.customize.facial_feature_color, 2); + assert_eq!(chardat.customize.eyebrows, 0); + assert_eq!(chardat.customize.left_eye_color, 11); + assert_eq!(chardat.customize.eyes, 0); + assert_eq!(chardat.customize.nose, 0); + assert_eq!(chardat.customize.jaw, 0); + assert_eq!(chardat.customize.mouth, 0); + assert_eq!(chardat.customize.lips_tone_fur_pattern, 43); + assert_eq!(chardat.customize.race_feature_size, 25); + assert_eq!(chardat.customize.race_feature_type, 2); + assert_eq!(chardat.customize.bust, 0); + assert_eq!(chardat.customize.face_paint_color, 36); + assert_eq!(chardat.customize.face_paint, 0); + assert_eq!(chardat.customize.voice, 19); assert_eq!(chardat.comment, "Stormblood Comment Text"); } @@ -375,34 +408,33 @@ mod tests { let chardat = common_setup("shadowbringers.dat"); assert_eq!(chardat.version, 4); - assert_eq!(chardat.checksum, 404034306); - assert_eq!(chardat.race, Race::Viera); - assert_eq!(chardat.gender, Gender::Female); - assert_eq!(chardat.age, 1); - assert_eq!(chardat.height, 50); - assert_eq!(chardat.subrace, Subrace::Rava); - assert_eq!(chardat.face, 1); - assert_eq!(chardat.hair, 8); - assert_eq!(chardat.enable_highlights, false); - assert_eq!(chardat.skin_tone, 12); - assert_eq!(chardat.right_eye_color, 43); - assert_eq!(chardat.hair_tone, 53); - assert_eq!(chardat.highlights, 0); - assert_eq!(chardat.facial_features, 4); - assert_eq!(chardat.facial_feature_color, 0); - assert_eq!(chardat.eyebrows, 2); - assert_eq!(chardat.left_eye_color, 43); - assert_eq!(chardat.eyes, 131); - assert_eq!(chardat.nose, 2); - assert_eq!(chardat.jaw, 1); - assert_eq!(chardat.mouth, 131); - assert_eq!(chardat.lips_tone_fur_pattern, 171); - assert_eq!(chardat.ear_muscle_tail_size, 50); - assert_eq!(chardat.tail_ears_type, 2); - assert_eq!(chardat.bust, 100); - assert_eq!(chardat.face_paint_color, 131); - assert_eq!(chardat.face_paint, 3); - assert_eq!(chardat.voice, 160); + assert_eq!(chardat.customize.race, Race::Viera); + assert_eq!(chardat.customize.gender, Gender::Female); + assert_eq!(chardat.customize.age, 1); + assert_eq!(chardat.customize.height, 50); + assert_eq!(chardat.customize.subrace, Subrace::Rava); + assert_eq!(chardat.customize.face, 1); + assert_eq!(chardat.customize.hair, 8); + assert_eq!(chardat.customize.enable_highlights, false); + assert_eq!(chardat.customize.skin_tone, 12); + assert_eq!(chardat.customize.right_eye_color, 43); + assert_eq!(chardat.customize.hair_tone, 53); + assert_eq!(chardat.customize.highlights, 0); + assert_eq!(chardat.customize.facial_features, 4); + assert_eq!(chardat.customize.facial_feature_color, 0); + assert_eq!(chardat.customize.eyebrows, 2); + assert_eq!(chardat.customize.left_eye_color, 43); + assert_eq!(chardat.customize.eyes, 131); + assert_eq!(chardat.customize.nose, 2); + assert_eq!(chardat.customize.jaw, 1); + assert_eq!(chardat.customize.mouth, 131); + assert_eq!(chardat.customize.lips_tone_fur_pattern, 171); + assert_eq!(chardat.customize.race_feature_size, 50); + assert_eq!(chardat.customize.race_feature_type, 2); + assert_eq!(chardat.customize.bust, 100); + assert_eq!(chardat.customize.face_paint_color, 131); + assert_eq!(chardat.customize.face_paint, 3); + assert_eq!(chardat.customize.voice, 160); assert_eq!(chardat.comment, "Shadowbringers Comment Text"); }