#include "STDInc.hpp" #include namespace Components { bool Updater::UpdateRestart; bool Updater::AutomaticUpdate; Utils::ConcurrentList::Container Updater::update_data; std::string GetDLLName() { static const auto name = "game.dll"; return name; } std::string GetExeName() { static const auto name = "iw3sp-mod.exe"; return name; } void Updater::SetUpdateDownloadStatus(bool done, bool success) { update_data.access([done, success](Updater::update_data_t& data_) { data_.download.done = done; data_.download.success = success; }); } bool Updater::CheckFile(const std::string& name, const std::string& sha) { std::string data; if (name == GetDLLName() || name == GetExeName()) { if (!Utils::IO::ReadFile(name, &data)) { return false; } } else { std::filesystem::path game_folder = Dvars::Functions::Dvar_FindVar("fs_game")->current.string; const auto path = (game_folder / name).generic_string(); if (!Utils::IO::ReadFile(path, &data)) { return false; } } // Checking out our SHA file checksum if (Utils::Cryptography::SHA1::Compute(data, true) != sha) { return false; } return true; } bool Updater::WriteFile(const std::string& name, const std::string& data) { if (name == GetDLLName() && Utils::IO::FileExists(name) && !Utils::IO::moveFile(name, name + ".old")) { return false; } if (name == GetExeName() && Utils::IO::FileExists(name) && !Utils::IO::moveFile(name, name + ".old")) { return false; } std::filesystem::path game_folder = Dvars::Functions::Dvar_FindVar("fs_game")->current.string; const auto path = (game_folder / name).generic_string(); return Utils::IO::WriteFile(path, data); } std::string Updater::get_time_str() { return Utils::String::VA("%i", uint32_t(time(nullptr))); } std::optional Updater::DownloadFile(const std::string& name) { return Utils::HTTP::GetData(URL_MASTER + name + "?" + Updater::get_time_str(), {}, {}, true); } std::vector Updater::FindGarbageFiles(const std::vector& update_files) { std::vector garbage_files{}; std::filesystem::path game_folder = Dvars::Functions::Dvar_FindVar("fs_game")->current.string; const auto path = (game_folder).generic_string(); if (!Utils::IO::DirectoryExists(path)) { return {}; } const auto current_files = Utils::IO::ListFiles(path); for (const auto& file : current_files) { bool found = false; for (const auto& update_file : update_files) { const auto update_file_ = (game_folder / update_file).generic_string(); const auto path_a = std::filesystem::path(file); const auto path_b = std::filesystem::path(update_file_); const auto is_directory = Utils::IO::DirectoryExists(file); const auto compare = path_a.compare(path_b); if ((is_directory && compare == -1) || compare == 0) { found = true; break; } } if (!found) { Game::Com_Printf(0, "[Updater] Found extra file %s\n", file.data()); if (file.ends_with(".ff")) { update_data.access([](Updater::update_data_t& data_) { data_.restart_required = true; }); } garbage_files.push_back(file); } } return garbage_files; } void Updater::ResetData() { update_data.access([](Updater::update_data_t& data_) { data_ = {}; }); } void Updater::CancelUpdate() { Game::Com_Printf(0, "[Updater]: Cancelling update\n"); return update_data.access([](Updater::update_data_t& data_) { data_.cancelled = true; }); } bool Updater::UpdateAvailable() { return update_data.access([](Updater::update_data_t& data_) { return data_.required_files.size() > 0 || data_.garbage_files.size() > 0; }); } bool Updater::UpdateCancelled() { return update_data.access([](Updater::update_data_t& data_) { return data_.cancelled; }); } void Updater::CL_StartUpdate() { if (Updater::UpdateCancelled()) { return; } const auto garbage_files = update_data.access>([](Updater::update_data_t& data_) { return data_.garbage_files; }); for (const auto& file : garbage_files) { try { std::filesystem::remove_all(file); } catch (...) { Game::Com_Printf(0, "[Updater]: ^1Failed to delete %s\n", file.c_str()); } } Scheduler::Once([] { Game::menuDef_t* menu = Game::DB_FindXAssetHeader(Game::ASSET_TYPE_MENU, "updater_download_menu").menu; const auto required_files = update_data.access>([](Updater::update_data_t& data_) { return data_.required_files; }); std::vector downloads; for (const auto& file : required_files) { update_data.access([&](Updater::update_data_t& data_) { data_.current_file = file.name; }); menu->items[4]->text = file.name.c_str(); Game::Com_Printf(0, "[Updater]: Downloading file %s\n", file.name.data()); const auto data = Updater::DownloadFile(file.name); if (Updater::UpdateCancelled()) { Updater::ResetData(); return; } if (!data.has_value()) { Updater::SetUpdateDownloadStatus(true, false); Game::Com_Printf(0, "[Updater]: ^1Failed to download file %s\n", file.name.c_str()); return; } const auto& value = data.value(); if (file.hash != Utils::Cryptography::SHA1::Compute(value, true)) { Updater::SetUpdateDownloadStatus(true, false); Game::Com_Printf(0, "[Updater]: ^1Failed to download file %s\n", file.name.c_str()); return; } downloads.emplace_back(file.name, data.value()); } for (const auto& download : downloads) { if (!Updater::WriteFile(download.name, download.data)) { Updater::SetUpdateDownloadStatus(true, false); Game::Com_Printf(0, "[Updater]: ^1Failed to write file %s\n", download.name.c_str()); return; } } Updater::SetUpdateDownloadStatus(true, true); menu->items[4]->text = ""; Updater::ResetData(); Updater::UpdateRestart = true; Command::Execute("closemenu updater_download_menu", false); Command::Execute("openmenu updater_restart", false); }, Scheduler::Pipeline::ASYNC); } void Updater::CL_GetAutoUpdate(bool needCheck) { if (needCheck) { Updater::AutomaticUpdate = true; // If used disable the auto-update then just exit from function. if (!Dvars::Functions::Dvar_FindVar("iw3sp_auto_update") || !Dvars::Functions::Dvar_FindVar("iw3sp_auto_update")->current.enabled) return; } else Updater::AutomaticUpdate = false; CancelUpdate(); ResetData(); Scheduler::Once([] { const auto files_data = Utils::HTTP::GetData(URL_MASTER + "files.json"s, {}, {}, false); if (UpdateCancelled()) { ResetData(); return; } // Check the result of our existence request json list or when our request cannot reach the server, then drop error. if (!files_data.has_value()) { Game::Com_Printf(0, "[Updater]: ^1json file is empty or HTTP request can't be reach to the server. Abort The Update.\n"); if (!Updater::AutomaticUpdate) { Command::Execute("closemenu updater_checking_for_updates", false); Command::Execute("closemenu updater_checking_for_updates_internal", false); Command::Execute("openmenu updater_server_offline", false); } return; } rapidjson::Document j; j.Parse(files_data.value().data()); if (!j.IsArray()) { SetUpdateDownloadStatus(true, false); Game::Com_Printf(0, "[Updater]: ^1json file doesn't have array list. Abort The Update.\n"); return; } std::vector required_files; std::vector update_files; const auto files = j.GetArray(); for (const auto& file : files) { if (!file.IsArray() || file.Size() != 3 || !file[0].IsString() || !file[2].IsString()) { continue; } const auto name = file[0].GetString(); const auto sha = file[2].GetString(); update_files.push_back(name); if (!CheckFile(name, sha)) { std::string name_ = name; if (name_.contains(GetDLLName()) || name_.contains(GetExeName())) { update_data.access([](update_data_t& data_) { data_.restart_required = true; }); } if (name_.ends_with(".ff")) { update_data.access([](update_data_t& data_) { data_.restart_required = true; }); } Game::Com_Printf(0, "[Updater]: ^3File which need download - %s\n", name); required_files.emplace_back(name, sha); } } const auto garbage_files = FindGarbageFiles(update_files); update_data.access([&](update_data_t& data_) { data_.check.done = true; data_.check.success = true; data_.required_files = required_files; data_.garbage_files = garbage_files; }); if (UpdateAvailable()) { Command::Execute("closemenu updater_checking_for_updates", false); Command::Execute("closemenu updater_checking_for_updates_internal", false); Command::Execute("openmenu updater_popmenu", false); } else { if (!Updater::AutomaticUpdate) { Command::Execute("closemenu updater_checking_for_updates", false); Command::Execute("closemenu updater_checking_for_updates_internal", false); Command::Execute("openmenu updater_new_updates_no_found", false); } } }, Scheduler::Pipeline::ASYNC); } void Updater::DeleteOldFiles() { Utils::IO::RemoveFile("__iw3sp_mod"); Utils::IO::RemoveFile(GetDLLName() + ".old"); Utils::IO::RemoveFile(GetExeName() + ".old"); } Updater::Updater() { Scheduler::Once([] { // Deleting our garbage files after auto-updating :> Updater::DeleteOldFiles(); }, Scheduler::Pipeline::MAIN); Events::OnDvarInit([] { Game::dvar_s* cl_auto_update = Dvars::Register::Dvar_RegisterBool("iw3sp_auto_update", "Enable automatic update checks on launch.", true, Game::saved); }); UIScript::Add("update_download_cancel", []([[maybe_unused]] const UIScript::Token& token, [[maybe_unused]] const Game::uiInfo_s* info) { Updater::CancelUpdate(); }); UIScript::Add("check_avaliable_updates_internal", []([[maybe_unused]] const UIScript::Token& token, [[maybe_unused]] const Game::uiInfo_s* info) { Updater::CL_GetAutoUpdate(false); }); UIScript::Add("check_avaliable_updates", []([[maybe_unused]] const UIScript::Token& token, [[maybe_unused]] const Game::uiInfo_s* info) { Updater::CL_GetAutoUpdate(true); }); UIScript::Add("update_start_download", []([[maybe_unused]] const UIScript::Token& token, [[maybe_unused]] const Game::uiInfo_s* info) { Updater::CL_StartUpdate(); }); } Updater::~Updater() { } }