diff --git a/src/Components/Loader.cpp b/src/Components/Loader.cpp index fc09bb18..f50b2689 100644 --- a/src/Components/Loader.cpp +++ b/src/Components/Loader.cpp @@ -59,6 +59,7 @@ #include "Modules/Threading.hpp" #include "Modules/Toast.hpp" #include "Modules/UIFeeder.hpp" +#include "Modules/Updater.hpp" #include "Modules/VisionFile.hpp" #include "Modules/Voice.hpp" #include "Modules/Vote.hpp" @@ -172,6 +173,7 @@ namespace Components Register(new Threading()); Register(new Toast()); Register(new UIFeeder()); + Register(new Updater()); Register(new VisionFile()); Register(new Voice()); Register(new Vote()); diff --git a/src/Components/Modules/QuickPatch.cpp b/src/Components/Modules/QuickPatch.cpp index fb3a1561..e8a7ded9 100644 --- a/src/Components/Modules/QuickPatch.cpp +++ b/src/Components/Modules/QuickPatch.cpp @@ -246,9 +246,10 @@ namespace Components auto workingDir = std::filesystem::current_path().string(); const std::string binary = *Game::sys_exitCmdLine; + const std::string command = binary == "iw4x-sp.exe" ? "iw4x-sp" : "iw4x"; SetEnvironmentVariableA("MW2_INSTALL", workingDir.data()); - Utils::Library::LaunchProcess(binary, "-singleplayer", workingDir); + Utils::Library::LaunchProcess(binary, std::format("{} --pass \"{}\"", command, GetCommandLineA()), workingDir); } __declspec(naked) void QuickPatch::SND_GetAliasOffset_Stub() @@ -320,7 +321,7 @@ namespace Components Utils::Hook::Set(0x51FCDD, QuickPatch::R_AddImageToList_Hk); - Utils::Hook::Set(0x41DB8C, "iw4-sp.exe"); + Utils::Hook::Set(0x41DB8C, "iw4x-sp.exe"); Utils::Hook(0x4D6989, QuickPatch::Sys_SpawnQuitProcess_Hk, HOOK_CALL).install()->quick(); // Fix crash as nullptr goes unchecked diff --git a/src/Components/Modules/Singleton.cpp b/src/Components/Modules/Singleton.cpp index 9ae0b43b..60c7e520 100644 --- a/src/Components/Modules/Singleton.cpp +++ b/src/Components/Modules/Singleton.cpp @@ -6,6 +6,8 @@ namespace Components { + HANDLE Singleton::Mutex; + bool Singleton::FirstInstance = true; bool Singleton::IsFirstInstance() @@ -13,6 +15,14 @@ namespace Components return FirstInstance; } + void Singleton::preDestroy() + { + if (INVALID_HANDLE_VALUE != Mutex) + { + CloseHandle(Mutex); + } + } + Singleton::Singleton() { if (Flags::HasFlag("version")) @@ -31,7 +41,8 @@ namespace Components if (Loader::IsPerformingUnitTests() || Dedicated::IsEnabled() || ZoneBuilder::IsEnabled()) return; - FirstInstance = (CreateMutexA(nullptr, FALSE, "iw4x_mutex") && GetLastError() != ERROR_ALREADY_EXISTS); + Mutex = CreateMutexA(nullptr, FALSE, "iw4x_mutex"); + FirstInstance = ((INVALID_HANDLE_VALUE != Mutex) && GetLastError() != ERROR_ALREADY_EXISTS); if (!FirstInstance && !ConnectProtocol::Used() && MessageBoxA(nullptr, "Do you want to start another instance?\nNot all features will be available!", "Game already running", MB_ICONEXCLAMATION | MB_YESNO) == IDNO) { diff --git a/src/Components/Modules/Singleton.hpp b/src/Components/Modules/Singleton.hpp index db2a11c8..1f5de4e3 100644 --- a/src/Components/Modules/Singleton.hpp +++ b/src/Components/Modules/Singleton.hpp @@ -7,9 +7,12 @@ namespace Components public: Singleton(); + void preDestroy() override; + static bool IsFirstInstance(); private: + static HANDLE Mutex; static bool FirstInstance; }; } diff --git a/src/Components/Modules/Updater.cpp b/src/Components/Modules/Updater.cpp new file mode 100644 index 00000000..9a5964aa --- /dev/null +++ b/src/Components/Modules/Updater.cpp @@ -0,0 +1,106 @@ +#include + +#include "Updater.hpp" +#include "Scheduler.hpp" + +#include + +#include +#include +#include + +namespace Components +{ + namespace + { + const Game::dvar_t* cl_updateAvailable; + + // If they use the alterware-launcher once to install they will have this file + // If they don't, what are they waiting for? + constexpr auto* revisionFile = ".iw4xrevision"; + + void CheckForUpdate() + { + std::string revision; + if (!Utils::IO::ReadFile(revisionFile, &revision) || revision.empty()) + { + Logger::Print(".iw4xrevision does not exist. Notifying the user an update is available\n"); + Game::Dvar_SetBool(cl_updateAvailable, true); + } + + const auto result = Utils::WebIO("IW4x", "https://api.github.com/repos/iw4x/iw4x-client/releases/latest").setTimeout(5000)->get(); + if (result.empty()) + { + // Nothing to do in this situation. We won't know if we need to update or not + Logger::Print("Could not fetch latest tag from GitHub\n"); + return; + } + + rapidjson::Document doc{}; + const rapidjson::ParseResult parseResult = doc.Parse(result); + if (!parseResult || !doc.IsObject()) + { + // Nothing to do in this situation. We won't know if we need to update or not + Logger::Print("GitHub sent an invalid reply\n"); + return; + } + + if (!doc.HasMember("tag_name") || !doc["tag_name"].IsString()) + { + // Nothing to do in this situation. We won't know if we need to update or not + Logger::Print("GitHub sent an invalid reply\n"); + return; + } + + const auto* tag = doc["tag_name"].GetString(); + if (revision != tag) + { + // A new version came out! + Game::Dvar_SetBool(cl_updateAvailable, true); + } + } + + std::optional GetLauncher() + { + if (Utils::IO::FileExists("alterware-launcher.exe")) + { + return "alterware-launcher.exe"; + } + + if (Utils::IO::FileExists("alterware-launcher-x86.exe")) + { + return "alterware-launcher-x86.exe"; + } + + if (Utils::IsWineEnvironment() && Utils::IO::FileExists("alterware-launcher")) + { + return "alterware-launcher"; + } + + return {}; + } + } + + Updater::Updater() + { + cl_updateAvailable = Game::Dvar_RegisterBool("cl_updateAvailable", false, Game::DVAR_NONE, "Whether an update is available or not"); + Scheduler::Once(CheckForUpdate, Scheduler::Pipeline::ASYNC); + + UIScript::Add("checkForUpdate", [](const UIScript::Token& /*token*/, const Game::uiInfo_s* /*info*/) + { + CheckForUpdate(); + }); + + UIScript::Add("getAutoUpdate", [](const UIScript::Token& /*token*/, const Game::uiInfo_s* /*info*/) + { + if (const auto exe = GetLauncher(); exe.has_value()) + { + Game::Sys_QuitAndStartProcess(exe.value().data()); + return; + } + + // No launcher was found on the system, time to tell them to download it from GitHub + Utils::OpenUrl("https://forum.alterware.dev/t/how-to-install-the-alterware-launcher/56"); + }); + } +} diff --git a/src/Components/Modules/Updater.hpp b/src/Components/Modules/Updater.hpp new file mode 100644 index 00000000..46e65980 --- /dev/null +++ b/src/Components/Modules/Updater.hpp @@ -0,0 +1,10 @@ +#pragma once + +namespace Components +{ + class Updater : public Component + { + public: + Updater(); + }; +} diff --git a/src/Game/System.cpp b/src/Game/System.cpp index 50f9d3ab..0571204a 100644 --- a/src/Game/System.cpp +++ b/src/Game/System.cpp @@ -25,6 +25,7 @@ namespace Game Sys_SetValue_t Sys_SetValue = Sys_SetValue_t(0x4B2F50); Sys_CreateFile_t Sys_CreateFile = Sys_CreateFile_t(0x4B2EF0); Sys_OutOfMemErrorInternal_t Sys_OutOfMemErrorInternal = Sys_OutOfMemErrorInternal_t(0x4B2E60); + Sys_QuitAndStartProcess_t Sys_QuitAndStartProcess = Sys_QuitAndStartProcess_t(0x45FCF0); char(*sys_exitCmdLine)[1024] = reinterpret_cast(0x649FB68); diff --git a/src/Game/System.hpp b/src/Game/System.hpp index 70b37c1c..84aff611 100644 --- a/src/Game/System.hpp +++ b/src/Game/System.hpp @@ -71,6 +71,9 @@ namespace Game typedef void(*Sys_OutOfMemErrorInternal_t)(const char* filename, int line); extern Sys_OutOfMemErrorInternal_t Sys_OutOfMemErrorInternal; + typedef void(*Sys_QuitAndStartProcess_t)(const char*); + extern Sys_QuitAndStartProcess_t Sys_QuitAndStartProcess; + extern char(*sys_exitCmdLine)[1024]; extern RTL_CRITICAL_SECTION* s_criticalSection; diff --git a/src/Utils/Library.cpp b/src/Utils/Library.cpp index f7740ff5..70d661de 100644 --- a/src/Utils/Library.cpp +++ b/src/Utils/Library.cpp @@ -104,20 +104,20 @@ namespace Utils void Library::LaunchProcess(const std::string& process, const std::string& commandLine, const std::string& currentDir) { - STARTUPINFOA startup_info; - PROCESS_INFORMATION process_info; + STARTUPINFOA startupInfo; + PROCESS_INFORMATION processInfo; - ZeroMemory(&startup_info, sizeof(startup_info)); - ZeroMemory(&process_info, sizeof(process_info)); - startup_info.cb = sizeof(startup_info); + ZeroMemory(&startupInfo, sizeof(startupInfo)); + ZeroMemory(&processInfo, sizeof(processInfo)); + startupInfo.cb = sizeof(startupInfo); CreateProcessA(process.data(), const_cast(commandLine.data()), nullptr, nullptr, false, NULL, nullptr, currentDir.data(), - &startup_info, &process_info); + &startupInfo, &processInfo); - if (process_info.hThread && process_info.hThread != INVALID_HANDLE_VALUE) - CloseHandle(process_info.hThread); - if (process_info.hProcess && process_info.hProcess != INVALID_HANDLE_VALUE) - CloseHandle(process_info.hProcess); + if (processInfo.hThread && processInfo.hThread != INVALID_HANDLE_VALUE) + CloseHandle(processInfo.hThread); + if (processInfo.hProcess && processInfo.hProcess != INVALID_HANDLE_VALUE) + CloseHandle(processInfo.hProcess); } }