diff --git a/cmake/gen-build-info.cmake b/cmake/gen-build-info.cmake index 00df7ea..5773c89 100644 --- a/cmake/gen-build-info.cmake +++ b/cmake/gen-build-info.cmake @@ -31,7 +31,7 @@ else() set(GIT_DIRTY "false") endif() -# remote url (assume origin)) +# remote url (assume origin) execute_process( COMMAND git remote get-url origin OUTPUT_VARIABLE GIT_REMOTE_URL @@ -56,6 +56,16 @@ if(NOT GIT_DESCRIBE_RESULT EQUAL 0) set(GIT_TAG "?.?.?") endif() +# parse repo owner and name from remote url +if(GIT_REMOTE_URL MATCHES "([^/]+)/([^/]+)/?$") + set(GIT_REPO_OWNER "${CMAKE_MATCH_1}") + set(GIT_REPO_NAME "${CMAKE_MATCH_2}") + string(REGEX REPLACE "\\.git$" "" GIT_REPO_NAME "${GIT_REPO_NAME}") +else() + set(GIT_REPO_OWNER "") + set(GIT_REPO_NAME "") +endif() + # output to cpp file file(WRITE ${OUTPUT_FILE} "// DO NOT EDIT THIS FILE. IT IS AUTOMATICALLY GENERATED BY gen-build-info.cmake.\n" @@ -64,6 +74,8 @@ file(WRITE ${OUTPUT_FILE} "const std::string BuildInfo::c_sTargetSDKVersion = \"${ZHMMODSDK_VER}\";\n" "const std::string BuildInfo::c_sBuildTag = \"${GIT_TAG}\";\n" "const std::string BuildInfo::c_sRemoteUrl = \"${GIT_REMOTE_URL}\";\n" + "const std::string BuildInfo::c_sRepoOwner = \"${GIT_REPO_OWNER}\";\n" + "const std::string BuildInfo::c_sRepoName = \"${GIT_REPO_NAME}\";\n" "const std::string BuildInfo::c_sBranch = \"${GIT_BRANCH}\";\n" "const std::string BuildInfo::c_sCommit = \"${GIT_COMMIT_HASH}\";\n" "const bool BuildInfo::c_bIsDirty = ${GIT_DIRTY};\n" @@ -74,6 +86,8 @@ message(STATUS "Generated build info:") message(STATUS " Target SDK Version: ${ZHMMODSDK_VER}") message(STATUS " Build Tag: ${GIT_TAG}") message(STATUS " Remote URL: ${GIT_REMOTE_URL}") +message(STATUS " Repo Owner: ${GIT_REPO_OWNER}") +message(STATUS " Repo Name: ${GIT_REPO_NAME}") message(STATUS " Branch: ${GIT_BRANCH}") message(STATUS " Commit: ${GIT_COMMIT_HASH}") message(STATUS " Is Dirty?: ${GIT_DIRTY}") diff --git a/src/BuildInfo.h b/src/BuildInfo.h index 9dce5f9..1949e3a 100644 --- a/src/BuildInfo.h +++ b/src/BuildInfo.h @@ -10,6 +10,8 @@ namespace BuildInfo extern const std::string c_sBuildTag; extern const std::string c_sRemoteUrl; + extern const std::string c_sRepoOwner; + extern const std::string c_sRepoName; extern const std::string c_sBranch; extern const std::string c_sCommit; extern const bool c_bIsDirty; diff --git a/src/ChaosMod.cpp b/src/ChaosMod.cpp index 584c0eb..fc66fa5 100644 --- a/src/ChaosMod.cpp +++ b/src/ChaosMod.cpp @@ -11,6 +11,7 @@ #include "Helpers/Utils.h" #include "Helpers/CompanionMod.h" #include "Helpers/Repository/ZHMRepositoryHelper.h" +#include "Helpers/UpdateCheck/ZUpdateCheck.h" #include "Registry.h" #include "ZConfigurationAccessor.h" @@ -23,6 +24,7 @@ ChaosMod::ChaosMod() : m_fFullEffectDuration(60.0f), m_EffectTimer(std::bind(&ChaosMod::OnEffectTimerTrigger, this), 30.0f), m_bEffectTimersUseRealtime(false), m_SlowUpdateTimer(std::bind(&ChaosMod::OnEffectSlowUpdate, this), 0.2f, ZTimer::ETimeMode::RealTime, true), // ~5 FPS + m_pUpdateCheck(std::make_unique()), m_pConfiguration(std::make_unique(this, "ChaosMod")) { } @@ -72,6 +74,17 @@ void ChaosMod::Init() m_pVotingIntegration = GetDefaultVotingIntegration(); LoadConfiguration(); + + bool s_bUpdateCheckDefault = +#ifdef _DEBUG + false; +#else + true; +#endif + if (m_pConfiguration->GetBool("CheckForUpdates", s_bUpdateCheckDefault)) + { + m_pUpdateCheck->CheckUpdatesAsync(); + } } void ChaosMod::OnEngineInitialized() diff --git a/src/ChaosMod.h b/src/ChaosMod.h index e090dd4..4b5a819 100644 --- a/src/ChaosMod.h +++ b/src/ChaosMod.h @@ -13,6 +13,8 @@ #include #include +class ZUpdateCheck; + class ZConfigurationAccessor; class ChaosMod : public IPluginInterface @@ -35,6 +37,7 @@ class ChaosMod : public IPluginInterface private: // Misc. ZTimer m_SlowUpdateTimer; std::queue> m_qDeferredFrameUpdateActions; + std::unique_ptr m_pUpdateCheck; void ForeachEffect(const bool p_bIsLifecycleCall, std::function p_pEffect)> p_Callback); diff --git a/src/ChaosModUI.cpp b/src/ChaosModUI.cpp index 80c721e..ceb2176 100644 --- a/src/ChaosModUI.cpp +++ b/src/ChaosModUI.cpp @@ -12,6 +12,7 @@ #include "Helpers/CompanionMod.h" #include "Helpers/ZPerfCounter.h" #include "Helpers/Utils.h" +#include "Helpers/UpdateCheck/ZUpdateCheck.h" #include "BuildInfo.h" @@ -122,6 +123,25 @@ void ChaosMod::DrawMainUI(const bool p_bHasFocus) ImGui::SeparatorText("About"); ImGui::TextWrapped(fmt::format("ZHMChaosMod Version {}, developed by {}.", BuildInfo::GetDisplayVersion(), m_sAuthorNames).c_str()); + + switch (m_pUpdateCheck->GetResult()) + { + case ZUpdateCheck::EResult::None: + default: + break; + case ZUpdateCheck::EResult::Checking: + ImGui::TextUnformatted("Checking for updates..."); + break; + case ZUpdateCheck::EResult::UpToDate: + ImGui::TextUnformatted("You are using the latest version of the Chaos Mod."); + break; + case ZUpdateCheck::EResult::UpdateAvailable: + ImGui::TextLinkOpenURL(fmt::format("An update is available: {}", m_pUpdateCheck->GetLatestVersion()).c_str(), m_pUpdateCheck->GetUpdateUrl().c_str()); + break; + case ZUpdateCheck::EResult::Failed: + ImGui::TextUnformatted("Failed to check for updates."); + break; + } } ImGui::PopFont(); diff --git a/src/Helpers/UpdateCheck/ZUpdateCheck.cpp b/src/Helpers/UpdateCheck/ZUpdateCheck.cpp new file mode 100644 index 0000000..14c4bd6 --- /dev/null +++ b/src/Helpers/UpdateCheck/ZUpdateCheck.cpp @@ -0,0 +1,130 @@ +#include "ZUpdateCheck.h" + +#include + +#include +#include + +#include + +#define TAG "[ZUpdateCheck] " + +using json = nlohmann::json; + +ZUpdateCheck::~ZUpdateCheck() +{ + if (m_UpdateCheckThread.joinable()) + { + m_UpdateCheckThread.join(); + } +} + +void ZUpdateCheck::CheckUpdatesAsync() +{ + std::lock_guard s_Lock(m_ResultMutex); + if (m_eResult != EResult::None) + { + Logger::Debug(TAG "Update check already performed, skipping start of another check."); + return; + } + + if (m_UpdateCheckThread.joinable()) + { + Logger::Error(TAG "Update check thread already running, but state does not match!"); + return; + } + + m_UpdateCheckThread = std::thread(&ZUpdateCheck::CheckUpdatesInternal, this); + m_eResult = EResult::Checking; +} + +void ZUpdateCheck::CheckUpdatesInternal() +{ + Logger::Info(TAG "Starting update check for {}/{}...", BuildInfo::c_sRepoOwner, BuildInfo::c_sRepoName); + + std::string s_sUrl = "https://api.github.com/repos/" + + BuildInfo::c_sRepoOwner + + "/" + + BuildInfo::c_sRepoName + + "/releases/latest"; + Logger::Debug(TAG "Update check URL: {}", s_sUrl); + + ix::HttpClient s_Client; + ix::HttpRequestArgsPtr s_pRequest = s_Client.createRequest(); + const auto s_pResponse = s_Client.get(s_sUrl, s_pRequest); + if (!s_pResponse) + { + Logger::Error(TAG "Failed to perform update check HTTP request!"); + + { + std::lock_guard s_Lock(m_ResultMutex); + m_eResult = EResult::Failed; + } + return; + } + + if (s_pResponse->statusCode < 200 || s_pResponse->statusCode >= 300) + { + Logger::Error(TAG "update check failed: {} {}", s_pResponse->statusCode, s_pResponse->body); + + { + std::lock_guard s_Lock(m_ResultMutex); + m_eResult = EResult::Failed; + } + return; + } + + json s_ResponseJson; + try + { + s_ResponseJson = json::parse(s_pResponse->body); + } + catch (const json::exception& e) + { + Logger::Error(TAG "Failed to parse update check response as JSON: {}", e.what()); + { + std::lock_guard s_Lock(m_ResultMutex); + m_eResult = EResult::Failed; + } + return; + } + + const auto s_sName = s_ResponseJson.value("name", ""); + const auto s_sTagName = s_ResponseJson.value("tag_name", ""); + const auto s_bIsPrerelease = s_ResponseJson.value("prerelease", false); + const auto s_bIsDraft = s_ResponseJson.value("draft", false); + + if (s_sName.empty() || s_sTagName.empty()) + { + Logger::Error(TAG "Invalid response from update check: missing name or tag_name field!"); + { + std::lock_guard s_Lock(m_ResultMutex); + m_eResult = EResult::Failed; + } + return; + } + + Logger::Debug(TAG "Got latest release: name={}, tag_name={} (build={}), prerelease={}, draft={}", s_sName, s_sTagName, BuildInfo::c_sBuildTag, s_bIsPrerelease, s_bIsDraft); + + auto s_bUpdateAvailable = false; + if (!s_bIsPrerelease && !s_bIsDraft) + { + s_bUpdateAvailable = s_sTagName != BuildInfo::c_sBuildTag; + } + + { + std::lock_guard s_Lock(m_ResultMutex); + m_sLatestTag = s_sTagName; + m_sLatestVersionName = s_sName; + m_eResult = s_bUpdateAvailable ? EResult::UpdateAvailable : EResult::UpToDate; + } +} + +std::string ZUpdateCheck::GetUpdateUrl() const +{ + return "https://github.com/" + + BuildInfo::c_sRepoOwner + + "/" + + BuildInfo::c_sRepoName + + "/releases/latest"; +} diff --git a/src/Helpers/UpdateCheck/ZUpdateCheck.h b/src/Helpers/UpdateCheck/ZUpdateCheck.h new file mode 100644 index 0000000..2b35b26 --- /dev/null +++ b/src/Helpers/UpdateCheck/ZUpdateCheck.h @@ -0,0 +1,58 @@ +#pragma once +#include +#include +#include + +class ZUpdateCheck +{ + public: + enum class EResult + { + None, + Checking, + UpToDate, + UpdateAvailable, + Failed + }; + + ~ZUpdateCheck(); + + /** + * Start an asynchronous check for updates. + * @note this function will only perform the check once, even when called multiple times. + */ + void CheckUpdatesAsync(); + + /** + * Get the result of the update check. + */ + EResult GetResult() const + { + std::lock_guard s_Lock(m_ResultMutex); + return m_eResult; + } + + /** + * Get the display name of the latest version as displayed on Github releases. + */ + std::string GetLatestVersion() const + { + std::lock_guard s_Lock(m_ResultMutex); + return m_sLatestVersionName; + } + + /** + * Get the update URL that a user can visit to download the latest version. + */ + std::string GetUpdateUrl() const; + + private: + std::thread m_UpdateCheckThread; + + mutable std::recursive_mutex m_ResultMutex; + EResult m_eResult = EResult::None; + std::string m_sLatestTag; + std::string m_sLatestVersionName; + + void CheckUpdatesInternal(); +};