From 6d2a5bf5bcaead71ce8c97b205d44d35f72b41bc Mon Sep 17 00:00:00 2001 From: m Date: Sun, 11 Sep 2022 08:07:19 -0500 Subject: [PATCH] Mod downloading (#228) * basic template for mod downloading * better checks for unloading? * Mod download stuff * debug prints * basic UI popup for mod downloading * change notify location + abort download * Prevent mods from loading lua files * cleanup, fixes, mod aborting * use correct pipelines * change some messages * fix mod downloading * not a component * fix hardcoded changes Co-authored-by: Federico Cecchetto --- data/cdata/ui_scripts/mods/__init__.lua | 4 + data/cdata/ui_scripts/mods/download.lua | 13 ++ src/client/component/download.cpp | 184 ++++++++++++++++++++++ src/client/component/download.hpp | 10 ++ src/client/component/fastfiles.cpp | 18 +++ src/client/component/party.cpp | 141 ++++++++++++++--- src/client/component/party.hpp | 2 + src/client/component/ui_scripting.cpp | 6 + src/client/game/scripting/lua/context.cpp | 2 + src/client/game/symbols.hpp | 3 + src/common/utils/http.cpp | 10 +- src/common/utils/http.hpp | 4 +- 12 files changed, 369 insertions(+), 28 deletions(-) create mode 100644 data/cdata/ui_scripts/mods/download.lua create mode 100644 src/client/component/download.cpp create mode 100644 src/client/component/download.hpp diff --git a/data/cdata/ui_scripts/mods/__init__.lua b/data/cdata/ui_scripts/mods/__init__.lua index ca2d3da9..5c75897c 100644 --- a/data/cdata/ui_scripts/mods/__init__.lua +++ b/data/cdata/ui_scripts/mods/__init__.lua @@ -1 +1,5 @@ require("loading") + +if (Engine.InFrontend()) then + require("download") +end diff --git a/data/cdata/ui_scripts/mods/download.lua b/data/cdata/ui_scripts/mods/download.lua new file mode 100644 index 00000000..96105c4d --- /dev/null +++ b/data/cdata/ui_scripts/mods/download.lua @@ -0,0 +1,13 @@ +Engine.GetLuiRoot():registerEventHandler("mod_download_start", function(element, event) + local popup = LUI.openpopupmenu("generic_waiting_popup_", { + oncancel = function() + download.abort() + end, + withcancel = true, + text = "Downloading " .. event.request.name .. "..." + }) + + Engine.GetLuiRoot():registerEventHandler("mod_download_done", function() + LUI.FlowManager.RequestLeaveMenu(popup) + end) +end) diff --git a/src/client/component/download.cpp b/src/client/component/download.cpp new file mode 100644 index 00000000..aa83c10a --- /dev/null +++ b/src/client/component/download.cpp @@ -0,0 +1,184 @@ +#include +#include "loader/component_loader.hpp" + +#include "download.hpp" +#include "console.hpp" +#include "scheduler.hpp" +#include "party.hpp" + +#include "game/ui_scripting/execution.hpp" + +#include +#include +#include + +namespace download +{ + namespace + { + struct globals_t + { + bool abort{}; + bool active{}; + }; + + utils::concurrency::container globals; + + bool download_aborted() + { + return globals.access([](globals_t& globals_) + { + return globals_.abort; + }); + } + + void mark_unactive() + { + globals.access([](globals_t& globals_) + { + globals_.active = false; + }); + } + + void mark_active() + { + globals.access([](globals_t& globals_) + { + globals_.active = true; + }); + } + + bool download_active() + { + return globals.access([](globals_t& globals_) + { + return globals_.active; + }); + } + + int progress_callback(size_t progress) + { + console::debug("Download progress: %lli\n", progress); + if (download_aborted()) + { + return -1; + } + + return 0; + } + } + + void start_download(const game::netadr_s& target, const utils::info_string& info) + { + if (download_active()) + { + scheduler::schedule([=]() + { + if (!download_active()) + { + start_download(target, info); + return scheduler::cond_end; + } + + return scheduler::cond_continue; + }); + + return; + } + + globals.access([&](globals_t& globals_) + { + globals_ = {}; + }); + + const auto base = info.get("sv_wwwBaseUrl"); + if (base.empty()) + { + party::menu_error("Download failed: Server doesn't have 'sv_wwwBaseUrl' dvar set."); + return; + } + + const auto mod = info.get("fs_game") + "/mod.ff"; + const auto url = base + "/" + mod; + + console::debug("Downloading %s from %s: %s\n", mod.data(), base.data(), url.data()); + + scheduler::once([=]() + { + const ui_scripting::table mod_data_table{}; + mod_data_table.set("name", mod.data()); + + ui_scripting::notify("mod_download_start", + { + {"request", mod_data_table} + }); + }, scheduler::pipeline::lui); + + scheduler::once([=]() + { + { + const auto _0 = gsl::finally(&mark_unactive); + + mark_active(); + + if (download_aborted()) + { + return; + } + + const auto data = utils::http::get_data(url, {}, {}, &progress_callback); + if (!data.has_value()) + { + party::menu_error("Download failed: An unknown error occurred, please try again."); + return; + } + + if (download_aborted()) + { + return; + } + + const auto& result = data.value(); + if (result.code != CURLE_OK) + { + party::menu_error(utils::string::va("Download failed: %s (%i)\n", result.code, curl_easy_strerror(result.code))); + return; + } + + utils::io::write_file(mod, result.buffer, false); + } + + scheduler::once([]() + { + ui_scripting::notify("mod_download_done", {}); + }, scheduler::pipeline::lui); + + // reconnect back to target after download of mod is finished + scheduler::once([=]() + { + party::connect(target); + }, scheduler::pipeline::main); + }, scheduler::pipeline::async); + } + + void stop_download() + { + if (!download_active()) + { + return; + } + + globals.access([&](globals_t& globals_) + { + globals_.abort = true; + }); + + scheduler::once([]() + { + ui_scripting::notify("mod_download_done", {}); + }, scheduler::pipeline::lui); + + party::menu_error("Download for server mod has been cancelled."); + } +} + diff --git a/src/client/component/download.hpp b/src/client/component/download.hpp new file mode 100644 index 00000000..71d62a71 --- /dev/null +++ b/src/client/component/download.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "game/game.hpp" +#include + +namespace download +{ + void start_download(const game::netadr_s& target, const utils::info_string& info); + void stop_download(); +} diff --git a/src/client/component/fastfiles.cpp b/src/client/component/fastfiles.cpp index 90804fd4..5b3351a0 100644 --- a/src/client/component/fastfiles.cpp +++ b/src/client/component/fastfiles.cpp @@ -296,6 +296,18 @@ namespace fastfiles game::DB_LoadXAssets(data.data(), static_cast(data.size()), syncMode); } + + void load_lua_file_asset_stub(void* a1) + { + const auto fastfile = fastfiles::get_current_fastfile(); + if (fastfile == "mod") + { + console::error("Mod tried to load a lua file!\n"); + return; + } + + utils::hook::invoke(0x39CA90_b, a1); + } } bool exists(const std::string& zone) @@ -387,6 +399,12 @@ namespace fastfiles utils::hook::jump(0x398061_b, utils::hook::assemble(mp::skip_extra_zones_stub), true); } + // prevent mod.ff from loading lua files + if (game::environment::is_mp()) + { + utils::hook::call(0x3757B4_b, load_lua_file_asset_stub); + } + command::add("loadzone", [](const command::params& params) { if (params.size() < 2) diff --git a/src/client/component/party.cpp b/src/client/component/party.cpp index e8be9910..16cbabe7 100644 --- a/src/client/component/party.cpp +++ b/src/client/component/party.cpp @@ -7,6 +7,9 @@ #include "network.hpp" #include "scheduler.hpp" #include "server_list.hpp" +#include "download.hpp" + +#include "game/game.hpp" #include "steam/steam.hpp" @@ -14,6 +17,7 @@ #include #include #include +#include namespace party { @@ -29,6 +33,8 @@ namespace party std::string sv_motd; int sv_maxclients; + std::optional mod_hash{}; + void perform_game_initialization() { command::execute("onlinegame 1", true); @@ -144,13 +150,91 @@ namespace party cl_disconnect_hook.invoke(showMainMenu); } - void menu_error(const std::string& error) + bool download_mod(const game::netadr_s& target, const utils::info_string& info) { - utils::hook::invoke(0x17D770_b, error.data(), "MENU_NOTICE"); - utils::hook::set(0x2ED2F78_b, 1); + const auto server_fs_game = utils::string::to_lower(info.get("fs_game")); + if (!server_fs_game.starts_with("mods/") || server_fs_game.contains('.')) + { + console::info("Invalid server fs_game value %s\n", server_fs_game.data()); + return true; + } + + const auto source_hash = info.get("modHash"); + if (source_hash.empty()) + { + menu_error("Connection failed: Server mod hash is empty."); + return true; + } + + static const auto fs_game = game::Dvar_FindVar("fs_game"); + const auto client_fs_game = utils::string::to_lower(fs_game->current.string); + + const auto mod_path = server_fs_game + "/mod.ff"; + auto has_to_download = !utils::io::file_exists(mod_path); + if (!has_to_download) + { + const auto data = utils::io::read_file(mod_path); + const auto hash = utils::cryptography::sha1::compute(data, true); + + has_to_download = source_hash != hash; + } + else + { + console::debug("Failed to find mod, downloading\n"); + } + + if (has_to_download) + { + console::debug("Starting download of mod '%s'\n", server_fs_game.data()); + + download::stop_download(); + download::start_download(target, info); + + return true; + } + else if (client_fs_game != server_fs_game) + { + game::Dvar_SetFromStringByNameFromSource("fs_game", server_fs_game.data(), + game::DVAR_SOURCE_INTERNAL); + command::execute("vid_restart"); + + // set fs_game to the mod the server is on, "restart" game, and then (hopefully) reconnect + scheduler::once([=]() + { + command::execute("lui_open_popup popup_acceptinginvite", false); + // connecting too soon after vid_restart causes a crash ingame (awesome game) + scheduler::once([=]() + { + connect(target); + }, scheduler::pipeline::main, 5s); + }, scheduler::pipeline::main); + + return true; + } + + return false; } } + void menu_error(const std::string& error) + { + // print error to console + console::error("%s\n", error.data()); + + scheduler::once([&]() + { + // check if popup_acceptinginvite is open and close if so + if (game::Menu_IsMenuOpenAndVisible(0, "popup_acceptinginvite")) + { + utils::hook::invoke(0x26BE80_b, 0, "popup_acceptinginvite", 0, *game::hks::lua_state); // LUI_LeaveMenuByName + } + + // set ui error information + utils::hook::invoke(0x17D770_b, error.data(), "MENU_NOTICE"); // Com_SetLocalizedErrorMessage + utils::hook::set(0x2ED2F78_b, 1); + }, scheduler::pipeline::lui); + } + void clear_sv_motd() { party::sv_motd.clear(); @@ -586,6 +670,26 @@ namespace party info.set("playmode", utils::string::va("%i", game::Com_GetCurrentCoDPlayMode())); info.set("sv_running", utils::string::va("%i", get_dvar_bool("sv_running") && !game::VirtualLobby_Loaded())); info.set("dedicated", utils::string::va("%i", get_dvar_bool("dedicated"))); + info.set("sv_wwwBaseUrl", get_dvar_string("sv_wwwBaseUrl")); + + const auto fs_game = get_dvar_string("fs_game"); + info.set("fs_game", fs_game); + + if (mod_hash.has_value()) + { + info.set("modHash", mod_hash.value()); + } + else + { + const auto mod_path = fs_game + "/mod.ff"; + if (utils::io::file_exists(mod_path)) + { + const auto mod_data = utils::io::read_file(mod_path); + const auto sha = utils::cryptography::sha1::compute(mod_data, true); + mod_hash = sha; // todo: clear this somewhere + info.set("modHash", sha); + } + } network::send(target, "infoResponse", info.build(), '\n'); }); @@ -602,54 +706,49 @@ namespace party if (info.get("challenge") != connect_state.challenge) { - const auto str = "Invalid challenge."; - printf("%s\n", str); - menu_error(str); + menu_error("Connection failed: Invalid challenge."); return; } const auto gamename = info.get("gamename"); if (gamename != "H1"s) { - const auto str = "Invalid gamename."; - printf("%s\n", str); - menu_error(str); + menu_error("Connection failed: Invalid gamename."); return; } const auto playmode = info.get("playmode"); if (game::CodPlayMode(std::atoi(playmode.data())) != game::Com_GetCurrentCoDPlayMode()) { - const auto str = "Invalid playmode."; - printf("%s\n", str); - menu_error(str); + menu_error("Connection failed: Invalid playmode."); return; } const auto sv_running = info.get("sv_running"); if (!std::atoi(sv_running.data())) { - const auto str = "Server not running."; - printf("%s\n", str); - menu_error(str); + menu_error("Connection failed: Server not running."); return; } const auto mapname = info.get("mapname"); if (mapname.empty()) { - const auto str = "Invalid map."; - printf("%s\n", str); - menu_error(str); + menu_error("Connection failed: Invalid map."); return; } const auto gametype = info.get("gametype"); if (gametype.empty()) { - const auto str = "Invalid gametype."; - printf("%s\n", str); - menu_error(str); + menu_error("Connection failed: Invalid gametype."); + return; + } + + // returning true doesn't exactly mean there is a error, but more or less means that we are cancelling the connection for more than one different reason. + // if there is a genuine error that occurs, menu_error is called within the function. + if (download_mod(target, info)) + { return; } diff --git a/src/client/component/party.hpp b/src/client/component/party.hpp index 3efa7b79..e770a426 100644 --- a/src/client/component/party.hpp +++ b/src/client/component/party.hpp @@ -3,6 +3,8 @@ namespace party { + void menu_error(const std::string& error); + void reset_connect_state(); void connect(const game::netadr_s& target); diff --git a/src/client/component/ui_scripting.cpp b/src/client/component/ui_scripting.cpp index ac566a2e..d0b3a3b4 100644 --- a/src/client/component/ui_scripting.cpp +++ b/src/client/component/ui_scripting.cpp @@ -9,6 +9,7 @@ #include "localized_strings.hpp" #include "console.hpp" +#include "download.hpp" #include "game_module.hpp" #include "fps.hpp" #include "server_list.hpp" @@ -371,6 +372,11 @@ namespace ui_scripting updater_table["getlasterror"] = updater::get_last_error; updater_table["getcurrentfile"] = updater::get_current_file; + + auto download_table = table(); + lua["download"] = download_table; + + download_table["abort"] = download::stop_download; } void start() diff --git a/src/client/game/scripting/lua/context.cpp b/src/client/game/scripting/lua/context.cpp index 12dd2851..0546a8c1 100644 --- a/src/client/game/scripting/lua/context.cpp +++ b/src/client/game/scripting/lua/context.cpp @@ -705,6 +705,8 @@ namespace scripting::lua const auto& request_callbacks_ = http_requests[cur_task_id]; handle_error(request_callbacks_.on_progress(value)); }, ::scheduler::pipeline::server); + + return 0; }); ::scheduler::once([cur_task_id, result]() diff --git a/src/client/game/symbols.hpp b/src/client/game/symbols.hpp index 225ae112..1bc6c1f2 100644 --- a/src/client/game/symbols.hpp +++ b/src/client/game/symbols.hpp @@ -254,6 +254,9 @@ namespace game WEAK symbol connectionState{0x0, 0x2EC82C8}; + // TODO: move to dvars.cpp when done + WEAK symbol fs_gameDirVal{0x0, 0x2EC86B8}; + WEAK symbol g_poolSize{0x0, 0x0}; WEAK symbol g_compressor{0x2574804, 0x3962804}; diff --git a/src/common/utils/http.cpp b/src/common/utils/http.cpp index 06e6ac77..509e55bf 100644 --- a/src/common/utils/http.cpp +++ b/src/common/utils/http.cpp @@ -8,7 +8,7 @@ namespace utils::http { struct progress_helper { - const std::function* callback{}; + const std::function* callback{}; std::exception_ptr exception{}; }; @@ -18,9 +18,9 @@ namespace utils::http try { - if (*helper->callback) + if (*helper->callback && (*helper->callback)(dlnow) == -1) { - (*helper->callback)(dlnow); + return -1; } } catch (...) @@ -43,7 +43,7 @@ namespace utils::http } std::optional get_data(const std::string& url, const std::string& fields, - const headers& headers, const std::function& callback) + const headers& headers, const std::function& callback) { curl_slist* header_list = nullptr; auto* curl = curl_easy_init(); @@ -104,7 +104,7 @@ namespace utils::http } std::future> get_data_async(const std::string& url, const std::string& fields, - const headers& headers, const std::function& callback) + const headers& headers, const std::function& callback) { return std::async(std::launch::async, [url, fields, headers, callback]() { diff --git a/src/common/utils/http.hpp b/src/common/utils/http.hpp index bb4c90e5..5dc2d579 100644 --- a/src/common/utils/http.hpp +++ b/src/common/utils/http.hpp @@ -18,7 +18,7 @@ namespace utils::http using headers = std::unordered_map; std::optional get_data(const std::string& url, const std::string& fields = {}, - const headers& headers = {}, const std::function& callback = {}); + const headers& headers = {}, const std::function& callback = {}); std::future> get_data_async(const std::string& url, const std::string& fields = {}, - const headers& headers = {}, const std::function& callback = {}); + const headers& headers = {}, const std::function& callback = {}); }