diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 8a3a615b..7ffe05dd 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -1476,7 +1476,7 @@ static void server_command_loadchar(shared_ptr c, const std::string& arg } c->load_backup_character(c->login->account->account_id, index); - if (c->version() == Version::GC_V3) { + if ((c->version() == Version::GC_V3) || (c->version() == Version::XB_V3)) { // TODO: Support extended player info on other versions auto s = c->require_server_state(); if (c->config.check_flag(Client::Flag::NO_SEND_FUNCTION_CALL) || @@ -1485,33 +1485,47 @@ static void server_command_loadchar(shared_ptr c, const std::string& arg return; } - auto gc_char = make_shared(c->character()->to_gc()); - prepare_client_for_patches(c, [wc = weak_ptr(c), gc_char]() { - auto c = wc.lock(); - if (!c) { - return; + auto send_set_extended_player_info = [](shared_ptr c, shared_ptr char_file) -> void { + prepare_client_for_patches(c, [wc = weak_ptr(c), char_file]() { + auto c = wc.lock(); + if (!c) { + return; + } + try { + auto s = c->require_server_state(); + auto fn = s->function_code_index->get_patch("SetExtendedPlayerInfo", c->config.specific_version); + send_function_call(c, fn, {}, char_file.get(), sizeof(CharT)); + c->function_call_response_queue.emplace_back([wc = weak_ptr(c)](uint32_t, uint32_t) -> void { + auto c = wc.lock(); + if (!c) { + return; + } + auto l = c->lobby.lock(); + if (l) { + auto s = c->require_server_state(); + send_player_leave_notification(l, c->lobby_client_id); + s->send_lobby_join_notifications(l, c); + } + }); + } catch (const exception& e) { + c->log.warning("Failed to set extended player info: %s", e.what()); + send_text_message_printf(c, "Failed to set\nplayer info:\n%s", e.what()); + } + }); + }; + + if (c->version() == Version::GC_V3) { + auto gc_char = make_shared(c->character()->to_gc()); + send_set_extended_player_info.operator()(c, gc_char); + } else if (c->version() == Version::XB_V3) { + if (!c->login || !c->login->xb_license) { + throw runtime_error("XB client is not logged in"); } - try { - auto s = c->require_server_state(); - auto fn = s->function_code_index->get_patch("SetExtendedPlayerInfo", c->config.specific_version); - send_function_call(c, fn, {}, gc_char.get(), sizeof(PSOGCCharacterFile::Character)); - c->function_call_response_queue.emplace_back([wc = weak_ptr(c)](uint32_t, uint32_t) -> void { - auto c = wc.lock(); - if (!c) { - return; - } - auto l = c->lobby.lock(); - if (l) { - auto s = c->require_server_state(); - send_player_leave_notification(l, c->lobby_client_id); - s->send_lobby_join_notifications(l, c); - } - }); - } catch (const exception& e) { - c->log.warning("Failed to set extended player info: %s", e.what()); - send_text_message_printf(c, "Failed to set\nplayer info:\n%s", e.what()); - } - }); + auto xb_char = make_shared(c->character()->to_xb(c->login->xb_license->user_id)); + send_set_extended_player_info.operator()(c, xb_char); + } else { + throw logic_error("unimplemented extended player info version"); + } } else { // On v1 and v2, the client will assign its character data from the lobby diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 73c9d85e..b0c9cf07 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -7193,4 +7193,4 @@ struct G_RejectBattleStartRequest_Ep3_6xB4x53 { // 30 (C->S): Extended player info // Requested with the GetExtendedPlayerInfo patch. -// Format is PSOGCCharacterFile::Character. +// Format is PSOGCCharacterFile::Character or PSOXBCharacterFileCharacter. diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 4ba5bfef..0b9d5838 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -3269,11 +3269,12 @@ static void on_30(shared_ptr c, uint16_t, uint32_t, string& data) { shared_ptr bb_char; switch (c->version()) { - case Version::GC_V3: { - auto gc_char = check_size_t(data); - bb_char = PSOBBCharacterFile::create_from_gc(gc_char); + case Version::GC_V3: + bb_char = PSOBBCharacterFile::create_from_gc(check_size_t(data)); + break; + case Version::XB_V3: + bb_char = PSOBBCharacterFile::create_from_xb(check_size_t(data)); break; - } case Version::DC_NTE: case Version::DC_V1_11_2000_PROTOTYPE: case Version::DC_V1: @@ -3283,7 +3284,6 @@ static void on_30(shared_ptr c, uint16_t, uint32_t, string& data) { case Version::GC_NTE: case Version::GC_EP3_NTE: case Version::GC_EP3: - case Version::XB_V3: case Version::BB_V4: default: throw logic_error("extended player data command not implemented for version"); diff --git a/src/SaveFileFormats.cc b/src/SaveFileFormats.cc index 2efd3835..ec043b2f 100644 --- a/src/SaveFileFormats.cc +++ b/src/SaveFileFormats.cc @@ -459,6 +459,47 @@ shared_ptr PSOBBCharacterFile::create_from_gc(const PSOGCCha return ret; } +shared_ptr PSOBBCharacterFile::create_from_xb(const PSOXBCharacterFileCharacter& xb) { + auto ret = make_shared(); + ret->inventory = xb.inventory; + uint8_t language = ret->inventory.language; + ret->disp = xb.disp.to_bb(language, language); + ret->creation_timestamp = xb.creation_timestamp.load(); + ret->play_time_seconds = xb.play_time_seconds.load(); + ret->option_flags = xb.option_flags.load(); + ret->save_count = xb.save_count.load(); + ret->quest_flags = xb.quest_flags; + ret->death_count = xb.death_count.load(); + ret->bank = xb.bank; + ret->guild_card = xb.guild_card; + for (size_t z = 0; z < std::min(ret->symbol_chats.size(), xb.symbol_chats.size()); z++) { + auto& ret_sc = ret->symbol_chats[z]; + const auto& xb_sc = xb.symbol_chats[z]; + ret_sc.present = xb_sc.present.load(); + ret_sc.name.encode(xb_sc.name.decode(language), language); + ret_sc.spec = xb_sc.spec; + } + for (size_t z = 0; z < std::min(ret->shortcuts.size(), xb.shortcuts.size()); z++) { + ret->shortcuts[z] = xb.shortcuts[z].convert(language); + } + ret->auto_reply.encode(xb.auto_reply.decode(language), language); + ret->info_board.encode(xb.info_board.decode(language), language); + ret->battle_records = xb.battle_records; + ret->unknown_a4 = xb.unknown_a4; + ret->challenge_records = xb.challenge_records; + for (size_t z = 0; z < std::min(ret->tech_menu_shortcut_entries.size(), xb.tech_menu_shortcut_entries.size()); z++) { + ret->tech_menu_shortcut_entries[z] = xb.tech_menu_shortcut_entries[z].load(); + } + ret->choice_search_config = xb.choice_search_config; + ret->unknown_a6 = xb.unknown_a6; + for (size_t z = 0; z < std::min(ret->quest_counters.size(), xb.quest_counters.size()); z++) { + ret->quest_counters[z] = xb.quest_counters[z].load(); + } + ret->offline_battle_records = xb.offline_battle_records; + ret->unknown_a7 = xb.unknown_a7; + return ret; +} + PSOGCCharacterFile::Character PSOBBCharacterFile::to_gc() const { uint8_t language = this->inventory.language; @@ -501,6 +542,50 @@ PSOGCCharacterFile::Character PSOBBCharacterFile::to_gc() const { return ret; } +PSOXBCharacterFileCharacter PSOBBCharacterFile::to_xb(uint64_t xb_user_id) const { + uint8_t language = this->inventory.language; + + PSOXBCharacterFileCharacter ret; + ret.inventory = this->inventory; + ret.disp = this->disp.to_dcpcv3(language, language); + ret.creation_timestamp = this->creation_timestamp.load(); + ret.play_time_seconds = this->play_time_seconds.load(); + ret.option_flags = this->option_flags.load(); + ret.save_count = this->save_count.load(); + ret.quest_flags = this->quest_flags; + ret.death_count = this->death_count.load(); + ret.bank = this->bank; + ret.guild_card = this->guild_card; + ret.guild_card.xb_user_id_high = (xb_user_id >> 32) & 0xFFFFFFFF; + ret.guild_card.xb_user_id_low = xb_user_id & 0xFFFFFFFF; + for (size_t z = 0; z < std::min(ret.symbol_chats.size(), this->symbol_chats.size()); z++) { + auto& ret_sc = ret.symbol_chats[z]; + const auto& gc_sc = this->symbol_chats[z]; + ret_sc.present = gc_sc.present.load(); + ret_sc.name.encode(gc_sc.name.decode(language), language); + ret_sc.spec = gc_sc.spec; + } + for (size_t z = 0; z < std::min(ret.shortcuts.size(), this->shortcuts.size()); z++) { + ret.shortcuts[z] = this->shortcuts[z].convert(language); + } + ret.auto_reply.encode(this->auto_reply.decode(language), language); + ret.info_board.encode(this->info_board.decode(language), language); + ret.battle_records = this->battle_records; + ret.unknown_a4 = this->unknown_a4; + ret.challenge_records = this->challenge_records; + for (size_t z = 0; z < std::min(ret.tech_menu_shortcut_entries.size(), this->tech_menu_shortcut_entries.size()); z++) { + ret.tech_menu_shortcut_entries[z] = this->tech_menu_shortcut_entries[z].load(); + } + ret.choice_search_config = this->choice_search_config; + ret.unknown_a6 = this->unknown_a6; + for (size_t z = 0; z < std::min(ret.quest_counters.size(), this->quest_counters.size()); z++) { + ret.quest_counters[z] = this->quest_counters[z].load(); + } + ret.offline_battle_records = this->offline_battle_records; + ret.unknown_a7 = this->unknown_a7; + return ret; +} + SaveFileSymbolChatEntryBB PSOBBCharacterFile::DefaultSymbolChatEntry::to_entry(uint8_t language) const { SaveFileSymbolChatEntryBB ret; ret.present = 1; diff --git a/src/SaveFileFormats.hh b/src/SaveFileFormats.hh index d3cad45b..0833edac 100644 --- a/src/SaveFileFormats.hh +++ b/src/SaveFileFormats.hh @@ -124,17 +124,19 @@ template struct SaveFileSymbolChatEntryT { using U32T = std::conditional_t; - /* PC:GC:BB */ - /* 00:00:00 */ U32T present; - /* 04:04:04 */ pstring name; - /* 34:1C:2C */ SymbolChatT spec; - /* 70:58:68 */ + /* PC:GC:XB:BB */ + /* 00:00:00:00 */ U32T present; + /* 04:04:04:04 */ pstring name; + /* 34:1C:1C:2C */ SymbolChatT spec; + /* 70:58:58:68 */ } __packed__; using SaveFileSymbolChatEntryPC = SaveFileSymbolChatEntryT; using SaveFileSymbolChatEntryGC = SaveFileSymbolChatEntryT; +using SaveFileSymbolChatEntryXB = SaveFileSymbolChatEntryT; using SaveFileSymbolChatEntryBB = SaveFileSymbolChatEntryT; check_struct_size(SaveFileSymbolChatEntryPC, 0x70); check_struct_size(SaveFileSymbolChatEntryGC, 0x58); +check_struct_size(SaveFileSymbolChatEntryXB, 0x58); check_struct_size(SaveFileSymbolChatEntryBB, 0x68); template @@ -208,8 +210,10 @@ struct SaveFileChatShortcutEntryT { } } __packed__; using SaveFileShortcutEntryGC = SaveFileChatShortcutEntryT; +using SaveFileShortcutEntryXB = SaveFileChatShortcutEntryT; using SaveFileShortcutEntryBB = SaveFileChatShortcutEntryT; check_struct_size(SaveFileShortcutEntryGC, 0x54); +check_struct_size(SaveFileShortcutEntryXB, 0x54); check_struct_size(SaveFileShortcutEntryBB, 0xA4); struct PSOGCCharacterFile { @@ -389,6 +393,49 @@ struct PSOGCSnapshotFile { Image decode_image() const; } __packed_ws__(PSOGCSnapshotFile, 0x1818C); +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 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 = 0; + /* 0434:0018 */ parray unknown_a2; + /* 0450:0034 */ parray unknown_a3; + /* 0460:0044 */ QuestFlags quest_flags; + /* 0660:0244 */ le_uint32_t death_count = 0; + /* 0664:0248 */ PlayerBank 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; + /* 04 */ parray unknown_a2; + /* 20 */ parray unknown_a3; + /* 30 */ + } __packed_ws__(UnknownA8Entry, 0x30); + /* 27D8:23BC */ parray unknown_a8; + /* 28C8:24AC */ +} __packed_ws__(PSOXBCharacterFileCharacter, 0x28C8); + template std::string decrypt_data_section(const void* data_section, size_t size, uint32_t round1_seed, size_t max_decrypt_bytes = 0) { if (max_decrypt_bytes == 0) { @@ -745,6 +792,7 @@ struct PSOBBCharacterFile { const PlayerDispDataBBPreview& preview, std::shared_ptr level_table); static std::shared_ptr create_from_gc(const PSOGCCharacterFile::Character& gc_char); + static std::shared_ptr create_from_xb(const PSOXBCharacterFileCharacter& xb_char); void add_item(const ItemData& item, const ItemData::StackLimits& limits); ItemData remove_item(uint32_t item_id, uint32_t amount, const ItemData::StackLimits& limits); @@ -769,6 +817,7 @@ struct PSOBBCharacterFile { void clear_all_material_usage(); PSOGCCharacterFile::Character to_gc() const; + PSOXBCharacterFileCharacter to_xb(uint64_t xb_user_id) const; } __packed_ws__(PSOBBCharacterFile, 0x2EA4); struct PSOBBGuildCardFile { diff --git a/src/SendCommands.cc b/src/SendCommands.cc index ac4a0fc0..b81781fb 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -2336,37 +2336,29 @@ void send_self_leave_notification(shared_ptr c) { send_command_t(c, 0x69, c->lobby_client_id, cmd); } -static bool send_get_extended_player_info(shared_ptr c) { - // TODO: Support extended player info on other versions. - if (c->version() != Version::GC_V3) { - return false; - } - auto s = c->require_server_state(); - if (c->config.check_flag(Client::Flag::NO_SEND_FUNCTION_CALL) || - c->config.check_flag(Client::Flag::SEND_FUNCTION_CALL_CHECKSUM_ONLY)) { - return false; - } - - prepare_client_for_patches(c, [wc = weak_ptr(c)]() { - auto c = wc.lock(); - if (!c) { - return; - } - try { - auto s = c->require_server_state(); - auto fn = s->function_code_index->get_patch("GetExtendedPlayerInfo", c->config.specific_version); - send_function_call(c, fn); - c->function_call_response_queue.emplace_back(empty_function_call_response_handler); - } catch (const exception& e) { - c->log.warning("Failed to send extended player info request: %s", e.what()); - send_get_player_info(c, false); - } - }); - return true; -} - void send_get_player_info(shared_ptr c, bool request_extended) { - if (!request_extended || !send_get_extended_player_info(c)) { + // TODO: Support extended player info on other versions. + if (request_extended && + !c->config.check_flag(Client::Flag::NO_SEND_FUNCTION_CALL) && + !c->config.check_flag(Client::Flag::SEND_FUNCTION_CALL_CHECKSUM_ONLY) && + ((c->version() == Version::GC_V3) || (c->version() == Version::XB_V3))) { + auto s = c->require_server_state(); + prepare_client_for_patches(c, [wc = weak_ptr(c)]() { + auto c = wc.lock(); + if (!c) { + return; + } + try { + auto s = c->require_server_state(); + auto fn = s->function_code_index->get_patch("GetExtendedPlayerInfo", c->config.specific_version); + send_function_call(c, fn); + c->function_call_response_queue.emplace_back(empty_function_call_response_handler); + } catch (const exception& e) { + c->log.warning("Failed to send extended player info request: %s", e.what()); + send_get_player_info(c, false); + } + }); + } else { send_command(c, (c->version() == Version::DC_NTE) ? 0x8D : 0x95, 0x00); } }