diff --git a/.gitmodules b/.gitmodules index 8f94ef0a..91a44940 100644 --- a/.gitmodules +++ b/.gitmodules @@ -61,3 +61,12 @@ [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 +[submodule "thirdparty/cpp-httplib"] + path = thirdparty/cpp-httplib + url = https://github.com/yhirose/cpp-httplib +[submodule "thirdparty/curl"] + path = thirdparty/curl + url = https://github.com/curl/curl diff --git a/UnleashedRecomp/CMakeLists.txt b/UnleashedRecomp/CMakeLists.txt index 8dc06358..3724a1c8 100644 --- a/UnleashedRecomp/CMakeLists.txt +++ b/UnleashedRecomp/CMakeLists.txt @@ -208,6 +208,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 +322,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 +368,7 @@ target_link_libraries(UnleashedRecomp PRIVATE tomlplusplus::tomlplusplus UnleashedRecompLib xxHash::xxhash + CURL::libcurl ) target_include_directories(UnleashedRecomp PRIVATE diff --git a/UnleashedRecomp/install/update_checker.cpp b/UnleashedRecomp/install/update_checker.cpp new file mode 100644 index 00000000..9a82e299 --- /dev/null +++ b/UnleashedRecomp/install/update_checker.cpp @@ -0,0 +1,166 @@ +#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/blueskythlikesclouds/MikuMikuLibrary/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::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; + g_updateCheckerResult = InProgress; + std::thread thread(&updateCheckerThread); + thread.detach(); + + return true; +} + +UpdateChecker::Result UpdateChecker::check() +{ + if (g_updateCheckerFinished) + { + return g_updateCheckerResult; + } + else if (g_updateCheckerInProgress) + { + return UpdateChecker::InProgress; + } + else + { + return UpdateChecker::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..60fe2a97 --- /dev/null +++ b/UnleashedRecomp/install/update_checker.h @@ -0,0 +1,18 @@ +#pragma once + +struct UpdateChecker +{ + enum Result + { + NotStarted, + InProgress, + AlreadyUpToDate, + UpdateAvailable, + Failed + }; + + static void initialize(); + static bool start(); + static Result check(); + static void visitWebsite(); +}; diff --git a/UnleashedRecomp/main.cpp b/UnleashedRecomp/main.cpp index 65aaab3e..c90d2614 100644 --- a/UnleashedRecomp/main.cpp +++ b/UnleashedRecomp/main.cpp @@ -15,12 +15,17 @@ #include #include #include +#include #include #include #include #include #include +#ifdef _WIN32 +#include +#endif + const size_t XMAIOBegin = 0x7FEA0000; const size_t XMAIOEnd = XMAIOBegin + 0x0000FFFF; @@ -172,6 +177,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/user/config_def.h b/UnleashedRecomp/user/config_def.h index 569f86e2..22ebd6ba 100644 --- a/UnleashedRecomp/user/config_def.h +++ b/UnleashedRecomp/user/config_def.h @@ -85,3 +85,5 @@ CONFIG_DEFINE_HIDDEN("Exports", bool, SaveScoreAtCheckpoints, false); CONFIG_DEFINE_HIDDEN("Exports", bool, SkipIntroLogos, false); CONFIG_DEFINE_HIDDEN("Exports", bool, UseOfficialTitleOnTitleBar, false); CONFIG_DEFINE_HIDDEN("Exports", bool, HUDToggleHotkey, 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" ] }