From c98d1081a324b577fa5357b430b137cec6a6abb9 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Fri, 12 Apr 2024 22:09:26 -0700 Subject: [PATCH] add support for auto-patching --- src/Account.cc | 14 ++++ src/Account.hh | 2 + src/Client.hh | 1 + src/FunctionCompiler.cc | 32 +++++++-- src/FunctionCompiler.hh | 2 + src/HTTPServer.cc | 5 ++ src/Menu.hh | 2 + src/ReceiveCommands.cc | 154 ++++++++++++++++++++++++++++++++++------ 8 files changed, 183 insertions(+), 29 deletions(-) diff --git a/src/Account.cc b/src/Account.cc index 800915f3..4f567713 100644 --- a/src/Account.cc +++ b/src/Account.cc @@ -228,6 +228,13 @@ Account::Account(const JSON& json) this->ep3_current_meseta = json.get_int("Ep3CurrentMeseta", 0); this->ep3_total_meseta_earned = json.get_int("Ep3TotalMesetaEarned", 0); this->bb_team_id = json.get_int("BBTeamID", 0); + + try { + for (const auto& it : json.get_list("AutoPatchesEnabled")) { + this->auto_patches_enabled.emplace(it->as_string()); + } + } catch (const out_of_range&) { + } } JSON Account::json() const { @@ -255,6 +262,12 @@ JSON Account::json() const { for (const auto& it : this->bb_licenses) { bb_json.emplace_back(it.second->json()); } + + JSON auto_patches_json = JSON::list(); + for (const auto& it : this->auto_patches_enabled) { + auto_patches_json.emplace_back(it); + } + return JSON::dict({ {"FormatVersion", 1}, {"AccountID", this->account_id}, @@ -271,6 +284,7 @@ JSON Account::json() const { {"Ep3CurrentMeseta", this->ep3_current_meseta}, {"Ep3TotalMesetaEarned", this->ep3_total_meseta_earned}, {"BBTeamID", this->bb_team_id}, + {"AutoPatchesEnabled", std::move(auto_patches_json)}, }); } diff --git a/src/Account.hh b/src/Account.hh index 9488ec67..3d6d49ec 100644 --- a/src/Account.hh +++ b/src/Account.hh @@ -90,6 +90,8 @@ struct Account { uint32_t bb_team_id = 0; bool is_temporary = false; // If true, isn't saved to disk + std::unordered_set auto_patches_enabled; + std::unordered_map> dc_nte_licenses; std::unordered_map> dc_licenses; std::unordered_map> pc_licenses; diff --git a/src/Client.hh b/src/Client.hh index 516d6275..d8018274 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -61,6 +61,7 @@ public: HAS_EP3_MEDIA_UPDATES = 0x0000000010000000, USE_OVERRIDE_RANDOM_SEED = 0x0000000020000000, HAS_GUILD_CARD_NUMBER = 0x0000000040000000, + HAS_AUTO_PATCHES = 0x0000004000000000, AT_BANK_COUNTER = 0x0000000080000000, // Server-side only SHOULD_SEND_ARTIFICIAL_ITEM_STATE = 0x0001000000000000, // Server-side only SHOULD_SEND_ARTIFICIAL_ENEMY_AND_SET_STATE = 0x0040000000000000, // Server-side only diff --git a/src/FunctionCompiler.cc b/src/FunctionCompiler.cc index 96019062..7d63e666 100644 --- a/src/FunctionCompiler.cc +++ b/src/FunctionCompiler.cc @@ -325,13 +325,33 @@ shared_ptr FunctionCodeIndex::patch_menu(uint32_t specific_version) ret->items.emplace_back(PatchesMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0); for (const auto& it : this->name_and_specific_version_to_patch_function) { const auto& fn = it.second; - if (!fn->hide_from_patches_menu && ends_with(it.first, suffix)) { - ret->items.emplace_back( - fn->menu_item_id, - fn->long_name.empty() ? fn->short_name : fn->long_name, - fn->description, - MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL); + if (fn->hide_from_patches_menu || !ends_with(it.first, suffix)) { + continue; } + ret->items.emplace_back( + fn->menu_item_id, + fn->long_name.empty() ? fn->short_name : fn->long_name, + fn->description, + MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL); + } + return ret; +} + +shared_ptr FunctionCodeIndex::patch_switches_menu( + uint32_t specific_version, const std::unordered_set& auto_patches_enabled) const { + auto suffix = string_printf("-%08" PRIX32, specific_version); + + auto ret = make_shared(MenuID::PATCH_SWITCHES, "Patch switches"); + ret->items.emplace_back(PatchesMenuItemID::GO_BACK, "Go back", "Return to the\nmain menu", 0); + for (const auto& it : this->name_and_specific_version_to_patch_function) { + const auto& fn = it.second; + if (fn->hide_from_patches_menu || !ends_with(it.first, suffix)) { + continue; + } + string name; + name.push_back(auto_patches_enabled.count(fn->short_name) ? '*' : '-'); + name += fn->long_name.empty() ? fn->short_name : fn->long_name; + ret->items.emplace_back(fn->menu_item_id, name, fn->description, MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL); } return ret; } diff --git a/src/FunctionCompiler.hh b/src/FunctionCompiler.hh index ee77ee7a..b797e9b4 100644 --- a/src/FunctionCompiler.hh +++ b/src/FunctionCompiler.hh @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "Menu.hh" @@ -69,6 +70,7 @@ struct FunctionCodeIndex { std::map> name_and_specific_version_to_patch_function; std::shared_ptr patch_menu(uint32_t specific_version) const; + std::shared_ptr patch_switches_menu(uint32_t specific_version, const std::unordered_set& auto_patches_enabled) const; bool patch_menu_empty(uint32_t specific_version) const; std::shared_ptr get_patch(const std::string& name, uint32_t specific_version) const; diff --git a/src/HTTPServer.cc b/src/HTTPServer.cc index 7305e7dc..a8e683a6 100644 --- a/src/HTTPServer.cc +++ b/src/HTTPServer.cc @@ -291,6 +291,10 @@ JSON HTTPServer::generate_account_json_st(shared_ptr a) { for (const auto& it : a->bb_licenses) { bb_licenses_json.emplace_back(it.first); } + auto auto_patches_json = JSON::list(); + for (const auto& it : a->auto_patches_enabled) { + auto_patches_json.emplace_back(it); + } return JSON::dict({ {"AccountID", a->account_id}, {"Flags", a->flags}, @@ -307,6 +311,7 @@ JSON HTTPServer::generate_account_json_st(shared_ptr a) { {"GCLicenses", std::move(gc_licenses_json)}, {"XBLicenses", std::move(xb_licenses_json)}, {"BBLicenses", std::move(bb_licenses_json)}, + {"AutoPatchesEnabled", std::move(auto_patches_json)}, }); }; diff --git a/src/Menu.hh b/src/Menu.hh index 33dd518b..dabff650 100644 --- a/src/Menu.hh +++ b/src/Menu.hh @@ -24,6 +24,7 @@ constexpr uint32_t QUEST_CATEGORIES = 0x66010166; constexpr uint32_t PROXY_DESTINATIONS = 0x77000077; constexpr uint32_t PROGRAMS = 0x88000088; constexpr uint32_t PATCHES = 0x99000099; +constexpr uint32_t PATCH_SWITCHES = 0x99010199; constexpr uint32_t PROXY_OPTIONS = 0xAA0000AA; constexpr uint32_t TOURNAMENTS = 0xBB0000BB; constexpr uint32_t TOURNAMENTS_FOR_SPEC = 0xBB1111BB; @@ -36,6 +37,7 @@ constexpr uint32_t INFORMATION = 0x11333311; constexpr uint32_t DOWNLOAD_QUESTS = 0x11444411; constexpr uint32_t PROXY_DESTINATIONS = 0x11555511; constexpr uint32_t PATCHES = 0x11666611; +constexpr uint32_t PATCH_SWITCHES = 0x11676711; constexpr uint32_t PROGRAMS = 0x11777711; constexpr uint32_t DISCONNECT = 0x11888811; constexpr uint32_t CLEAR_LICENSE = 0x11999911; diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 598c9592..853322db 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -118,6 +118,63 @@ static shared_ptr proxy_options_menu_for_client(shared_ptr c, std::function on_complete) { + // TODO: This function is bad. Ideally we would use coroutines and clean up + // all these terrible callbacks. + + if (function_compiler_available() && + !c->config.check_flag(Client::Flag::HAS_AUTO_PATCHES) && + !c->config.check_flag(Client::Flag::NO_SEND_FUNCTION_CALL)) { + prepare_client_for_patches(c, [wc = weak_ptr(c), on_complete = std::move(on_complete)]() -> void { + auto c = wc.lock(); + if (!c) { + return; + } + + auto s = c->require_server_state(); + size_t num_patches_sent = 0; + for (const auto& patch_name : c->login->account->auto_patches_enabled) { + try { + send_function_call(c, s->function_code_index->get_patch(patch_name, c->config.specific_version)); + num_patches_sent++; + } catch (const out_of_range&) { + c->log.warning("Client has auto patch %s enabled, but it is not available for specific_version %08" PRIX32, + patch_name.c_str(), c->config.specific_version); + } + } + + // We can't just blast all the commands at the client, since the sequence + // ends with a reconnect command, which changes the encryption state! We + // have to wait until all the patch responses have been received before + // sending any command that the client will respond to. It's OK to send + // the 04 immediately before the 19 (since the client does not respond to + // 04), but it is not OK to send the B2s without waiting. + + if (num_patches_sent == 0) { + c->config.set_flag(Client::Flag::HAS_AUTO_PATCHES); + send_update_client_config(c, false); + on_complete(); + + } else { + while (c->function_call_response_queue.size() < num_patches_sent - 1) { + c->function_call_response_queue.emplace_back(empty_function_call_response_handler); + } + c->function_call_response_queue.emplace_back([wc, on_complete = std::move(on_complete)](uint32_t, uint32_t) { + auto c = wc.lock(); + if (!c) { + return; + } + c->config.set_flag(Client::Flag::HAS_AUTO_PATCHES); + send_update_client_config(c, false); + on_complete(); + }); + } + }); + } else { + on_complete(); + } +} + void send_client_to_login_server(shared_ptr c) { string port_name = login_port_name_for_version(c->version()); auto s = c->require_server_state(); @@ -125,35 +182,49 @@ void send_client_to_login_server(shared_ptr c) { } void send_client_to_lobby_server(shared_ptr c) { - auto s = c->require_server_state(); - string port_name = lobby_port_name_for_version(c->version()); - send_reconnect(c, s->connect_address_for_client(c), - s->name_to_port_config.at(port_name)->port); + send_first_pre_lobby_commands(c, [wc = weak_ptr(c)]() { + auto c = wc.lock(); + if (!c) { + return; + } + + auto s = c->require_server_state(); + string port_name = lobby_port_name_for_version(c->version()); + send_reconnect(c, s->connect_address_for_client(c), + s->name_to_port_config.at(port_name)->port); + }); } void send_client_to_proxy_server(shared_ptr c) { - auto s = c->require_server_state(); + send_first_pre_lobby_commands(c, [wc = weak_ptr(c)]() { + auto c = wc.lock(); + if (!c) { + return; + } - string port_name = proxy_port_name_for_version(c->version()); - uint16_t local_port = s->name_to_port_config.at(port_name)->port; + auto s = c->require_server_state(); - s->proxy_server->delete_session(c->login->account->account_id); - auto ses = s->proxy_server->create_logged_in_session(c->login, local_port, c->version(), c->config); - if (!c->can_use_chat_commands()) { - ses->config.clear_flag(Client::Flag::PROXY_CHAT_COMMANDS_ENABLED); - } - if (!s->proxy_allow_save_files) { - ses->config.clear_flag(Client::Flag::PROXY_SAVE_FILES); - } - if (!s->proxy_enable_login_options) { - ses->config.clear_flag(Client::Flag::PROXY_SUPPRESS_REMOTE_LOGIN); - ses->config.clear_flag(Client::Flag::PROXY_ZERO_REMOTE_GUILD_CARD); - } - if (ses->config.check_flag(Client::Flag::PROXY_ZERO_REMOTE_GUILD_CARD)) { - ses->remote_guild_card_number = 0; - } + string port_name = proxy_port_name_for_version(c->version()); + uint16_t local_port = s->name_to_port_config.at(port_name)->port; - send_reconnect(c, s->connect_address_for_client(c), local_port); + s->proxy_server->delete_session(c->login->account->account_id); + auto ses = s->proxy_server->create_logged_in_session(c->login, local_port, c->version(), c->config); + if (!c->can_use_chat_commands()) { + ses->config.clear_flag(Client::Flag::PROXY_CHAT_COMMANDS_ENABLED); + } + if (!s->proxy_allow_save_files) { + ses->config.clear_flag(Client::Flag::PROXY_SAVE_FILES); + } + if (!s->proxy_enable_login_options) { + ses->config.clear_flag(Client::Flag::PROXY_SUPPRESS_REMOTE_LOGIN); + ses->config.clear_flag(Client::Flag::PROXY_ZERO_REMOTE_GUILD_CARD); + } + if (ses->config.check_flag(Client::Flag::PROXY_ZERO_REMOTE_GUILD_CARD)) { + ses->remote_guild_card_number = 0; + } + + send_reconnect(c, s->connect_address_for_client(c), local_port); + }); } static void send_proxy_destinations_menu(shared_ptr c) { @@ -267,6 +338,8 @@ static void send_main_menu(shared_ptr c) { if (!s->function_code_index->patch_menu_empty(c->config.specific_version)) { main_menu->items.emplace_back(MainMenuItemID::PATCHES, "Patches", "Change game\nbehaviors", MenuItem::Flag::INVISIBLE_ON_DC | MenuItem::Flag::INVISIBLE_ON_PC | MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL); + main_menu->items.emplace_back(MainMenuItemID::PATCH_SWITCHES, "Patch switches", + "Automatically\napply patches every\ntime you connect", MenuItem::Flag::INVISIBLE_ON_DC | MenuItem::Flag::INVISIBLE_ON_PC | MenuItem::Flag::REQUIRES_SEND_FUNCTION_CALL); } if (!s->dol_file_index->empty()) { main_menu->items.emplace_back(MainMenuItemID::PROGRAMS, "Programs", @@ -2041,6 +2114,21 @@ static void on_10(shared_ptr c, uint16_t, uint32_t, string& data) { }); break; + case MainMenuItemID::PATCH_SWITCHES: + if (!function_compiler_available()) { + throw runtime_error("function compiler not available"); + } + if (c->config.check_flag(Client::Flag::NO_SEND_FUNCTION_CALL)) { + throw runtime_error("client does not support send_function_call"); + } + // We have to prepare the client for patches here, even though we + // don't send them from this mennu, because we need to know the + // client's specific_version before sending the menu. + prepare_client_for_patches(c, [c]() -> void { + send_menu(c, c->require_server_state()->function_code_index->patch_switches_menu(c->config.specific_version, c->login->account->auto_patches_enabled)); + }); + break; + case MainMenuItemID::PROGRAMS: if (!function_compiler_available()) { throw runtime_error("function compiler not available"); @@ -2407,6 +2495,26 @@ static void on_10(shared_ptr c, uint16_t, uint32_t, string& data) { } break; + case MenuID::PATCH_SWITCHES: + if (item_id == PatchesMenuItemID::GO_BACK) { + send_main_menu(c); + + } else { + if (c->config.check_flag(Client::Flag::NO_SEND_FUNCTION_CALL)) { + throw runtime_error("client does not support send_function_call"); + } + + auto s = c->require_server_state(); + uint64_t key = (static_cast(item_id) << 32) | c->config.specific_version; + auto fn = s->function_code_index->menu_item_id_and_specific_version_to_patch_function.at(key); + if (!c->login->account->auto_patches_enabled.emplace(fn->short_name).second) { + c->login->account->auto_patches_enabled.erase(fn->short_name); + } + c->login->account->save(); + send_menu(c, s->function_code_index->patch_switches_menu(c->config.specific_version, c->login->account->auto_patches_enabled)); + } + break; + case MenuID::PROGRAMS: if (item_id == ProgramsMenuItemID::GO_BACK) { send_main_menu(c);