From f2518d16ca61ec5b0bcd9551b376467a32b794d2 Mon Sep 17 00:00:00 2001 From: shadow578 <52449218+shadow578@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:40:58 +0200 Subject: [PATCH 1/5] include repository owner and name in buildinfo --- cmake/gen-build-info.cmake | 15 ++++++++++++++- src/BuildInfo.h | 2 ++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/cmake/gen-build-info.cmake b/cmake/gen-build-info.cmake index 00df7ea..d820530 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,15 @@ if(NOT GIT_DESCRIBE_RESULT EQUAL 0) set(GIT_TAG "?.?.?") endif() +# parse repo owner and name from remote url +if(GIT_REMOTE_URL MATCHES "([^/]+)/([^/]+)\\.git$") + set(GIT_REPO_OWNER "${CMAKE_MATCH_1}") + set(GIT_REPO_NAME "${CMAKE_MATCH_2}") +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 +73,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 +85,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; From f2237d8c82aa26a470e240150ed3bd6b14e85bc7 Mon Sep 17 00:00:00 2001 From: shadow578 <52449218+shadow578@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:41:16 +0200 Subject: [PATCH 2/5] add update check on startup --- src/ChaosMod.cpp | 4 + src/ChaosMod.h | 3 + src/ChaosModUI.cpp | 19 ++++ src/Helpers/UpdateCheck/ZUpdateCheck.cpp | 109 +++++++++++++++++++++++ src/Helpers/UpdateCheck/ZUpdateCheck.h | 55 ++++++++++++ 5 files changed, 190 insertions(+) create mode 100644 src/Helpers/UpdateCheck/ZUpdateCheck.cpp create mode 100644 src/Helpers/UpdateCheck/ZUpdateCheck.h diff --git a/src/ChaosMod.cpp b/src/ChaosMod.cpp index 584c0eb..30e45cb 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,8 @@ void ChaosMod::Init() m_pVotingIntegration = GetDefaultVotingIntegration(); LoadConfiguration(); + + 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..760147a 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,24 @@ 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: + case ZUpdateCheck::EResult::Checking: + default: + 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..ba79be0 --- /dev/null +++ b/src/Helpers/UpdateCheck/ZUpdateCheck.cpp @@ -0,0 +1,109 @@ +#include "ZUpdateCheck.h" + +#include + +#include +#include + +#include + +#define TAG "[ZUpdateCheck] " + +using json = nlohmann::json; + +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; + } + + const auto s_ResponseJson = json::parse(s_pResponse->body); + + 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..2c75cc7 --- /dev/null +++ b/src/Helpers/UpdateCheck/ZUpdateCheck.h @@ -0,0 +1,55 @@ +#pragma once +#include +#include + +class ZUpdateCheck +{ + public: + enum class EResult + { + None, + Checking, + UpToDate, + UpdateAvailable, + Failed + }; + + /** + * 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(); +}; From 0c9cd3a7b4fb836efe2be60b116bd614c0cbdae2 Mon Sep 17 00:00:00 2001 From: shadow578 <52449218+shadow578@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:49:58 +0200 Subject: [PATCH 3/5] disable update check by default in debug builds --- src/ChaosMod.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/ChaosMod.cpp b/src/ChaosMod.cpp index 30e45cb..fc66fa5 100644 --- a/src/ChaosMod.cpp +++ b/src/ChaosMod.cpp @@ -75,7 +75,16 @@ void ChaosMod::Init() LoadConfiguration(); - m_pUpdateCheck->CheckUpdatesAsync(); + bool s_bUpdateCheckDefault = +#ifdef _DEBUG + false; +#else + true; +#endif + if (m_pConfiguration->GetBool("CheckForUpdates", s_bUpdateCheckDefault)) + { + m_pUpdateCheck->CheckUpdatesAsync(); + } } void ChaosMod::OnEngineInitialized() From 4c41e0b1f41cad669227df1ce76e955fb2edc3be Mon Sep 17 00:00:00 2001 From: shadow578 <52449218+shadow578@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:54:57 +0200 Subject: [PATCH 4/5] autoformat --- src/Helpers/UpdateCheck/ZUpdateCheck.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Helpers/UpdateCheck/ZUpdateCheck.h b/src/Helpers/UpdateCheck/ZUpdateCheck.h index 2c75cc7..46d62f9 100644 --- a/src/Helpers/UpdateCheck/ZUpdateCheck.h +++ b/src/Helpers/UpdateCheck/ZUpdateCheck.h @@ -39,7 +39,7 @@ class ZUpdateCheck } /** - * Get the update URL that a user can visit to download the latest version. + * Get the update URL that a user can visit to download the latest version. */ std::string GetUpdateUrl() const; From 96cb6e8da7a5c7828cdbf048bfef773b4fe05bcc Mon Sep 17 00:00:00 2001 From: shadow578 <52449218+shadow578@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:11:56 +0200 Subject: [PATCH 5/5] misc. fixes --- cmake/gen-build-info.cmake | 7 ++++--- src/ChaosModUI.cpp | 3 ++- src/Helpers/UpdateCheck/ZUpdateCheck.cpp | 23 ++++++++++++++++++++++- src/Helpers/UpdateCheck/ZUpdateCheck.h | 3 +++ 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/cmake/gen-build-info.cmake b/cmake/gen-build-info.cmake index d820530..5773c89 100644 --- a/cmake/gen-build-info.cmake +++ b/cmake/gen-build-info.cmake @@ -57,9 +57,10 @@ if(NOT GIT_DESCRIBE_RESULT EQUAL 0) endif() # parse repo owner and name from remote url -if(GIT_REMOTE_URL MATCHES "([^/]+)/([^/]+)\\.git$") - set(GIT_REPO_OWNER "${CMAKE_MATCH_1}") - set(GIT_REPO_NAME "${CMAKE_MATCH_2}") +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 "") diff --git a/src/ChaosModUI.cpp b/src/ChaosModUI.cpp index 760147a..ceb2176 100644 --- a/src/ChaosModUI.cpp +++ b/src/ChaosModUI.cpp @@ -127,8 +127,9 @@ void ChaosMod::DrawMainUI(const bool p_bHasFocus) switch (m_pUpdateCheck->GetResult()) { case ZUpdateCheck::EResult::None: - case ZUpdateCheck::EResult::Checking: default: + break; + case ZUpdateCheck::EResult::Checking: ImGui::TextUnformatted("Checking for updates..."); break; case ZUpdateCheck::EResult::UpToDate: diff --git a/src/Helpers/UpdateCheck/ZUpdateCheck.cpp b/src/Helpers/UpdateCheck/ZUpdateCheck.cpp index ba79be0..14c4bd6 100644 --- a/src/Helpers/UpdateCheck/ZUpdateCheck.cpp +++ b/src/Helpers/UpdateCheck/ZUpdateCheck.cpp @@ -11,6 +11,14 @@ using json = nlohmann::json; +ZUpdateCheck::~ZUpdateCheck() +{ + if (m_UpdateCheckThread.joinable()) + { + m_UpdateCheckThread.join(); + } +} + void ZUpdateCheck::CheckUpdatesAsync() { std::lock_guard s_Lock(m_ResultMutex); @@ -66,7 +74,20 @@ void ZUpdateCheck::CheckUpdatesInternal() return; } - const auto s_ResponseJson = json::parse(s_pResponse->body); + 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", ""); diff --git a/src/Helpers/UpdateCheck/ZUpdateCheck.h b/src/Helpers/UpdateCheck/ZUpdateCheck.h index 46d62f9..2b35b26 100644 --- a/src/Helpers/UpdateCheck/ZUpdateCheck.h +++ b/src/Helpers/UpdateCheck/ZUpdateCheck.h @@ -1,6 +1,7 @@ #pragma once #include #include +#include class ZUpdateCheck { @@ -14,6 +15,8 @@ class ZUpdateCheck Failed }; + ~ZUpdateCheck(); + /** * Start an asynchronous check for updates. * @note this function will only perform the check once, even when called multiple times.