diff --git a/.github/workflows/reusable-build-toolchain.yml b/.github/workflows/reusable-build-toolchain.yml
index 7e2f84e4ca7..bb7db873219 100644
--- a/.github/workflows/reusable-build-toolchain.yml
+++ b/.github/workflows/reusable-build-toolchain.yml
@@ -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
@@ -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
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 39509160788..d414d7df3db 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -89,6 +89,8 @@ endif()
add_subdirectory(resources)
+include(cmake/crashpad.cmake)
+
add_subdirectory(Core)
# Add main build targets
diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt
index e29b9f93290..90b9620fada 100644
--- a/Core/GameEngine/CMakeLists.txt
+++ b/Core/GameEngine/CMakeLists.txt
@@ -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
@@ -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
diff --git a/Core/GameEngine/Include/Common/CrashReporting.h b/Core/GameEngine/Include/Common/CrashReporting.h
new file mode 100644
index 00000000000..d40cdb95755
--- /dev/null
+++ b/Core/GameEngine/Include/Common/CrashReporting.h
@@ -0,0 +1,31 @@
+/*
+** 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 .
+*/
+
+#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);
+ const char* backendName();
+ void captureFatal();
+ void shutdown();
+}
diff --git a/Core/GameEngine/Source/Common/System/CrashReporting.cpp b/Core/GameEngine/Source/Common/System/CrashReporting.cpp
new file mode 100644
index 00000000000..377f2e56bfe
--- /dev/null
+++ b/Core/GameEngine/Source/Common/System/CrashReporting.cpp
@@ -0,0 +1,178 @@
+/*
+** 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 .
+*/
+
+// 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(
+ GetProcAddress(module, "RtsCrashpadInitialize"));
+ capture = reinterpret_cast(GetProcAddress(module, "RtsCrashpadCaptureFatal"));
+ stop = reinterpret_cast(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", backendName()));
+#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
+}
+
+const char* CrashReporting::backendName()
+{
+#ifdef RTS_USE_CRASHPAD
+ if (capture)
+ {
+ return "crashpad";
+ }
+#endif
+#ifdef RTS_ENABLE_CRASHDUMP
+ if (TheMiniDumper && TheMiniDumper->IsInitialized())
+ {
+ return "minidumper";
+ }
+#endif
+
+ return "unavailable";
+}
+
+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
+}
diff --git a/Core/GameEngine/Source/Common/System/Debug.cpp b/Core/GameEngine/Source/Common/System/Debug.cpp
index 815ec558ea8..27e212f2c5d 100644
--- a/Core/GameEngine/Source/Common/System/Debug.cpp
+++ b/Core/GameEngine/Source/Common/System/Debug.cpp
@@ -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;
@@ -732,21 +730,20 @@ double SimpleProfiler::getAverageTime()
static void TriggerMiniDump()
{
-#ifdef RTS_ENABLE_CRASHDUMP
- if (TheMiniDumper && TheMiniDumper->IsInitialized())
- {
- // Create both minimal and full memory dumps
- TheMiniDumper->TriggerMiniDump(DumpType_Minimal);
- TheMiniDumper->TriggerMiniDump(DumpType_Full);
- }
-
- MiniDumper::shutdownMiniDumper();
-#endif
+ CrashReporting::captureFatal();
}
void ReleaseCrash(const char *reason)
{
+ TriggerMiniDump();
+
+ // We are shutting down, and TheGlobalData has been freed. jba. [4/15/2003]
+ if (TheGlobalData == nullptr)
+ {
+ return;
+ }
+
/// do additional reporting on the crash, if possible
if (!DX8Wrapper_IsWindowed) {
@@ -755,15 +752,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));
@@ -833,6 +824,13 @@ 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)
+ {
+ TriggerMiniDump();
+ return;
+ }
+
if (!TheGameText) {
ReleaseCrash(m.str());
// This won't ever return
diff --git a/Core/GameEngine/Source/Common/System/MiniDumper.cpp b/Core/GameEngine/Source/Common/System/MiniDumper.cpp
index 53a36ebf627..38a1f794a61 100644
--- a/Core/GameEngine/Source/Common/System/MiniDumper.cpp
+++ b/Core/GameEngine/Source/Common/System/MiniDumper.cpp
@@ -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);
}
diff --git a/Dependencies/Crashpad/CMakeLists.txt b/Dependencies/Crashpad/CMakeLists.txt
new file mode 100644
index 00000000000..77cf22030da
--- /dev/null
+++ b/Dependencies/Crashpad/CMakeLists.txt
@@ -0,0 +1,64 @@
+# Acquire separately with this directory's manifest. Disabled builds never run
+# vcpkg or look for Crashpad. A copied, pinned installed prefix works without a
+# vcpkg toolchain in the game build.
+set(RTS_CRASHPAD_PACKAGE_ROOT "" CACHE PATH "Pinned x86-windows-static-md Crashpad installed prefix")
+set(RTS_CRASHPAD_REVISION "7e0af1d4d45b526f01677e74a56f4a951b70517d")
+set(RTS_CRASHPAD_MINI_CHROMIUM_REVISION "dce72d97d1c2e9beb5e206c6a05a702269794ca3")
+set(package "${RTS_CRASHPAD_PACKAGE_ROOT}")
+if(NOT EXISTS "${package}/share/crashpad/vcpkg.spdx.json")
+ message(FATAL_ERROR "Set RTS_CRASHPAD_PACKAGE_ROOT to the x86-windows-static-md prefix built with Dependencies/Crashpad/vcpkg.json. See docs/crashpad.md.")
+endif()
+file(READ "${package}/share/crashpad/vcpkg.spdx.json" provenance)
+foreach(required "crashpad:x86-windows-static-md@2024-04-11#7"
+ "${RTS_CRASHPAD_REVISION}" "${RTS_CRASHPAD_MINI_CHROMIUM_REVISION}")
+ string(FIND "${provenance}" "${required}" position)
+ if(position EQUAL -1)
+ message(FATAL_ERROR "Crashpad package does not match the pinned x86 package: missing ${required}")
+ endif()
+endforeach()
+
+# The baseline port's config uses these two variables even outside a vcpkg
+# toolchain. Keep the workaround scoped to this dependency directory.
+get_filename_component(_VCPKG_INSTALLED_DIR "${package}" DIRECTORY)
+get_filename_component(VCPKG_TARGET_TRIPLET "${package}" NAME)
+list(PREPEND CMAKE_PREFIX_PATH "${package}")
+find_package(crashpad CONFIG REQUIRED PATHS "${package}/share/crashpad" NO_DEFAULT_PATH)
+set(handler "${package}/tools/crashpad/crashpad_handler.exe")
+if(NOT EXISTS "${handler}")
+ message(FATAL_ERROR "The pinned package is missing its matching crashpad_handler.exe")
+endif()
+set(RTS_CRASHPAD_HANDLER "${handler}" PARENT_SCOPE)
+
+# Deploy the redistributable x86 VC runtime from the selected Visual Studio.
+# The Windows 10+ Universal CRT is supplied by the OS. Debug builds still need
+# a developer installation of the non-redistributable debug CRT.
+set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE)
+include(InstallRequiredSystemLibraries)
+if(NOT CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS)
+ message(FATAL_ERROR "The Visual Studio x86 redistributable runtime was not found")
+endif()
+set(RTS_CRASHPAD_RUNTIME_FILES "${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS}" PARENT_SCOPE)
+
+add_library(rts_crashpad SHARED CrashpadBridge.cpp CrashpadBridge.h CrashpadBridge.def)
+target_compile_features(rts_crashpad PRIVATE cxx_std_20)
+target_link_libraries(rts_crashpad PRIVATE crashpad::crashpad)
+set_target_properties(rts_crashpad PROPERTIES
+ MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL"
+ DISABLE_PRECOMPILE_HEADERS ON)
+target_compile_options(rts_crashpad PRIVATE /Zi)
+target_link_options(rts_crashpad PRIVATE /DEBUG)
+target_compile_definitions(rts_crashpad PRIVATE
+ "RTS_CRASHPAD_COMPILER=\"${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}\""
+ "RTS_CRASHPAD_CONFIGURATION=\"$\""
+ "RTS_CRASHPAD_REVISION=\"${RTS_CRASHPAD_REVISION}\"")
+
+set(notices "${CMAKE_CURRENT_BINARY_DIR}/notices")
+file(MAKE_DIRECTORY "${notices}")
+configure_file("${package}/share/crashpad/copyright" "${notices}/crashpad-LICENSE" COPYONLY)
+configure_file("${package}/share/zlib/copyright" "${notices}/zlib-LICENSE" COPYONLY)
+configure_file("${package}/share/crashpad/vcpkg.spdx.json" "${notices}/crashpad-package.spdx.json" COPYONLY)
+configure_file("${package}/share/zlib/vcpkg.spdx.json" "${notices}/zlib-package.spdx.json" COPYONLY)
+configure_file("${package}/share/crashpad/vcpkg_abi_info.txt" "${notices}/crashpad-package-abi.txt" COPYONLY)
+configure_file(getopt-LICENSE "${notices}/getopt-LICENSE" COPYONLY)
+configure_file(mini-chromium-LICENSE "${notices}/mini-chromium-LICENSE" COPYONLY)
+set(RTS_CRASHPAD_NOTICES "${notices}" PARENT_SCOPE)
diff --git a/Dependencies/Crashpad/CrashpadBridge.cpp b/Dependencies/Crashpad/CrashpadBridge.cpp
new file mode 100644
index 00000000000..fe6620c8d81
--- /dev/null
+++ b/Dependencies/Crashpad/CrashpadBridge.cpp
@@ -0,0 +1,254 @@
+/*
+** 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 .
+*/
+
+// Deliberately compiled outside the game PCH, forced includes and allocator.
+#include
+#include
+#include
+#include
+#include
+
+#include "CrashpadBridge.h"
+#include "client/crashpad_client.h"
+#include "client/crash_report_database.h"
+#include "client/prune_crash_reports.h"
+#include "client/settings.h"
+#include "util/misc/capture_context.h"
+
+namespace
+{
+ std::unique_ptr client;
+ LPTOP_LEVEL_EXCEPTION_FILTER previousFilter;
+ void (*previousAbortHandler)(int);
+ HANDLE stopEvent;
+ HANDLE captureEvent;
+ HANDLE completedEvent;
+ HANDLE watchdogThread;
+ volatile LONG capturedFatal;
+ bool attempted;
+ bool active;
+
+ // DumpWithoutCrash waits indefinitely in the pinned Windows client. Keep
+ // the request on the faulting thread, and bound fatal capture with a thread
+ // created while the process is healthy. Do not terminate a reporting thread
+ // and then let the game reuse memory still referenced by the handler.
+ DWORD WINAPI Watchdog(void*)
+ {
+ const HANDLE events[] = {stopEvent, captureEvent};
+ if (WaitForMultipleObjects(2, events, FALSE, INFINITE) == WAIT_OBJECT_0 + 1)
+ {
+ if (WaitForSingleObject(completedEvent, 15000) != WAIT_OBJECT_0)
+ {
+ TerminateProcess(GetCurrentProcess(), 1);
+ }
+ }
+
+ return 0;
+ }
+
+ LONG WINAPI AlreadyCaptured(EXCEPTION_POINTERS*)
+ {
+ // A failure in best-effort fatal diagnostics must not create a second
+ // report that hides the original explicit fatal capture.
+ TerminateProcess(GetCurrentProcess(), 1);
+ return EXCEPTION_EXECUTE_HANDLER;
+ }
+
+ void StopWatchdog()
+ {
+ if (watchdogThread)
+ {
+ SetEvent(stopEvent);
+ WaitForSingleObject(watchdogThread, INFINITE);
+ CloseHandle(watchdogThread);
+ watchdogThread = nullptr;
+ }
+
+ if (stopEvent)
+ {
+ CloseHandle(stopEvent);
+ stopEvent = nullptr;
+ }
+
+ if (captureEvent)
+ {
+ CloseHandle(captureEvent);
+ captureEvent = nullptr;
+ }
+
+ if (completedEvent)
+ {
+ CloseHandle(completedEvent);
+ completedEvent = nullptr;
+ }
+ }
+
+ bool Initialize(const char* userDirectory, const char* game,
+ const char* version, const char* revision, int dirty)
+ {
+ const int count = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
+ userDirectory, -1, nullptr, 0);
+ if (count <= 1)
+ {
+ return false;
+ }
+
+ std::vector userPath(count);
+ if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, userDirectory,
+ -1, userPath.data(), count))
+ {
+ return false;
+ }
+
+ const base::FilePath user(userPath.data());
+ if (!user.IsAbsolute())
+ {
+ return false;
+ }
+
+ std::vector executable(32768);
+ const DWORD length = GetModuleFileNameW(nullptr, executable.data(),
+ static_cast(executable.size()));
+ if (!length || length >= executable.size())
+ {
+ return false;
+ }
+
+ const base::FilePath handler = base::FilePath(executable.data()).DirName()
+ .Append(L"crashpad_handler.exe");
+ if (GetFileAttributesW(handler.value().c_str()) == INVALID_FILE_ATTRIBUTES)
+ {
+ return false;
+ }
+
+ const base::FilePath dumps = user.Append(L"CrashDumps");
+ if (!CreateDirectoryW(dumps.value().c_str(), nullptr) && GetLastError() != ERROR_ALREADY_EXISTS)
+ {
+ return false;
+ }
+
+ const base::FilePath databasePath = dumps.Append(L"Crashpad");
+ auto database = crashpad::CrashReportDatabase::Initialize(databasePath);
+ if (!database || !database->GetSettings()->SetUploadsEnabled(false))
+ {
+ return false;
+ }
+
+ // Prune through the database so active/locked reports stay protected.
+ // Disable the handler's different default policy below. The size limit
+ // applies at startup; reports from concurrent runs can exceed it until
+ // the next startup.
+ crashpad::BinaryPruneCondition retention(crashpad::BinaryPruneCondition::OR,
+ new crashpad::AgePruneCondition(30),
+ new crashpad::DatabaseSizePruneCondition(500 * 1024));
+ crashpad::PruneCrashReportDatabase(database.get(), &retention);
+
+ stopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
+ captureEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
+ completedEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
+ if (!stopEvent || !captureEvent || !completedEvent)
+ {
+ return false;
+ }
+
+ watchdogThread = CreateThread(nullptr, 0, Watchdog, nullptr, 0, nullptr);
+ if (!watchdogThread)
+ {
+ return false;
+ }
+
+ const std::map annotations = {
+ {"game", game}, {"version", version}, {"revision", revision},
+ {"dirty", dirty ? "true" : "false"}, {"architecture", "x86"},
+ {"compiler", RTS_CRASHPAD_COMPILER}, {"configuration", RTS_CRASHPAD_CONFIGURATION},
+ {"backend", "crashpad"}, {"crashpad_revision", RTS_CRASHPAD_REVISION}
+ };
+ client = std::make_unique();
+ // Wait for the IPC ping before claiming readiness, but do not block game
+ // startup forever if a valid executable never services the pipe. On a
+ // timeout the DLL must stay loaded for the upstream background thread.
+ // Windows has no automatic restart here. No URL, metrics or attachments.
+ return client->StartHandler(handler, databasePath, base::FilePath(), "",
+ annotations, {"--no-periodic-tasks"}, false, true)
+ && client->WaitForHandlerStart(5000);
+ }
+}
+
+extern "C" int __cdecl RtsCrashpadInitialize(const char* userDirectory,
+ const char* game, const char* version, const char* revision, int dirty)
+{
+ if (attempted)
+ {
+ return active;
+ }
+
+ attempted = true;
+ previousFilter = SetUnhandledExceptionFilter(nullptr);
+ SetUnhandledExceptionFilter(previousFilter);
+ previousAbortHandler = std::signal(SIGABRT, SIG_DFL);
+ std::signal(SIGABRT, previousAbortHandler);
+ try
+ {
+ active = Initialize(userDirectory, game, version, revision, dirty);
+ }
+ catch (...)
+ {
+ active = false;
+ }
+
+ if (!active)
+ {
+ RtsCrashpadShutdown();
+ }
+
+ return active;
+}
+
+extern "C" void __cdecl RtsCrashpadCaptureFatal()
+{
+ if (!active || InterlockedCompareExchange(&capturedFatal, 1, 0) != 0)
+ {
+ return;
+ }
+
+ CONTEXT context;
+ crashpad::CaptureContext(&context);
+ if (!SetEvent(captureEvent))
+ {
+ TerminateProcess(GetCurrentProcess(), 1);
+ }
+
+ crashpad::CrashpadClient::DumpWithoutCrash(context);
+ SetEvent(completedEvent);
+ SetUnhandledExceptionFilter(AlreadyCaptured);
+ // Removes the heap-corruption vectored handler too. The client leaves its
+ // IPC state alive for process lifetime; never unload this DLL.
+ client.reset();
+ std::signal(SIGABRT, SIG_DFL);
+}
+
+extern "C" void __cdecl RtsCrashpadShutdown()
+{
+ active = false;
+ SetUnhandledExceptionFilter(previousFilter);
+ std::signal(SIGABRT, previousAbortHandler);
+ client.reset();
+ StopWatchdog();
+ // The handler observes process exit and exits itself. Crashpad's Windows
+ // API intentionally retains its IPC handles until then.
+}
diff --git a/Dependencies/Crashpad/CrashpadBridge.def b/Dependencies/Crashpad/CrashpadBridge.def
new file mode 100644
index 00000000000..01786e581ae
--- /dev/null
+++ b/Dependencies/Crashpad/CrashpadBridge.def
@@ -0,0 +1,4 @@
+EXPORTS
+ RtsCrashpadInitialize
+ RtsCrashpadCaptureFatal
+ RtsCrashpadShutdown
diff --git a/Dependencies/Crashpad/CrashpadBridge.h b/Dependencies/Crashpad/CrashpadBridge.h
new file mode 100644
index 00000000000..88b23577793
--- /dev/null
+++ b/Dependencies/Crashpad/CrashpadBridge.h
@@ -0,0 +1,31 @@
+/*
+** 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 .
+*/
+
+#pragma once
+
+// C ABI only. No allocation or C++ object crosses the game/DLL boundary.
+// The game loads these exports explicitly so a missing DLL permits fallback.
+typedef int (__cdecl* RtsCrashpadInitializeFunction)(const char* userDirectory,
+ const char* game, const char* version, const char* revision, int dirty);
+typedef void (__cdecl* RtsCrashpadCaptureFatalFunction)();
+typedef void (__cdecl* RtsCrashpadShutdownFunction)();
+
+extern "C" int __cdecl RtsCrashpadInitialize(const char* userDirectory,
+ const char* game, const char* version, const char* revision, int dirty);
+extern "C" void __cdecl RtsCrashpadCaptureFatal();
+extern "C" void __cdecl RtsCrashpadShutdown();
diff --git a/Dependencies/Crashpad/getopt-LICENSE b/Dependencies/Crashpad/getopt-LICENSE
new file mode 100644
index 00000000000..4444b1201e3
--- /dev/null
+++ b/Dependencies/Crashpad/getopt-LICENSE
@@ -0,0 +1,5 @@
+Copyright (C) 1997 Gregory Pietsch
+
+[These files] are hereby placed in the public domain without restrictions. Just
+give the author credit, don't claim you wrote it or prevent anyone else from
+using it.
diff --git a/Dependencies/Crashpad/mini-chromium-LICENSE b/Dependencies/Crashpad/mini-chromium-LICENSE
new file mode 100644
index 00000000000..179bf344522
--- /dev/null
+++ b/Dependencies/Crashpad/mini-chromium-LICENSE
@@ -0,0 +1,27 @@
+// Copyright 2006-2008 The Chromium Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Dependencies/Crashpad/vcpkg.json b/Dependencies/Crashpad/vcpkg.json
new file mode 100644
index 00000000000..e25fbd62a52
--- /dev/null
+++ b/Dependencies/Crashpad/vcpkg.json
@@ -0,0 +1,5 @@
+{
+ "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
+ "builtin-baseline": "b02e341c927f16d991edbd915d8ea43eac52096c",
+ "dependencies": ["crashpad"]
+}
diff --git a/Generals/CMakeLists.txt b/Generals/CMakeLists.txt
index a226d1d46a5..3b047c099ce 100644
--- a/Generals/CMakeLists.txt
+++ b/Generals/CMakeLists.txt
@@ -38,6 +38,7 @@ if("${CMAKE_HOST_SYSTEM}" MATCHES "Windows" AND "${CMAKE_SYSTEM}" MATCHES "Windo
endif()
if(RTS_INSTALL_PREFIX_GENERALS)
+ rts_install_crashpad(g_generals "${RTS_INSTALL_PREFIX_GENERALS}")
install(TARGETS g_generals RUNTIME DESTINATION "${RTS_INSTALL_PREFIX_GENERALS}")
install(FILES $ DESTINATION "${RTS_INSTALL_PREFIX_GENERALS}" OPTIONAL)
diff --git a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp
index 039321096d0..b317e67f120 100644
--- a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp
+++ b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp
@@ -27,6 +27,7 @@
// Author: Michael S. Booth, April 2001
#include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine
+#include "Common/CrashReporting.h"
#include "Common/ActionManager.h"
#include "Common/AudioAffect.h"
@@ -402,6 +403,9 @@ void GameEngine::init()
// special-case: parse command-line parameters after loading global data
CommandLine::parseCommandLineForEngineInit();
+ // Generals obtains its user-data path from the GameData INI files above.
+ CrashReporting::userDirectoryReady(TheGlobalData->getPath_UserData());
+
TheArchiveFileSystem->loadMods();
// doesn't require resets so just create a single instance here.
diff --git a/Generals/Code/Main/CMakeLists.txt b/Generals/Code/Main/CMakeLists.txt
index 8df79b91bb7..3cc5214fcf8 100644
--- a/Generals/Code/Main/CMakeLists.txt
+++ b/Generals/Code/Main/CMakeLists.txt
@@ -91,3 +91,5 @@ endif()
if(MINGW AND COMMAND add_debug_strip_target)
add_debug_strip_target(g_generals)
endif()
+
+rts_enable_crashpad(g_generals g_gameengine)
diff --git a/Generals/Code/Main/WinMain.cpp b/Generals/Code/Main/WinMain.cpp
index a2a4caa2241..c29dc9b89ef 100644
--- a/Generals/Code/Main/WinMain.cpp
+++ b/Generals/Code/Main/WinMain.cpp
@@ -67,6 +67,7 @@
#ifdef RTS_ENABLE_CRASHDUMP
#include "Common/MiniDumper.h"
#endif
+#include "Common/CrashReporting.h"
// GLOBALS ////////////////////////////////////////////////////////////////////
@@ -838,12 +839,13 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
gLoadScreenBitmap = (HBITMAP)LoadImage(hInstance, "Install_Final.bmp", IMAGE_BITMAP, 0, 0, LR_SHARED|LR_LOADFROMFILE);
#ifdef RTS_ENABLE_CRASHDUMP
- // Initialize minidump facilities - requires TheGlobalData so performed after parseCommandLineForStartup
- MiniDumper::initMiniDumper(TheGlobalData->getPath_UserData());
+ // Begin reporting with TheGlobalData available; the INIs provide the user-data path later.
+ CrashReporting::initialize(TheGlobalData->getPath_UserData(), VERSION_MAJOR, VERSION_MINOR, VERSION_BUILDNUM);
#endif
// register windows class and create application window
if(!TheGlobalData->m_headless && initializeAppWindows(hInstance, nCmdShow, TheGlobalData->m_windowed) == false)
{
+ CrashReporting::shutdown();
return exitcode;
}
@@ -881,6 +883,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
DEBUG_LOG(("Generals is already running...Bail!"));
delete TheVersion;
TheVersion = nullptr;
+ CrashReporting::shutdown();
shutdownMemoryManager();
return exitcode;
}
@@ -911,7 +914,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
}
#ifdef RTS_ENABLE_CRASHDUMP
- MiniDumper::shutdownMiniDumper();
+ CrashReporting::shutdown();
#endif
TheAsciiStringCriticalSection = nullptr;
TheUnicodeStringCriticalSection = nullptr;
diff --git a/GeneralsMD/CMakeLists.txt b/GeneralsMD/CMakeLists.txt
index 311a5a0d409..6484aff13be 100644
--- a/GeneralsMD/CMakeLists.txt
+++ b/GeneralsMD/CMakeLists.txt
@@ -38,6 +38,7 @@ if("${CMAKE_HOST_SYSTEM}" MATCHES "Windows" AND "${CMAKE_SYSTEM}" MATCHES "Windo
endif()
if(RTS_INSTALL_PREFIX_ZEROHOUR)
+ rts_install_crashpad(z_generals "${RTS_INSTALL_PREFIX_ZEROHOUR}")
install(TARGETS z_generals RUNTIME DESTINATION "${RTS_INSTALL_PREFIX_ZEROHOUR}")
install(FILES $ DESTINATION "${RTS_INSTALL_PREFIX_ZEROHOUR}" OPTIONAL)
diff --git a/GeneralsMD/Code/Main/CMakeLists.txt b/GeneralsMD/Code/Main/CMakeLists.txt
index d1f4dc06683..0ee8660f8e0 100644
--- a/GeneralsMD/Code/Main/CMakeLists.txt
+++ b/GeneralsMD/Code/Main/CMakeLists.txt
@@ -78,3 +78,5 @@ endif()
if(MINGW AND COMMAND add_debug_strip_target)
add_debug_strip_target(z_generals)
endif()
+
+rts_enable_crashpad(z_generals z_gameengine)
diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp
index d5040bc1dd7..e2be5260924 100644
--- a/GeneralsMD/Code/Main/WinMain.cpp
+++ b/GeneralsMD/Code/Main/WinMain.cpp
@@ -69,6 +69,7 @@
#ifdef RTS_ENABLE_CRASHDUMP
#include "Common/MiniDumper.h"
#endif
+#include "Common/CrashReporting.h"
// GLOBALS ////////////////////////////////////////////////////////////////////
@@ -865,13 +866,14 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
#endif
#ifdef RTS_ENABLE_CRASHDUMP
- // Initialize minidump facilities - requires TheGlobalData so performed after parseCommandLineForStartup
- MiniDumper::initMiniDumper(TheGlobalData->getPath_UserData());
+ // Initialize reporting after parseCommandLineForStartup provides the user-data path.
+ CrashReporting::initialize(TheGlobalData->getPath_UserData(), VERSION_MAJOR, VERSION_MINOR, VERSION_BUILDNUM);
#endif
// register windows class and create application window
if(!TheGlobalData->m_headless && initializeAppWindows(hInstance, nCmdShow, TheGlobalData->m_windowed) == false)
{
+ CrashReporting::shutdown();
return exitcode;
}
@@ -909,6 +911,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
DEBUG_LOG(("Generals is already running...Bail!"));
delete TheVersion;
TheVersion = nullptr;
+ CrashReporting::shutdown();
shutdownMemoryManager();
return exitcode;
}
@@ -940,7 +943,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
}
#ifdef RTS_ENABLE_CRASHDUMP
- MiniDumper::shutdownMiniDumper();
+ CrashReporting::shutdown();
#endif
TheUnicodeStringCriticalSection = nullptr;
TheDmaCriticalSection = nullptr;
diff --git a/cmake/crashpad.cmake b/cmake/crashpad.cmake
new file mode 100644
index 00000000000..d0f42eea25b
--- /dev/null
+++ b/cmake/crashpad.cmake
@@ -0,0 +1,49 @@
+option(RTS_BUILD_OPTION_CRASHPAD "Use optional local-only Crashpad reporting in the games" OFF)
+add_feature_info(Crashpad RTS_BUILD_OPTION_CRASHPAD "Local-only out-of-process game crash reports")
+
+if(RTS_BUILD_OPTION_CRASHPAD)
+ if(NOT RTS_CRASHDUMP_ENABLE)
+ message(FATAL_ERROR "RTS_BUILD_OPTION_CRASHPAD requires RTS_CRASHDUMP_ENABLE=ON")
+ endif()
+ if(NOT WIN32 OR NOT MSVC OR IS_VS6_BUILD OR NOT CMAKE_SIZEOF_VOID_P EQUAL 4
+ OR MSVC_VERSION LESS 1920)
+ message(FATAL_ERROR "Crashpad requires modern MSVC or ClangCL targeting Win32 (x86)")
+ endif()
+ if(RTS_BUILD_OPTION_ASAN OR RTS_BUILD_CORE_FUZZ)
+ message(FATAL_ERROR "Keep Crashpad disabled in sanitizer and fuzzing builds")
+ endif()
+ add_subdirectory(Dependencies/Crashpad)
+endif()
+
+function(rts_enable_crashpad game engine)
+ if(NOT RTS_BUILD_OPTION_CRASHPAD)
+ return()
+ endif()
+ # Tools may link the same engine, but never initialize this optional backend.
+ # Neither the engine nor tools link the Crashpad client or DLL import library.
+ target_compile_definitions(${engine} PRIVATE RTS_USE_CRASHPAD=1)
+ target_include_directories(${engine} PRIVATE "${CMAKE_SOURCE_DIR}/Dependencies/Crashpad")
+ # Refresh runtime files before linking, even when only the DLL changed.
+ add_custom_target(${game}_crashpad_runtime
+ COMMAND ${CMAKE_COMMAND} -E make_directory "$"
+ COMMAND ${CMAKE_COMMAND} -E copy_if_different
+ "$" "$"
+ "${RTS_CRASHPAD_HANDLER}"
+ ${RTS_CRASHPAD_RUNTIME_FILES} "$"
+ COMMAND ${CMAKE_COMMAND} -E copy_directory
+ "${RTS_CRASHPAD_NOTICES}" "$/crashpad-notices"
+ DEPENDS rts_crashpad
+ VERBATIM)
+ add_dependencies(${game} ${game}_crashpad_runtime)
+endfunction()
+
+function(rts_install_crashpad game destination)
+ if(RTS_BUILD_OPTION_CRASHPAD)
+ install(FILES "$" "$"
+ "${RTS_CRASHPAD_HANDLER}"
+ DESTINATION "${destination}")
+ install(DIRECTORY "${RTS_CRASHPAD_NOTICES}/" DESTINATION "${destination}/crashpad-notices")
+ install(FILES ${RTS_CRASHPAD_RUNTIME_FILES} DESTINATION "${destination}"
+ CONFIGURATIONS Release RelWithDebInfo MinSizeRel)
+ endif()
+endfunction()
diff --git a/docs/crashpad.md b/docs/crashpad.md
new file mode 100644
index 00000000000..85a1c1c5868
--- /dev/null
+++ b/docs/crashpad.md
@@ -0,0 +1,83 @@
+# Optional local Crashpad reports
+
+`RTS_BUILD_OPTION_CRASHPAD` defaults to `OFF`. It enables local crash reporting
+through a separate handler process for modern MSVC and ClangCL Win32 game builds.
+WorldBuilder and other tools keep their existing reporting behavior. Disabled
+builds do not acquire or link Crashpad. Unsupported configurations, including
+VC6, non-Windows, x64, sanitizer/fuzzing builds and `RTS_CRASHDUMP_ENABLE=OFF`,
+reject an explicit request to enable it.
+
+The optional backend expects Windows 10 or newer for the Universal CRT. It does
+not change the OS requirements of default builds. A failure to load or start
+Crashpad falls back to MiniDumper; the game can start if neither is available.
+
+## Building
+
+Use the separate pinned dependency manifest so ordinary builds keep their
+existing dependencies. From the repository root in Git Bash, with `VCPKG_ROOT`
+pointing to vcpkg:
+
+```bash
+"$VCPKG_ROOT/vcpkg.exe" install \
+ --x-manifest-root=Dependencies/Crashpad \
+ --x-install-root=build/crashpad-package \
+ --triplet=x86-windows-static-md --host-triplet=x86-windows-static-md
+
+cmake -S . -B build/crashpad -G "Visual Studio 18 2026" -A Win32 -T ClangCL \
+ -DRTS_BUILD_OPTION_CRASHPAD=ON \
+ -DRTS_CRASHPAD_PACKAGE_ROOT="$PWD/build/crashpad-package/x86-windows-static-md" \
+ -DCMAKE_MSVC_DEBUG_INFORMATION_FORMAT=Embedded
+cmake --build build/crashpad --config Release --target g_generals z_generals --parallel 4
+```
+
+Omit `-T ClangCL` to build the games and adapter with MSVC. The dependency uses
+vcpkg's MSVC/GN build. Both triplets must be native x86 for the pinned port's
+Python helper. Configure checks the package's SPDX record against the pinned
+port version, architecture and source revisions. Use the handler from that same
+package. Debug builds need the Visual Studio debug CRT.
+
+## Capture and fallback
+
+The game loads `rts_crashpad.dll` by absolute path beside its executable. The DLL
+isolates Crashpad's C++ requirements and CRT allocations from the game's memory
+allocator. Zero Hour initializes it in WinMain. Generals uses MiniDumper until
+its user-data path is available after GameData loading and startup options.
+
+Startup waits up to five seconds for handler readiness. Failure restores the
+previous exception handlers and selects MiniDumper. A handler that dies later
+is not restarted and does not trigger automatic fallback. The DLL stays loaded
+because upstream retains process-lifetime IPC pointers; the handler exits when
+the game process exits.
+
+Crashpad handles unhandled exceptions and both explicit fatal-error paths.
+Explicit fatal errors capture once, before legacy diagnostics. A watchdog
+terminates the game if that capture stalls for 15 seconds, provided the process
+remains schedulable. Headless mode adds no dialogs. Crashpad reports omit the
+legacy unhandled-exception text and do not provide the legacy full-memory dump.
+
+## Reports and deployment
+
+Reports stay in `/CrashDumps/Crashpad/`. Uploads are disabled, with no
+upload URL or attachments. Each report is a binary minidump with build annotations
+including the game version, Git revision, dirty state, compiler and configuration.
+Crashpad does not generate a readable diagnosis of the crash.
+
+At startup, the database API prunes reports older than 30 days and older reports
+beyond a 500 MiB report-size budget. Active reports remain under Crashpad's
+control. This is not a hard disk quota; new reports can exceed it until the next
+startup. The handler's default pruning policy is disabled.
+
+After the game exits, copy the desired `.dmp` from the database's `reports`
+directory to share it. Keep the original database files in place. Dumps contain
+process memory; sharing is a separate user action.
+
+Enabled game outputs and CMake installs include the matching handler, adapter
+DLL/PDB, redistributable VC runtime DLLs and dependency notices. Set
+`RTS_INSTALL_PREFIX_GENERALS` and `RTS_INSTALL_PREFIX_ZEROHOUR` to explicit staging
+paths when testing installation. Runtime paths do not depend on the build tree.
+
+Keep the exact game executable/PDB, adapter DLL/PDB and handler for each
+distributed build, together with its source revision. Preserve the source
+snapshot for dirty builds. Open the dump as a crash dump in WinDbg or Visual
+Studio and load those matching PDBs to inspect the exception and call stack.
+CI artifact retention remains 30 days; preserve release symbols separately.