mirror of
				https://github.com/hedge-dev/UnleashedRecomp.git
				synced 2025-10-30 07:11:05 +00:00 
			
		
		
		
	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>
This commit is contained in:
		
							parent
							
								
									54d5588d79
								
							
						
					
					
						commit
						cd38776576
					
				
					 11 changed files with 282 additions and 15 deletions
				
			
		
							
								
								
									
										3
									
								
								.gitmodules
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitmodules
									
										
									
									
										vendored
									
									
								
							|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
							
								
								
									
										170
									
								
								UnleashedRecomp/install/update_checker.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								UnleashedRecomp/install/update_checker.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,170 @@ | |||
| #include "update_checker.h" | ||||
| 
 | ||||
| #include <curl/curl.h> | ||||
| #include <nlohmann/json.hpp> | ||||
| 
 | ||||
| #include "version.h" | ||||
| 
 | ||||
| #ifdef WIN32 | ||||
| #include <shellapi.h> | ||||
| #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<bool> g_updateCheckerInProgress = false; | ||||
| static std::atomic<bool> 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 | ||||
| } | ||||
							
								
								
									
										18
									
								
								UnleashedRecomp/install/update_checker.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								UnleashedRecomp/install/update_checker.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -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(); | ||||
| }; | ||||
|  | @ -377,10 +377,16 @@ std::unordered_map<std::string, std::unordered_map<ELanguage, std::string>> 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." } | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ | |||
| #include <user/registry.h> | ||||
| #include <kernel/xdbf.h> | ||||
| #include <install/installer.h> | ||||
| #include <install/update_checker.h> | ||||
| #include <os/logger.h> | ||||
| #include <os/process.h> | ||||
| #include <os/registry.h> | ||||
|  | @ -22,6 +23,10 @@ | |||
| #include <ui/installer_wizard.h> | ||||
| #include <mod/mod_loader.h> | ||||
| 
 | ||||
| #ifdef _WIN32 | ||||
| #include <timeapi.h> | ||||
| #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(); | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| #include "CTitleStateIntro_patches.h" | ||||
| #include <api/SWA.h> | ||||
| #include <install/update_checker.h> | ||||
| #include <locale/locale.h> | ||||
| #include <ui/fader.h> | ||||
| #include <ui/message_window.h> | ||||
|  | @ -7,8 +8,9 @@ | |||
| #include <user/paths.h> | ||||
| #include <app.h> | ||||
| 
 | ||||
| static bool g_faderBegun = false; | ||||
| 
 | ||||
| bool g_quitMessageOpen = false; | ||||
| static bool g_quitMessageFaderBegun = false; | ||||
| static int g_quitMessageResult = -1; | ||||
| 
 | ||||
| static std::atomic<bool> 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<std::string, 2> 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<std::string, 2> 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); | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
							
								
								
									
										1
									
								
								thirdparty/json
									
										
									
									
										vendored
									
									
										Submodule
									
								
							
							
						
						
									
										1
									
								
								thirdparty/json
									
										
									
									
										vendored
									
									
										Submodule
									
								
							|  | @ -0,0 +1 @@ | |||
| Subproject commit 606b6347edf0758c531abb6c36743e09a4c48a84 | ||||
|  | @ -10,6 +10,7 @@ | |||
|             "platform": "windows" | ||||
|         }, | ||||
|         "directx-dxc", | ||||
|         "freetype" | ||||
|         "freetype", | ||||
|         "curl" | ||||
|     ] | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Darío
						Darío