From cd38776576e97f52c82bcfeb817cc6c4f78486ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo?= Date: Fri, 31 Jan 2025 10:47:15 -0300 Subject: [PATCH] Update checker. (#251) * Update checker. * Fix build and enum class. * Get rid of submodule for httplib. * Get rid of submodule for curl. * Minor style changes and fix video.cpp Linux build error. * CTitleStateIntro_patches: implemented update message * Update update_checker.cpp * CTitleStateIntro_patches: fix fade out accepting input --------- Co-authored-by: Hyper <34012267+hyperbx@users.noreply.github.com> --- .gitmodules | 3 + UnleashedRecomp/CMakeLists.txt | 4 + UnleashedRecomp/gpu/video.cpp | 2 - UnleashedRecomp/install/update_checker.cpp | 170 ++++++++++++++++++ UnleashedRecomp/install/update_checker.h | 18 ++ UnleashedRecomp/locale/locale.cpp | 8 +- UnleashedRecomp/main.cpp | 17 ++ .../patches/CTitleStateIntro_patches.cpp | 69 +++++-- UnleashedRecomp/user/config_def.h | 2 + thirdparty/json | 1 + vcpkg.json | 3 +- 11 files changed, 282 insertions(+), 15 deletions(-) create mode 100644 UnleashedRecomp/install/update_checker.cpp create mode 100644 UnleashedRecomp/install/update_checker.h create mode 160000 thirdparty/json diff --git a/.gitmodules b/.gitmodules index 8f94ef0a..dce7e560 100644 --- a/.gitmodules +++ b/.gitmodules @@ -61,3 +61,6 @@ [submodule "thirdparty/implot"] path = thirdparty/implot url = https://github.com/epezent/implot.git +[submodule "thirdparty/json"] + path = thirdparty/json + url = https://github.com/nlohmann/json diff --git a/UnleashedRecomp/CMakeLists.txt b/UnleashedRecomp/CMakeLists.txt index a46210ce..c44c4b35 100644 --- a/UnleashedRecomp/CMakeLists.txt +++ b/UnleashedRecomp/CMakeLists.txt @@ -164,6 +164,7 @@ set(UNLEASHED_RECOMP_INSTALL_CXX_SOURCES "install/installer.cpp" "install/iso_file_system.cpp" "install/memory_mapped_file.cpp" + "install/update_checker.cpp" "install/xcontent_file_system.cpp" "install/xex_patcher.cpp" "install/hashes/apotos_shamar.cpp" @@ -208,6 +209,7 @@ set(UNLEASHED_RECOMP_THIRDPARTY_INCLUDES "${UNLEASHED_RECOMP_THIRDPARTY_ROOT}/ddspp" "${UNLEASHED_RECOMP_THIRDPARTY_ROOT}/imgui" "${UNLEASHED_RECOMP_THIRDPARTY_ROOT}/implot" + "${UNLEASHED_RECOMP_THIRDPARTY_ROOT}/json/include" "${UNLEASHED_RECOMP_THIRDPARTY_ROOT}/libmspack/libmspack/mspack" "${UNLEASHED_RECOMP_THIRDPARTY_ROOT}/magic_enum/include" "${UNLEASHED_RECOMP_THIRDPARTY_ROOT}/stb" @@ -321,6 +323,7 @@ if (CMAKE_SYSTEM_NAME MATCHES "Linux") endif() find_package(directx-dxc REQUIRED) +find_package(CURL REQUIRED) if (UNLEASHED_RECOMP_D3D12) file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/D3D12) @@ -366,6 +369,7 @@ target_link_libraries(UnleashedRecomp PRIVATE tomlplusplus::tomlplusplus UnleashedRecompLib xxHash::xxhash + CURL::libcurl ) target_include_directories(UnleashedRecomp PRIVATE diff --git a/UnleashedRecomp/gpu/video.cpp b/UnleashedRecomp/gpu/video.cpp index 3547b08a..3c640a61 100644 --- a/UnleashedRecomp/gpu/video.cpp +++ b/UnleashedRecomp/gpu/video.cpp @@ -6094,9 +6094,7 @@ static void CompileParticleMaterialPipeline(const Hedgehog::Sparkle::CParticleMa } } -#ifdef _DEBUG static std::thread::id g_mainThreadId = std::this_thread::get_id(); -#endif // SWA::CGameModeStage::ExitLoading PPC_FUNC_IMPL(__imp__sub_825369A0); diff --git a/UnleashedRecomp/install/update_checker.cpp b/UnleashedRecomp/install/update_checker.cpp new file mode 100644 index 00000000..5315a67b --- /dev/null +++ b/UnleashedRecomp/install/update_checker.cpp @@ -0,0 +1,170 @@ +#include "update_checker.h" + +#include +#include + +#include "version.h" + +#ifdef WIN32 +#include +#endif + +// UpdateChecker + +using json = nlohmann::json; + +static const char *CHECK_URL = "https://api.github.com/repos/hedge-dev/UnleashedRecomp/releases/latest"; +static const char *VISIT_URL = "https://github.com/hedge-dev/UnleashedRecomp/releases/latest"; +static const char *USER_AGENT = "UnleashedRecomp-Agent"; + +static std::atomic g_updateCheckerInProgress = false; +static std::atomic g_updateCheckerFinished = false; +static UpdateChecker::Result g_updateCheckerResult = UpdateChecker::Result::NotStarted; + +size_t updateCheckerWriteCallback(void *contents, size_t size, size_t nmemb, std::string *output) +{ + size_t totalSize = size * nmemb; + output->append((char *)contents, totalSize); + return totalSize; +} + +static bool parseVersion(const std::string &versionStr, int &major, int &minor, int &revision) +{ + size_t start = 0; + if (versionStr[0] == 'v') + { + start = 1; + } + + size_t firstDot = versionStr.find('.', start); + size_t secondDot = versionStr.find('.', firstDot + 1); + + if (firstDot == std::string::npos || secondDot == std::string::npos) + { + return false; + } + + try + { + major = std::stoi(versionStr.substr(start, firstDot - start)); + minor = std::stoi(versionStr.substr(firstDot + 1, secondDot - firstDot - 1)); + revision = std::stoi(versionStr.substr(secondDot + 1)); + } + catch (const std::exception &e) + { + fmt::println("Error while parsing version: {}.", e.what()); + return false; + } + + return true; +} + +void updateCheckerThread() +{ + CURL *curl = curl_easy_init(); + CURLcode res; + int major, minor, revision; + std::string response; + curl_easy_setopt(curl, CURLOPT_URL, CHECK_URL); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, updateCheckerWriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_USERAGENT, USER_AGENT); + + res = curl_easy_perform(curl); + if (res == CURLE_OK) + { + try + { + json root = json::parse(response); + auto tag_name_element = root.find("tag_name"); + if (tag_name_element != root.end() && tag_name_element->is_string()) + { + if (parseVersion(*tag_name_element, major, minor, revision)) + { + if ((g_versionMajor < major) || (g_versionMajor == major && g_versionMinor < minor) || (g_versionMajor == major && g_versionMinor == minor && g_versionRevision < revision)) + { + g_updateCheckerResult = UpdateChecker::Result::UpdateAvailable; + } + else + { + g_updateCheckerResult = UpdateChecker::Result::AlreadyUpToDate; + } + } + else + { + fmt::println("Error while parsing response: tag_name does not contain a valid version string."); + g_updateCheckerResult = UpdateChecker::Result::Failed; + } + } + else + { + fmt::println("Error while parsing response: tag_name not found or not the right type."); + g_updateCheckerResult = UpdateChecker::Result::Failed; + } + } + catch (const json::exception &e) + { + fmt::println("Error while parsing response: {}", e.what()); + g_updateCheckerResult = UpdateChecker::Result::Failed; + } + } + else + { + fmt::println("Error while performing request: {}", curl_easy_strerror(res)); + g_updateCheckerResult = UpdateChecker::Result::Failed; + } + + curl_easy_cleanup(curl); + + g_updateCheckerFinished = true; + g_updateCheckerInProgress = false; +} + +void UpdateChecker::initialize() +{ + curl_global_init(CURL_GLOBAL_DEFAULT); +} + +bool UpdateChecker::start() +{ + if (g_updateCheckerInProgress) + { + return false; + } + + g_updateCheckerInProgress = true; + g_updateCheckerFinished = false; + std::thread thread(&updateCheckerThread); + thread.detach(); + + return true; +} + +UpdateChecker::Result UpdateChecker::check() +{ + if (g_updateCheckerFinished) + { + return g_updateCheckerResult; + } + else if (g_updateCheckerInProgress) + { + return UpdateChecker::Result::InProgress; + } + else + { + return UpdateChecker::Result::NotStarted; + } +} + +void UpdateChecker::visitWebsite() +{ +#if defined(WIN32) + ShellExecuteA(0, 0, VISIT_URL, 0, 0, SW_SHOW); +#elif defined(__linux__) + std::string command = "xdg-open " + std::string(VISIT_URL) + " &"; + std::system(command.c_str()); +#else + static_assert(false, "Visit website not implemented for this platform."); +#endif +} diff --git a/UnleashedRecomp/install/update_checker.h b/UnleashedRecomp/install/update_checker.h new file mode 100644 index 00000000..484eaaaa --- /dev/null +++ b/UnleashedRecomp/install/update_checker.h @@ -0,0 +1,18 @@ +#pragma once + +struct UpdateChecker +{ + enum class Result + { + NotStarted, + InProgress, + AlreadyUpToDate, + UpdateAvailable, + Failed + }; + + static void initialize(); + static bool start(); + static Result check(); + static void visitWebsite(); +}; diff --git a/UnleashedRecomp/locale/locale.cpp b/UnleashedRecomp/locale/locale.cpp index c72f78c7..da8dd981 100644 --- a/UnleashedRecomp/locale/locale.cpp +++ b/UnleashedRecomp/locale/locale.cpp @@ -377,10 +377,16 @@ std::unordered_map> g_lo { ELanguage::English, "The achievement data could not be loaded.\nYour achievements will not be saved." } } }, + { + "Title_Message_UpdateAvailable", + { + { ELanguage::English, "An update is available!\n\nWould you like to visit the\nreleases page to download it?" } + } + }, { "Video_BackendError", { - { ELanguage::English, "Unable to create a D3D12 (Windows) or Vulkan backend.\n\nPlease make sure that:\n\n- Your system meets the minimum requirements.\n- Your GPU drivers are up to date.\n- Your operating system is on the latest version available." }, + { ELanguage::English, "Unable to create a D3D12 (Windows) or Vulkan backend.\n\nPlease make sure that:\n\n- Your system meets the minimum requirements.\n- Your GPU drivers are up to date.\n- Your operating system is on the latest version available." } } }, { diff --git a/UnleashedRecomp/main.cpp b/UnleashedRecomp/main.cpp index 96355e9f..012b9b23 100644 --- a/UnleashedRecomp/main.cpp +++ b/UnleashedRecomp/main.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +23,10 @@ #include #include +#ifdef _WIN32 +#include +#endif + const size_t XMAIOBegin = 0x7FEA0000; const size_t XMAIOEnd = XMAIOBegin + 0x0000FFFF; @@ -173,6 +178,18 @@ int main(int argc, char *argv[]) Config::Load(); + // Check the time since the last time an update was checked. Store the new time if the difference is more than six hours. + constexpr double TimeBetweenUpdateChecksInSeconds = 6 * 60 * 60; + time_t timeNow = std::time(nullptr); + double timeDifferenceSeconds = difftime(timeNow, Config::LastChecked); + if (timeDifferenceSeconds > TimeBetweenUpdateChecksInSeconds) + { + UpdateChecker::initialize(); + UpdateChecker::start(); + Config::LastChecked = timeNow; + Config::Save(); + } + if (Config::ShowConsole) os::process::ShowConsole(); diff --git a/UnleashedRecomp/patches/CTitleStateIntro_patches.cpp b/UnleashedRecomp/patches/CTitleStateIntro_patches.cpp index e8dcad98..f84b08aa 100644 --- a/UnleashedRecomp/patches/CTitleStateIntro_patches.cpp +++ b/UnleashedRecomp/patches/CTitleStateIntro_patches.cpp @@ -1,5 +1,6 @@ #include "CTitleStateIntro_patches.h" #include +#include #include #include #include @@ -7,8 +8,9 @@ #include #include +static bool g_faderBegun = false; + bool g_quitMessageOpen = false; -static bool g_quitMessageFaderBegun = false; static int g_quitMessageResult = -1; static std::atomic g_corruptSaveMessageOpen = false; @@ -17,6 +19,9 @@ static int g_corruptSaveMessageResult = -1; static bool g_corruptAchievementsMessageOpen = false; static int g_corruptAchievementsMessageResult = -1; +static bool g_updateAvailableMessageOpen = false; +static int g_updateAvailableMessageResult = -1; + static bool ProcessQuitMessage() { if (g_corruptSaveMessageOpen) @@ -25,7 +30,7 @@ static bool ProcessQuitMessage() if (!g_quitMessageOpen) return false; - if (g_quitMessageFaderBegun) + if (g_faderBegun) return true; std::array options = { Localise("Common_Yes"), Localise("Common_No") }; @@ -36,7 +41,7 @@ static bool ProcessQuitMessage() { case 0: Fader::FadeOut(1, []() { App::Exit(); }); - g_quitMessageFaderBegun = true; + g_faderBegun = true; break; case 1: @@ -87,14 +92,41 @@ static bool ProcessCorruptAchievementsMessage() return true; } -void StorageDevicePromptMidAsmHook() +static bool ProcessUpdateAvailableMessage() { - AchievementManager::Load(); + if (!g_updateAvailableMessageOpen) + return false; - if (AchievementManager::Status != EAchStatus::Success) - g_corruptAchievementsMessageOpen = true; + if (g_faderBegun) + return true; + + std::array options = { Localise("Common_Yes"), Localise("Common_No") }; + + if (MessageWindow::Open(Localise("Title_Message_UpdateAvailable"), &g_updateAvailableMessageResult, options) == MSG_CLOSED) + { + if (!g_updateAvailableMessageResult) + { + Fader::FadeOut(1, + // + []() + { + UpdateChecker::visitWebsite(); + App::Exit(); + } + ); + + g_faderBegun = true; + } + + g_updateAvailableMessageOpen = false; + g_updateAvailableMessageResult = -1; + } + + return true; } +void StorageDevicePromptMidAsmHook() {} + // Save data validation hook. PPC_FUNC_IMPL(__imp__sub_822C55B0); PPC_FUNC(sub_822C55B0) @@ -115,12 +147,27 @@ PPC_FUNC(sub_82587E50) { __imp__sub_82587E50(ctx, base); } - else if (!ProcessCorruptSaveMessage() && !ProcessCorruptAchievementsMessage()) + else if (!ProcessUpdateAvailableMessage() && !ProcessCorruptSaveMessage() && !ProcessCorruptAchievementsMessage() && !g_faderBegun) { - auto pInputState = SWA::CInputState::GetInstance(); + if (auto pInputState = SWA::CInputState::GetInstance()) + { + auto& rPadState = pInputState->GetPadState(); + auto isAccepted = rPadState.IsTapped(SWA::eKeyState_A) || rPadState.IsTapped(SWA::eKeyState_Start); + auto isDeclined = rPadState.IsTapped(SWA::eKeyState_B); - if (pInputState && pInputState->GetPadState().IsTapped(SWA::eKeyState_B)) - g_quitMessageOpen = true; + if (isAccepted) + { + g_updateAvailableMessageOpen = UpdateChecker::check() == UpdateChecker::Result::UpdateAvailable; + + AchievementManager::Load(); + + if (AchievementManager::Status != EAchStatus::Success) + g_corruptAchievementsMessageOpen = true; + } + + if (isDeclined) + g_quitMessageOpen = true; + } if (!ProcessQuitMessage()) __imp__sub_82587E50(ctx, base); diff --git a/UnleashedRecomp/user/config_def.h b/UnleashedRecomp/user/config_def.h index dbd7c3de..b9cb8220 100644 --- a/UnleashedRecomp/user/config_def.h +++ b/UnleashedRecomp/user/config_def.h @@ -108,3 +108,5 @@ CONFIG_DEFINE_HIDDEN("Exports", bool, HUDToggleHotkey, false); CONFIG_DEFINE_HIDDEN("Exports", bool, SaveScoreAtCheckpoints, false); CONFIG_DEFINE_HIDDEN("Exports", bool, SkipIntroLogos, false); CONFIG_DEFINE_HIDDEN("Exports", bool, UseOfficialTitleOnTitleBar, false); + +CONFIG_DEFINE("Update", time_t, LastChecked, 0); diff --git a/thirdparty/json b/thirdparty/json new file mode 160000 index 00000000..606b6347 --- /dev/null +++ b/thirdparty/json @@ -0,0 +1 @@ +Subproject commit 606b6347edf0758c531abb6c36743e09a4c48a84 diff --git a/vcpkg.json b/vcpkg.json index d930d54c..2fd8eaa7 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -10,6 +10,7 @@ "platform": "windows" }, "directx-dxc", - "freetype" + "freetype", + "curl" ] }