From 5338527b496c451707afb003b0df18d9609f92a6 Mon Sep 17 00:00:00 2001 From: Torhan Bartel Date: Fri, 16 Oct 2026 02:01:02 +0200 Subject: [PATCH] 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) --- CMakeLists.txt | 4 +- src/mods/guards.cpp | 30 +++++++++++ src/re/crash.cpp | 118 ++++++++++++++++++++++++++++++++++++++++++++ src/re/crash.h | 10 ++++ src/re/dllmain.cpp | 2 + 5 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 src/mods/guards.cpp create mode 100644 src/re/crash.cpp create mode 100644 src/re/crash.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 1fa9dea..1bc7f98 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/src/mods/guards.cpp b/src/mods/guards.cpp new file mode 100644 index 0000000..f51c594 --- /dev/null +++ b/src/mods/guards.cpp @@ -0,0 +1,30 @@ +// Guards against crashes in the game's own code (each with what is known of +// its cause). + +#include + +#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 diff --git a/src/re/crash.cpp b/src/re/crash.cpp new file mode 100644 index 0000000..296c07b --- /dev/null +++ b/src/re/crash.cpp @@ -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 +#include + +#include + +#include + +#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(address), &module)) { + GetModuleFileNameA(module, path, MAX_PATH); + const char* name = strrchr(path, '\\'); + snprintf(out, size, "%08x %s+%x", static_cast(address), name ? name + 1 : path, + static_cast(address - reinterpret_cast(module))); + } else { + snprintf(out, size, "%08x", static_cast(address)); + } +} + +bool IsCode(uintptr_t address) { + MEMORY_BASIC_INFORMATION info; + if (!VirtualQuery(reinterpret_cast(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(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(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(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; +} diff --git a/src/re/crash.h b/src/re/crash.h new file mode 100644 index 0000000..9fcffda --- /dev/null +++ b/src/re/crash.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace re { + +// Writes sacred_re_crash.txt / .dmp into `dir` when the game crashes. +void InstallCrashHandler(const std::wstring& dir); + +} // namespace re diff --git a/src/re/dllmain.cpp b/src/re/dllmain.cpp index b8c7f73..3bf8f08 100644 --- a/src/re/dllmain.cpp +++ b/src/re/dllmain.cpp @@ -8,6 +8,7 @@ #include #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");