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 <fedecek3@gmail.com>
This commit is contained in:
m 2022-09-11 08:07:19 -05:00
parent b48e47d244
commit 6d2a5bf5bc
12 changed files with 369 additions and 28 deletions

View File

@ -1 +1,5 @@
require("loading") require("loading")
if (Engine.InFrontend()) then
require("download")
end

View File

@ -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)

View File

@ -0,0 +1,184 @@
#include <std_include.hpp>
#include "loader/component_loader.hpp"
#include "download.hpp"
#include "console.hpp"
#include "scheduler.hpp"
#include "party.hpp"
#include "game/ui_scripting/execution.hpp"
#include <utils/concurrency.hpp>
#include <utils/http.hpp>
#include <utils/io.hpp>
namespace download
{
namespace
{
struct globals_t
{
bool abort{};
bool active{};
};
utils::concurrency::container<globals_t> globals;
bool download_aborted()
{
return globals.access<bool>([](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<bool>([](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.");
}
}

View File

@ -0,0 +1,10 @@
#pragma once
#include "game/game.hpp"
#include <utils/info_string.hpp>
namespace download
{
void start_download(const game::netadr_s& target, const utils::info_string& info);
void stop_download();
}

View File

@ -296,6 +296,18 @@ namespace fastfiles
game::DB_LoadXAssets(data.data(), static_cast<std::uint32_t>(data.size()), syncMode); game::DB_LoadXAssets(data.data(), static_cast<std::uint32_t>(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<void>(0x39CA90_b, a1);
}
} }
bool exists(const std::string& zone) 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); 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) command::add("loadzone", [](const command::params& params)
{ {
if (params.size() < 2) if (params.size() < 2)

View File

@ -7,6 +7,9 @@
#include "network.hpp" #include "network.hpp"
#include "scheduler.hpp" #include "scheduler.hpp"
#include "server_list.hpp" #include "server_list.hpp"
#include "download.hpp"
#include "game/game.hpp"
#include "steam/steam.hpp" #include "steam/steam.hpp"
@ -14,6 +17,7 @@
#include <utils/info_string.hpp> #include <utils/info_string.hpp>
#include <utils/cryptography.hpp> #include <utils/cryptography.hpp>
#include <utils/hook.hpp> #include <utils/hook.hpp>
#include <utils/io.hpp>
namespace party namespace party
{ {
@ -29,6 +33,8 @@ namespace party
std::string sv_motd; std::string sv_motd;
int sv_maxclients; int sv_maxclients;
std::optional<std::string> mod_hash{};
void perform_game_initialization() void perform_game_initialization()
{ {
command::execute("onlinegame 1", true); command::execute("onlinegame 1", true);
@ -144,13 +150,91 @@ namespace party
cl_disconnect_hook.invoke<void>(showMainMenu); cl_disconnect_hook.invoke<void>(showMainMenu);
} }
void menu_error(const std::string& error) bool download_mod(const game::netadr_s& target, const utils::info_string& info)
{ {
utils::hook::invoke<void>(0x17D770_b, error.data(), "MENU_NOTICE"); const auto server_fs_game = utils::string::to_lower(info.get("fs_game"));
utils::hook::set(0x2ED2F78_b, 1); 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<void>(0x26BE80_b, 0, "popup_acceptinginvite", 0, *game::hks::lua_state); // LUI_LeaveMenuByName
}
// set ui error information
utils::hook::invoke<void>(0x17D770_b, error.data(), "MENU_NOTICE"); // Com_SetLocalizedErrorMessage
utils::hook::set(0x2ED2F78_b, 1);
}, scheduler::pipeline::lui);
}
void clear_sv_motd() void clear_sv_motd()
{ {
party::sv_motd.clear(); party::sv_motd.clear();
@ -586,6 +670,26 @@ namespace party
info.set("playmode", utils::string::va("%i", game::Com_GetCurrentCoDPlayMode())); 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("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("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'); network::send(target, "infoResponse", info.build(), '\n');
}); });
@ -602,54 +706,49 @@ namespace party
if (info.get("challenge") != connect_state.challenge) if (info.get("challenge") != connect_state.challenge)
{ {
const auto str = "Invalid challenge."; menu_error("Connection failed: Invalid challenge.");
printf("%s\n", str);
menu_error(str);
return; return;
} }
const auto gamename = info.get("gamename"); const auto gamename = info.get("gamename");
if (gamename != "H1"s) if (gamename != "H1"s)
{ {
const auto str = "Invalid gamename."; menu_error("Connection failed: Invalid gamename.");
printf("%s\n", str);
menu_error(str);
return; return;
} }
const auto playmode = info.get("playmode"); const auto playmode = info.get("playmode");
if (game::CodPlayMode(std::atoi(playmode.data())) != game::Com_GetCurrentCoDPlayMode()) if (game::CodPlayMode(std::atoi(playmode.data())) != game::Com_GetCurrentCoDPlayMode())
{ {
const auto str = "Invalid playmode."; menu_error("Connection failed: Invalid playmode.");
printf("%s\n", str);
menu_error(str);
return; return;
} }
const auto sv_running = info.get("sv_running"); const auto sv_running = info.get("sv_running");
if (!std::atoi(sv_running.data())) if (!std::atoi(sv_running.data()))
{ {
const auto str = "Server not running."; menu_error("Connection failed: Server not running.");
printf("%s\n", str);
menu_error(str);
return; return;
} }
const auto mapname = info.get("mapname"); const auto mapname = info.get("mapname");
if (mapname.empty()) if (mapname.empty())
{ {
const auto str = "Invalid map."; menu_error("Connection failed: Invalid map.");
printf("%s\n", str);
menu_error(str);
return; return;
} }
const auto gametype = info.get("gametype"); const auto gametype = info.get("gametype");
if (gametype.empty()) if (gametype.empty())
{ {
const auto str = "Invalid gametype."; menu_error("Connection failed: Invalid gametype.");
printf("%s\n", str); return;
menu_error(str); }
// 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; return;
} }

View File

@ -3,6 +3,8 @@
namespace party namespace party
{ {
void menu_error(const std::string& error);
void reset_connect_state(); void reset_connect_state();
void connect(const game::netadr_s& target); void connect(const game::netadr_s& target);

View File

@ -9,6 +9,7 @@
#include "localized_strings.hpp" #include "localized_strings.hpp"
#include "console.hpp" #include "console.hpp"
#include "download.hpp"
#include "game_module.hpp" #include "game_module.hpp"
#include "fps.hpp" #include "fps.hpp"
#include "server_list.hpp" #include "server_list.hpp"
@ -371,6 +372,11 @@ namespace ui_scripting
updater_table["getlasterror"] = updater::get_last_error; updater_table["getlasterror"] = updater::get_last_error;
updater_table["getcurrentfile"] = updater::get_current_file; updater_table["getcurrentfile"] = updater::get_current_file;
auto download_table = table();
lua["download"] = download_table;
download_table["abort"] = download::stop_download;
} }
void start() void start()

View File

@ -705,6 +705,8 @@ namespace scripting::lua
const auto& request_callbacks_ = http_requests[cur_task_id]; const auto& request_callbacks_ = http_requests[cur_task_id];
handle_error(request_callbacks_.on_progress(value)); handle_error(request_callbacks_.on_progress(value));
}, ::scheduler::pipeline::server); }, ::scheduler::pipeline::server);
return 0;
}); });
::scheduler::once([cur_task_id, result]() ::scheduler::once([cur_task_id, result]()

View File

@ -254,6 +254,9 @@ namespace game
WEAK symbol<int> connectionState{0x0, 0x2EC82C8}; WEAK symbol<int> connectionState{0x0, 0x2EC82C8};
// TODO: move to dvars.cpp when done
WEAK symbol<dvar_t> fs_gameDirVal{0x0, 0x2EC86B8};
WEAK symbol<int> g_poolSize{0x0, 0x0}; WEAK symbol<int> g_poolSize{0x0, 0x0};
WEAK symbol<int> g_compressor{0x2574804, 0x3962804}; WEAK symbol<int> g_compressor{0x2574804, 0x3962804};

View File

@ -8,7 +8,7 @@ namespace utils::http
{ {
struct progress_helper struct progress_helper
{ {
const std::function<void(size_t)>* callback{}; const std::function<int(size_t)>* callback{};
std::exception_ptr exception{}; std::exception_ptr exception{};
}; };
@ -18,9 +18,9 @@ namespace utils::http
try try
{ {
if (*helper->callback) if (*helper->callback && (*helper->callback)(dlnow) == -1)
{ {
(*helper->callback)(dlnow); return -1;
} }
} }
catch (...) catch (...)
@ -43,7 +43,7 @@ namespace utils::http
} }
std::optional<result> get_data(const std::string& url, const std::string& fields, std::optional<result> get_data(const std::string& url, const std::string& fields,
const headers& headers, const std::function<void(size_t)>& callback) const headers& headers, const std::function<int(size_t)>& callback)
{ {
curl_slist* header_list = nullptr; curl_slist* header_list = nullptr;
auto* curl = curl_easy_init(); auto* curl = curl_easy_init();
@ -104,7 +104,7 @@ namespace utils::http
} }
std::future<std::optional<result>> get_data_async(const std::string& url, const std::string& fields, std::future<std::optional<result>> get_data_async(const std::string& url, const std::string& fields,
const headers& headers, const std::function<void(size_t)>& callback) const headers& headers, const std::function<int(size_t)>& callback)
{ {
return std::async(std::launch::async, [url, fields, headers, callback]() return std::async(std::launch::async, [url, fields, headers, callback]()
{ {

View File

@ -18,7 +18,7 @@ namespace utils::http
using headers = std::unordered_map<std::string, std::string>; using headers = std::unordered_map<std::string, std::string>;
std::optional<result> get_data(const std::string& url, const std::string& fields = {}, std::optional<result> get_data(const std::string& url, const std::string& fields = {},
const headers& headers = {}, const std::function<void(size_t)>& callback = {}); const headers& headers = {}, const std::function<int(size_t)>& callback = {});
std::future<std::optional<result>> get_data_async(const std::string& url, const std::string& fields = {}, std::future<std::optional<result>> get_data_async(const std::string& url, const std::string& fields = {},
const headers& headers = {}, const std::function<void(size_t)>& callback = {}); const headers& headers = {}, const std::function<int(size_t)>& callback = {});
} }