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
11 changes: 11 additions & 0 deletions .github/workflows/reusable-build-toolchain.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ jobs:
if: contains(inputs.preset, 'vcpkg')
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11.6
with:
# The optional Crashpad package has its own separate manifest.
vcpkgJsonGlob: 'vcpkg.json'
runVcpkgInstall: false
doNotCache: true

Expand Down Expand Up @@ -176,6 +178,15 @@ jobs:
Where-Object { $_.Extension -in @(".exe", ".dll", ".pdb") } -Verbose
}

# Preserve dependency notices alongside the optional Crashpad runtime.
$gameOutput = if ("${{ inputs.preset }}" -like "win32*") {
"$buildDir\${{ inputs.game }}\$configToUse"
} else {
"$buildDir\${{ inputs.game }}"
}
if (Test-Path "$gameOutput\crashpad-notices") {
Copy-Item -Path "$gameOutput\crashpad-notices" -Destination $artifactsDir -Recurse -Force
}
$files | Move-Item -Destination $artifactsDir -Verbose -Force

- name: Upload ${{ inputs.game }} ${{ inputs.preset }}${{ inputs.tools && '+t' || '' }}${{ inputs.extras && '+e' || '' }} Artifact
Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ endif()

add_subdirectory(resources)

include(cmake/crashpad.cmake)

add_subdirectory(Core)

