- The shot end's button check (0x4def52) answers "held" for the auto-attack target, like the melee swing end. - An art order counts as started when the art's cooldown rises, so bow arts and arts ordered mid-swing hand over to auto-attack. - re/crash.cpp: a watchdog writes sacred_re_hang.dmp when the engine tick stops for 10 s while the game is in front. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
186 lines
7.1 KiB
C++
186 lines
7.1 KiB
C++
// 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.
|
|
//
|
|
// Hangs: a watchdog thread writes sacred_re_hang.dmp (all threads) when the
|
|
// engine tick (re::EngineHeartbeat) has stopped for kHangMs while the game
|
|
// window is in front; once per hang. The game freezing on F2 while walking
|
|
// (2026-10-18) left no report.
|
|
|
|
#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);
|
|
// Frame pointer chain (our debug build and most of sacred.exe keep ebp).
|
|
fprintf(file, "frames (ebp chain):\n");
|
|
uintptr_t frame = c.Ebp;
|
|
for (int i = 0; i < 64 && frame; ++i) {
|
|
MEMORY_BASIC_INFORMATION mem;
|
|
if (!VirtualQuery(reinterpret_cast<void*>(frame), &mem, sizeof(mem)) || mem.State != MEM_COMMIT) {
|
|
break;
|
|
}
|
|
const auto* slots = reinterpret_cast<const uintptr_t*>(frame);
|
|
DescribeAddress(slots[1], where, sizeof(where));
|
|
fprintf(file, " %s\n", where);
|
|
if (slots[0] <= frame) {
|
|
break;
|
|
}
|
|
frame = slots[0];
|
|
}
|
|
fprintf(file, "code addresses on the stack (innermost first; may include data):\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);
|
|
}
|
|
}
|
|
|
|
constexpr DWORD kHangMs = 10000;
|
|
volatile LONG g_lastBeat = 0; // GetTickCount of the last engine tick, 0: no game running
|
|
|
|
bool GameInFront() {
|
|
DWORD process = 0;
|
|
GetWindowThreadProcessId(GetForegroundWindow(), &process);
|
|
return process == GetCurrentProcessId();
|
|
}
|
|
|
|
void WriteHangDump() {
|
|
HANDLE dump = CreateFileW((g_dir + L"sacred_re_hang.dmp").c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS,
|
|
FILE_ATTRIBUTE_NORMAL, nullptr);
|
|
if (dump != INVALID_HANDLE_VALUE) {
|
|
MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), dump,
|
|
static_cast<MINIDUMP_TYPE>(MiniDumpWithIndirectlyReferencedMemory | MiniDumpWithThreadInfo),
|
|
nullptr, nullptr, nullptr);
|
|
CloseHandle(dump);
|
|
}
|
|
}
|
|
|
|
DWORD WINAPI Watchdog(void*) {
|
|
bool reported = false;
|
|
for (;;) {
|
|
Sleep(1000);
|
|
const DWORD beat = static_cast<DWORD>(g_lastBeat);
|
|
// Signed: a tick may land between reading the beat and the clock.
|
|
if (beat == 0 || static_cast<LONG>(GetTickCount() - beat) < static_cast<LONG>(kHangMs)) {
|
|
reported = false;
|
|
continue;
|
|
}
|
|
if (!reported && GameInFront()) {
|
|
reported = true;
|
|
Log("hang: no engine tick for %lu ms, writing sacred_re_hang.dmp", GetTickCount() - beat);
|
|
WriteHangDump();
|
|
Log("hang: dump written");
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
if (HANDLE thread = CreateThread(nullptr, 0, &Watchdog, nullptr, 0, nullptr)) {
|
|
CloseHandle(thread);
|
|
}
|
|
}
|
|
|
|
void EngineHeartbeat(bool running) {
|
|
InterlockedExchange(&g_lastBeat, running ? static_cast<LONG>(GetTickCount() | 1) : 0);
|
|
}
|
|
|
|
} // 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;
|
|
}
|