2023-08-06 08:25:04 -04:00
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later
2023-09-22 16:39:56 -04:00
use std ::io ::{ BufWriter , Cursor } ;
2023-08-06 08:25:04 -04:00
2025-03-08 11:27:22 -05:00
use crate ::common_file_operations ::{ read_bool_from , read_string , write_bool_as , write_string } ;
2024-04-20 13:18:03 -04:00
use crate ::{ ByteBuffer , ByteSpan } ;
use binrw ::binrw ;
use binrw ::{ BinRead , BinWrite } ;
2023-08-06 08:25:04 -04:00
2023-07-08 15:29:00 -04:00
use crate ::race ::{ Gender , Race , Subrace } ;
fn convert_dat_race ( x : u8 ) -> Race {
match x {
1 = > Race ::Hyur ,
2 = > Race ::Elezen ,
3 = > Race ::Lalafell ,
4 = > Race ::Miqote ,
5 = > Race ::Roegadyn ,
6 = > Race ::AuRa ,
7 = > Race ::Hrothgar ,
8 = > Race ::Viera ,
2024-04-20 13:18:03 -04:00
_ = > Race ::Hyur ,
2023-07-08 15:29:00 -04:00
}
}
2023-08-02 16:27:10 -04:00
fn convert_race_dat ( race : & Race ) -> u8 {
match race {
Race ::Hyur = > 1 ,
Race ::Elezen = > 2 ,
Race ::Lalafell = > 3 ,
Race ::Miqote = > 4 ,
Race ::Roegadyn = > 5 ,
Race ::AuRa = > 6 ,
Race ::Hrothgar = > 7 ,
2024-04-20 13:18:03 -04:00
Race ::Viera = > 8 ,
2023-08-02 16:27:10 -04:00
}
}
2023-07-08 15:29:00 -04:00
fn convert_dat_gender ( x : u8 ) -> Gender {
match x {
0 = > Gender ::Male ,
1 = > Gender ::Female ,
2024-04-20 13:18:03 -04:00
_ = > Gender ::Male ,
2023-07-08 15:29:00 -04:00
}
}
2023-08-02 16:27:10 -04:00
fn convert_gender_dat ( gender : & Gender ) -> u8 {
match gender {
Gender ::Male = > 0 ,
Gender ::Female = > 1 ,
}
}
2023-07-08 15:29:00 -04:00
fn convert_dat_subrace ( x : u8 ) -> Subrace {
match x {
1 = > Subrace ::Midlander ,
2 = > Subrace ::Highlander ,
3 = > Subrace ::Wildwood ,
4 = > Subrace ::Duskwight ,
5 = > Subrace ::Plainsfolk ,
6 = > Subrace ::Dunesfolk ,
7 = > Subrace ::Seeker ,
8 = > Subrace ::Keeper ,
2024-04-20 13:18:03 -04:00
9 = > Subrace ::SeaWolf ,
2023-07-08 15:29:00 -04:00
10 = > Subrace ::Hellsguard ,
11 = > Subrace ::Raen ,
12 = > Subrace ::Xaela ,
13 = > Subrace ::Hellion ,
14 = > Subrace ::Lost ,
15 = > Subrace ::Rava ,
16 = > Subrace ::Veena ,
2024-04-20 13:18:03 -04:00
_ = > Subrace ::Midlander ,
2023-07-08 15:29:00 -04:00
}
}
2023-08-02 16:27:10 -04:00
fn convert_subrace_dat ( subrace : & Subrace ) -> u8 {
match subrace {
Subrace ::Midlander = > 1 ,
Subrace ::Highlander = > 2 ,
Subrace ::Wildwood = > 3 ,
Subrace ::Duskwight = > 4 ,
Subrace ::Plainsfolk = > 5 ,
Subrace ::Dunesfolk = > 6 ,
Subrace ::Seeker = > 7 ,
Subrace ::Keeper = > 8 ,
2024-04-20 13:18:03 -04:00
Subrace ::SeaWolf = > 9 ,
2023-08-02 16:27:10 -04:00
Subrace ::Hellsguard = > 10 ,
Subrace ::Raen = > 11 ,
Subrace ::Xaela = > 12 ,
Subrace ::Hellion = > 13 ,
Subrace ::Lost = > 14 ,
Subrace ::Rava = > 15 ,
2024-04-20 13:18:03 -04:00
Subrace ::Veena = > 16 ,
2023-08-02 16:27:10 -04:00
}
}
#[ binrw ]
2023-07-08 15:29:00 -04:00
#[ br(little) ]
#[ repr(C) ]
#[ derive(Debug) ]
2025-03-08 13:12:08 -05:00
pub struct CustomizeData {
2023-09-22 18:13:20 -04:00
/// The race of the character.
2025-03-08 13:12:08 -05:00
#[ br(map = convert_dat_race) ]
#[ bw(map = convert_race_dat) ]
2023-07-08 15:29:00 -04:00
pub race : Race ,
2023-09-22 18:13:20 -04:00
/// The gender of the character.
2025-03-08 13:12:08 -05:00
#[ br(map = convert_dat_gender) ]
#[ bw(map = convert_gender_dat) ]
2023-07-08 15:29:00 -04:00
pub gender : Gender ,
2023-09-22 18:13:20 -04:00
/// The age of the character. Normal = 1, Old = 3, Young = 4.
pub age : u8 ,
2025-03-08 11:27:22 -05:00
/// The height of the character from 0 to 100.
2023-07-08 15:29:00 -04:00
pub height : u8 ,
2023-09-22 18:13:20 -04:00
/// The character's subrace.
2025-03-08 13:12:08 -05:00
#[ br(map = convert_dat_subrace) ]
#[ bw(map = convert_subrace_dat) ]
2023-07-08 15:29:00 -04:00
pub subrace : Subrace ,
2023-09-22 18:13:20 -04:00
2025-03-08 11:27:22 -05:00
/// The character's selected face.
pub face : u8 ,
2023-09-22 18:13:20 -04:00
/// The character's selected hair.
2023-07-08 15:29:00 -04:00
pub hair : u8 ,
2023-09-22 18:13:20 -04:00
/// If hair highlights are enabled for this character.
2024-04-16 21:03:26 -04:00
#[ br(map = read_bool_from::<u8>) ]
#[ bw(map = write_bool_as::<u8>) ]
2023-07-08 15:29:00 -04:00
pub enable_highlights : bool ,
2023-09-22 18:13:20 -04:00
/// The character's skin tone.
2023-07-08 15:29:00 -04:00
pub skin_tone : u8 ,
2023-09-22 18:13:20 -04:00
/// The character's right eye color.
2023-07-08 15:29:00 -04:00
pub right_eye_color : u8 ,
2023-09-22 18:13:20 -04:00
/// The character's hair color.
2023-07-08 15:29:00 -04:00
pub hair_tone : u8 ,
2023-09-22 18:13:20 -04:00
/// The color of the hair highlights.
2023-07-08 15:29:00 -04:00
pub highlights : u8 ,
2023-09-22 18:13:20 -04:00
/// The selected facial features.
2023-07-08 15:29:00 -04:00
pub facial_features : u8 ,
2023-09-22 18:13:20 -04:00
2025-03-08 11:27:22 -05:00
/// The color of the character's facial features.'
pub facial_feature_color : u8 ,
2023-09-22 18:13:20 -04:00
/// The character's selected eyebrows.
2023-07-08 15:29:00 -04:00
pub eyebrows : u8 ,
2023-09-22 18:13:20 -04:00
/// The character's left eye color.
2023-07-08 15:29:00 -04:00
pub left_eye_color : u8 ,
2023-09-22 18:13:20 -04:00
/// The character's selected eyes.
2025-03-08 11:27:22 -05:00
/// If the character has selected "Small Iris" then it adds 128 to this.
2023-07-08 15:29:00 -04:00
pub eyes : u8 ,
2023-09-22 18:13:20 -04:00
/// The character's selected nose.
2023-07-08 15:29:00 -04:00
pub nose : u8 ,
2023-09-22 18:13:20 -04:00
/// The character's selected jaw.
2023-07-08 15:29:00 -04:00
pub jaw : u8 ,
2023-09-22 18:13:20 -04:00
/// The character's selected mouth.
2023-07-08 15:29:00 -04:00
pub mouth : u8 ,
2023-09-22 18:13:20 -04:00
/// The character's selected pattern.
2023-07-08 15:29:00 -04:00
pub lips_tone_fur_pattern : u8 ,
2023-09-22 18:13:20 -04:00
2025-03-08 11:27:22 -05:00
/// Depending on the race, it's either the ear size, muscle size, or tail size.
2025-03-08 13:12:08 -05:00
pub race_feature_size : u8 ,
2023-09-22 18:13:20 -04:00
2025-03-08 11:27:22 -05:00
/// Depending on the race, it's the selected tail or ears.
2025-03-08 13:12:08 -05:00
pub race_feature_type : u8 ,
2023-09-22 18:13:20 -04:00
2025-03-08 11:27:22 -05:00
/// The size of the character's bust from 0 to 100.
2023-07-08 15:29:00 -04:00
pub bust : u8 ,
2023-09-22 18:13:20 -04:00
2025-03-08 11:27:22 -05:00
/// The character's choice of face paint.
pub face_paint : u8 ,
2023-09-22 18:13:20 -04:00
/// The color of the face paint.
2025-03-08 11:27:22 -05:00
/// If the character has selected "Light" then it adds 128 to this.
2023-07-08 15:29:00 -04:00
pub face_paint_color : u8 ,
2023-09-22 18:13:20 -04:00
/// The character's chosen voice.
2023-07-08 15:29:00 -04:00
pub voice : u8 ,
2025-03-08 13:12:08 -05:00
}
/// 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 ,
2023-07-08 15:29:00 -04:00
2023-09-22 18:13:20 -04:00
/// The timestamp when the preset was created.
2025-03-08 11:27:22 -05:00
/// This is a UTC time in seconds since the Unix epoch.
2025-03-08 11:34:16 -05:00
#[ brw(pad_before = 1) ]
2025-03-08 11:27:22 -05:00
pub timestamp : u32 ,
// TODO: this is terrible, just read until string nul terminator
#[ br(count = 164) ]
2025-03-08 11:34:16 -05:00
#[ bw(pad_size_to = 164) ]
2025-03-08 11:27:22 -05:00
#[ br(map = read_string) ]
#[ bw(map = write_string) ]
comment : String ,
2023-07-08 15:29:00 -04:00
}
2023-09-22 18:13:20 -04:00
impl CharacterData {
/// Parses existing character data.
2023-10-13 16:16:04 -04:00
pub fn from_existing ( buffer : ByteSpan ) -> Option < CharacterData > {
2023-07-08 15:29:00 -04:00
let mut cursor = Cursor ::new ( buffer ) ;
2023-09-22 18:13:20 -04:00
CharacterData ::read ( & mut cursor ) . ok ( )
2023-07-08 15:29:00 -04:00
}
2023-09-22 16:39:56 -04:00
2023-09-22 18:13:20 -04:00
/// Write existing character data to a buffer.
2023-10-13 16:16:04 -04:00
pub fn write_to_buffer ( & self ) -> Option < ByteBuffer > {
let mut buffer = ByteBuffer ::new ( ) ;
2023-09-22 16:39:56 -04:00
{
let cursor = Cursor ::new ( & mut buffer ) ;
let mut writer = BufWriter ::new ( cursor ) ;
2024-04-16 21:28:33 -04:00
self . write_le ( & mut writer ) . ok ( ) ? ;
2023-09-22 16:39:56 -04:00
}
Some ( buffer )
}
2025-03-08 13:12:08 -05:00
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
}
2024-04-16 21:43:15 -04:00
}
#[ cfg(test) ]
mod tests {
use std ::fs ::read ;
use std ::path ::PathBuf ;
use super ::* ;
2024-04-20 13:18:03 -04:00
2024-04-16 21:43:15 -04:00
#[ test ]
fn test_invalid ( ) {
let mut d = PathBuf ::from ( env! ( " CARGO_MANIFEST_DIR " ) ) ;
d . push ( " resources/tests " ) ;
d . push ( " random " ) ;
// Feeding it invalid data should not panic
CharacterData ::from_existing ( & read ( d ) . unwrap ( ) ) ;
}
2025-03-08 11:27:22 -05:00
fn common_setup ( name : & str ) -> CharacterData {
let mut d = PathBuf ::from ( env! ( " CARGO_MANIFEST_DIR " ) ) ;
d . push ( " resources/tests/chardat " ) ;
d . push ( name ) ;
CharacterData ::from_existing ( & read ( d ) . unwrap ( ) ) . unwrap ( )
}
#[ test ]
fn read_arr ( ) {
let chardat = common_setup ( " arr.dat " ) ;
assert_eq! ( chardat . version , 1 ) ;
2025-03-08 13:12:08 -05:00
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 ) ;
2025-03-08 11:27:22 -05:00
assert_eq! ( chardat . comment , " Custom Comment Text " ) ;
}
#[ test ]
fn read_heavensward ( ) {
let chardat = common_setup ( " heavensward.dat " ) ;
assert_eq! ( chardat . version , 2 ) ;
2025-03-08 13:12:08 -05:00
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 ) ;
2025-03-08 11:27:22 -05:00
assert_eq! ( chardat . comment , " Heavensward Comment Text " ) ;
}
#[ test ]
fn read_stormblood ( ) {
let chardat = common_setup ( " stormblood.dat " ) ;
assert_eq! ( chardat . version , 3 ) ;
2025-03-08 13:12:08 -05:00
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 ) ;
2025-03-08 11:27:22 -05:00
assert_eq! ( chardat . comment , " Stormblood Comment Text " ) ;
}
#[ test ]
fn read_shadowbringers ( ) {
let chardat = common_setup ( " shadowbringers.dat " ) ;
assert_eq! ( chardat . version , 4 ) ;
2025-03-08 13:12:08 -05:00
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 ) ;
2025-03-08 11:27:22 -05:00
assert_eq! ( chardat . comment , " Shadowbringers Comment Text " ) ;
}
2025-03-08 11:34:16 -05:00
#[ test ]
fn write_shadowbringers ( ) {
let mut d = PathBuf ::from ( env! ( " CARGO_MANIFEST_DIR " ) ) ;
d . push ( " resources/tests/chardat " ) ;
d . push ( " shadowbringers.dat " ) ;
let chardat_bytes = & read ( d ) . unwrap ( ) ;
let chardat = CharacterData ::from_existing ( chardat_bytes ) . unwrap ( ) ;
assert_eq! ( * chardat_bytes , chardat . write_to_buffer ( ) . unwrap ( ) ) ;
}
2024-04-16 21:43:15 -04:00
}