Skip to content
Open
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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ jobs:
preset: ${{ matrix.preset }}
tools: ${{ matrix.tools }}
extras: ${{ matrix.extras }}
tests: ${{ startsWith(matrix.preset, 'win32') }}
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
benchmarks: ${{ matrix.preset == 'win32' }}
secrets: inherit

# Note build-generalsmd is split into two jobs for vc6 and win32 because replaycheck-generalsmd
Expand Down Expand Up @@ -170,6 +172,8 @@ jobs:
tools: ${{ matrix.tools }}
extras: ${{ matrix.extras }}
ffmpeg: ${{ matrix.ffmpeg || false}}
tests: true
benchmarks: ${{ matrix.preset == 'win32' }}
secrets: inherit

replaycheck-generalsmd:
Expand Down
20 changes: 20 additions & 0 deletions .github/workflows/reusable-build-toolchain.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ on:
default: false
type: boolean
description: "Build with FFmpeg support"
tests:
required: false
default: false
type: boolean
description: "Build and run the tests"
benchmarks:
required: false
default: false
type: boolean
description: "Build the benchmarks"

jobs:
build:
Expand Down Expand Up @@ -152,6 +162,8 @@ jobs:
$buildFlags += "-DRTS_BUILD_CORE_EXTRAS=${{ inputs.extras && 'ON' || 'OFF' }}"
$buildFlags += "-DRTS_BUILD_${gamePrefix}_EXTRAS=${{ inputs.extras && 'ON' || 'OFF' }}"
$buildFlags += "-DRTS_BUILD_OPTION_FFMPEG=${{ inputs.ffmpeg && 'ON' || 'OFF' }}"
$buildFlags += "-DRTS_BUILD_OPTION_TESTS=${{ inputs.tests && 'ON' || 'OFF' }}"
$buildFlags += "-DRTS_BUILD_OPTION_BENCHMARKS=${{ inputs.benchmarks && 'ON' || 'OFF' }}"

Write-Host "Build flags: $($buildFlags -join ' | ')"
cmake --preset ${{ inputs.preset }} $buildFlags
Expand All @@ -161,6 +173,14 @@ jobs:
run: |
cmake --build --preset ${{ inputs.preset }}

# The benchmarks are only built, because running them takes too long here.
# Verbose prints the Google Test output, which lists the test cases of each executable.
- name: Test ${{ inputs.game }} with CTest Using ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }} Preset
if: ${{ inputs.tests }}
shell: pwsh
run: |
ctest --preset ${{ inputs.preset }} --verbose

