add xbox disk file formats

This commit is contained in:
Martin Michelsen
2025-06-07 19:23:23 -07:00
parent d4bc880018
commit e8b2765a71
8 changed files with 278 additions and 130 deletions
+1
View File
@@ -754,6 +754,7 @@ The data formats that newserv can convert to/from are:
| PSO DC save file (.vms) | `encrypt-vms-save` | `decrypt-vms-save` |
| PSO PC save file | `encrypt-pc-save` | `decrypt-pc-save` |
| PSO GC save file (.gci) | `encrypt-gci-save` | `decrypt-gci-save` |
| PSO Xbox save file | None | `decrypt-xbox-save` |
| PSO GC snapshot file | None | `decode-gci-snapshot` |
| Quest script (.bin) | `assemble-quest-script` | `disassemble-quest-script` |
| Quest map (.dat) | None | `disassemble-quest-map` |
+1 -1
View File
@@ -1646,7 +1646,7 @@ ChatCommandDefinition cc_loadchar(
if (!a.c->login || !a.c->login->xb_license) {
throw runtime_error("XB client is not logged in");
}
PSOXBCharacterFileCharacter xb_char = *a.c->character();
PSOXBCharacterFile::Character xb_char = *a.c->character();
xb_char.guild_card.xb_user_id_high = (a.c->login->xb_license->user_id >> 32) & 0xFFFFFFFF;
xb_char.guild_card.xb_user_id_low = a.c->login->xb_license->user_id & 0xFFFFFFFF;
co_await send_set_extended_player_info(xb_char);
+1 -1
View File
@@ -7429,7 +7429,7 @@ struct G_RejectBattleStartRequest_Ep3_6xB4x53 {
// Requested with the GetExtendedPlayerInfo patch. Format depends on version:
// DC v2: PSODCV2CharacterFile
// GC v3: PSOGCCharacterFile::Character
// XB v3: PSOXBCharacterFileCharacter
// XB v3: PSOXBCharacterFile::Character
// 6xE4: Increment enemy damage threshold
// This command increments or decrements the minimum amount of damage an enemy
+131 -79
View File
@@ -859,85 +859,6 @@ Action a_encrypt_vms_save("encrypt-vms-save", "\
hexadecimal value.\n",
a_encrypt_decrypt_vms_save_fn);
static void a_encrypt_decrypt_gci_save_fn(phosg::Arguments& args) {
bool is_decrypt = (args.get<string>(0) == "decrypt-gci-save");
bool skip_checksum = args.get<bool>("skip-checksum");
string seed = args.get<string>("seed");
string system_filename = args.get<string>("sys");
int64_t override_round2_seed = args.get<int64_t>("round2-seed", -1, phosg::Arguments::IntFormat::HEX);
uint32_t round1_seed;
if (!system_filename.empty()) {
string system_data = phosg::load_file(system_filename);
phosg::StringReader r(system_data);
const auto& header = r.get<PSOGCIFileHeader>();
header.check();
const auto& system = r.get<PSOGCSystemFile>();
round1_seed = system.creation_timestamp;
} else if (!seed.empty()) {
round1_seed = stoul(seed, nullptr, 16);
} else {
throw runtime_error("either --sys or --seed must be given");
}
auto data = read_input_data(args);
phosg::StringReader r(data);
const auto& header = r.get<PSOGCIFileHeader>();
header.check();
size_t data_start_offset = r.where();
auto process_file = [&]<typename StructT>() {
if (is_decrypt) {
const void* data_section = r.getv(header.data_size);
auto decrypted = decrypt_fixed_size_data_section_t<StructT, true>(
data_section, header.data_size, round1_seed, skip_checksum, override_round2_seed);
*reinterpret_cast<StructT*>(data.data() + data_start_offset) = decrypted;
} else {
const auto& s = r.get<StructT>();
auto encrypted = encrypt_fixed_size_data_section_t<StructT, true>(s, round1_seed);
if (data_start_offset + encrypted.size() > data.size()) {
throw runtime_error("encrypted result exceeds file size");
}
memcpy(data.data() + data_start_offset, encrypted.data(), encrypted.size());
}
};
if (header.data_size == sizeof(PSOGCGuildCardFile)) {
process_file.template operator()<PSOGCGuildCardFile>();
} else if (header.is_ep12() && (header.data_size == sizeof(PSOGCCharacterFile))) {
process_file.template operator()<PSOGCCharacterFile>();
} else if (header.is_ep3() && (header.data_size == sizeof(PSOGCEp3CharacterFile))) {
auto* charfile = reinterpret_cast<PSOGCEp3CharacterFile*>(data.data() + data_start_offset);
if (!is_decrypt) {
for (size_t z = 0; z < charfile->characters.size(); z++) {
charfile->characters[z].ep3_config.encrypt(phosg::random_object<uint8_t>());
}
}
process_file.template operator()<PSOGCEp3CharacterFile>();
if (is_decrypt) {
for (size_t z = 0; z < charfile->characters.size(); z++) {
charfile->characters[z].ep3_config.decrypt();
}
}
} else {
throw runtime_error("unrecognized save type");
}
write_output_data(args, data.data(), data.size(), is_decrypt ? "gcid" : "gci");
}
Action a_decrypt_gci_save("decrypt-gci-save", nullptr, a_encrypt_decrypt_gci_save_fn);
Action a_encrypt_gci_save("encrypt-gci-save", "\
encrypt-gci-save CRYPT-OPTION INPUT-FILENAME [OUTPUT-FILENAME]\n\
decrypt-gci-save CRYPT-OPTION INPUT-FILENAME [OUTPUT-FILENAME]\n\
Encrypt or decrypt a character or Guild Card file in GCI format. If\n\
encrypting, the checksum is also recomputed and stored in the encrypted\n\
file. CRYPT-OPTION is required; it can be either --sys=SYSTEM-FILENAME\n\
(specifying the name of the corresponding PSO_SYSTEM .gci file) or\n\
--seed=ROUND1-SEED (specified as a 32-bit hexadecimal number).\n",
a_encrypt_decrypt_gci_save_fn);
static void a_encrypt_decrypt_pc_save_fn(phosg::Arguments& args) {
bool is_decrypt = (args.get<string>(0) == "decrypt-pc-save");
bool skip_checksum = args.get<bool>("skip-checksum");
@@ -1034,6 +955,137 @@ static void a_encrypt_decrypt_save_data_fn(phosg::Arguments& args) {
write_output_data(args, output_data.data(), output_data.size(), "dec");
}
static void a_encrypt_decrypt_gci_save_fn(phosg::Arguments& args) {
bool is_decrypt = (args.get<string>(0) == "decrypt-gci-save");
bool skip_checksum = args.get<bool>("skip-checksum");
string seed = args.get<string>("seed");
string system_filename = args.get<string>("sys");
int64_t override_round2_seed = args.get<int64_t>("round2-seed", -1, phosg::Arguments::IntFormat::HEX);
uint32_t round1_seed;
if (!system_filename.empty()) {
string system_data = phosg::load_file(system_filename);
phosg::StringReader r(system_data);
const auto& header = r.get<PSOGCIFileHeader>();
header.check();
const auto& system = r.get<PSOGCSystemFile>();
round1_seed = system.creation_timestamp;
} else if (!seed.empty()) {
round1_seed = stoul(seed, nullptr, 16);
} else {
throw runtime_error("either --sys or --seed must be given");
}
auto data = read_input_data(args);
phosg::StringReader r(data);
const auto& header = r.get<PSOGCIFileHeader>();
header.check();
size_t data_start_offset = r.where();
auto process_file = [&]<typename StructT>() {
if (is_decrypt) {
const void* data_section = r.getv(header.data_size);
auto decrypted = decrypt_fixed_size_data_section_t<StructT, true>(
data_section, header.data_size, round1_seed, skip_checksum, override_round2_seed);
*reinterpret_cast<StructT*>(data.data() + data_start_offset) = decrypted;
} else {
const auto& s = r.get<StructT>();
auto encrypted = encrypt_fixed_size_data_section_t<StructT, true>(s, round1_seed);
if (data_start_offset + encrypted.size() > data.size()) {
throw runtime_error("encrypted result exceeds file size");
}
memcpy(data.data() + data_start_offset, encrypted.data(), encrypted.size());
}
};
if (header.data_size == sizeof(PSOGCGuildCardFile)) {
process_file.template operator()<PSOGCGuildCardFile>();
} else if (header.is_ep12() && (header.data_size == sizeof(PSOGCCharacterFile))) {
process_file.template operator()<PSOGCCharacterFile>();
} else if (header.is_ep3() && (header.data_size == sizeof(PSOGCEp3CharacterFile))) {
auto* charfile = reinterpret_cast<PSOGCEp3CharacterFile*>(data.data() + data_start_offset);
if (!is_decrypt) {
for (size_t z = 0; z < charfile->characters.size(); z++) {
charfile->characters[z].ep3_config.encrypt(phosg::random_object<uint8_t>());
}
}
process_file.template operator()<PSOGCEp3CharacterFile>();
if (is_decrypt) {
for (size_t z = 0; z < charfile->characters.size(); z++) {
charfile->characters[z].ep3_config.decrypt();
}
}
} else {
throw runtime_error("unrecognized save type");
}
write_output_data(args, data.data(), data.size(), is_decrypt ? "gcid" : "gci");
}
Action a_decrypt_gci_save("decrypt-gci-save", nullptr, a_encrypt_decrypt_gci_save_fn);
Action a_encrypt_gci_save("encrypt-gci-save", "\
encrypt-gci-save CRYPT-OPTION INPUT-FILENAME [OUTPUT-FILENAME]\n\
decrypt-gci-save CRYPT-OPTION INPUT-FILENAME [OUTPUT-FILENAME]\n\
Encrypt or decrypt a character or Guild Card file in GCI format. If\n\
encrypting, the checksum is also recomputed and stored in the encrypted\n\
file. CRYPT-OPTION is required; it can be either --sys=SYSTEM-FILENAME\n\
(specifying the name of the corresponding PSO_SYSTEM .gci file) or\n\
--seed=ROUND1-SEED (specified as a 32-bit hexadecimal number).\n",
a_encrypt_decrypt_gci_save_fn);
Action a_decrypt_xbox_save(
"decrypt-xbox-save", "\
decrypt-xbox-save CRYPT-OPTION INPUT-FILENAME [OUTPUT-FILENAME]\n\
Decrypt a character or Guild Card file in Xbox format. CRYPT-OPTION is\n\
required; it can be either --sys=SYSTEM-FILENAME (specifying the name of\n\
the corresponding PSO_SYSTEM file) or --seed=ROUND1-SEED (specified as a\n\
32-bit hexadecimal number).\n",
+[](phosg::Arguments& args) {
bool skip_checksum = args.get<bool>("skip-checksum");
string seed = args.get<string>("seed");
string system_filename = args.get<string>("sys");
int64_t override_round2_seed = args.get<int64_t>("round2-seed", -1, phosg::Arguments::IntFormat::HEX);
uint32_t round1_seed;
if (!system_filename.empty()) {
string system_data = phosg::load_file(system_filename);
phosg::StringReader r(system_data);
const auto& header = r.get<PSOXBFileHeader>();
header.check();
const auto& system = r.get<PSOXBSystemFile>();
round1_seed = system.creation_timestamp;
} else if (!seed.empty()) {
round1_seed = stoul(seed, nullptr, 16);
} else {
throw runtime_error("either --sys or --seed must be given");
}
auto data = read_input_data(args);
phosg::StringReader r(data);
const auto& header = r.get<PSOXBFileHeader>();
header.check();
size_t data_start_offset = r.where();
auto process_file = [&]<typename StructT>() {
const void* data_section = r.getv(header.data_size);
auto decrypted = decrypt_fixed_size_data_section_t<StructT, false>(
data_section, header.data_size, round1_seed, skip_checksum, override_round2_seed);
*reinterpret_cast<StructT*>(data.data() + data_start_offset) = decrypted;
};
if (header.data_size == sizeof(PSOXBGuildCardFile)) {
process_file.template operator()<PSOXBGuildCardFile>();
} else if (header.data_size == sizeof(PSOXBCharacterFile)) {
process_file.template operator()<PSOXBCharacterFile>();
} else {
throw runtime_error("unrecognized save type");
}
write_output_data(args, data.data(), data.size(), "dec");
});
// TODO: Write usage text for these actions
Action a_decrypt_save_data("decrypt-save-data", nullptr, a_encrypt_decrypt_save_data_fn);
Action a_encrypt_save_data("encrypt-save-data", nullptr, a_encrypt_decrypt_save_data_fn);
+1 -1
View File
@@ -3533,7 +3533,7 @@ static asio::awaitable<void> on_30(shared_ptr<Client> c, Channel::Message& msg)
ch = PSOBBCharacterFile::create_from_file(msg.check_size_t<PSOGCCharacterFile::Character>());
break;
case Version::XB_V3:
ch = PSOBBCharacterFile::create_from_file(msg.check_size_t<PSOXBCharacterFileCharacter>());
ch = PSOBBCharacterFile::create_from_file(msg.check_size_t<PSOXBCharacterFile::Character>());
break;
case Version::GC_EP3_NTE:
case Version::GC_EP3:
+19 -3
View File
@@ -312,6 +312,22 @@ PSOGCEp3CharacterFile::Character::operator PSOGCEp3NTECharacter() const {
return ret;
}
bool PSOXBFileHeader::checksum_correct() const {
uint32_t cs = phosg::crc32(&this->game_name, this->game_name.bytes());
cs = phosg::crc32(&this->file_name, this->file_name.bytes(), cs);
cs = phosg::crc32(&this->banner, this->banner.bytes(), cs);
cs = phosg::crc32(&this->icon, this->icon.bytes(), cs);
cs = phosg::crc32(&this->data_size, sizeof(this->data_size), cs);
cs = phosg::crc32("\0\0\0\0", 4, cs); // this->checksum (treated as zero)
return (cs == this->checksum);
}
void PSOXBFileHeader::check() const {
if (!this->checksum_correct()) {
throw runtime_error("Xbox file intermediate header checksum is incorrect");
}
}
void PSOBBGuildCardFile::Entry::clear() {
this->data.clear();
this->unknown_a1.clear(0);
@@ -830,7 +846,7 @@ shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_file(const PSOGCE
return ret;
}
shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_file(const PSOXBCharacterFileCharacter& src) {
shared_ptr<PSOBBCharacterFile> PSOBBCharacterFile::create_from_file(const PSOXBCharacterFile::Character& src) {
auto ret = PSOBBCharacterFile::create_from_config(
src.guild_card.guild_card_number,
src.inventory.language,
@@ -1101,10 +1117,10 @@ PSOBBCharacterFile::operator PSOGCCharacterFile::Character() const {
return ret;
}
PSOBBCharacterFile::operator PSOXBCharacterFileCharacter() const {
PSOBBCharacterFile::operator PSOXBCharacterFile::Character() const {
uint8_t language = this->inventory.language;
PSOXBCharacterFileCharacter ret;
PSOXBCharacterFile::Character ret;
ret.inventory = this->inventory;
ret.inventory.encode_for_client(Version::XB_V3, nullptr);
ret.disp = this->disp.to_dcpcv3<false>(language, language);
+123 -44
View File
@@ -91,6 +91,31 @@ struct PSOGCIFileHeader {
bool is_nte() const;
} __packed_ws__(PSOGCIFileHeader, 0x2088);
struct PSOXBFileHeader {
// The signature is computed by doing the following:
// // TODO: Should flags be 0 or 1? It looks like it should be 0 for
// // character files, but not sure about this
// auto handle = XCalculateSignatureBegin(flags);
// XCalculateSignatureUpdate(
// handle,
// &header.source_size,
// total_size - offsetof(PSOXBFileHeader, source_size));
// XCalculateSignatureEnd(handle, header.signature);
/* 0000 */ parray<uint8_t, 0x14> signature;
/* 0014 */ le_uint32_t source_size = 0; // == total file size - 0x4000
/* 0018 */ parray<uint8_t, 0x3FE8> unused; // Always blank (zeroes)
/* 4000 */ pstring<TextEncoding::MARKED, 0x20> game_name;
/* 4020 */ pstring<TextEncoding::MARKED, 0x20> file_name;
/* 4040 */ parray<uint8_t, 0x1800> banner; // Always blank (zeroes)
/* 5840 */ parray<uint8_t, 0x800> icon; // Always blank (zeroes)
/* 6040 */ le_uint32_t data_size = 0;
/* 6044 */ le_uint32_t checksum = 0; // Starts at game_name
/* 6048 */
bool checksum_correct() const;
void check() const;
} __packed_ws__(PSOXBFileHeader, 0x6048);
////////////////////////////////////////////////////////////////////////////////
// Subordinate structures
@@ -280,6 +305,35 @@ struct PSOGCEp3SystemFile {
/* 012C */
} __packed_ws__(PSOGCEp3SystemFile, 0x12C);
struct PSOXBSystemFile {
/* 0000 */ le_uint32_t checksum = 0;
/* 0004 */ le_int16_t music_volume = -50;
/* 0006 */ int8_t sound_volume = 0;
/* 0007 */ uint8_t language = 0;
/* 0008 */ be_int32_t server_time_delta_frames = 200;
/* 000C */ be_uint16_t udp_behavior = 0; // 0 = auto, 1 = on, 2 = off
/* 000E */ be_uint16_t surround_sound_enabled = 0;
/* 0010 */ parray<uint8_t, 0x0100> event_flags;
/* 0110 */ parray<uint8_t, 8> unknown_a1;
struct UserEntry {
/* 00 */ le_uint32_t xb_user_id_high = 0;
/* 04 */ le_uint32_t xb_user_id_low = 0;
/* 08 */ le_uint32_t unknown_a2;
/* 0C */ le_uint32_t last_write_year;
/* 10 */ le_uint32_t last_write_month;
/* 14 */ le_uint32_t last_write_day;
/* 18 */ le_uint32_t last_write_hour;
/* 1C */ le_uint32_t last_write_minute;
/* 20 */ le_uint32_t last_write_second;
/* 24 */ le_uint32_t flags = 1; // 1 = not present
/* 28 */ pstring<TextEncoding::ASCII, 0x10> gamertag;
/* 38 */
} __packed_ws__(UserEntry, 0x38);
/* 0118 */ parray<UserEntry, 4> users;
/* 01F8 */ le_uint32_t creation_timestamp = 0;
/* 01FC */
} __packed_ws__(PSOXBSystemFile, 0x1FC);
struct PSOBBMinimalSystemFile {
/* 0000 */ be_uint32_t checksum = 0;
/* 0004 */ be_int16_t music_volume = 0;
@@ -717,48 +771,65 @@ struct PSOGCEp3CharacterFile {
/* 194B0 */
} __packed_ws__(PSOGCEp3CharacterFile, 0x194B0);
struct PSOXBCharacterFileCharacter {
// This structure is internally split into two by the game. The offsets here
// are relative to the start of this structure (first column), and relative
// to the start of the second internal structure (second column).
// Most fields have the same meanings as in PSOGCCharacterFile::Character.
/* 0000:---- */ PlayerInventory inventory;
/* 034C:---- */ PlayerDispDataDCPCV3 disp;
/* 041C:0000 */ le_uint32_t validation_flags = 0;
/* 0420:0004 */ le_uint32_t creation_timestamp = 0;
/* 0424:0008 */ le_uint32_t signature = 0xC87ED5B1;
/* 0428:000C */ le_uint32_t play_time_seconds = 0;
/* 042C:0010 */ le_uint32_t option_flags = 0x00040058;
/* 0430:0014 */ le_uint32_t save_count = 1;
/* 0434:0018 */ pstring<TextEncoding::ASCII, 0x1C> ppp_username;
/* 0450:0034 */ pstring<TextEncoding::ASCII, 0x10> ppp_password;
/* 0460:0044 */ QuestFlags quest_flags;
/* 0660:0244 */ le_uint32_t death_count = 0;
/* 0664:0248 */ PlayerBank200 bank;
/* 192C:1510 */ GuildCardXB guild_card;
/* 1B58:173C */ parray<SaveFileSymbolChatEntryDCXB, 12> symbol_chats;
/* 1F78:1B5C */ parray<SaveFileShortcutEntryXB, 16> shortcuts;
/* 24B8:209C */ pstring<TextEncoding::MARKED, 0xAC> auto_reply;
/* 2518:20FC */ pstring<TextEncoding::MARKED, 0xAC> info_board;
// TODO: The following fields are guesses and have not been verified.
/* 2610:21F4 */ PlayerRecordsBattle battle_records;
/* 2628:220C */ parray<uint8_t, 4> unknown_a4;
/* 262C:2210 */ PlayerRecordsChallengeV3 challenge_records;
/* 272C:2310 */ parray<le_uint16_t, 20> tech_menu_shortcut_entries;
/* 2754:2338 */ ChoiceSearchConfig choice_search_config;
/* 276C:2350 */ parray<uint8_t, 0x10> unknown_a6;
/* 277C:2360 */ parray<le_uint32_t, 0x10> quest_counters;
/* 27BC:23A0 */ PlayerRecordsBattle offline_battle_records;
/* 27D4:23B8 */ parray<uint8_t, 4> unknown_a7;
struct UnknownA8Entry {
/* 00 */ le_uint32_t unknown_a1 = 0;
/* 04 */ parray<uint8_t, 0x1C> unknown_a2;
/* 20 */ parray<le_float, 4> unknown_a3;
/* 30 */
} __packed_ws__(UnknownA8Entry, 0x30);
/* 27D8:23BC */ parray<UnknownA8Entry, 5> unknown_a8;
/* 28C8:24AC */
} __packed_ws__(PSOXBCharacterFileCharacter, 0x28C8);
struct PSOXBCharacterFile {
struct Character {
// This structure is internally split into two by the game. The offsets here
// are relative to the start of this structure (first column), and relative
// to the start of the second internal structure (second column).
// Most fields have the same meanings as in PSOGCCharacterFile::Character.
/* 0000:---- */ PlayerInventory inventory;
/* 034C:---- */ PlayerDispDataDCPCV3 disp;
/* 041C:0000 */ le_uint32_t validation_flags = 0;
/* 0420:0004 */ le_uint32_t creation_timestamp = 0;
/* 0424:0008 */ le_uint32_t signature = 0xC87ED5B1;
/* 0428:000C */ le_uint32_t play_time_seconds = 0;
/* 042C:0010 */ le_uint32_t option_flags = 0x00040058;
/* 0430:0014 */ le_uint32_t save_count = 1;
/* 0434:0018 */ pstring<TextEncoding::ASCII, 0x1C> ppp_username;
/* 0450:0034 */ pstring<TextEncoding::ASCII, 0x10> ppp_password;
/* 0460:0044 */ QuestFlags quest_flags;
/* 0660:0244 */ le_uint32_t death_count = 0;
/* 0664:0248 */ PlayerBank200 bank;
/* 192C:1510 */ GuildCardXB guild_card;
/* 1B58:173C */ parray<SaveFileSymbolChatEntryDCXB, 12> symbol_chats;
/* 1F78:1B5C */ parray<SaveFileShortcutEntryXB, 16> shortcuts;
/* 24B8:209C */ pstring<TextEncoding::MARKED, 0xAC> auto_reply;
/* 2518:20FC */ pstring<TextEncoding::MARKED, 0xAC> info_board;
// TODO: The following fields are guesses and have not been verified.
/* 2610:21F4 */ PlayerRecordsBattle battle_records;
/* 2628:220C */ parray<uint8_t, 4> unknown_a4;
/* 262C:2210 */ PlayerRecordsChallengeV3 challenge_records;
/* 272C:2310 */ parray<le_uint16_t, 20> tech_menu_shortcut_entries;
/* 2754:2338 */ ChoiceSearchConfig choice_search_config;
/* 276C:2350 */ parray<uint8_t, 0x10> unknown_a6;
/* 277C:2360 */ parray<le_uint32_t, 0x10> quest_counters;
/* 27BC:23A0 */ PlayerRecordsBattle offline_battle_records;
/* 27D4:23B8 */ parray<uint8_t, 4> unknown_a7;
struct UnknownA8Entry {
/* 00 */ le_uint32_t unknown_a1 = 0;
/* 04 */ parray<uint8_t, 0x1C> unknown_a2;
/* 20 */ parray<le_float, 4> unknown_a3;
/* 30 */
} __packed_ws__(UnknownA8Entry, 0x30);
/* 27D8:23BC */ parray<UnknownA8Entry, 5> unknown_a8;
/* 28C8:24AC */
} __packed_ws__(Character, 0x28C8);
struct CharEntry {
Character character;
parray<uint8_t, 0x18> unknown_a1;
} __packed_ws__(CharEntry, 0x28E0);
/* 00000 */ le_uint32_t checksum = 0;
/* 00004 */ parray<CharEntry, 15> characters;
/* 26524 */ pstring<TextEncoding::ASCII, 0x10> serial_number;
/* 26534 */ pstring<TextEncoding::ASCII, 0x10> access_key;
/* 26544 */ pstring<TextEncoding::ASCII, 0x10> password;
/* 26554 */ be_uint64_t bgm_test_songs_unlocked = 0;
/* 2655C */ le_uint32_t save_count = 1;
/* 26560 */ le_uint32_t round2_seed = 0;
/* 26564 */
} __packed_ws__(PSOXBCharacterFile, 0x26564);
struct PSOBBCharacterFile {
// Most fields have the same meanings as in PSOGCCharacterFile::Character.
@@ -816,7 +887,7 @@ struct PSOBBCharacterFile {
static std::shared_ptr<PSOBBCharacterFile> create_from_file(const PSOGCNTECharacterFileCharacter& src);
static std::shared_ptr<PSOBBCharacterFile> create_from_file(const PSOGCCharacterFile::Character& src);
static std::shared_ptr<PSOBBCharacterFile> create_from_file(const PSOGCEp3CharacterFile::Character& src);
static std::shared_ptr<PSOBBCharacterFile> create_from_file(const PSOXBCharacterFileCharacter& src);
static std::shared_ptr<PSOBBCharacterFile> create_from_file(const PSOXBCharacterFile::Character& src);
PSODCNTECharacterFile::Character as_dc_nte(uint64_t hardware_id) const;
PSODC112000CharacterFile::Character as_11_2000(uint64_t hardware_id) const;
@@ -825,7 +896,7 @@ struct PSOBBCharacterFile {
operator PSOGCNTECharacterFileCharacter() const;
operator PSOGCCharacterFile::Character() const;
operator PSOGCEp3CharacterFile::Character() const;
operator PSOXBCharacterFileCharacter() const;
operator PSOXBCharacterFile::Character() const;
void add_item(const ItemData& item, const ItemData::StackLimits& limits);
ItemData remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits);
@@ -947,6 +1018,14 @@ struct PSOGCGuildCardFile {
/* E28C */
} __packed_ws__(PSOGCGuildCardFile, 0xE28C);
struct PSOXBGuildCardFile {
/* 00000 */ le_uint32_t checksum = 0;
/* 00004 */ parray<GuildCardXB, 100> entries;
/* 0D934 */ parray<GuildCardXB, 0x1C> blocked_senders;
/* 11604 */ le_uint32_t creation_timestamp = 0;
/* 11608 */ le_uint32_t round2_seed = 0;
} __packed_ws__(PSOXBGuildCardFile, 0x1160C);
struct PSOBBGuildCardFile {
struct Entry {
/* 0000 */ GuildCardBB data;
@@ -50,4 +50,4 @@ get_data_ptr:
data:
.data <VERS 0x0062D844 0x0062DDE4 0x0063591C 0x00632E04 0x0063269C 0x00632E04 0x0063319C> # char_file_part1
.data <VERS 0x0062D8E8 0x0062DE88 0x006359C0 0x00632EA8 0x00632740 0x00632EA8 0x00633240> # char_file_part2
# Server adds a PSOXBCharacterFileCharacter here
# Server adds a PSOXBCharacterFile::Character here