From ddbb922b95b60d596d98545b4a0f14c60810be90 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Sat, 27 Apr 2024 18:14:52 -0700 Subject: [PATCH] support joinable quests on all versions --- src/ChatCommands.cc | 1 + src/Client.cc | 1 + src/Client.hh | 3 + src/CommandFormats.hh | 6 +- src/Quest.cc | 23 +- src/Quest.hh | 3 +- src/ReceiveCommands.cc | 108 ++++- src/ReceiveSubcommands.cc | 724 +++++++++++++++---------------- src/ReceiveSubcommands.hh | 75 ++++ src/SendCommands.cc | 79 +++- src/SendCommands.hh | 17 + system/quests/battle/b88001.json | 11 +- 12 files changed, 655 insertions(+), 396 deletions(-) diff --git a/src/ChatCommands.cc b/src/ChatCommands.cc index 0ecec9bb..189d2835 100644 --- a/src/ChatCommands.cc +++ b/src/ChatCommands.cc @@ -1051,6 +1051,7 @@ static void server_command_playrec(shared_ptr c, const std::string& args } s->change_client_lobby(c, game); c->config.set_flag(Client::Flag::LOADING); + c->log.info("LOADING flag set"); } } else { send_text_message(c, "$C4This command cannot\nbe used in a game"); diff --git a/src/Client.cc b/src/Client.cc index 53f4f394..3cac079d 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -13,6 +13,7 @@ #include "IPStackSimulator.hh" #include "Loggers.hh" +#include "SendCommands.hh" #include "Server.hh" #include "Version.hh" diff --git a/src/Client.hh b/src/Client.hh index 5e4310dd..f1637b23 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -24,6 +24,7 @@ extern const uint64_t CLIENT_CONFIG_MAGIC; class Server; struct Lobby; +class Parsed6x70Data; class Client : public std::enable_shared_from_this { public: @@ -67,6 +68,7 @@ public: SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE = 0x0040000000000000, // Server-side only SHOULD_SEND_ARTIFICIAL_OBJECT_STATE = 0x0080000000000000, // Server-side only SHOULD_SEND_ARTIFICIAL_FLAG_STATE = 0x0002000000000000, // Server-side only + SHOULD_SEND_ARTIFICIAL_PLAYER_STATES = 0x0200000000000000, // Server-side only SHOULD_SEND_ENABLE_SAVE = 0x0004000000000000, SWITCH_ASSIST_ENABLED = 0x0000000100000000, IS_CLIENT_CUSTOMIZATION = 0x0100000000000000, @@ -241,6 +243,7 @@ public: bool should_update_play_time; std::unordered_set blocked_senders; std::unique_ptr v1_v2_last_reported_disp; + std::shared_ptr last_reported_6x70; // These are null unless the client is within the trade sequence (D0-D4 or EE commands) std::unique_ptr pending_item_trade; std::unique_ptr pending_card_trade; diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 0b8e8dfe..394cc4c9 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -4974,11 +4974,11 @@ struct G_SyncPlayerDispAndInventory_BB_6x70 { /* 04C8 */ } __packed_ws__(G_SyncPlayerDispAndInventory_BB_6x70, 0x4C0); -// 6x71: Unknown (used while loading into game) +// 6x71: Unblock game join (used while loading into game) -struct G_Unknown_6x71 { +struct G_UnblockGameJoin_6x71 { G_UnusedHeader header; -} __packed_ws__(G_Unknown_6x71, 4); +} __packed_ws__(G_UnblockGameJoin_6x71, 4); // 6x72: Player done loading into game diff --git a/src/Quest.cc b/src/Quest.cc index b0fcf362..30d4dba5 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -205,7 +205,8 @@ VersionedQuest::VersionedQuest( std::shared_ptr battle_rules, ssize_t challenge_template_index, std::shared_ptr available_expression, - std::shared_ptr enabled_expression) + std::shared_ptr enabled_expression, + bool force_joinable) : quest_number(quest_number), category_id(category_id), episode(Episode::NONE), @@ -233,7 +234,7 @@ VersionedQuest::VersionedQuest( throw invalid_argument("file is too small for header"); } auto* header = reinterpret_cast(bin_decompressed.data()); - this->joinable = false; + this->joinable = force_joinable; this->episode = Episode::EP1; if (this->quest_number == 0xFFFFFFFF) { this->quest_number = fnv1a32(header, sizeof(header)) & 0xFFFF; @@ -249,7 +250,7 @@ VersionedQuest::VersionedQuest( throw invalid_argument("file is too small for header"); } auto* header = reinterpret_cast(bin_decompressed.data()); - this->joinable = false; + this->joinable = force_joinable; this->episode = Episode::EP1; if (this->quest_number == 0xFFFFFFFF) { this->quest_number = header->quest_number; @@ -266,7 +267,7 @@ VersionedQuest::VersionedQuest( throw invalid_argument("file is too small for header"); } auto* header = reinterpret_cast(bin_decompressed.data()); - this->joinable = false; + this->joinable = force_joinable; this->episode = Episode::EP1; if (this->quest_number == 0xFFFFFFFF) { this->quest_number = header->quest_number; @@ -289,7 +290,7 @@ VersionedQuest::VersionedQuest( throw invalid_argument("file is incorrect size"); } auto* map = reinterpret_cast(bin_decompressed.data()); - this->joinable = false; + this->joinable = force_joinable; this->episode = Episode::EP3; if (this->quest_number == 0xFFFFFFFF) { this->quest_number = map->map_number; @@ -307,7 +308,7 @@ VersionedQuest::VersionedQuest( throw invalid_argument("file is too small for header"); } auto* header = reinterpret_cast(bin_decompressed.data()); - this->joinable = false; + this->joinable = force_joinable; this->episode = find_quest_episode_from_script(bin_decompressed.data(), bin_decompressed.size(), this->version); if (this->quest_number == 0xFFFFFFFF) { this->quest_number = header->quest_number; @@ -323,7 +324,7 @@ VersionedQuest::VersionedQuest( throw invalid_argument("file is too small for header"); } auto* header = reinterpret_cast(bin_decompressed.data()); - this->joinable = header->joinable; + this->joinable = header->joinable || force_joinable; this->episode = find_quest_episode_from_script(bin_decompressed.data(), bin_decompressed.size(), this->version); if (this->quest_number == 0xFFFFFFFF) { this->quest_number = header->quest_number; @@ -674,6 +675,7 @@ QuestIndex::QuestIndex( ssize_t challenge_template_index = -1; shared_ptr available_expression; shared_ptr enabled_expression; + bool force_joinable = false; try { json_filedata = &json_files.at(basename); } catch (const out_of_range&) { @@ -704,6 +706,10 @@ QuestIndex::QuestIndex( enabled_expression = make_shared(metadata_json.get_string("EnabledIf")); } catch (const out_of_range&) { } + try { + force_joinable = metadata_json.get_bool("Joinable"); + } catch (const out_of_range&) { + } } auto vq = make_shared( @@ -717,7 +723,8 @@ QuestIndex::QuestIndex( battle_rules, challenge_template_index, available_expression, - enabled_expression); + enabled_expression, + force_joinable); auto category_name = this->category_index->at(vq->category_id)->name; string filenames_str = bin_filedata->filename; diff --git a/src/Quest.hh b/src/Quest.hh index 42df7f24..724711af 100644 --- a/src/Quest.hh +++ b/src/Quest.hh @@ -90,7 +90,8 @@ struct VersionedQuest { std::shared_ptr battle_rules = nullptr, ssize_t challenge_template_index = -1, std::shared_ptr available_expression = nullptr, - std::shared_ptr enabled_expression = nullptr); + std::shared_ptr enabled_expression = nullptr, + bool force_joinable = false); std::string bin_filename() const; std::string dat_filename() const; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 0b299aeb..80a5c261 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -465,6 +465,20 @@ static void on_1D(shared_ptr c, uint16_t, uint32_t, string&) { c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE); send_game_flag_state(c); // 6x6F } + if (c->config.check_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_PLAYER_STATES)) { + c->config.clear_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_PLAYER_STATES); + auto l = c->require_lobby(); + if (l->is_game()) { + for (auto lc : l->clients) { + // If we haven't received a 6x70 from this client, maybe they're VERY + // far behind and wil lstll send it, or maybe they'll time out and be + // disconnected soon - either way, we shouldn't fail if it's missing. + if (lc && (lc != c) && lc->last_reported_6x70) { + send_game_player_state(c, lc, true); // 6x70 + } + } + } + } } static void on_05_XB(shared_ptr c, uint16_t, uint32_t, string&) { @@ -1229,6 +1243,7 @@ static bool add_next_game_client(shared_ptr l) { s->change_client_lobby(c, l, true, target_client_id); c->config.set_flag(Client::Flag::LOADING); + c->log.info("LOADING flag set"); if (tourn) { c->config.set_flag(Client::Flag::LOADING_TOURNAMENT); } @@ -2045,6 +2060,7 @@ void set_lobby_quest(shared_ptr l, shared_ptr q, bool substi if (use_loading_flag) { lc->config.set_flag(Client::Flag::LOADING_QUEST); + lc->log.info("LOADING_QUEST flag set"); lc->disconnect_hooks.emplace(QUEST_BARRIER_DISCONNECT_HOOK_NAME, [l]() -> void { send_quest_barrier_if_all_clients_ready(l); }); @@ -2385,6 +2401,8 @@ static void on_10(shared_ptr c, uint16_t, uint32_t, string& data) { } if (game->is_game()) { c->config.set_flag(Client::Flag::LOADING); + c->log.info("LOADING flag set"); + // If no one was in the game before, then there's no leader to send // the game state - send it to the joining player (who is now the // leader) @@ -2834,30 +2852,49 @@ static void on_A2(shared_ptr c, uint16_t, uint32_t flag, string& data) { } } +static void on_joinable_quest_loaded(shared_ptr c) { + auto l = c->require_lobby(); + if (!l->is_game() || !l->quest) { + throw runtime_error("joinable quest load completed in non-game"); + } + auto leader_c = l->clients.at(l->leader_id); + if (!leader_c) { + throw logic_error("lobby leader is missing"); + } + + // On BB, ask the leader to send the quest state to the joining player (and + // we'll need to use the game join command queue to avoid any item ID races). + // On other versions, the server will have to generate the state commands; + // this happens when the response to the ping (1D) is received, so we don't + // need the game join command queue in that case. + if (leader_c->version() == Version::BB_V4) { + send_command(leader_c, 0xDD, c->lobby_client_id); + c->log.info("Creating game join command queue"); + c->game_join_command_queue = make_unique>(); + } else { + c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ITEM_STATE); + c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE); + c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_OBJECT_STATE); + c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_FLAG_STATE); + c->config.set_flag(Client::Flag::SHOULD_SEND_ARTIFICIAL_PLAYER_STATES); + } + send_command(c, 0x1D, 0x00); + + if (!is_v1_or_v2(c->version())) { + send_command(c, 0xAC, 0x00); + } +} + static void on_AC_V3_BB(shared_ptr c, uint16_t, uint32_t, string& data) { check_size_v(data.size(), 0); - auto l = c->require_lobby(); - if (c->config.check_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST)) { - if (l->base_version != Version::BB_V4) { - throw logic_error("joinable quest started on non-BB version"); - } - - auto leader_c = l->clients.at(l->leader_id); - if (!leader_c) { - throw logic_error("lobby leader is missing"); - } - - send_command(leader_c, 0xDD, c->lobby_client_id); - send_command(c, 0xAC, 0x00); - - c->log.info("Creating game join command queue"); - c->game_join_command_queue = make_unique>(); - send_command(c, 0x1D, 0x00); + on_joinable_quest_loaded(c); } else if (c->config.check_flag(Client::Flag::LOADING_QUEST)) { c->config.clear_flag(Client::Flag::LOADING_QUEST); + c->log.info("LOADING_QUEST flag cleared"); + auto l = c->require_lobby(); if (l->quest && send_quest_barrier_if_all_clients_ready(l)) { on_quest_loaded(l); } @@ -4343,6 +4380,7 @@ static void on_C1_PC(shared_ptr c, uint16_t, uint32_t, string& data) { if (game) { s->change_client_lobby(c, game); c->config.set_flag(Client::Flag::LOADING); + c->log.info("LOADING flag set"); } } @@ -4415,6 +4453,7 @@ static void on_0C_C1_E7_EC(shared_ptr c, uint16_t command, uint32_t, str if (game) { s->change_client_lobby(c, game); c->config.set_flag(Client::Flag::LOADING); + c->log.info("LOADING flag set"); // There is a bug in DC NTE and 11/2000 that causes them to assign item IDs // twice when joining a game. If there are other players in the game, this @@ -4472,6 +4511,7 @@ static void on_C1_BB(shared_ptr c, uint16_t, uint32_t, string& data) { if (game) { s->change_client_lobby(c, game); c->config.set_flag(Client::Flag::LOADING); + c->log.info("LOADING flag set"); } } @@ -4501,6 +4541,7 @@ static void on_6F(shared_ptr c, uint16_t command, uint32_t, string& data // don't matter for Ep3) if (c->config.check_flag(Client::Flag::LOADING)) { c->config.clear_flag(Client::Flag::LOADING); + c->log.info("LOADING flag cleared"); // The client sends 6F when it has created its TObjPlayer and assigned its // item IDs. For the leader, however, this happens before any inbound commands @@ -4533,6 +4574,9 @@ static void on_6F(shared_ptr c, uint16_t command, uint32_t, string& data send_update_team_reward_flags(c); send_all_nearby_team_metadatas_to_client(c, false); + // On BB, send the joinable quest file as soon as the client is ready (6F). + // On other versions, we send joinable quests in the 99 handler instead, + // since we need to wait for the client's save to complete. // BB sends 016F when the client is done loading a quest. In that case, we // shouldn't send the quest to them again! if ((command == 0x006F) && l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS)) { @@ -4549,10 +4593,12 @@ static void on_6F(shared_ptr c, uint16_t command, uint32_t, string& data send_open_quest_file(c, bin_filename, bin_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->bin_contents); send_open_quest_file(c, dat_filename, dat_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->dat_contents); c->config.set_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST); + c->log.info("LOADING_RUNNING_JOINABLE_QUEST flag set"); should_resume_game = false; } else if ((command == 0x016F) && c->config.check_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST)) { c->config.clear_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST); + c->log.info("LOADING_RUNNING_JOINABLE_QUEST flag cleared"); } if (l->map) { send_rare_enemy_index_list(c, l->map->rare_enemy_indexes); @@ -4616,6 +4662,34 @@ static void on_99(shared_ptr c, uint16_t, uint32_t, string& data) { } else if (c->should_send_to_proxy_server) { send_client_to_proxy_server(c); } + + // See the comment in on_6F about why we do this here, but only for non-BB + // versions. + if (l && l->check_flag(Lobby::Flag::JOINABLE_QUEST_IN_PROGRESS) && (c->version() != Version::BB_V4)) { + if (!l->quest) { + throw runtime_error("JOINABLE_QUEST_IN_PROGRESS is set, but lobby has no quest"); + } + auto vq = l->quest->version(c->version(), c->language()); + if (!vq) { + throw runtime_error("JOINABLE_QUEST_IN_PROGRESS is set, but lobby has no quest for client version"); + } + string bin_filename = vq->bin_filename(); + string dat_filename = vq->dat_filename(); + + send_open_quest_file(c, bin_filename, bin_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->bin_contents); + send_open_quest_file(c, dat_filename, dat_filename, "", vq->quest_number, QuestFileType::ONLINE, vq->dat_contents); + c->config.set_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST); + c->log.info("LOADING_RUNNING_JOINABLE_QUEST flag set"); + + // On v1 and v2, there is no confirmation when the client is done + // downloading the quest file, so just set the in-quest state immediately. + // On v3 and later, we do this when we receive the AC command. + // TODO: This might not work for GC NTE, since we wait for file chunk + // confirmations (13 commands) but there is no AC command. + if (is_v1_or_v2(c->version())) { + on_joinable_quest_loaded(c); + } + } } static void on_D0_V3_BB(shared_ptr c, uint16_t, uint32_t, string& data) { diff --git a/src/ReceiveSubcommands.cc b/src/ReceiveSubcommands.cc index e171e604..bd59f469 100644 --- a/src/ReceiveSubcommands.cc +++ b/src/ReceiveSubcommands.cc @@ -27,7 +27,6 @@ struct SubcommandDefinition { enum Flag { ALWAYS_FORWARD_TO_WATCHERS = 0x01, ALLOW_FORWARD_TO_WATCHED_LOBBY = 0x02, - USE_JOIN_COMMAND_QUEUE = 0x04, }; uint8_t nte_subcommand; uint8_t proto_subcommand; @@ -189,7 +188,7 @@ static void forward_subcommand(shared_ptr c, uint8_t command, uint8_t fl if ((command == 0xCB) && (lc->version() == Version::GC_EP3_NTE)) { command = 0xC9; } - if ((def_flags & SDF::USE_JOIN_COMMAND_QUEUE) && lc->game_join_command_queue) { + if (lc->game_join_command_queue) { lc->log.info("Client not ready to receive join commands; adding to queue"); auto& cmd = lc->game_join_command_queue->emplace_back(); cmd.command = command; @@ -652,316 +651,350 @@ static void on_sync_joining_player_quest_flags(shared_ptr c, uint8_t com } } -class Parsed6x70Data { -public: - G_SyncPlayerDispAndInventory_BaseDCNTE base; - uint32_t unknown_a5_nte = 0; - uint32_t unknown_a6_nte = 0; - uint16_t bonus_hp_from_materials = 0; - uint16_t bonus_tp_from_materials = 0; - parray unknown_a5_112000; - parray unknown_a4_final; - uint32_t language = 0; - uint32_t player_tag = 0; - uint32_t guild_card_number = 0; - uint32_t unknown_a6 = 0; - uint32_t battle_team_number = 0; - Telepipe telepipe; - uint32_t unknown_a8 = 0; - parray unknown_a9_nte_112000; - G_Unknown_6x70_SubA1 unknown_a9_final; - uint32_t area = 0; - uint32_t flags2 = 0; - parray technique_levels_v1 = 0xFF; - PlayerVisualConfig visual; - std::string name; - PlayerStats stats; - uint32_t num_items = 0; - parray items; - uint32_t floor = 0; - uint64_t xb_user_id = 0; - uint32_t xb_unknown_a16 = 0; - - Parsed6x70Data(const G_SyncPlayerDispAndInventory_DCNTE_6x70& cmd, uint32_t guild_card_number) - : base(cmd.base), - unknown_a5_nte(cmd.unknown_a5), - unknown_a6_nte(cmd.unknown_a6), - bonus_hp_from_materials(0), - bonus_tp_from_materials(0), - language(0), - player_tag(0x00010000), - guild_card_number(guild_card_number), - unknown_a6(0), - battle_team_number(0), - telepipe(cmd.telepipe), - unknown_a8(cmd.unknown_a8), - unknown_a9_nte_112000(cmd.unknown_a9), - area(cmd.area), - flags2(cmd.flags2), - visual(cmd.visual), - stats(cmd.stats), - num_items(cmd.num_items), - items(cmd.items), - floor(cmd.area), - xb_user_id(this->default_xb_user_id()), - xb_unknown_a16(0) { - this->name = this->visual.name.decode(this->language); +static void transcode_inventory_items( + parray& items, + size_t num_items, + Version from_version, + Version to_version, + shared_ptr to_item_parameter_table) { + if (num_items > 30) { + throw runtime_error("invalid inventory item count"); } - - Parsed6x70Data(const G_SyncPlayerDispAndInventory_DC112000_6x70& cmd, uint32_t guild_card_number, uint8_t language) - : base(cmd.base), - unknown_a5_nte(0), - unknown_a6_nte(0), - bonus_hp_from_materials(cmd.bonus_hp_from_materials), - bonus_tp_from_materials(cmd.bonus_tp_from_materials), - unknown_a5_112000(cmd.unknown_a5), - language(language), - player_tag(0x00010000), - guild_card_number(guild_card_number), - unknown_a6(0), - battle_team_number(0), - telepipe(cmd.telepipe), - unknown_a8(cmd.unknown_a8), - unknown_a9_nte_112000(cmd.unknown_a9), - area(cmd.area), - flags2(cmd.flags2), - visual(cmd.visual), - stats(cmd.stats), - num_items(cmd.num_items), - items(cmd.items), - floor(cmd.area), - xb_user_id(this->default_xb_user_id()), - xb_unknown_a16(0) { - this->name = this->visual.name.decode(this->language); - } - - Parsed6x70Data(const G_SyncPlayerDispAndInventory_DC_PC_6x70& cmd, uint32_t guild_card_number) - : Parsed6x70Data(cmd.base, guild_card_number) { - this->stats = cmd.stats; - this->num_items = cmd.num_items; - this->items = cmd.items; - this->floor = cmd.base.area; - this->xb_user_id = this->default_xb_user_id(); - this->xb_unknown_a16 = 0; - this->name = this->visual.name.decode(this->language); - } - - Parsed6x70Data(const G_SyncPlayerDispAndInventory_GC_6x70& cmd, uint32_t guild_card_number) - : Parsed6x70Data(cmd.base, guild_card_number) { - this->stats = cmd.stats; - this->num_items = cmd.num_items; - this->items = cmd.items; - this->floor = cmd.floor; - this->xb_user_id = this->default_xb_user_id(); - this->xb_unknown_a16 = 0; - this->name = this->visual.name.decode(this->language); - } - - Parsed6x70Data(const G_SyncPlayerDispAndInventory_XB_6x70& cmd, uint32_t guild_card_number) - : Parsed6x70Data(cmd.base, guild_card_number) { - this->stats = cmd.stats; - this->num_items = cmd.num_items; - this->items = cmd.items; - this->floor = cmd.floor; - this->xb_user_id = (static_cast(cmd.xb_user_id_high) << 32) | cmd.xb_user_id_low; - this->xb_unknown_a16 = cmd.unknown_a16; - this->name = this->visual.name.decode(this->language); - } - - Parsed6x70Data(const G_SyncPlayerDispAndInventory_BB_6x70& cmd, uint32_t guild_card_number) - : Parsed6x70Data(cmd.base, guild_card_number) { - this->stats = cmd.stats; - this->num_items = cmd.num_items; - this->items = cmd.items; - this->floor = cmd.floor; - this->xb_user_id = this->default_xb_user_id(); - this->xb_unknown_a16 = cmd.unknown_a16; - this->name = cmd.name.decode(cmd.base.language); - this->visual.name.encode(this->name, cmd.base.language); - } - - G_SyncPlayerDispAndInventory_DCNTE_6x70 as_dc_nte() const { - G_SyncPlayerDispAndInventory_DCNTE_6x70 ret; - ret.base = this->base; - ret.unknown_a5 = this->unknown_a5_nte; - ret.unknown_a6 = this->unknown_a6; - ret.telepipe = this->telepipe; - ret.unknown_a8 = this->unknown_a8; - ret.unknown_a9 = this->unknown_a9_nte_112000; - ret.area = this->area; - ret.flags2 = this->flags2; - ret.visual = this->visual; - ret.stats = this->stats; - ret.num_items = this->num_items; - ret.items = this->items; - return ret; - } - - G_SyncPlayerDispAndInventory_DC112000_6x70 as_dc_112000() const { - G_SyncPlayerDispAndInventory_DC112000_6x70 ret; - ret.base = this->base; - ret.bonus_hp_from_materials = this->bonus_hp_from_materials; - ret.bonus_tp_from_materials = this->bonus_tp_from_materials; - ret.unknown_a5 = this->unknown_a5_112000; - ret.telepipe = this->telepipe; - ret.unknown_a8 = this->unknown_a8; - ret.unknown_a9 = this->unknown_a9_nte_112000; - ret.area = this->area; - ret.flags2 = this->flags2; - ret.visual = this->visual; - ret.stats = this->stats; - ret.num_items = this->num_items; - ret.items = this->items; - return ret; - } - - G_SyncPlayerDispAndInventory_DC_PC_6x70 as_dc_pc() const { - G_SyncPlayerDispAndInventory_DC_PC_6x70 ret; - ret.base = this->base_v1(); - ret.stats = this->stats; - ret.num_items = this->num_items; - ret.items = this->items; - return ret; - } - - G_SyncPlayerDispAndInventory_GC_6x70 as_gc() const { - G_SyncPlayerDispAndInventory_GC_6x70 ret; - ret.base = this->base_v1(); - ret.stats = this->stats; - ret.num_items = this->num_items; - ret.items = this->items; - ret.floor = this->floor; - return ret; - } - - G_SyncPlayerDispAndInventory_XB_6x70 as_xb() const { - G_SyncPlayerDispAndInventory_XB_6x70 ret; - ret.base = this->base_v1(); - ret.stats = this->stats; - ret.num_items = this->num_items; - ret.items = this->items; - ret.floor = this->floor; - ret.xb_user_id_high = this->xb_user_id >> 32; - ret.xb_user_id_low = this->xb_user_id; - ret.unknown_a16 = this->xb_unknown_a16; - return ret; - } - - G_SyncPlayerDispAndInventory_BB_6x70 as_bb(uint8_t language) const { - G_SyncPlayerDispAndInventory_BB_6x70 ret; - ret.base = this->base_v1(); - ret.name.encode(this->name, language); - ret.base.visual.name.encode(string_printf("%10" PRId32, this->guild_card_number), language); - ret.stats = this->stats; - ret.num_items = this->num_items; - ret.items = this->items; - ret.floor = this->floor; - ret.xb_user_id_high = this->xb_user_id >> 32; - ret.xb_user_id_low = this->xb_user_id; - ret.unknown_a16 = this->xb_unknown_a16; - return ret; - } - - uint64_t default_xb_user_id() const { - return (0xAE00000000000000 | this->guild_card_number); - } - - void clear_v1_unused_item_fields() { - for (size_t z = 0; z < min(this->num_items, 30); z++) { - auto& item = this->items[z]; - item.unknown_a1 = 0; - item.extension_data1 = 0; - item.extension_data2 = 0; + if (from_version != to_version) { + for (size_t z = 0; z < num_items; z++) { + items[z].data.decode_for_version(from_version); + items[z].data.encode_for_version(to_version, to_item_parameter_table); } } - - void clear_dc_protos_unused_item_fields() { - for (size_t z = 0; z < min(this->num_items, 30); z++) { - auto& item = this->items[z]; - item.unknown_a1 = 0; - item.extension_data1 = 0; - item.extension_data2 = 0; - item.data.data2d = 0; + for (size_t z = num_items; z < 30; z++) { + auto& item = items[z]; + item.present = 0; + item.unknown_a1 = 0; + item.flags = 0; + item.data.clear(); + } + if (is_v1(to_version)) { + for (size_t z = 0; z < 30; z++) { + auto& item = items[z]; + item.extension_data1 = 0x00; + item.extension_data2 = 0x00; + } + } else { + for (size_t z = 20; z < 30; z++) { + items[z].extension_data1 = 0x00; + } + for (size_t z = 16; z < 30; z++) { + items[z].extension_data2 = 0x00; } } +} - void transcode_inventory_items( - Version from_version, - Version to_version, - shared_ptr to_item_parameter_table) { - if (this->num_items > 30) { - throw runtime_error("invalid inventory item count"); - } - if (from_version != to_version) { - for (size_t z = 0; z < this->num_items; z++) { - this->items[z].data.decode_for_version(from_version); - this->items[z].data.encode_for_version(to_version, to_item_parameter_table); - } - } - for (size_t z = this->num_items; z < 30; z++) { - auto& item = this->items[z]; - item.present = 0; - item.unknown_a1 = 0; - item.flags = 0; - item.data.clear(); - } - if (is_v1(to_version)) { - for (size_t z = 0; z < 30; z++) { - auto& item = this->items[z]; - item.extension_data1 = 0x00; - item.extension_data2 = 0x00; - } +Parsed6x70Data::Parsed6x70Data( + const G_SyncPlayerDispAndInventory_DCNTE_6x70& cmd, uint32_t guild_card_number, Version from_version) + : from_version(from_version), + item_version(from_version), + base(cmd.base), + unknown_a5_nte(cmd.unknown_a5), + unknown_a6_nte(cmd.unknown_a6), + bonus_hp_from_materials(0), + bonus_tp_from_materials(0), + language(0), + player_tag(0x00010000), + guild_card_number(guild_card_number), + unknown_a6(0), + battle_team_number(0), + telepipe(cmd.telepipe), + unknown_a8(cmd.unknown_a8), + unknown_a9_nte_112000(cmd.unknown_a9), + area(cmd.area), + flags2(cmd.flags2), + visual(cmd.visual), + stats(cmd.stats), + num_items(cmd.num_items), + items(cmd.items), + floor(cmd.area), + xb_user_id(this->default_xb_user_id()), + xb_unknown_a16(0) { + this->name = this->visual.name.decode(this->language); +} + +Parsed6x70Data::Parsed6x70Data( + const G_SyncPlayerDispAndInventory_DC112000_6x70& cmd, uint32_t guild_card_number, uint8_t language, Version from_version) + : from_version(from_version), + item_version(from_version), + base(cmd.base), + unknown_a5_nte(0), + unknown_a6_nte(0), + bonus_hp_from_materials(cmd.bonus_hp_from_materials), + bonus_tp_from_materials(cmd.bonus_tp_from_materials), + unknown_a5_112000(cmd.unknown_a5), + language(language), + player_tag(0x00010000), + guild_card_number(guild_card_number), + unknown_a6(0), + battle_team_number(0), + telepipe(cmd.telepipe), + unknown_a8(cmd.unknown_a8), + unknown_a9_nte_112000(cmd.unknown_a9), + area(cmd.area), + flags2(cmd.flags2), + visual(cmd.visual), + stats(cmd.stats), + num_items(cmd.num_items), + items(cmd.items), + floor(cmd.area), + xb_user_id(this->default_xb_user_id()), + xb_unknown_a16(0) { + this->name = this->visual.name.decode(this->language); +} + +Parsed6x70Data::Parsed6x70Data( + const G_SyncPlayerDispAndInventory_DC_PC_6x70& cmd, uint32_t guild_card_number, Version from_version) + : Parsed6x70Data(cmd.base, guild_card_number, from_version) { + this->stats = cmd.stats; + this->num_items = cmd.num_items; + this->items = cmd.items; + this->floor = cmd.base.area; + this->xb_user_id = this->default_xb_user_id(); + this->xb_unknown_a16 = 0; + this->name = this->visual.name.decode(this->language); +} + +Parsed6x70Data::Parsed6x70Data( + const G_SyncPlayerDispAndInventory_GC_6x70& cmd, uint32_t guild_card_number, Version from_version) + : Parsed6x70Data(cmd.base, guild_card_number, from_version) { + this->stats = cmd.stats; + this->num_items = cmd.num_items; + this->items = cmd.items; + this->floor = cmd.floor; + this->xb_user_id = this->default_xb_user_id(); + this->xb_unknown_a16 = 0; + this->name = this->visual.name.decode(this->language); +} + +Parsed6x70Data::Parsed6x70Data( + const G_SyncPlayerDispAndInventory_XB_6x70& cmd, uint32_t guild_card_number, Version from_version) + : Parsed6x70Data(cmd.base, guild_card_number, from_version) { + this->stats = cmd.stats; + this->num_items = cmd.num_items; + this->items = cmd.items; + this->floor = cmd.floor; + this->xb_user_id = (static_cast(cmd.xb_user_id_high) << 32) | cmd.xb_user_id_low; + this->xb_unknown_a16 = cmd.unknown_a16; + this->name = this->visual.name.decode(this->language); +} + +Parsed6x70Data::Parsed6x70Data( + const G_SyncPlayerDispAndInventory_BB_6x70& cmd, uint32_t guild_card_number, Version from_version) + : Parsed6x70Data(cmd.base, guild_card_number, from_version) { + this->stats = cmd.stats; + this->num_items = cmd.num_items; + this->items = cmd.items; + this->floor = cmd.floor; + this->xb_user_id = this->default_xb_user_id(); + this->xb_unknown_a16 = cmd.unknown_a16; + this->name = cmd.name.decode(cmd.base.language); + this->visual.name.encode(this->name, cmd.base.language); +} + +G_SyncPlayerDispAndInventory_DCNTE_6x70 Parsed6x70Data::as_dc_nte(shared_ptr s) const { + G_SyncPlayerDispAndInventory_DCNTE_6x70 ret; + ret.base = this->base; + ret.unknown_a5 = this->unknown_a5_nte; + ret.unknown_a6 = this->unknown_a6; + ret.telepipe = this->telepipe; + ret.unknown_a8 = this->unknown_a8; + ret.unknown_a9 = this->unknown_a9_nte_112000; + ret.area = this->area; + ret.flags2 = this->flags2; + ret.visual = this->visual; + ret.stats = this->stats; + ret.num_items = this->num_items; + ret.items = this->items; + + transcode_inventory_items( + ret.items, ret.num_items, this->item_version, Version::DC_NTE, s->item_parameter_table_for_encode(Version::DC_NTE)); + ret.visual.enforce_lobby_join_limits_for_version(Version::DC_NTE); + if (s->version_name_colors) { + ret.visual.name_color = s->name_color_for_version(this->from_version); + ret.visual.compute_name_color_checksum(); + } + return ret; +} + +G_SyncPlayerDispAndInventory_DC112000_6x70 Parsed6x70Data::as_dc_112000(shared_ptr s) const { + G_SyncPlayerDispAndInventory_DC112000_6x70 ret; + ret.base = this->base; + ret.bonus_hp_from_materials = this->bonus_hp_from_materials; + ret.bonus_tp_from_materials = this->bonus_tp_from_materials; + ret.unknown_a5 = this->unknown_a5_112000; + ret.telepipe = this->telepipe; + ret.unknown_a8 = this->unknown_a8; + ret.unknown_a9 = this->unknown_a9_nte_112000; + ret.area = this->area; + ret.flags2 = this->flags2; + ret.visual = this->visual; + ret.stats = this->stats; + ret.num_items = this->num_items; + ret.items = this->items; + + transcode_inventory_items( + ret.items, ret.num_items, this->item_version, Version::DC_V1_11_2000_PROTOTYPE, s->item_parameter_table_for_encode(Version::DC_V1_11_2000_PROTOTYPE)); + ret.visual.enforce_lobby_join_limits_for_version(Version::DC_V1_11_2000_PROTOTYPE); + if (s->version_name_colors) { + ret.visual.name_color = s->name_color_for_version(this->from_version); + ret.visual.compute_name_color_checksum(); + } + return ret; +} + +G_SyncPlayerDispAndInventory_DC_PC_6x70 Parsed6x70Data::as_dc_pc(shared_ptr s, Version to_version) const { + G_SyncPlayerDispAndInventory_DC_PC_6x70 ret; + ret.base = this->base_v1(); + ret.stats = this->stats; + ret.num_items = this->num_items; + ret.items = this->items; + + transcode_inventory_items( + ret.items, ret.num_items, this->item_version, to_version, s->item_parameter_table_for_encode(to_version)); + ret.base.visual.enforce_lobby_join_limits_for_version(to_version); + if (s->version_name_colors) { + ret.base.visual.name_color = s->name_color_for_version(this->from_version); + ret.base.visual.compute_name_color_checksum(); + } + return ret; +} + +G_SyncPlayerDispAndInventory_GC_6x70 Parsed6x70Data::as_gc_gcnte(shared_ptr s, Version to_version) const { + G_SyncPlayerDispAndInventory_GC_6x70 ret; + ret.base = this->base_v1(); + ret.stats = this->stats; + ret.num_items = this->num_items; + ret.items = this->items; + ret.floor = this->floor; + + transcode_inventory_items( + ret.items, ret.num_items, this->item_version, to_version, s->item_parameter_table_for_encode(to_version)); + ret.base.visual.enforce_lobby_join_limits_for_version(to_version); + if (s->version_name_colors) { + ret.base.visual.name_color = s->name_color_for_version(this->from_version); + if (is_v1_or_v2(to_version)) { + ret.base.visual.compute_name_color_checksum(); } else { - for (size_t z = 20; z < 30; z++) { - this->items[z].extension_data1 = 0x00; - } - for (size_t z = 16; z < 30; z++) { - this->items[z].extension_data2 = 0x00; - } + ret.base.visual.name_color_checksum = 0; } } + return ret; +} -protected: - Parsed6x70Data(const G_SyncPlayerDispAndInventory_BaseV1& base, uint32_t guild_card_number) { - this->base = base.base; - this->bonus_hp_from_materials = base.bonus_hp_from_materials; - this->bonus_tp_from_materials = base.bonus_tp_from_materials; - this->unknown_a4_final = base.unknown_a4; - this->language = base.language; - this->player_tag = base.player_tag; - this->guild_card_number = guild_card_number; // Ignore the client's GC# - this->unknown_a6 = base.unknown_a6; - this->battle_team_number = base.battle_team_number; - this->telepipe = base.telepipe; - this->unknown_a8 = base.unknown_a8; - this->unknown_a9_final = base.unknown_a9; - this->area = base.area; - this->flags2 = base.flags2; - this->technique_levels_v1 = base.technique_levels_v1; - this->visual = base.visual; - } +G_SyncPlayerDispAndInventory_XB_6x70 Parsed6x70Data::as_xb(shared_ptr s) const { + G_SyncPlayerDispAndInventory_XB_6x70 ret; + ret.base = this->base_v1(); + ret.stats = this->stats; + ret.num_items = this->num_items; + ret.items = this->items; + ret.floor = this->floor; + ret.xb_user_id_high = this->xb_user_id >> 32; + ret.xb_user_id_low = this->xb_user_id; + ret.unknown_a16 = this->xb_unknown_a16; - G_SyncPlayerDispAndInventory_BaseV1 base_v1() const { - G_SyncPlayerDispAndInventory_BaseV1 ret; - ret.base = this->base; - ret.bonus_hp_from_materials = this->bonus_hp_from_materials; - ret.bonus_tp_from_materials = this->bonus_tp_from_materials; - ret.unknown_a4 = this->unknown_a4_final; - ret.language = this->language; - ret.player_tag = this->player_tag; - ret.guild_card_number = this->guild_card_number; - ret.unknown_a6 = this->unknown_a6; - ret.battle_team_number = this->battle_team_number; - ret.telepipe = this->telepipe; - ret.unknown_a8 = this->unknown_a8; - ret.unknown_a9 = this->unknown_a9_final; - ret.area = this->area; - ret.flags2 = this->flags2; - ret.technique_levels_v1 = this->technique_levels_v1; - ret.visual = this->visual; - return ret; + transcode_inventory_items( + ret.items, ret.num_items, this->item_version, Version::XB_V3, s->item_parameter_table_for_encode(Version::XB_V3)); + ret.base.visual.enforce_lobby_join_limits_for_version(Version::XB_V3); + if (s->version_name_colors) { + ret.base.visual.name_color = s->name_color_for_version(this->from_version); + ret.base.visual.name_color_checksum = 0; } -}; + return ret; +} + +G_SyncPlayerDispAndInventory_BB_6x70 Parsed6x70Data::as_bb(shared_ptr s, uint8_t language) const { + G_SyncPlayerDispAndInventory_BB_6x70 ret; + ret.base = this->base_v1(); + ret.name.encode(this->name, language); + ret.base.visual.name.encode(string_printf("%10" PRId32, this->guild_card_number), language); + ret.stats = this->stats; + ret.num_items = this->num_items; + ret.items = this->items; + ret.floor = this->floor; + ret.xb_user_id_high = this->xb_user_id >> 32; + ret.xb_user_id_low = this->xb_user_id; + ret.unknown_a16 = this->xb_unknown_a16; + + transcode_inventory_items( + ret.items, ret.num_items, this->item_version, Version::BB_V4, s->item_parameter_table_for_encode(Version::BB_V4)); + ret.base.visual.enforce_lobby_join_limits_for_version(Version::BB_V4); + if (s->version_name_colors) { + ret.base.visual.name_color = s->name_color_for_version(this->from_version); + ret.base.visual.name_color_checksum = 0; + } + return ret; +} + +uint64_t Parsed6x70Data::default_xb_user_id() const { + return (0xAE00000000000000 | this->guild_card_number); +} + +void Parsed6x70Data::clear_v1_unused_item_fields() { + for (size_t z = 0; z < min(this->num_items, 30); z++) { + auto& item = this->items[z]; + item.unknown_a1 = 0; + item.extension_data1 = 0; + item.extension_data2 = 0; + } +} + +void Parsed6x70Data::clear_dc_protos_unused_item_fields() { + for (size_t z = 0; z < min(this->num_items, 30); z++) { + auto& item = this->items[z]; + item.unknown_a1 = 0; + item.extension_data1 = 0; + item.extension_data2 = 0; + item.data.data2d = 0; + } +} + +Parsed6x70Data::Parsed6x70Data( + const G_SyncPlayerDispAndInventory_BaseV1& base, uint32_t guild_card_number, Version from_version) + : from_version(from_version), + item_version(this->from_version), + base(base.base), + bonus_hp_from_materials(base.bonus_hp_from_materials), + bonus_tp_from_materials(base.bonus_tp_from_materials), + unknown_a4_final(base.unknown_a4), + language(base.language), + player_tag(base.player_tag), + guild_card_number(guild_card_number), // Ignore the client's GC# + unknown_a6(base.unknown_a6), + battle_team_number(base.battle_team_number), + telepipe(base.telepipe), + unknown_a8(base.unknown_a8), + unknown_a9_final(base.unknown_a9), + area(base.area), + flags2(base.flags2), + technique_levels_v1(base.technique_levels_v1), + visual(base.visual) {} + +G_SyncPlayerDispAndInventory_BaseV1 Parsed6x70Data::base_v1() const { + G_SyncPlayerDispAndInventory_BaseV1 ret; + ret.base = this->base; + ret.bonus_hp_from_materials = this->bonus_hp_from_materials; + ret.bonus_tp_from_materials = this->bonus_tp_from_materials; + ret.unknown_a4 = this->unknown_a4_final; + ret.language = this->language; + ret.player_tag = this->player_tag; + ret.guild_card_number = this->guild_card_number; + ret.unknown_a6 = this->unknown_a6; + ret.battle_team_number = this->battle_team_number; + ret.telepipe = this->telepipe; + ret.unknown_a8 = this->unknown_a8; + ret.unknown_a9 = this->unknown_a9_final; + ret.area = this->area; + ret.flags2 = this->flags2; + ret.technique_levels_v1 = this->technique_levels_v1; + ret.visual = this->visual; + return ret; +} static void on_sync_joining_player_disp_and_inventory( shared_ptr c, uint8_t command, uint8_t flag, void* data, size_t size) { @@ -985,92 +1018,53 @@ static void on_sync_joining_player_disp_and_inventory( send_command(target, 0x62, target->lobby_client_id, &data, sizeof(data)); } - unique_ptr parsed; - switch (c_v) { case Version::DC_NTE: - parsed = make_unique( + c->last_reported_6x70.reset(new Parsed6x70Data( check_size_t(data, size), - c->login->account->account_id); - parsed->clear_dc_protos_unused_item_fields(); + c->login->account->account_id, c_v)); + c->last_reported_6x70->clear_dc_protos_unused_item_fields(); break; case Version::DC_V1_11_2000_PROTOTYPE: - parsed = make_unique( + c->last_reported_6x70.reset(new Parsed6x70Data( check_size_t(data, size), - c->login->account->account_id, - c->language()); - parsed->clear_dc_protos_unused_item_fields(); + c->login->account->account_id, c->language(), c_v)); + c->last_reported_6x70->clear_dc_protos_unused_item_fields(); break; case Version::DC_V1: case Version::DC_V2: case Version::PC_NTE: case Version::PC_V2: - parsed = make_unique( + c->last_reported_6x70.reset(new Parsed6x70Data( check_size_t(data, size), - c->login->account->account_id); + c->login->account->account_id, c_v)); if (c_v == Version::DC_V1) { - parsed->clear_v1_unused_item_fields(); + c->last_reported_6x70->clear_v1_unused_item_fields(); } break; case Version::GC_NTE: case Version::GC_V3: case Version::GC_EP3_NTE: case Version::GC_EP3: - parsed = make_unique( + c->last_reported_6x70.reset(new Parsed6x70Data( check_size_t(data, size), - c->login->account->account_id); + c->login->account->account_id, c_v)); break; case Version::XB_V3: - parsed = make_unique( + c->last_reported_6x70.reset(new Parsed6x70Data( check_size_t(data, size), - c->login->account->account_id); + c->login->account->account_id, c_v)); break; case Version::BB_V4: - parsed = make_unique( + c->last_reported_6x70.reset(new Parsed6x70Data( check_size_t(data, size), - c->login->account->account_id); + c->login->account->account_id, c_v)); break; default: throw logic_error("6x70 command from unknown game version"); } - parsed->transcode_inventory_items(c_v, target_v, s->item_parameter_table_for_encode(target_v)); - parsed->visual.enforce_lobby_join_limits_for_version(target_v); - if (s->version_name_colors) { - parsed->visual.name_color = s->name_color_for_version(c_v); - if (is_v1_or_v2(target_v)) { - parsed->visual.compute_name_color_checksum(); - } - } - - switch (target_v) { - case Version::DC_NTE: - forward_subcommand_t(target, command, flag, parsed->as_dc_nte()); - break; - case Version::DC_V1_11_2000_PROTOTYPE: - forward_subcommand_t(target, command, flag, parsed->as_dc_112000()); - break; - case Version::DC_V1: - case Version::DC_V2: - case Version::PC_NTE: - case Version::PC_V2: - forward_subcommand_t(target, command, flag, parsed->as_dc_pc()); - break; - case Version::GC_NTE: - case Version::GC_V3: - case Version::GC_EP3_NTE: - case Version::GC_EP3: - forward_subcommand_t(target, command, flag, parsed->as_gc()); - break; - case Version::XB_V3: - forward_subcommand_t(target, command, flag, parsed->as_xb()); - break; - case Version::BB_V4: - forward_subcommand_t(target, command, flag, parsed->as_bb(target->language())); - break; - default: - throw logic_error("6x70 command from unknown game version"); - } + send_game_player_state(target, c, false); } static void on_forward_check_client(shared_ptr c, uint8_t command, uint8_t flag, void* data, size_t size) { @@ -1346,6 +1340,9 @@ static void on_set_player_visible(shared_ptr c, uint8_t command, uint8_t send_update_team_reward_flags(c); send_all_nearby_team_metadatas_to_client(c, false); } + } else if (c->config.check_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST) && (c->version() != Version::BB_V4)) { + c->config.clear_flag(Client::Flag::LOADING_RUNNING_JOINABLE_QUEST); + c->log.info("LOADING_RUNNING_JOINABLE_QUEST flag cleared"); } } } @@ -1360,6 +1357,7 @@ static void on_change_floor_6x1F(shared_ptr c, uint8_t command, uint8_t // the loading flag here instead. if (c->config.check_flag(Client::Flag::LOADING)) { c->config.clear_flag(Client::Flag::LOADING); + c->log.info("LOADING flag cleared"); send_resume_game(c->require_lobby(), c); c->require_lobby()->assign_inventory_and_bank_item_ids(c, true); } @@ -4470,14 +4468,14 @@ const SubcommandDefinition subcommand_definitions[0x100] = { /* 6x68 */ {0x59, 0x60, 0x68, on_forward_check_game}, /* 6x69 */ {0x5A, 0x61, 0x69, on_npc_control}, /* 6x6A */ {0x5B, 0x62, 0x6A, on_forward_check_game}, - /* 6x6B */ {0x5C, 0x63, 0x6B, on_sync_joining_player_compressed_state, SDF::USE_JOIN_COMMAND_QUEUE}, - /* 6x6C */ {0x5D, 0x64, 0x6C, on_sync_joining_player_compressed_state, SDF::USE_JOIN_COMMAND_QUEUE}, - /* 6x6D */ {0x5E, 0x65, 0x6D, on_sync_joining_player_compressed_state, SDF::USE_JOIN_COMMAND_QUEUE}, - /* 6x6E */ {0x5F, 0x66, 0x6E, on_sync_joining_player_compressed_state, SDF::USE_JOIN_COMMAND_QUEUE}, - /* 6x6F */ {0x00, 0x00, 0x6F, on_sync_joining_player_quest_flags, SDF::USE_JOIN_COMMAND_QUEUE}, - /* 6x70 */ {0x60, 0x67, 0x70, on_sync_joining_player_disp_and_inventory, SDF::USE_JOIN_COMMAND_QUEUE}, - /* 6x71 */ {0x00, 0x00, 0x71, on_forward_check_game_loading, SDF::USE_JOIN_COMMAND_QUEUE}, - /* 6x72 */ {0x61, 0x68, 0x72, on_forward_check_game_loading, SDF::USE_JOIN_COMMAND_QUEUE}, + /* 6x6B */ {0x5C, 0x63, 0x6B, on_sync_joining_player_compressed_state}, + /* 6x6C */ {0x5D, 0x64, 0x6C, on_sync_joining_player_compressed_state}, + /* 6x6D */ {0x5E, 0x65, 0x6D, on_sync_joining_player_compressed_state}, + /* 6x6E */ {0x5F, 0x66, 0x6E, on_sync_joining_player_compressed_state}, + /* 6x6F */ {0x00, 0x00, 0x6F, on_sync_joining_player_quest_flags}, + /* 6x70 */ {0x60, 0x67, 0x70, on_sync_joining_player_disp_and_inventory}, + /* 6x71 */ {0x00, 0x00, 0x71, on_forward_check_game_loading}, + /* 6x72 */ {0x61, 0x68, 0x72, on_forward_check_game_loading}, /* 6x73 */ {0x00, 0x00, 0x73, on_forward_check_game_quest}, /* 6x74 */ {0x62, 0x69, 0x74, on_word_select, SDF::ALWAYS_FORWARD_TO_WATCHERS}, /* 6x75 */ {0x00, 0x00, 0x75, on_set_quest_flag}, diff --git a/src/ReceiveSubcommands.hh b/src/ReceiveSubcommands.hh index 961d8806..d240b7f0 100644 --- a/src/ReceiveSubcommands.hh +++ b/src/ReceiveSubcommands.hh @@ -36,3 +36,78 @@ DropReconcileResult reconcile_drop_request_with_map( const Client::Config& config, std::shared_ptr map, bool mark_drop); + +class Parsed6x70Data { +public: + Version from_version; + Version item_version; + + G_SyncPlayerDispAndInventory_BaseDCNTE base; + uint32_t unknown_a5_nte = 0; + uint32_t unknown_a6_nte = 0; + uint16_t bonus_hp_from_materials = 0; + uint16_t bonus_tp_from_materials = 0; + parray unknown_a5_112000; + parray unknown_a4_final; + uint32_t language = 0; + uint32_t player_tag = 0; + uint32_t guild_card_number = 0; + uint32_t unknown_a6 = 0; + uint32_t battle_team_number = 0; + Telepipe telepipe; + uint32_t unknown_a8 = 0; + parray unknown_a9_nte_112000; + G_Unknown_6x70_SubA1 unknown_a9_final; + uint32_t area = 0; + uint32_t flags2 = 0; + parray technique_levels_v1 = 0xFF; + PlayerVisualConfig visual; + std::string name; + PlayerStats stats; + uint32_t num_items = 0; + parray items; + uint32_t floor = 0; + uint64_t xb_user_id = 0; + uint32_t xb_unknown_a16 = 0; + + Parsed6x70Data( + const G_SyncPlayerDispAndInventory_DCNTE_6x70& cmd, + uint32_t guild_card_number, + Version from_version); + Parsed6x70Data( + const G_SyncPlayerDispAndInventory_DC112000_6x70& cmd, + uint32_t guild_card_number, + uint8_t language, + Version from_version); + Parsed6x70Data( + const G_SyncPlayerDispAndInventory_DC_PC_6x70& cmd, + uint32_t guild_card_number, + Version from_version); + Parsed6x70Data( + const G_SyncPlayerDispAndInventory_GC_6x70& cmd, + uint32_t guild_card_number, + Version from_version); + Parsed6x70Data( + const G_SyncPlayerDispAndInventory_XB_6x70& cmd, + uint32_t guild_card_number, + Version from_version); + Parsed6x70Data( + const G_SyncPlayerDispAndInventory_BB_6x70& cmd, + uint32_t guild_card_number, + Version from_version); + + G_SyncPlayerDispAndInventory_DCNTE_6x70 as_dc_nte(std::shared_ptr s) const; + G_SyncPlayerDispAndInventory_DC112000_6x70 as_dc_112000(std::shared_ptr s) const; + G_SyncPlayerDispAndInventory_DC_PC_6x70 as_dc_pc(std::shared_ptr s, Version to_version) const; + G_SyncPlayerDispAndInventory_GC_6x70 as_gc_gcnte(std::shared_ptr s, Version to_version) const; + G_SyncPlayerDispAndInventory_XB_6x70 as_xb(std::shared_ptr s) const; + G_SyncPlayerDispAndInventory_BB_6x70 as_bb(std::shared_ptr s, uint8_t language) const; + + uint64_t default_xb_user_id() const; + void clear_v1_unused_item_fields(); + void clear_dc_protos_unused_item_fields(); + +protected: + Parsed6x70Data(const G_SyncPlayerDispAndInventory_BaseV1& base, uint32_t guild_card_number, Version from_version); + G_SyncPlayerDispAndInventory_BaseV1 base_v1() const; +}; diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 7c50868b..4d782a10 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -19,6 +19,7 @@ #include "FileContentsCache.hh" #include "PSOProtocol.hh" #include "ProxyServer.hh" +#include "ReceiveSubcommands.hh" #include "StaticGameData.hh" #include "Text.hh" @@ -2436,7 +2437,13 @@ void send_arrow_update(shared_ptr l) { } } -// tells the player that the joining player is done joining, and the game can resume +void send_unblock_join(shared_ptr c) { + if (!is_pre_v1(c->version())) { + static const be_uint32_t data = 0x71010000; + send_command(c, 0x60, 0x00, &data, sizeof(be_uint32_t)); + } +} + void send_resume_game(shared_ptr l, shared_ptr ready_client) { for (auto lc : l->clients) { if (lc && (lc != ready_client) && !is_pre_v1(lc->version())) { @@ -2567,7 +2574,7 @@ void send_game_join_sync_command_compressed( } if (c->game_join_command_queue) { - c->log.info("Client not ready to receive join commands; adding to queue"); + c->log.info("Client not ready to receive game commands; adding to queue"); auto& cmd = c->game_join_command_queue->emplace_back(); cmd.command = 0x6D; cmd.flag = c->lobby_client_id; @@ -2789,6 +2796,74 @@ void send_game_flag_state(shared_ptr c) { } } +void send_game_player_state(shared_ptr to_c, shared_ptr from_c, bool apply_overrides) { + if (!from_c->last_reported_6x70) { + throw runtime_error("source client did not send a 6x70 command"); + } + if (!from_c->login) { + throw logic_error("source client is not logged in"); + } + + auto s = to_c->require_server_state(); + Parsed6x70Data to_send = *from_c->last_reported_6x70; + + to_send.base.client_id = from_c->lobby_client_id; + to_send.player_tag = 0x00010000; + to_send.guild_card_number = from_c->login->account->account_id; + + if (apply_overrides) { + auto from_p = from_c->character(); + to_send.base.x = from_c->x; + to_send.base.z = from_c->z; + to_send.bonus_hp_from_materials = from_p->inventory.hp_from_materials; + to_send.bonus_tp_from_materials = from_p->inventory.tp_from_materials; + to_send.language = from_c->language(); + // TODO: Deal with telepipes. Probably we should track their state via the + // subcommands sent when they're created/destroyed, but currently we don't. + to_send.area = from_c->floor; + to_send.technique_levels_v1 = from_p->disp.technique_levels_v1; + to_send.visual = from_p->disp.visual; + to_send.name = from_p->disp.name.decode(from_c->language()); + if (to_c->version() != Version::BB_V4) { + to_send.visual.name.encode(to_send.name, to_c->language()); + } + to_send.stats = from_p->disp.stats; + to_send.num_items = from_p->inventory.num_items; + to_send.items = from_p->inventory.items; + to_send.item_version = Version::BB_V4; // Server-side items are stored in BB encoding + to_send.floor = from_c->floor; + } + + switch (to_c->version()) { + case Version::DC_NTE: + send_or_enqueue_command(to_c, 0x6D, to_c->lobby_client_id, to_send.as_dc_nte(s)); + break; + case Version::DC_V1_11_2000_PROTOTYPE: + send_or_enqueue_command(to_c, 0x6D, to_c->lobby_client_id, to_send.as_dc_112000(s)); + break; + case Version::DC_V1: + case Version::DC_V2: + case Version::PC_NTE: + case Version::PC_V2: + send_or_enqueue_command(to_c, 0x6D, to_c->lobby_client_id, to_send.as_dc_pc(s, to_c->version())); + break; + case Version::GC_NTE: + case Version::GC_V3: + case Version::GC_EP3_NTE: + case Version::GC_EP3: + send_or_enqueue_command(to_c, 0x6D, to_c->lobby_client_id, to_send.as_gc_gcnte(s, to_c->version())); + break; + case Version::XB_V3: + send_or_enqueue_command(to_c, 0x6D, to_c->lobby_client_id, to_send.as_xb(s)); + break; + case Version::BB_V4: + send_or_enqueue_command(to_c, 0x6D, to_c->lobby_client_id, to_send.as_bb(s, to_c->language())); + break; + default: + throw logic_error("attempting to send 6x70 command to unknown game version"); + } +} + void send_drop_item_to_channel(shared_ptr s, Channel& ch, const ItemData& item, bool from_enemy, uint8_t floor, float x, float z, uint16_t entity_id) { uint8_t subcommand = get_pre_v1_subcommand(ch.version, 0x51, 0x58, 0x5F); diff --git a/src/SendCommands.hh b/src/SendCommands.hh index 584be9f8..3bd4026e 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -32,6 +32,21 @@ extern const std::unordered_set bb_crypt_initial_client_commands; // pointer is given but size is accidentally not given (e.g. if the type of // data in the calling function is changed from string to void*). +template +void send_or_enqueue_command(std::shared_ptr c, uint16_t command, uint32_t flag, const CmdT& cmd) { + if (c->game_join_command_queue) { + c->log.info("Client not ready to receive game commands; adding to queue"); + auto& q_cmd = c->game_join_command_queue->emplace_back(); + q_cmd.command = command; + q_cmd.flag = flag; + // TODO: It'd be nice to avoid this copy. Maybe take in a pointer to cmd + // and move it into q_cmd somehow, so q_cmd can free it when needed? + q_cmd.data.assign(reinterpret_cast(&cmd), sizeof(cmd)); + } else { + send_command(c, command, flag, &cmd, sizeof(cmd)); + } +} + void send_command(std::shared_ptr c, uint16_t command, uint32_t flag, const std::vector>& blocks); @@ -293,6 +308,7 @@ void send_execute_item_trade(std::shared_ptr c, const std::vector c, const std::vector>& card_to_count); void send_arrow_update(std::shared_ptr l); +void send_unblock_join(std::shared_ptr c); void send_resume_game(std::shared_ptr l, std::shared_ptr ready_client); enum PlayerStatsChange { @@ -331,6 +347,7 @@ void send_game_enemy_state(std::shared_ptr c); void send_game_object_state(std::shared_ptr c); void send_game_set_state(std::shared_ptr c); void send_game_flag_state(std::shared_ptr c); +void send_game_player_state(std::shared_ptr to_c, std::shared_ptr from_c, bool apply_overrides); void send_drop_item_to_channel(std::shared_ptr s, Channel& ch, const ItemData& item, bool from_enemy, uint8_t floor, float x, float z, uint16_t request_id); void send_drop_item_to_lobby(std::shared_ptr l, const ItemData& item, diff --git a/system/quests/battle/b88001.json b/system/quests/battle/b88001.json index 2756ed6f..0f667cd0 100644 --- a/system/quests/battle/b88001.json +++ b/system/quests/battle/b88001.json @@ -35,8 +35,8 @@ // "ChallengeTemplateIndex": 0, // Quests may be set to be unavailable until a preceding quest has been - // cleared or a team reward has been purchased. To enable this feature, set a - // value for AvailableIf in the quest's JSON file. (This is ignored if the + // cleared or a team reward has been purchased. To enable this behavior, set + // a value for AvailableIf in the quest's JSON file. (This is ignored if the // player has the DISABLE_QUEST_REQUIREMENTS flag in their account.) This // field's value should be an expression that tests any of the following: // F_XXXX: Quest flag specified in hex (e.g. F_014D) @@ -54,4 +54,11 @@ // to false, this is ignored. This field is also ignored if the player has // the DISABLE_QUEST_REQUIREMENTS flag in their account. // "EnabledIf": "!F_0169", + + // On BB, a quest's joinability flag is part of the quest file header, but + // other versions don't natively support joining quests in progress. This + // flag, if present, enables non-BB quests to be joined when already in + // progress. Note that this will likely not work properly unless the quest + // script is designed to support joining players. + // "Joinable": true, }