- name: Collect ${{ inputs.game }} ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }} Artifact
shell: pwsh
run: |
Expand Down
29 changes: 14 additions & 15 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ if((WIN32 OR "${CMAKE_SYSTEM}" MATCHES "Windows") AND ${CMAKE_SIZEOF_VOID_P} EQU
include(cmake/dx8.cmake)
endif()

# Define a dummy stlport target when not on VC6.
if (IS_VS6_BUILD)
include(cmake/stlport.cmake)
else()
Expand All @@ -69,24 +68,24 @@ include(cmake/gamespy.cmake)
include(cmake/lzhl.cmake)
include(cmake/stb.cmake)

if (IS_VS6_BUILD)
# The original max sdk does not compile against a modern compiler.
# If there is a desire to make this work, then a fixed max sdk needs to be created.
add_subdirectory(Dependencies/MaxSDK)
if(RTS_BUILD_OPTION_PROFILE_TRACY)
include(cmake/tracy.cmake)
else()
add_library(core_profile_tracy INTERFACE)
endif()

add_subdirectory(Dependencies/Precompiled)
add_subdirectory(Dependencies/Utility)
add_subdirectory(Dependencies/Bink)
add_subdirectory(Dependencies/Miles)
if (WIN32)
add_subdirectory(Dependencies/DbgHelp)
add_subdirectory(Dependencies/Usp10)
else()
add_library(deps_dbghelp INTERFACE)
add_library(deps_usp10 INTERFACE)
if(RTS_BUILD_OPTION_TESTS)
# Must come before the subdirectories that add tests.
enable_testing()
include(cmake/googletest.cmake)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
endif()

if(RTS_BUILD_OPTION_BENCHMARKS)
include(cmake/googlebenchmark.cmake)
endif()

add_subdirectory(Dependencies)

add_subdirectory(resources)

add_subdirectory(Core)
Expand Down
62 changes: 62 additions & 0 deletions CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,68 @@
"description": "Build MinGW-w64 32-bit (i686) Profile"
}
],
"testPresets": [
{
"name": "default",
"displayName": "Default Test (don't use directly!)",
"hidden": true,
"output": {
"outputOnFailure": true
},
"execution": {
"noTestsAction": "error",
"timeout": 300
}
},
{
"name": "win32",
"inherits": "default",
"configurePreset": "win32",
"displayName": "Test Windows 32bit Release",
"description": "Test Windows 32bit Release",
"configuration": "Release"
},
{
"name": "win32-profile",
"inherits": "default",
"configurePreset": "win32-profile",
"displayName": "Test Windows 32bit Profile",
"description": "Test Windows 32bit Profile",
"configuration": "Release"
},
{
"name": "win32-debug",
"inherits": "default",
"configurePreset": "win32-debug",
"displayName": "Test Windows 32bit Debug",
"description": "Test Windows 32bit Debug",
"configuration": "Debug"
},
{
"name": "win32-vcpkg",
"inherits": "default",
"configurePreset": "win32-vcpkg",
"displayName": "Test Windows 32bit VCPKG Release",
"description": "Test Windows 32bit VCPKG Release",
"configuration": "Release"
},
{
"name": "win32-vcpkg-profile",
"inherits": "default",
"configurePreset": "win32-vcpkg-profile",
"displayName": "Test Windows 32bit VCPKG Profile",
"description": "Test Windows 32bit VCPKG Profile",
"configuration": "Release"
},
{
"name": "win32-vcpkg-debug",
"inherits": "default",
"configurePreset": "win32-vcpkg-debug",
"displayName": "Test Windows 32bit VCPKG Debug",
"description": "Test Windows 32bit VCPKG Debug",
"configuration": "Debug"
}
],
"workflowPresets": [
{
"name": "vc6",
Expand Down
8 changes: 8 additions & 0 deletions Core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,11 @@ add_subdirectory(GameEngine)
add_subdirectory(GameEngineDevice)

add_subdirectory(Tools)

if(RTS_BUILD_OPTION_TESTS)
add_subdirectory(GoogleTest)
endif()

if(RTS_BUILD_OPTION_BENCHMARKS)
add_subdirectory(GoogleBenchmark)
endif()
5 changes: 5 additions & 0 deletions Core/GameEngine/Include/Common/Debug.h
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,11 @@ DEBUG_EXTERN_C void ReleaseCrashLocalized(const AsciiString& p, const AsciiStrin
#define RELEASE_CRASH(m) do { ReleaseCrash(m); } while (0)
#define RELEASE_CRASHLOCALIZED(p, m) do { ReleaseCrashLocalized(p, m); } while (0)

// Debug and release crashes go to the crash handler instead of a crash box,
// for example to fail a unit test. A debug crash then continues, a release crash still exits.
typedef void (*DebugCrashHandler)(const char *message);
DEBUG_EXTERN_C void DebugSetCrashHandler(DebugCrashHandler handler);


#ifdef DEBUG_PROFILE

Expand Down
36 changes: 33 additions & 3 deletions Core/GameEngine/Source/Common/System/Debug.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ static char theLogFileNamePrev[ _MAX_PATH ];
static char theBuffer[ LARGE_BUFFER ]; // make it big to avoid weird overflow bugs in debug mode
static int theDebugFlags = 0;
static DWORD theMainThreadID = 0;
static DebugCrashHandler theCrashHandler = nullptr;
// ----------------------------------------------------------------------------
// PUBLIC DATA
// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -548,6 +549,13 @@ void DebugCrash(const char *format, ...)
#endif
}

if (theCrashHandler != nullptr)
{
// Every crash reaches the handler, because the ignore option of the crash box does not apply.
theCrashHandler(theCrashBuffer);
return;
}

strlcat(theCrashBuffer, "\n\nAbort->exception; Retry->debugger; Ignore->continue", ARRAY_SIZE(theCrashBuffer));

const int result = doCrashBox(theCrashBuffer, useLogging);
Expand Down Expand Up @@ -624,6 +632,18 @@ void DebugSetFlags(int flags)

#endif // ALLOW_DEBUG_UTILS

// ----------------------------------------------------------------------------
// DebugSetCrashHandler
// ----------------------------------------------------------------------------
/**
Set the handler that receives debug and release crashes instead of the crash box.
Pass nullptr to show the crash box again.
*/
void DebugSetCrashHandler(DebugCrashHandler handler)
{
theCrashHandler = handler;
}

#ifdef DEBUG_PROFILE
// ----------------------------------------------------------------------------
SimpleProfiler::SimpleProfiler()
Expand Down Expand Up @@ -747,6 +767,11 @@ static void TriggerMiniDump()

void ReleaseCrash(const char *reason)
{
if (theCrashHandler != nullptr)
{
theCrashHandler(reason);
}

/// do additional reporting on the crash, if possible

if (!DX8Wrapper_IsWindowed) {
Expand Down Expand Up @@ -808,7 +833,7 @@ void ReleaseCrash(const char *reason)
#if defined(RTS_DEBUG)
/* static */ char buff[8192]; // not so static so we can be threadsafe
snprintf(buff, 8192, "Sorry, a serious error occurred. (%s)", reason);
if (!(TheGlobalData && TheGlobalData->m_headless))
if (theCrashHandler == nullptr && !(TheGlobalData && TheGlobalData->m_headless))
{
::MessageBox(nullptr, buff, "Technical Difficulties...", MB_OK|MB_SYSTEMMODAL|MB_ICONERROR);
}
Expand All @@ -818,7 +843,7 @@ void ReleaseCrash(const char *reason)
// ::MessageBox(nullptr, "You have encountered a serious error. Serious errors can be caused by many things including viruses, overheated hardware and hardware that does not meet the minimum specifications for the game. Please visit the forums at www.generals.ea.com for suggested courses of action or consult your manual for Technical Support contact information.", "Technical Difficulties...", MB_OK|MB_TASKMODAL|MB_ICONERROR);

// crash error message changed again 8/22/03 M Lorenzen... made this message box modal to the system so it will appear on top of any task-modal windows, splash-screen, etc.
if (!(TheGlobalData && TheGlobalData->m_headless))
if (theCrashHandler == nullptr && !(TheGlobalData && TheGlobalData->m_headless))
{
::MessageBox(nullptr, "You have encountered a serious error. Serious errors can be caused by many things including viruses, overheated hardware and hardware that does not meet the minimum specifications for the game. Please visit the forums at www.generals.ea.com for suggested courses of action or consult your manual for Technical Support contact information.",
"Technical Difficulties...",
Expand All @@ -839,6 +864,11 @@ void ReleaseCrashLocalized(const AsciiString& p, const AsciiString& m)
return;
}

if (theCrashHandler != nullptr)
{
theCrashHandler(m.str());
}

TriggerMiniDump();

UnicodeString prompt = TheGameText->fetch(p);
Expand All @@ -853,7 +883,7 @@ void ReleaseCrashLocalized(const AsciiString& p, const AsciiString& m)
}
}

if (!(TheGlobalData && TheGlobalData->m_headless))
if (theCrashHandler == nullptr && !(TheGlobalData && TheGlobalData->m_headless))
{
::MessageBoxW(nullptr, mesg.str(), prompt.str(), MB_OK | MB_SYSTEMMODAL | MB_ICONERROR);
}
Expand Down
19 changes: 19 additions & 0 deletions Core/GoogleBenchmark/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Core GameEngine code needs a game to compile, so g_googlebenchmark and z_googlebenchmark compile these sources.
set(COREI_GOOGLEBENCHMARK_SRC
GoogleBenchmarkMain.cpp
GameEngine/Common/AsciiStringBenchmark.cpp
Libraries/Compression/CompressionManagerBenchmark.cpp
)

add_library(corei_googlebenchmark INTERFACE)

target_sources(corei_googlebenchmark INTERFACE ${COREI_GOOGLEBENCHMARK_SRC})

target_link_libraries(corei_googlebenchmark INTERFACE
benchmark::benchmark
comctl32
depsi_googlebenchmark
imm32
vfw32
winmm
)
32 changes: 32 additions & 0 deletions Core/GoogleBenchmark/GameEngine/Common/AsciiStringBenchmark.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
** Command & Conquer Generals Zero Hour(tm)
** Copyright 2026 TheSuperHackers
**
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 3 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#include <benchmark/benchmark.h>

#include "Common/AsciiString.h"

static void BM_AsciiStringFormat(benchmark::State &state)
{
for (auto _ : state)
{
AsciiString text;
text.format("%s %d", "Generals", 2003);
benchmark::DoNotOptimize(text.str());
}
}
BENCHMARK(BM_AsciiStringFormat);
Loading
Loading