diff --git a/README.md b/README.md index c7d32cfa..a7a75062 100644 --- a/README.md +++ b/README.md @@ -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` | diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 903f4e84..e2a8dd8b 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -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); diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 60988865..2c1859d4 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -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 diff --git a/src/Main.cc b/src/Main.cc index 5b1c4e5a..87f2b76e 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -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(0) == "decrypt-gci-save"); - bool skip_checksum = args.get("skip-checksum"); - string seed = args.get("seed"); - string system_filename = args.get("sys"); - int64_t override_round2_seed = args.get("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(); - header.check(); - const auto& system = r.get(); - 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(); - header.check(); - - size_t data_start_offset = r.where(); - - auto process_file = [&]() { - if (is_decrypt) { - const void* data_section = r.getv(header.data_size); - auto decrypted = decrypt_fixed_size_data_section_t( - data_section, header.data_size, round1_seed, skip_checksum, override_round2_seed); - *reinterpret_cast(data.data() + data_start_offset) = decrypted; - } else { - const auto& s = r.get(); - auto encrypted = encrypt_fixed_size_data_section_t(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()(); - } else if (header.is_ep12() && (header.data_size == sizeof(PSOGCCharacterFile))) { - process_file.template operator()(); - } else if (header.is_ep3() && (header.data_size == sizeof(PSOGCEp3CharacterFile))) { - auto* charfile = reinterpret_cast(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()); - } - } - process_file.template operator()(); - 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(0) == "decrypt-pc-save"); bool skip_checksum = args.get("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(0) == "decrypt-gci-save"); + bool skip_checksum = args.get("skip-checksum"); + string seed = args.get("seed"); + string system_filename = args.get("sys"); + int64_t override_round2_seed = args.get("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(); + header.check(); + const auto& system = r.get(); + 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(); + header.check(); + + size_t data_start_offset = r.where(); + + auto process_file = [&]() { + if (is_decrypt) { + const void* data_section = r.getv(header.data_size); + auto decrypted = decrypt_fixed_size_data_section_t( + data_section, header.data_size, round1_seed, skip_checksum, override_round2_seed); + *reinterpret_cast(data.data() + data_start_offset) = decrypted; + } else { + const auto& s = r.get(); + auto encrypted = encrypt_fixed_size_data_section_t(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()(); + } else if (header.is_ep12() && (header.data_size == sizeof(PSOGCCharacterFile))) { + process_file.template operator()(); + } else if (header.is_ep3() && (header.data_size == sizeof(PSOGCEp3CharacterFile))) { + auto* charfile = reinterpret_cast(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()); + } + } + process_file.template operator()(); + 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("skip-checksum"); + string seed = args.get("seed"); + string system_filename = args.get("sys"); + int64_t override_round2_seed = args.get("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(); + header.check(); + const auto& system = r.get(); + 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(); + header.check(); + + size_t data_start_offset = r.where(); + + auto process_file = [&]() { + const void* data_section = r.getv(header.data_size); + auto decrypted = decrypt_fixed_size_data_section_t( + data_section, header.data_size, round1_seed, skip_checksum, override_round2_seed); + *reinterpret_cast(data.data() + data_start_offset) = decrypted; + }; + + if (header.data_size == sizeof(PSOXBGuildCardFile)) { + process_file.template operator()(); + } else if (header.data_size == sizeof(PSOXBCharacterFile)) { + process_file.template operator()(); + } 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); diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 72cd20f8..652551ee 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -3533,7 +3533,7 @@ static asio::awaitable on_30(shared_ptr c, Channel::Message& msg) ch = PSOBBCharacterFile::create_from_file(msg.check_size_t()); break; case Version::XB_V3: - ch = PSOBBCharacterFile::create_from_file(msg.check_size_t()); + ch = PSOBBCharacterFile::create_from_file(msg.check_size_t()); break; case Version::GC_EP3_NTE: case Version::GC_EP3: diff --git a/src/SaveFileFormats.cc b/src/SaveFileFormats.cc index 29bfeaf7..54473db2 100644 --- a/src/SaveFileFormats.cc +++ b/src/SaveFileFormats.cc @@ -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::create_from_file(const PSOGCE return ret; } -shared_ptr PSOBBCharacterFile::create_from_file(const PSOXBCharacterFileCharacter& src) { +shared_ptr 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(language, language); diff --git a/src/SaveFileFormats.hh b/src/SaveFileFormats.hh index 378b09c5..34c6cbb0 100644 --- a/src/SaveFileFormats.hh +++ b/src/SaveFileFormats.hh @@ -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 signature; + /* 0014 */ le_uint32_t source_size = 0; // == total file size - 0x4000 + /* 0018 */ parray unused; // Always blank (zeroes) + /* 4000 */ pstring game_name; + /* 4020 */ pstring file_name; + /* 4040 */ parray banner; // Always blank (zeroes) + /* 5840 */ parray 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 event_flags; + /* 0110 */ parray 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 gamertag; + /* 38 */ + } __packed_ws__(UserEntry, 0x38); + /* 0118 */ parray 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 ppp_username; - /* 0450:0034 */ pstring 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 symbol_chats; - /* 1F78:1B5C */ parray shortcuts; - /* 24B8:209C */ pstring auto_reply; - /* 2518:20FC */ pstring info_board; - // TODO: The following fields are guesses and have not been verified. - /* 2610:21F4 */ PlayerRecordsBattle battle_records; - /* 2628:220C */ parray unknown_a4; - /* 262C:2210 */ PlayerRecordsChallengeV3 challenge_records; - /* 272C:2310 */ parray tech_menu_shortcut_entries; - /* 2754:2338 */ ChoiceSearchConfig choice_search_config; - /* 276C:2350 */ parray unknown_a6; - /* 277C:2360 */ parray quest_counters; - /* 27BC:23A0 */ PlayerRecordsBattle offline_battle_records; - /* 27D4:23B8 */ parray unknown_a7; - struct UnknownA8Entry { - /* 00 */ le_uint32_t unknown_a1 = 0; - /* 04 */ parray unknown_a2; - /* 20 */ parray unknown_a3; - /* 30 */ - } __packed_ws__(UnknownA8Entry, 0x30); - /* 27D8:23BC */ parray 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 ppp_username; + /* 0450:0034 */ pstring 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 symbol_chats; + /* 1F78:1B5C */ parray shortcuts; + /* 24B8:209C */ pstring auto_reply; + /* 2518:20FC */ pstring info_board; + // TODO: The following fields are guesses and have not been verified. + /* 2610:21F4 */ PlayerRecordsBattle battle_records; + /* 2628:220C */ parray unknown_a4; + /* 262C:2210 */ PlayerRecordsChallengeV3 challenge_records; + /* 272C:2310 */ parray tech_menu_shortcut_entries; + /* 2754:2338 */ ChoiceSearchConfig choice_search_config; + /* 276C:2350 */ parray unknown_a6; + /* 277C:2360 */ parray quest_counters; + /* 27BC:23A0 */ PlayerRecordsBattle offline_battle_records; + /* 27D4:23B8 */ parray unknown_a7; + struct UnknownA8Entry { + /* 00 */ le_uint32_t unknown_a1 = 0; + /* 04 */ parray unknown_a2; + /* 20 */ parray unknown_a3; + /* 30 */ + } __packed_ws__(UnknownA8Entry, 0x30); + /* 27D8:23BC */ parray unknown_a8; + /* 28C8:24AC */ + } __packed_ws__(Character, 0x28C8); + + struct CharEntry { + Character character; + parray unknown_a1; + } __packed_ws__(CharEntry, 0x28E0); + + /* 00000 */ le_uint32_t checksum = 0; + /* 00004 */ parray characters; + /* 26524 */ pstring serial_number; + /* 26534 */ pstring access_key; + /* 26544 */ pstring 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 create_from_file(const PSOGCNTECharacterFileCharacter& src); static std::shared_ptr create_from_file(const PSOGCCharacterFile::Character& src); static std::shared_ptr create_from_file(const PSOGCEp3CharacterFile::Character& src); - static std::shared_ptr create_from_file(const PSOXBCharacterFileCharacter& src); + static std::shared_ptr 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 entries; + /* 0D934 */ parray 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; diff --git a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4___.patch.s b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4___.patch.s index 7232790b..7a3b94ef 100644 --- a/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4___.patch.s +++ b/system/client-functions/ExtendedPlayerInfo/SetExtendedPlayerInfo.4___.patch.s @@ -50,4 +50,4 @@ get_data_ptr: data: .data # char_file_part1 .data # char_file_part2 - # Server adds a PSOXBCharacterFileCharacter here + # Server adds a PSOXBCharacterFile::Character here