From 7f8be511b390e88c792d5f4a1c72654847eab53a Mon Sep 17 00:00:00 2001 From: JohnsterID <69278611+JohnsterID@users.noreply.github.com> Date: Tue, 6 Jan 2026 05:14:27 +0000 Subject: [PATCH 1/7] bugfix(system): Update crash message to show file locations and GitHub issues link --- .../GameEngine/Source/Common/System/Debug.cpp | 111 ++++++++++++++++-- 1 file changed, 99 insertions(+), 12 deletions(-) diff --git a/Core/GameEngine/Source/Common/System/Debug.cpp b/Core/GameEngine/Source/Common/System/Debug.cpp index cbabe05197a..5c52af7e250 100644 --- a/Core/GameEngine/Source/Common/System/Debug.cpp +++ b/Core/GameEngine/Source/Common/System/Debug.cpp @@ -746,6 +746,43 @@ static void TriggerMiniDump() } +// TheSuperHackers @bugfix JohnsterID 06/01/2025 Helper function to extract crash location from stack trace +static void extractCrashLocation(char* outBuffer, size_t bufferSize) +{ + outBuffer[0] = '\0'; + + if (bufferSize == 0 || g_LastErrorDump.isEmpty()) { + return; + } + + const char* stackStr = g_LastErrorDump.str(); + + // Skip leading whitespace/newlines + while (*stackStr && (*stackStr == ' ' || *stackStr == '\t' || *stackStr == '\n' || *stackStr == '\r')) { + stackStr++; + } + + // Skip if no content or contains "" + if (!*stackStr || strstr(stackStr, "") != NULL) { + return; + } + + // Find end of first line + const char* lineEnd = stackStr; + const size_t maxLineLength = bufferSize < 401 ? bufferSize - 1 : 400; + + while (*lineEnd && *lineEnd != '\n' && *lineEnd != '\r' && (size_t)(lineEnd - stackStr) < maxLineLength) { + lineEnd++; + } + + size_t len = lineEnd - stackStr; + if (len > 0) { + strncpy(outBuffer, stackStr, len); + outBuffer[len] = '\0'; + } +} + + void ReleaseCrash(const char *reason) { /// do additional reporting on the crash, if possible @@ -806,23 +843,50 @@ void ReleaseCrash(const char *reason) } } + // TheSuperHackers @bugfix JohnsterID 06/01/2025 Update crash message to show crash report locations + // and point users to repo. Removes outdated EA forum references. + // Also shows crash location from stack trace when debug symbols are available. + char crashInfoPath[_MAX_PATH]; + strlcpy(crashInfoPath, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(crashInfoPath)); + strlcat(crashInfoPath, RELEASECRASH_FILE_NAME, ARRAY_SIZE(crashInfoPath)); + +#ifdef RTS_ENABLE_CRASHDUMP + char crashDumpDir[_MAX_PATH]; + strlcpy(crashDumpDir, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(crashDumpDir)); + strlcat(crashDumpDir, "CrashDumps\\", ARRAY_SIZE(crashDumpDir)); +#endif + + char crashLocation[512]; + extractCrashLocation(crashLocation, ARRAY_SIZE(crashLocation)); + + char buff[2560]; + char* p = buff; + char* end = buff + sizeof(buff); + + p += snprintf(p, end - p, "The game encountered a critical error and needs to close.\n\n"); + #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); - ::MessageBox(nullptr, buff, "Technical Difficulties...", MB_OK|MB_SYSTEMMODAL|MB_ICONERROR); -#else -// crash error messaged changed 3/6/03 BGC -// ::MessageBox(nullptr, "Sorry, a serious error occurred.", "Technical Difficulties...", MB_OK|MB_TASKMODAL|MB_ICONERROR); -// ::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); + if (reason && *reason) { + p += snprintf(p, end - p, "Error: %s\n", reason); + } +#endif -// 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. - ::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_SYSTEMMODAL|MB_ICONERROR); + if (crashLocation[0] != '\0') { + p += snprintf(p, end - p, "Location:\n%s\n\n", crashLocation); + } else if (p > buff && *(p - 1) != '\n') { + p += snprintf(p, end - p, "\n"); + } + p += snprintf(p, end - p, "Crash report saved to:\n%s\n", crashInfoPath); +#ifdef RTS_ENABLE_CRASHDUMP + p += snprintf(p, end - p, "\nMinidump files saved to:\n%s\n", crashDumpDir); #endif + snprintf(p, end - p, "\nPlease report the issue:\nhttps://github.com/TheSuperHackers/GeneralsGameCode/issues"); + + ::MessageBox(NULL, buff, "Game Crash", MB_OK|MB_SYSTEMMODAL|MB_ICONERROR); + _exit(1); } @@ -848,9 +912,31 @@ void ReleaseCrashLocalized(const AsciiString& p, const AsciiString& m) } } + // TheSuperHackers @bugfix JohnsterID 06/01/2025 Append crash file locations to localized error message + char crashInfoPath[_MAX_PATH]; + strlcpy(crashInfoPath, TheGlobalData->getPath_UserData().str(), ARRAY_SIZE(crashInfoPath)); + strlcat(crashInfoPath, RELEASECRASH_FILE_NAME, ARRAY_SIZE(crashInfoPath)); + + char crashInfoAppendix[1024]; + snprintf(crashInfoAppendix, sizeof(crashInfoAppendix), + "\n\nCrash report: %s" +#ifdef RTS_ENABLE_CRASHDUMP + "\nMinidump files: %sCrashDumps\\" +#endif + "\n\nReport issue: https://github.com/TheSuperHackers/GeneralsGameCode/issues", + crashInfoPath +#ifdef RTS_ENABLE_CRASHDUMP + , TheGlobalData->getPath_UserData().str() +#endif + ); + if (TheSystemIsUnicode) { - ::MessageBoxW(nullptr, mesg.str(), prompt.str(), MB_OK|MB_SYSTEMMODAL|MB_ICONERROR); + UnicodeString appendix; + appendix.translate(crashInfoAppendix); + UnicodeString fullMessage = mesg; + fullMessage.concat(appendix); + ::MessageBoxW(NULL, fullMessage.str(), prompt.str(), MB_OK|MB_SYSTEMMODAL|MB_ICONERROR); } else { @@ -859,6 +945,7 @@ void ReleaseCrashLocalized(const AsciiString& p, const AsciiString& m) AsciiString promptA, mesgA; promptA.translate(prompt); mesgA.translate(mesg); + mesgA.concat(crashInfoAppendix); //Make sure main window is not TOP_MOST ::SetWindowPos(ApplicationHWnd, HWND_NOTOPMOST, 0, 0, 0, 0,SWP_NOSIZE |SWP_NOMOVE); ::MessageBoxA(nullptr, mesgA.str(), promptA.str(), MB_OK|MB_TASKMODAL|MB_ICONERROR); From 7fefd593d8fabb95e77e7c664d50b28d635285c5 Mon Sep 17 00:00:00 2001 From: JohnsterID <69278611+JohnsterID@users.noreply.github.com> Date: Sun, 11 Jan 2026 09:22:37 +0000 Subject: [PATCH 2/7] bugfix(system): Address code review comments in crash message handling - Change crash report URL to GeneralsCrashReports repository - Fix buffer write order in extractCrashLocation to check bufferSize first - Replace manual whitespace checks with standard isspace function --- Core/GameEngine/Source/Common/System/Debug.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Core/GameEngine/Source/Common/System/Debug.cpp b/Core/GameEngine/Source/Common/System/Debug.cpp index 5c52af7e250..3cd79837ca5 100644 --- a/Core/GameEngine/Source/Common/System/Debug.cpp +++ b/Core/GameEngine/Source/Common/System/Debug.cpp @@ -749,16 +749,16 @@ static void TriggerMiniDump() // TheSuperHackers @bugfix JohnsterID 06/01/2025 Helper function to extract crash location from stack trace static void extractCrashLocation(char* outBuffer, size_t bufferSize) { - outBuffer[0] = '\0'; - if (bufferSize == 0 || g_LastErrorDump.isEmpty()) { return; } + outBuffer[0] = '\0'; + const char* stackStr = g_LastErrorDump.str(); // Skip leading whitespace/newlines - while (*stackStr && (*stackStr == ' ' || *stackStr == '\t' || *stackStr == '\n' || *stackStr == '\r')) { + while (*stackStr && isspace(static_cast(*stackStr))) { stackStr++; } @@ -883,7 +883,7 @@ void ReleaseCrash(const char *reason) p += snprintf(p, end - p, "\nMinidump files saved to:\n%s\n", crashDumpDir); #endif - snprintf(p, end - p, "\nPlease report the issue:\nhttps://github.com/TheSuperHackers/GeneralsGameCode/issues"); + snprintf(p, end - p, "\nPlease report the issue:\nhttps://github.com/TheSuperHackers/GeneralsCrashReports/issues"); ::MessageBox(NULL, buff, "Game Crash", MB_OK|MB_SYSTEMMODAL|MB_ICONERROR); @@ -923,7 +923,7 @@ void ReleaseCrashLocalized(const AsciiString& p, const AsciiString& m) #ifdef RTS_ENABLE_CRASHDUMP "\nMinidump files: %sCrashDumps\\" #endif - "\n\nReport issue: https://github.com/TheSuperHackers/GeneralsGameCode/issues", + "\n\nReport issue: https://github.com/TheSuperHackers/GeneralsCrashReports/issues", crashInfoPath #ifdef RTS_ENABLE_CRASHDUMP , TheGlobalData->getPath_UserData().str() From fd1a47b062499001d299df4de3a1d9bbfbd8d790 Mon Sep 17 00:00:00 2001 From: JohnsterID <69278611+JohnsterID@users.noreply.github.com> Date: Sun, 11 Jan 2026 09:36:41 +0000 Subject: [PATCH 3/7] bugfix(debug): Improve crash location extraction with better Unknown handling - Add comments explaining crash location extraction behavior - Fix overly conservative Unknown rejection that discarded partial stacks - Only reject if first line (crash location) is Unknown, not entire trace - Document 400 char limit rationale (512 byte buffer with safety margin) Previous behavior rejected any stack trace containing anywhere, which discarded partially useful traces where the crash location had symbols but deeper frames didn't. Now allows display of useful crash locations even when some stack frames lack symbol information. --- Core/GameEngine/Source/Common/System/Debug.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Core/GameEngine/Source/Common/System/Debug.cpp b/Core/GameEngine/Source/Common/System/Debug.cpp index 3cd79837ca5..3b60c3d04f4 100644 --- a/Core/GameEngine/Source/Common/System/Debug.cpp +++ b/Core/GameEngine/Source/Common/System/Debug.cpp @@ -762,13 +762,16 @@ static void extractCrashLocation(char* outBuffer, size_t bufferSize) stackStr++; } - // Skip if no content or contains "" - if (!*stackStr || strstr(stackStr, "") != NULL) { + // Skip if no content or first line starts with "" + // Only check the first line (crash location) rather than entire stack trace, + // so we can still show useful info even if deeper frames lack symbols. + if (!*stackStr || strncmp(stackStr, "", 9) == 0) { return; } // Find end of first line const char* lineEnd = stackStr; + // Limit to 400 chars to fit in 512 byte buffer with safety margin const size_t maxLineLength = bufferSize < 401 ? bufferSize - 1 : 400; while (*lineEnd && *lineEnd != '\n' && *lineEnd != '\r' && (size_t)(lineEnd - stackStr) < maxLineLength) { From 0dd4f125731d1015147a5d94f78aca5027654b08 Mon Sep 17 00:00:00 2001 From: JohnsterID <69278611+JohnsterID@users.noreply.github.com> Date: Mon, 12 Jan 2026 00:19:09 +0000 Subject: [PATCH 4/7] bugfix(system): Add defensive check in extractCrashLocation for empty strings Add null and empty string checks after calling g_LastErrorDump.str() to handle cases where isEmpty() doesn't work reliably across different build configurations and STL implementations. Issue discovered in testing: Location field showed different values across builds when calling ReleaseCrash() without exception context: - Win32 Release: Empty (correct) - VC6 Release: Shows 'T' (incorrect - isEmpty() failed) - Win32 Debug: Shows corrupted data (incorrect - isEmpty() failed) The additional check ensures consistent behavior across all builds. This doesn't affect real crashes - exceptions always populate g_LastErrorDump through DumpExceptionInfo() which properly clears and fills the string. Relates to #2069 --- Core/GameEngine/Source/Common/System/Debug.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Core/GameEngine/Source/Common/System/Debug.cpp b/Core/GameEngine/Source/Common/System/Debug.cpp index 3b60c3d04f4..6c68a1fd4b5 100644 --- a/Core/GameEngine/Source/Common/System/Debug.cpp +++ b/Core/GameEngine/Source/Common/System/Debug.cpp @@ -757,6 +757,13 @@ static void extractCrashLocation(char* outBuffer, size_t bufferSize) const char* stackStr = g_LastErrorDump.str(); + // TheSuperHackers @bugfix JohnsterID 12/01/2026 Defensive check for null or empty string + // isEmpty() check above doesn't work reliably across all build configs (VC6/STLPort vs Win32). + // This ensures consistent behavior when ReleaseCrash called without exception context. + if (!stackStr || !*stackStr) { + return; + } + // Skip leading whitespace/newlines while (*stackStr && isspace(static_cast(*stackStr))) { stackStr++; From 433deda58e5321766c8a514f9bb7215960ae3bb0 Mon Sep 17 00:00:00 2001 From: JohnsterID <69278611+JohnsterID@users.noreply.github.com> Date: Mon, 12 Jan 2026 01:10:53 +0000 Subject: [PATCH 5/7] bugfix(system): Fix extractCrashLocation to reject invalid/garbage data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add check to verify stack trace starts with two spaces (valid format from WriteStackLine). This prevents extraction of garbage/uninitialized data from g_LastErrorDump when ReleaseCrash() is called without exception context. Issue: Previous defensive check (!stackStr || !*stackStr) didn't work because g_LastErrorDump contained garbage data ('T', '^', corrupted bytes) which are not NULL or empty - they're just invalid. Root cause: g_LastErrorDump is a global variable that may contain uninitialized or leftover data. When ReleaseCrash() is called directly, DumpExceptionInfo() never runs to clear() and populate it properly. Different STL implementations (VC6/STLPort vs Win32) initialize globals differently, causing inconsistent behavior across builds. Solution: Valid stack traces from WriteStackLine() always start with " " (two spaces). By checking for this prefix, we can distinguish between: - Valid exception stack traces: " filename(line) : function" ✓ - Garbage data: "T", "^", corrupted bytes ✗ Test results before fix: - VC6 Release: Shows "T" in Location field - Win32 Release: Shows "^" in Location field - Win32 Debug: Shows corrupted data in Location field Expected after fix: All builds show empty Location field when no exception. Relates to #2069 --- Core/GameEngine/Source/Common/System/Debug.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Core/GameEngine/Source/Common/System/Debug.cpp b/Core/GameEngine/Source/Common/System/Debug.cpp index 6c68a1fd4b5..efd69952024 100644 --- a/Core/GameEngine/Source/Common/System/Debug.cpp +++ b/Core/GameEngine/Source/Common/System/Debug.cpp @@ -757,13 +757,20 @@ static void extractCrashLocation(char* outBuffer, size_t bufferSize) const char* stackStr = g_LastErrorDump.str(); - // TheSuperHackers @bugfix JohnsterID 12/01/2026 Defensive check for null or empty string + // TheSuperHackers @bugfix JohnsterID 12/01/2026 Defensive checks for valid stack trace // isEmpty() check above doesn't work reliably across all build configs (VC6/STLPort vs Win32). // This ensures consistent behavior when ReleaseCrash called without exception context. if (!stackStr || !*stackStr) { return; } + // TheSuperHackers @bugfix JohnsterID 12/01/2026 Verify stack trace format + // Valid stack traces from WriteStackLine() start with " " (two spaces). + // This prevents extraction of garbage/uninitialized data from g_LastErrorDump. + if (stackStr[0] != ' ' || stackStr[1] != ' ') { + return; + } + // Skip leading whitespace/newlines while (*stackStr && isspace(static_cast(*stackStr))) { stackStr++; From 1996f1da7c97da801cf80c2f03cec9ece6ccfa03 Mon Sep 17 00:00:00 2001 From: JohnsterID <69278611+JohnsterID@users.noreply.github.com> Date: Mon, 12 Jan 2026 02:18:55 +0000 Subject: [PATCH 6/7] bugfix(system): Initialize g_LastErrorDump at startup to fix isEmpty() reliability Explicitly call g_LastErrorDump.clear() in DebugInit() to ensure the global AsciiString is properly initialized at program startup. This makes isEmpty() work reliably across all build configurations. Root cause: g_LastErrorDump is a global variable that may contain uninitialized or garbage data depending on STL implementation (VC6/STLPort vs Win32). Without explicit initialization, isEmpty() returns false even though the string contains invalid data, causing extractCrashLocation to extract garbage ('T', '^', etc). Solution: Initialize to empty at startup. This is the correct fix rather than trying to validate content format, because: 1. isEmpty() is the intended check - now it works correctly 2. No heuristics needed to detect valid vs invalid content 3. Simpler and more maintainable code 4. Guaranteed consistent behavior across all builds Previous attempts: - a316c10d: Added NULL/empty check (didn't help - string had content) - 5a2ee727: Added two-space format check (didn't help - garbage started with spaces) This fix addresses the root cause by ensuring isEmpty() is reliable. Test results before fix: - VC6: Shows 'T' or ' T' in Location - Win32: Shows '^' or ' ^' in Location - Debug: Shows corrupted data in Location Expected after fix: All builds show empty Location when no exception. Relates to #2069 --- .../GameEngine/Source/Common/System/Debug.cpp | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Core/GameEngine/Source/Common/System/Debug.cpp b/Core/GameEngine/Source/Common/System/Debug.cpp index efd69952024..26656818ca5 100644 --- a/Core/GameEngine/Source/Common/System/Debug.cpp +++ b/Core/GameEngine/Source/Common/System/Debug.cpp @@ -368,6 +368,15 @@ void DebugInit(int flags) theMainThreadID = GetCurrentThreadId(); +#if defined(DEBUG_STACKTRACE) || defined(IG_DEBUG_STACKTRACE) + // TheSuperHackers @bugfix JohnsterID 12/01/2026 Initialize g_LastErrorDump at startup + // This ensures isEmpty() works reliably across all build configs. Without explicit + // initialization, different STL implementations (VC6/STLPort vs Win32) may leave + // garbage/uninitialized data in the global AsciiString, causing extractCrashLocation + // to extract invalid data when ReleaseCrash is called without exception context. + g_LastErrorDump.clear(); +#endif + #ifdef DEBUG_LOGGING // TheSuperHackers @info Debug initialization can happen very early. @@ -749,6 +758,8 @@ static void TriggerMiniDump() // TheSuperHackers @bugfix JohnsterID 06/01/2025 Helper function to extract crash location from stack trace static void extractCrashLocation(char* outBuffer, size_t bufferSize) { + // TheSuperHackers @bugfix JohnsterID 12/01/2026 g_LastErrorDump is explicitly initialized + // in DebugInit(), so isEmpty() now works reliably across all build configs. if (bufferSize == 0 || g_LastErrorDump.isEmpty()) { return; } @@ -757,20 +768,11 @@ static void extractCrashLocation(char* outBuffer, size_t bufferSize) const char* stackStr = g_LastErrorDump.str(); - // TheSuperHackers @bugfix JohnsterID 12/01/2026 Defensive checks for valid stack trace - // isEmpty() check above doesn't work reliably across all build configs (VC6/STLPort vs Win32). - // This ensures consistent behavior when ReleaseCrash called without exception context. + // Defensive check for null or empty string if (!stackStr || !*stackStr) { return; } - // TheSuperHackers @bugfix JohnsterID 12/01/2026 Verify stack trace format - // Valid stack traces from WriteStackLine() start with " " (two spaces). - // This prevents extraction of garbage/uninitialized data from g_LastErrorDump. - if (stackStr[0] != ' ' || stackStr[1] != ' ') { - return; - } - // Skip leading whitespace/newlines while (*stackStr && isspace(static_cast(*stackStr))) { stackStr++; From ad387789872adf6c4f8db7089329d570bb913d00 Mon Sep 17 00:00:00 2001 From: JohnsterID <69278611+JohnsterID@users.noreply.github.com> Date: Wed, 18 Mar 2026 06:38:27 +0000 Subject: [PATCH 7/7] Fix review feedback: simplify extractCrashLocation, add UnHandledExceptionFilter crash dialog Addresses review comments #7-11: - Simplified extractCrashLocation to show first 5 lines of stack trace - Removed arbitrary 400 char limit, now uses full buffer (bufferSize - 1) - Fixed buffer initialization order to prevent 'Location: T' garbage - Updated g_LastErrorDump comment to clarify static initialization timing - Show frames as-is without filtering - Added ReleaseCrash call to UnHandledExceptionFilter so crash dialog appears for unhandled exceptions, not just DEBUG_CRASH macro calls --- .../GameEngine/Source/Common/System/Debug.cpp | 65 ++++++++++--------- Generals/Code/Main/WinMain.cpp | 6 ++ GeneralsMD/Code/Main/WinMain.cpp | 6 ++ 3 files changed, 45 insertions(+), 32 deletions(-) diff --git a/Core/GameEngine/Source/Common/System/Debug.cpp b/Core/GameEngine/Source/Common/System/Debug.cpp index 26656818ca5..6ef7ba278b4 100644 --- a/Core/GameEngine/Source/Common/System/Debug.cpp +++ b/Core/GameEngine/Source/Common/System/Debug.cpp @@ -369,11 +369,10 @@ void DebugInit(int flags) theMainThreadID = GetCurrentThreadId(); #if defined(DEBUG_STACKTRACE) || defined(IG_DEBUG_STACKTRACE) - // TheSuperHackers @bugfix JohnsterID 12/01/2026 Initialize g_LastErrorDump at startup - // This ensures isEmpty() works reliably across all build configs. Without explicit - // initialization, different STL implementations (VC6/STLPort vs Win32) may leave - // garbage/uninitialized data in the global AsciiString, causing extractCrashLocation - // to extract invalid data when ReleaseCrash is called without exception context. + // TheSuperHackers @bugfix JohnsterID 12/01/2026 Initialize g_LastErrorDump at startup. + // A crash can occur before static initialization completes. Without explicit + // initialization here, different STL implementations (VC6/STLPort vs Win32) may + // leave garbage/uninitialized data in the global AsciiString. g_LastErrorDump.clear(); #endif @@ -755,50 +754,52 @@ static void TriggerMiniDump() } -// TheSuperHackers @bugfix JohnsterID 06/01/2025 Helper function to extract crash location from stack trace +// TheSuperHackers @bugfix JohnsterID 06/01/2025 Helper function to extract crash location from stack trace. +// Extracts the first few lines of the stack dump for display in the crash dialog. static void extractCrashLocation(char* outBuffer, size_t bufferSize) { - // TheSuperHackers @bugfix JohnsterID 12/01/2026 g_LastErrorDump is explicitly initialized - // in DebugInit(), so isEmpty() now works reliably across all build configs. - if (bufferSize == 0 || g_LastErrorDump.isEmpty()) { + if (bufferSize == 0) { return; } + // Always initialize output buffer first to prevent garbage on early return. outBuffer[0] = '\0'; - const char* stackStr = g_LastErrorDump.str(); + if (g_LastErrorDump.isEmpty()) { + return; + } - // Defensive check for null or empty string + const char* stackStr = g_LastErrorDump.str(); if (!stackStr || !*stackStr) { return; } - // Skip leading whitespace/newlines - while (*stackStr && isspace(static_cast(*stackStr))) { - stackStr++; - } + // Extract first 5 lines from stack trace for context. + const int maxLines = 5; + int lineCount = 0; + size_t written = 0; + const size_t maxWrite = bufferSize - 1; - // Skip if no content or first line starts with "" - // Only check the first line (crash location) rather than entire stack trace, - // so we can still show useful info even if deeper frames lack symbols. - if (!*stackStr || strncmp(stackStr, "", 9) == 0) { - return; - } + while (*stackStr && lineCount < maxLines && written < maxWrite) { + // Copy characters until newline or buffer full + while (*stackStr && *stackStr != '\n' && *stackStr != '\r' && written < maxWrite) { + outBuffer[written++] = *stackStr++; + } + + // Skip newline characters + while (*stackStr && (*stackStr == '\n' || *stackStr == '\r')) { + stackStr++; + } - // Find end of first line - const char* lineEnd = stackStr; - // Limit to 400 chars to fit in 512 byte buffer with safety margin - const size_t maxLineLength = bufferSize < 401 ? bufferSize - 1 : 400; + lineCount++; - while (*lineEnd && *lineEnd != '\n' && *lineEnd != '\r' && (size_t)(lineEnd - stackStr) < maxLineLength) { - lineEnd++; + // Add newline between lines if more content follows + if (*stackStr && lineCount < maxLines && written < maxWrite) { + outBuffer[written++] = '\n'; + } } - size_t len = lineEnd - stackStr; - if (len > 0) { - strncpy(outBuffer, stackStr, len); - outBuffer[len] = '\0'; - } + outBuffer[written] = '\0'; } diff --git a/Generals/Code/Main/WinMain.cpp b/Generals/Code/Main/WinMain.cpp index cf1fcc8b27a..af63fb1fc7e 100644 --- a/Generals/Code/Main/WinMain.cpp +++ b/Generals/Code/Main/WinMain.cpp @@ -760,6 +760,12 @@ static LONG WINAPI UnHandledExceptionFilter( struct _EXCEPTION_POINTERS* e_info MiniDumper::shutdownMiniDumper(); #endif + + // TheSuperHackers @bugfix JohnsterID 20/01/2026 Show crash dialog for unhandled exceptions. + // This ensures users see crash information and GitHub link for all crashes, + // not just explicit DEBUG_CRASH calls. + ReleaseCrash("Unhandled exception"); + return EXCEPTION_EXECUTE_HANDLER; } diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index baac82f8a13..3104c388d94 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -782,6 +782,12 @@ static LONG WINAPI UnHandledExceptionFilter( struct _EXCEPTION_POINTERS* e_info MiniDumper::shutdownMiniDumper(); #endif + + // TheSuperHackers @bugfix JohnsterID 20/01/2026 Show crash dialog for unhandled exceptions. + // This ensures users see crash information and GitHub link for all crashes, + // not just explicit DEBUG_CRASH calls. + ReleaseCrash("Unhandled exception"); + return EXCEPTION_EXECUTE_HANDLER; }