# Add main build targets
Expand Down
2 changes: 2 additions & 0 deletions Core/GameEngine/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ set(GAMEENGINE_SRC
Include/Common/MapObject.h
Include/Common/MapReaderWriterInfo.h
Include/Common/MessageStream.h
Include/Common/CrashReporting.h
Include/Common/MiniDumper.h
Include/Common/MiniLog.h
Include/Common/MiscAudio.h
Expand Down Expand Up @@ -674,6 +675,7 @@ set(GAMEENGINE_SRC
# Source/Common/System/List.cpp
Source/Common/System/LocalFile.cpp
Source/Common/System/LocalFileSystem.cpp
Source/Common/System/CrashReporting.cpp
Source/Common/System/MiniDumper.cpp
Source/Common/System/ObjectStatusTypes.cpp
# Source/Common/System/QuotedPrintable.cpp
Expand Down
30 changes: 30 additions & 0 deletions Core/GameEngine/Include/Common/CrashReporting.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
** 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/>.
*/

#pragma once

class AsciiString;

// Keep this interface compatible with VC6 and tools that only use MiniDumper.
namespace CrashReporting
{
void initialize(const AsciiString& userDirectory, int major, int minor, int build);
void userDirectoryReady(const AsciiString& userDirectory);
void captureFatal();
void shutdown();
}
161 changes: 161 additions & 0 deletions Core/GameEngine/Source/Common/System/CrashReporting.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
** 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/>.
*/

// TheSuperHackers @feature Codex 01/10/2026 Select optional local Crashpad reporting with legacy MiniDumper fallback.
#include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine
#include "Common/CrashReporting.h"
#include "Common/MiniDumper.h"

#ifdef RTS_USE_CRASHPAD
#include "CrashpadBridge.h"
#include "gitinfo.h"

namespace
{
RtsCrashpadCaptureFatalFunction capture;
RtsCrashpadShutdownFunction stop;
bool awaitingUserDirectory;
int savedMajor;
int savedMinor;
int savedBuild;

bool StartCrashpad(const AsciiString& userDirectory, int major, int minor, int build)
{
wchar_t path[32768];
const DWORD count = GetModuleFileNameW(nullptr, path, ARRAY_SIZE(path));
if (!count || count >= ARRAY_SIZE(path))
{
return false;
}

wchar_t* leaf = wcsrchr(path, L'\\');
const wchar_t name[] = L"rts_crashpad.dll";
if (!leaf || (leaf + 1 - path) + ARRAY_SIZE(name) > ARRAY_SIZE(path))
{
return false;
}

memcpy(leaf + 1, name, sizeof(name));
HMODULE module = LoadLibraryExW(path, nullptr, LOAD_WITH_ALTERED_SEARCH_PATH);
if (!module)
{
return false;
}

RtsCrashpadInitializeFunction start = reinterpret_cast<RtsCrashpadInitializeFunction>(
GetProcAddress(module, "RtsCrashpadInitialize"));
capture = reinterpret_cast<RtsCrashpadCaptureFatalFunction>(GetProcAddress(module, "RtsCrashpadCaptureFatal"));
stop = reinterpret_cast<RtsCrashpadShutdownFunction>(GetProcAddress(module, "RtsCrashpadShutdown"));
char version[64];
snprintf(version, ARRAY_SIZE(version), "%d.%d.%d", major, minor, build);
#if RTS_ZEROHOUR
const char* game = "Zero Hour";
#else
const char* game = "Generals";
#endif
if (start && capture && stop && start(userDirectory.str(), game, version, GitSHA1, GitUncommittedChanges))
{
return true;
}

// Crashpad retains process-lifetime pointers even after failed startup.
// Keep the module loaded; its initializer restores the previous filter.
capture = nullptr;
stop = nullptr;
return false;
}
}
#endif

void CrashReporting::initialize(const AsciiString& userDirectory, int major, int minor, int build)
{
#ifdef RTS_USE_CRASHPAD
// Generals resolves UserDataLeafName from GameData.ini during engine init.
// Zero Hour already has its registry-derived path at WinMain startup.
awaitingUserDirectory = userDirectory.isEmpty();
savedMajor = major;
savedMinor = minor;
savedBuild = build;
if (!awaitingUserDirectory && StartCrashpad(userDirectory, major, minor, build))
{
OutputDebugStringA("Crash reporting: Crashpad (local only)\n");
DEBUG_LOG(("Crash reporting: Crashpad (local only)\n"));
return;
}

if (!awaitingUserDirectory)
{
OutputDebugStringA("Crashpad startup failed; trying MiniDumper\n");
DEBUG_LOG(("Crashpad startup failed; trying MiniDumper\n"));
}
#endif
#ifdef RTS_ENABLE_CRASHDUMP
MiniDumper::initMiniDumper(userDirectory);
DEBUG_LOG(("Crash reporting: %s\n",
TheMiniDumper && TheMiniDumper->IsInitialized() ? "minidumper" : "unavailable"));
#endif
}

void CrashReporting::userDirectoryReady(const AsciiString& userDirectory)
{
#ifdef RTS_USE_CRASHPAD
// Tools never call initialize(), so this cannot start Crashpad in a tool.
if (awaitingUserDirectory && !userDirectory.isEmpty())
{
MiniDumper::shutdownMiniDumper();
initialize(userDirectory, savedMajor, savedMinor, savedBuild);
}
#endif
}

void CrashReporting::captureFatal()
{
#ifdef RTS_USE_CRASHPAD
if (capture)
{
capture();
return;
}
#endif
#ifdef RTS_ENABLE_CRASHDUMP
if (TheMiniDumper && TheMiniDumper->IsInitialized())
{
// Preserve both minimal and full memory dumps for the legacy backend.
TheMiniDumper->TriggerMiniDump(DumpType_Minimal);
TheMiniDumper->TriggerMiniDump(DumpType_Full);
}

MiniDumper::shutdownMiniDumper();
#endif
}

void CrashReporting::shutdown()
{
#ifdef RTS_USE_CRASHPAD
awaitingUserDirectory = false;
if (stop)
{
stop();
stop = nullptr;
capture = nullptr;
}
#endif
#ifdef RTS_ENABLE_CRASHDUMP
MiniDumper::shutdownMiniDumper();
#endif
}
38 changes: 15 additions & 23 deletions Core/GameEngine/Source/Common/System/Debug.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,7 @@
#if defined(DEBUG_STACKTRACE) || defined(IG_DEBUG_STACKTRACE)
#include "Common/StackDump.h"
#endif
#ifdef RTS_ENABLE_CRASHDUMP
#include "Common/MiniDumper.h"
#endif
#include "Common/CrashReporting.h"

// Horrible reference, but we really, really need to know if we are windowed.
extern bool DX8Wrapper_IsWindowed;
Expand Down Expand Up @@ -730,23 +728,16 @@ double SimpleProfiler::getAverageTime()
}


static void TriggerMiniDump()
void ReleaseCrash(const char *reason)
{
#ifdef RTS_ENABLE_CRASHDUMP
if (TheMiniDumper && TheMiniDumper->IsInitialized())
CrashReporting::captureFatal();

// We are shutting down, and TheGlobalData has been freed. jba. [4/15/2003]
if (TheGlobalData == nullptr)
{
// Create both minimal and full memory dumps
TheMiniDumper->TriggerMiniDump(DumpType_Minimal);
TheMiniDumper->TriggerMiniDump(DumpType_Full);
return;
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

MiniDumper::shutdownMiniDumper();
#endif
}


void ReleaseCrash(const char *reason)
{
/// do additional reporting on the crash, if possible

if (!DX8Wrapper_IsWindowed) {
Expand All @@ -755,15 +746,9 @@ void ReleaseCrash(const char *reason)
}
}

TriggerMiniDump();

char prevbuf[ _MAX_PATH ];
char curbuf[ _MAX_PATH ];

if (TheGlobalData==nullptr) {
return; // We are shutting down, and TheGlobalData has been freed. jba. [4/15/2003]
}

strlcpy(prevbuf, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(prevbuf));
strlcat(prevbuf, RELEASECRASH_FILE_NAME_PREV, ARRAY_SIZE(prevbuf));
strlcpy(curbuf, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(curbuf));
Expand Down Expand Up @@ -833,13 +818,20 @@ void ReleaseCrash(const char *reason)

void ReleaseCrashLocalized(const AsciiString& p, const AsciiString& m)
{
// TheSuperHackers @bugfix Codex 01/10/2026 Match ReleaseCrash during shutdown instead of dereferencing freed global data.
if (TheGlobalData == nullptr)
{
CrashReporting::captureFatal();
return;
}

if (!TheGameText) {
ReleaseCrash(m.str());
// This won't ever return
return;
}

TriggerMiniDump();
CrashReporting::captureFatal();

UnicodeString prompt = TheGameText->fetch(p);
UnicodeString mesg = TheGameText->fetch(m);
Expand Down
9 changes: 8 additions & 1 deletion Core/GameEngine/Source/Common/System/MiniDumper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,14 @@ void MiniDumper::initMiniDumper(const AsciiString& userDirPath)

// Use placement new on the process heap so TheMiniDumper is placed outside the MemoryPoolFactory managed area.
// If the crash is due to corrupted MemoryPoolFactory structures, try to mitigate the chances of MiniDumper memory also being corrupted
TheMiniDumper = new (::HeapAlloc(::GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS, sizeof(MiniDumper))) MiniDumper;
// TheSuperHackers @bugfix Codex 01/10/2026 Reporting initialization must not crash the game when allocation fails.
void* storage = ::HeapAlloc(::GetProcessHeap(), 0, sizeof(MiniDumper));
if (!storage)
{
return;
}

TheMiniDumper = new (storage) MiniDumper;
TheMiniDumper->Initialize(userDirPath);
}

Expand Down
Loading
Loading