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:
1 parent
e914bd1590
commit
5338527b49
5 files changed
+163
-1
No files matched your search
+3
-1
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user