Skip to content
Draft
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
130 changes: 118 additions & 12 deletions Core/GameEngine/Source/Common/System/Debug.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps clarify that this is relevant because crash can occur before static initialization occurs? I assume that is the reason that triggers the issue?

#endif

#ifdef DEBUG_LOGGING

// TheSuperHackers @info Debug initialization can happen very early.
Expand Down Expand Up @@ -746,6 +755,53 @@ 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;
}

outBuffer[0] = '\0';

const char* stackStr = g_LastErrorDump.str();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you show a sample for what g_LastErrorDump contains?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example:

  C:\GeneralsGameCode\GeneralsMD\Code\GameLogic\Object.cpp(543) : Object::Update 0x00512A40
  C:\GeneralsGameCode\GeneralsMD\Code\GameLogic\GameLogic.cpp(1234) : GameLogic::UpdateFrame 0x00498B20
  C:\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\GameClient\GameClient.cpp(789) : GameClient::Update 0x00445E90
  C:\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\Common\Main\WinMain.cpp(456) : MainLoop 0x00401F50
  C:\GeneralsGameCode\GeneralsMD\Code\GameEngine\Source\Common\Main\WinMain.cpp(123) : WinMain 0x00401A20
  C:\Windows\System32\kernel32.dll(78) : BaseThreadInitThunk 0x76D51234
  C:\Windows\System32\ntdll.dll(34) : RtlUserThreadStart 0x77C91ABC

The function extracts only the first line (most relevant crash location) to display in the crash dialog though.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Can we simplify this function to its most basic requirement of just grabbing first line of multi-line text? Right now, this functions looks very complicated.

Also, if first line is <Unknown>, then just print that too. No special code-handling needed.

Maybe even print the first 5 lines or so.


// Defensive check for null or empty string
if (!stackStr || !*stackStr) {
return;
}

// Skip leading whitespace/newlines
while (*stackStr && isspace(static_cast<unsigned char>(*stackStr))) {
stackStr++;
}

// Skip if no content or first line starts with "<Unknown>"
// 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, "<Unknown>", 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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this 400 char limit come to be?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added code comment (512 byte buffer - 400 chars = 111 byte safety margin)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not understand the rationale behind this clamp. We pass a 512 buffer but only get max 400. Why? If our buffer is 512, why can we not have max 512 (-1)?


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
Expand Down Expand Up @@ -806,23 +862,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(NULL, buff, "Technical Difficulties...", MB_OK|MB_SYSTEMMODAL|MB_ICONERROR);
#else
// crash error messaged changed 3/6/03 BGC
// ::MessageBox(NULL, "Sorry, a serious error occurred.", "Technical Difficulties...", MB_OK|MB_TASKMODAL|MB_ICONERROR);
// ::MessageBox(NULL, "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(NULL, "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/GeneralsCrashReports/issues");

::MessageBox(NULL, buff, "Game Crash", MB_OK|MB_SYSTEMMODAL|MB_ICONERROR);

_exit(1);
}

Expand All @@ -848,9 +931,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/GeneralsCrashReports/issues",
crashInfoPath
#ifdef RTS_ENABLE_CRASHDUMP
, TheGlobalData->getPath_UserData().str()
#endif
);

if (TheSystemIsUnicode)
{
::MessageBoxW(NULL, 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
{
Expand All @@ -859,6 +964,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(NULL, mesgA.str(), promptA.str(), MB_OK|MB_TASKMODAL|MB_ICONERROR);
Expand Down