Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/build-linux-flatpak.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ on:
required: false
type: string
default: linux64-deploy
checkout_ref:
required: false
type: string
default: ""
description: "Git ref to checkout (branch, tag, or SHA). Leaves empty for default."
workflow_dispatch:
inputs:
preset:
Expand Down Expand Up @@ -45,6 +50,7 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v6
with:
ref: ${{ inputs.checkout_ref || github.ref }}
fetch-depth: 0
fetch-tags: true

Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/build-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ on:
type: string
default: "macos-vulkan"
description: "CMake preset for macOS"
checkout_ref:
required: false
type: string
default: ""
description: "Git ref to checkout (branch, tag, or SHA). Leaves empty for default."
workflow_dispatch:
inputs:
preset:
Expand Down Expand Up @@ -46,6 +51,7 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v6
with:
ref: ${{ inputs.checkout_ref || github.ref }}
fetch-depth: 0
fetch-tags: true

Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,14 @@ jobs:
uses: ./.github/workflows/build-linux-flatpak.yml
with:
preset: linux64-deploy
checkout_ref: refs/tags/${{ inputs.release_version }}

build-macos:
needs: [create-tag]
uses: ./.github/workflows/build-macos.yml
with:
preset: macos-vulkan
checkout_ref: refs/tags/${{ inputs.release_version }}

# GeneralsX @build BenderAI 21/04/2026 Roll back the pushed tag when anything
# downstream of create-tag fails, so a re-run with the same version is possible.
Expand Down
1 change: 1 addition & 0 deletions Core/GameEngine/Include/Common/version.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ class Version
UnicodeString getUnicodeProductAuthor() const; ///< Is decorated with localized string
UnicodeString getUnicodeProductString() const; ///< Returns a string that contains product title, version and other, if specified. Is decorated with localized string
UnicodeString getUnicodeProductVersionHashString() const; ///< Returns a string that contains the product string, game version and hashes. Is decorated with localized string
UnicodeString getUnicodeProjectWatermark() const; ///< Returns the project watermark with the git tag if present

Bool showFullVersion() const { return m_showFullVersion; }
void setShowFullVersion( Bool val ) { m_showFullVersion = val; }
Expand Down
46 changes: 46 additions & 0 deletions Core/GameEngine/Source/Common/UpdateChecker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ static int SDLCALL threadFunc(void* /*userData*/)
curl_slist_free_all(headers);
curl_easy_cleanup(curl);

fprintf(stderr, "[UpdateChecker] curl_easy_perform returned %d. Response length=%zu\n", (int)res, responseBody.length());
fflush(stderr);

