add xbox disk file formats
This commit is contained in:
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user