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:
parent
b48e47d244
commit
6d2a5bf5bc
@ -1 +1,5 @@
|
||||
require("loading")
|
||||
|
||||
if (Engine.InFrontend()) then
|
||||
require("download")
|
||||
end
|
||||
|
13
data/cdata/ui_scripts/mods/download.lua
Normal file
13
data/cdata/ui_scripts/mods/download.lua
Normal 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)
|
184
src/client/component/download.cpp
Normal file
184
src/client/component/download.cpp
Normal 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.");
|
||||
}
|
||||
}
|
||||
|
10
src/client/component/download.hpp
Normal file
10
src/client/component/download.hpp
Normal 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();
|
||||
}
|
@ -296,6 +296,18 @@ namespace fastfiles
|
||||
|
||||
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)
|
||||
@ -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)
|
||||
|
@ -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 <utils/info_string.hpp>
|
||||
#include <utils/cryptography.hpp>
|
||||
#include <utils/hook.hpp>
|
||||
#include <utils/io.hpp>
|
||||
|
||||
namespace party
|
||||
{
|
||||
@ -29,6 +33,8 @@ namespace party
|
||||
std::string sv_motd;
|
||||
int sv_maxclients;
|
||||
|
||||
std::optional<std::string> mod_hash{};
|
||||
|
||||
void perform_game_initialization()
|
||||
{
|
||||
command::execute("onlinegame 1", true);
|
||||
@ -144,13 +150,91 @@ namespace party
|
||||
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");
|
||||
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<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()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,8 @@
|
||||
|
||||
namespace party
|
||||
{
|
||||
void menu_error(const std::string& error);
|
||||
|
||||
void reset_connect_state();
|
||||
|
||||
void connect(const game::netadr_s& target);
|
||||
|
@ -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()
|
||||
|
@ -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]()
|
||||
|
@ -254,6 +254,9 @@ namespace game
|
||||
|
||||
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_compressor{0x2574804, 0x3962804};
|
||||
|
||||
|
@ -8,7 +8,7 @@ namespace utils::http
|
||||
{
|
||||
struct progress_helper
|
||||
{
|
||||
const std::function<void(size_t)>* callback{};
|
||||
const std::function<int(size_t)>* 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<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;
|
||||
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,
|
||||
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]()
|
||||
{
|
||||
|
@ -18,7 +18,7 @@ namespace utils::http
|
||||
using headers = std::unordered_map<std::string, std::string>;
|
||||
|
||||
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 = {},
|
||||
const headers& headers = {}, const std::function<void(size_t)>& callback = {});
|
||||
const headers& headers = {}, const std::function<int(size_t)>& callback = {});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user