if (res != CURLE_OK)
{
// Network error: fail silently
Expand All @@ -192,9 +195,14 @@ static int SDLCALL threadFunc(void* /*userData*/)
char latestTag[128] = {0};
if (!extractTagName(responseBody, latestTag, sizeof(latestTag)))
{
fprintf(stderr, "[UpdateChecker] Failed to extract tag_name from response. Response Body (truncated):\n%.256s\n", responseBody.c_str());
fflush(stderr);
SDL_SetAtomicInt(&s_done, 1);
return 0;
}

fprintf(stderr, "[UpdateChecker] Extracted remote latestTag='%s'\n", latestTag);
fflush(stderr);

// Compare publication date of the remote release against the local build's
// commit timestamp. Date comparison is tag-format-agnostic: it works for
Expand All @@ -206,13 +214,17 @@ static int SDLCALL threadFunc(void* /*userData*/)
if (forceCheck)
{
hasUpdate = latestTag[0] != '\0';
fprintf(stderr, "[UpdateChecker] forceCheck is true. hasUpdate=%d\n", (int)hasUpdate);
fflush(stderr);
}
else
{
// First check if the tag matches exactly. If it does, we are definitely on the latest version.
if (GitTag[0] != '\0' && strcmp(latestTag, GitTag) == 0)
{
hasUpdate = false;
fprintf(stderr, "[UpdateChecker] Local tag matches remote tag precisely ('%s'). No update.\n", GitTag);
fflush(stderr);
}
else
{
Expand All @@ -221,15 +233,34 @@ static int SDLCALL threadFunc(void* /*userData*/)
if (hasCommitTimestamp && extractPublishedAt(responseBody, publishedAt, sizeof(publishedAt)))
{
time_t remoteTime = parseISO8601(publishedAt);
fprintf(stderr, "[UpdateChecker] Extracted published_at='%s' (parsed %lld). Local commit time=%lld\n", publishedAt, (long long)remoteTime, (long long)GitCommitTimeStamp);
fflush(stderr);

// Signal update only when the remote release was published strictly
// AFTER the commit this binary was built from.
if (remoteTime != (time_t)-1 && remoteTime > GitCommitTimeStamp)
{
hasUpdate = true;
fprintf(stderr, "[UpdateChecker] Remote release is newer based on timestamp!\n");
fflush(stderr);
}
else
{
fprintf(stderr, "[UpdateChecker] Remote release is NOT newer based on timestamp.\n");
fflush(stderr);
}
}
else if (GitTag[0] != '\0')
{
// No usable published_at comparison; fall back to tag string comparison.
hasUpdate = (strcmp(latestTag, GitTag) != 0);
fprintf(stderr, "[UpdateChecker] No valid published_at found or no local timestamp. Falling back to tag string diff. hasUpdate=%d\n", (int)hasUpdate);
fflush(stderr);
}
else
{
fprintf(stderr, "[UpdateChecker] No local tag and no usable timestamp. Cannot determine update reliably. Assuming hasUpdate=false\n");
fflush(stderr);
}
}
}
Expand Down Expand Up @@ -259,17 +290,29 @@ void UpdateChecker::start()
// some packaged CI contexts even when the binary is a real release artifact).
// Set env var GENERALS_FORCE_UPDATE_CHECK=1 to bypass release guards (for testing).
const bool forceCheck = SDL_getenv("GENERALS_FORCE_UPDATE_CHECK") != nullptr;

fprintf(stderr, "[UpdateChecker] start() called. GitTag='%s', GitCommitTimeStamp=%lld, GitUncommittedChanges=%d, forceCheck=%d\n", GitTag, (long long)GitCommitTimeStamp, (int)GitUncommittedChanges, (int)forceCheck);
fflush(stderr);

if (!forceCheck)
{
const bool hasTag = (GitTag[0] != '\0');
const bool hasCommitTimestamp = (GitCommitTimeStamp > 0);
if (GitUncommittedChanges || (!hasTag && !hasCommitTimestamp))
{
fprintf(stderr, "[UpdateChecker] start() aborted. GitUncommittedChanges=%d, hasTag=%d, hasCommitTimestamp=%d\n", (int)GitUncommittedChanges, (int)hasTag, (int)hasCommitTimestamp);
fflush(stderr);
return;
}
}

// Respect the user opt-out setting
if (TheGlobalData && !TheGlobalData->m_checkForUpdates)
{
fprintf(stderr, "[UpdateChecker] start() aborted. User opted out of updates in settings.\n");
fflush(stderr);
return;
}

SDL_SetAtomicInt(&s_done, 0);
SDL_SetAtomicInt(&s_hasUpdate, 0);
Expand All @@ -280,6 +323,9 @@ void UpdateChecker::start()
// cleanup on exit and there is no safe single-owner shutdown hook here.
curl_global_init(CURL_GLOBAL_DEFAULT);

fprintf(stderr, "[UpdateChecker] Launching background thread to check %s\n", UpdateChecker::getReleasesUrl());
fflush(stderr);

s_thread = SDL_CreateThread(threadFunc, "UpdateChecker", nullptr);
if (!s_thread)
{
Expand Down
36 changes: 36 additions & 0 deletions Core/GameEngine/Source/Common/version.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
#include "Common/version.h"

#include "gitinfo.h"
#include <ctype.h>
#include <stdio.h>

Version *TheVersion = nullptr; ///< The Version singleton

Expand Down Expand Up @@ -451,3 +453,37 @@ UnicodeString Version::buildUnicodeGitCommitTime()
wcsftime(buf, len+1, L"%Y-%m-%d %H:%M:%S", time);
return str;
}

UnicodeString Version::getUnicodeProjectWatermark() const
{
char finalCredit[256];
if (GitTag && GitTag[0] != '\0')
{
const char* prefix = "generalsx-";
const char* tagBase = GitTag;

if (strncmp(tagBase, prefix, strlen(prefix)) == 0)
{
tagBase += strlen(prefix);
}

char formattedTag[128];
strncpy(formattedTag, tagBase, sizeof(formattedTag) - 1);
formattedTag[sizeof(formattedTag) - 1] = '\0';

if (formattedTag[0] != '\0')
{
formattedTag[0] = toupper((unsigned char)formattedTag[0]);
}

snprintf(finalCredit, sizeof(finalCredit), "GeneralsX %s - Multiplatform C&C Generals", formattedTag);
}
else
{
strncpy(finalCredit, "GeneralsX - Multiplatform C&C Generals", sizeof(finalCredit));
}

UnicodeString watermark;
watermark.translate(finalCredit);
return watermark;
}
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,11 @@ static void initLabelVersion()
NameKeyType versionID = TheNameKeyGenerator->nameToKey( "MainMenu.wnd:LabelVersion" );
GameWindow *labelVersion = TheWindowManager->winGetWindowFromId( nullptr, versionID );
UnicodeString creditText;
creditText.translate("GeneralsX - Multiplatform C&C Generals");
if (TheVersion) {
creditText = TheVersion->getUnicodeProjectWatermark();
} else {
creditText.translate("GeneralsX - Multiplatform C&C Generals");
}

if (labelVersion)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
#include "W3DDevice/GameClient/W3DGadget.h"

#include "GameClient/GUICallbacks.h"
#include "Common/version.h"

// forward declaration for credit draw added by GeneralsX
extern void W3DGeneralsXCreditDraw( GameWindow *window, WinInstanceData *instData );
Expand Down Expand Up @@ -421,7 +422,11 @@ void W3DGeneralsXCreditDraw( GameWindow *window, WinInstanceData *instData )
return;

UnicodeString ucredit;
ucredit.translate("GeneralsX - Multiplatform C&C Generals");
if (TheVersion) {
ucredit = TheVersion->getUnicodeProjectWatermark();
} else {
ucredit.translate("GeneralsX - Multiplatform C&C Generals");
}
instData->setText(ucredit);

DisplayString *dString = instData->getTextDisplayString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,11 @@ static void initLabelVersion()
NameKeyType versionID = TheNameKeyGenerator->nameToKey( "MainMenu.wnd:LabelVersion" );
GameWindow *labelVersion = TheWindowManager->winGetWindowFromId( nullptr, versionID );
UnicodeString creditText;
creditText.translate("GeneralsX - Multiplatform C&C Generals");
if (TheVersion) {
creditText = TheVersion->getUnicodeProjectWatermark();
} else {
creditText.translate("GeneralsX - Multiplatform C&C Generals");
}

if (labelVersion)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
#include "W3DDevice/GameClient/W3DGadget.h"

#include "GameClient/GUICallbacks.h"
#include "Common/version.h"

// forward declaration for credit draw added by GeneralsX
extern void W3DGeneralsXCreditDraw( GameWindow *window, WinInstanceData *instData );
Expand Down Expand Up @@ -419,7 +420,11 @@ void W3DGeneralsXCreditDraw( GameWindow *window, WinInstanceData *instData )
return;

UnicodeString ucredit;
ucredit.translate("GeneralsX - Multiplatform C&C Generals");
if (TheVersion) {
ucredit = TheVersion->getUnicodeProjectWatermark();
} else {
ucredit.translate("GeneralsX - Multiplatform C&C Generals");
}
instData->setText(ucredit);

DisplayString *dString = instData->getTextDisplayString();
Expand Down
Loading