Crash reports and a guard for burning bone particles without a device

- re/crash: unhandled exceptions write sacred_re_crash.txt (module+offset,
  registers, return addresses on the stack) and a minidump; the game's own
  filter is chained behind ours
- guards: cParticleSystem_burningBone::Render (0x774e10) skips drawing when
  given a null device (crash at sacred.exe+0x374e39 after Esc while walking)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-16 02:01:02 +02:00
1 parent e914bd1590
commit 5338527b49
5 files changed
+163 -1

No files matched your search

+3 -1
View File
@@ -28,6 +28,7 @@ add_library(sacred_re SHARED
src/re/log.cpp
src/re/config.cpp
src/re/patch.cpp
src/re/crash.cpp
src/re/sacred_re.def
src/game/engine/log.cpp
src/game/engine/cmdline.cpp
@@ -54,10 +55,11 @@ add_library(sacred_re SHARED
src/mods/recordcache.cpp
src/mods/megamap.cpp
src/mods/respec.cpp
src/mods/guards.cpp
)
target_include_directories(sacred_re PRIVATE src)
target_compile_options(sacred_re PRIVATE ${SACRED_RE_CXX_FLAGS})
target_link_libraries(sacred_re PRIVATE detours nlohmann_json::nlohmann_json)
target_link_libraries(sacred_re PRIVATE detours nlohmann_json::nlohmann_json dbghelp)
# --- sacred_re.exe: launches sacred.exe with sacred_re.dll injected ----------
add_executable(sacred_re_launcher WIN32 src/launcher/main.cpp)
+30
View File
@@ -0,0 +1,30 @@
// Guards against crashes in the game's own code (each with what is known of
// its cause).
#include <intrin.h>
#include "re/hook.h"
#include "re/log.h"
namespace game {
// cParticleSystem_burningBone::Render (vf04 0x774e10, vtable 0x895b84):
// crashed reading the device it was given, which was null (2026-10-16, Esc
// pressed while walking; Windows reported sacred.exe+0x374e39). Who renders
// it without a device is not known yet (sacred_re_crash.txt will tell). With
// no device there is nothing to draw.
void __fastcall BurningBone_Render(void* self, void* edx, void* device);
RE_HOOK(0x00774e10, BurningBone_Render);
void __fastcall BurningBone_Render(void* self, void* edx, void* device) {
if (!device) {
static bool logged = false;
if (!logged) {
logged = true;
re::Log("guards: burning bone particles rendered without a device, skipped (%p)", _ReturnAddress());
}
return;
}
BurningBone_Render_original(self, edx, device);
}
} // namespace game
+118
View File
@@ -0,0 +1,118 @@
// Crash report: on an unhandled exception, writes sacred_re_crash.txt (code,
// address as module + offset, registers, return addresses found on the stack)
// and sacred_re_crash.dmp next to the DLL. The game may install its own
// filter later; SetUnhandledExceptionFilter is hooked so ours stays first and
// passes the crash on to the game's afterwards.
#include "re/crash.h"
#include <cstdio>
#include <string>
#include <windows.h>
#include <dbghelp.h>
#include "re/hook.h"
#include "re/log.h"
namespace re {
namespace {
std::wstring g_dir;
LPTOP_LEVEL_EXCEPTION_FILTER g_gameFilter = nullptr;
volatile LONG g_reported = 0;
// "module+offset" for a code address.
void DescribeAddress(uintptr_t address, char* out, size_t size) {
HMODULE module = nullptr;
char path[MAX_PATH] = "?";
if (GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCSTR>(address), &module)) {
GetModuleFileNameA(module, path, MAX_PATH);
const char* name = strrchr(path, '\\');
snprintf(out, size, "%08x %s+%x", static_cast<unsigned>(address), name ? name + 1 : path,
static_cast<unsigned>(address - reinterpret_cast<uintptr_t>(module)));
} else {
snprintf(out, size, "%08x", static_cast<unsigned>(address));
}
}
bool IsCode(uintptr_t address) {
MEMORY_BASIC_INFORMATION info;
if (!VirtualQuery(reinterpret_cast<void*>(address), &info, sizeof(info)) || info.State != MEM_COMMIT) {
return false;
}
return (info.Protect & (PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE)) != 0;
}
void WriteReport(EXCEPTION_POINTERS* info) {
FILE* file = _wfopen((g_dir + L"sacred_re_crash.txt").c_str(), L"w");
if (!file) {
return;
}
const EXCEPTION_RECORD& record = *info->ExceptionRecord;
const CONTEXT& c = *info->ContextRecord;
char where[MAX_PATH + 32];
DescribeAddress(reinterpret_cast<uintptr_t>(record.ExceptionAddress), where, sizeof(where));
fprintf(file, "exception %08lx at %s, thread %lu\n", record.ExceptionCode, where, GetCurrentThreadId());
if (record.ExceptionCode == EXCEPTION_ACCESS_VIOLATION && record.NumberParameters >= 2) {
fprintf(file, "%s address %08x\n", record.ExceptionInformation[0] ? "writing" : "reading",
static_cast<unsigned>(record.ExceptionInformation[1]));
}
fprintf(file, "eax %08lx ebx %08lx ecx %08lx edx %08lx esi %08lx edi %08lx ebp %08lx esp %08lx\n", c.Eax, c.Ebx,
c.Ecx, c.Edx, c.Esi, c.Edi, c.Ebp, c.Esp);
fprintf(file, "return addresses on the stack (innermost first):\n");
const auto* stack = reinterpret_cast<const uintptr_t*>(c.Esp);
int found = 0;
for (int i = 0; i < 4096 && found < 48; ++i) {
MEMORY_BASIC_INFORMATION mem;
if (!VirtualQuery(stack + i, &mem, sizeof(mem)) || mem.State != MEM_COMMIT) {
break;
}
const uintptr_t value = stack[i];
if (IsCode(value)) {
DescribeAddress(value, where, sizeof(where));
fprintf(file, " [esp+%04x] %s\n", i * 4, where);
++found;
}
}
fclose(file);
HANDLE dump = CreateFileW((g_dir + L"sacred_re_crash.dmp").c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (dump != INVALID_HANDLE_VALUE) {
MINIDUMP_EXCEPTION_INFORMATION exception = {GetCurrentThreadId(), info, FALSE};
MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), dump, MiniDumpWithIndirectlyReferencedMemory,
&exception, nullptr, nullptr);
CloseHandle(dump);
}
}
LONG WINAPI Filter(EXCEPTION_POINTERS* info) {
if (InterlockedExchange(&g_reported, 1) == 0) {
WriteReport(info);
}
return g_gameFilter ? g_gameFilter(info) : EXCEPTION_CONTINUE_SEARCH;
}
} // namespace
void InstallCrashHandler(const std::wstring& dir) {
g_dir = dir;
g_gameFilter = SetUnhandledExceptionFilter(&Filter);
}
} // namespace re
// The game's own filter goes behind ours.
LPTOP_LEVEL_EXCEPTION_FILTER WINAPI Hooked_SetUnhandledExceptionFilter(LPTOP_LEVEL_EXCEPTION_FILTER filter);
RE_HOOK_API(SetUnhandledExceptionFilter, Hooked_SetUnhandledExceptionFilter);
LPTOP_LEVEL_EXCEPTION_FILTER WINAPI Hooked_SetUnhandledExceptionFilter(LPTOP_LEVEL_EXCEPTION_FILTER filter) {
if (filter == &re::Filter) {
return Hooked_SetUnhandledExceptionFilter_original(filter);
}
LPTOP_LEVEL_EXCEPTION_FILTER previous = re::g_gameFilter;
re::g_gameFilter = filter;
return previous;
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <string>
namespace re {
// Writes sacred_re_crash.txt / .dmp into `dir` when the game crashes.
void InstallCrashHandler(const std::wstring& dir);
} // namespace re
+2
View File
@@ -8,6 +8,7 @@
#include <detours.h>
#include "re/config.h"
#include "re/crash.h"
#include "re/hook.h"
#include "re/log.h"
#include "re/patch.h"
@@ -42,6 +43,7 @@ void Startup(HMODULE module) {
re::OpenLog((dir + L"sacred_re.log").c_str());
re::SetConfigPath(dir + L"sacred_re.ini");
re::Log("sacred_re loaded");
re::InstallCrashHandler(dir);
if (!IsSupportedGameBuild()) {
re::Log("unsupported sacred.exe build, no hooks installed");