diff --git a/source/Enderal DLL/src/Main.cpp b/source/Enderal DLL/src/Main.cpp index 6263deb7c..fb7b728ea 100644 --- a/source/Enderal DLL/src/Main.cpp +++ b/source/Enderal DLL/src/Main.cpp @@ -16,6 +16,7 @@ #include "Patches/PluginsTxtPatch.h" #include "Patches/FormTypeCollisionDetector.h" #include "Patches/GogVramLeakFix.h" +#include "Patches/MovementPathFollowerCrashFix.h" using namespace SKSE; @@ -80,6 +81,10 @@ namespace { logger::info("Installing light attach crash fix..."); AttachLightHitEffectCrash::Install(); } + + logger::info("Installing movement path follower crash fix..."); + MovementPathFollowerCrashFix::Install(); + if (g_settings.at("StayAtSystemPage")) { if (const auto pluginInfo = GetLoadInterface()->GetPluginInfo("StayAtSystemPage"); pluginInfo) { MessageBoxW(NULL, L"Stay At The System Page is already included in Enderal, please, disable it.", L"Enderal SE Error", MB_OK | MB_ICONERROR); diff --git a/source/Enderal DLL/src/Patches/MovementPathFollowerCrashFix.h b/source/Enderal DLL/src/Patches/MovementPathFollowerCrashFix.h new file mode 100644 index 000000000..61f927408 --- /dev/null +++ b/source/Enderal DLL/src/Patches/MovementPathFollowerCrashFix.h @@ -0,0 +1,85 @@ +#pragma once + +// Fix crash in MovementAgentPathFollowerVirtual when [RDI+0x48] is NULL +// Crash occurs when MOVSS tries to read from [RAX+0x8C] with RAX=NULL +// Address Library ID: 92556 (MovementAgentPathFollowerVirtual::sub_1115090) +// +// Original crash flow: +// MOV RAX, [RDI+0x48] ; RAX can be NULL +// LEA RDX, [RAX+0x58] +// MOVSS XMM2, [RAX+0x8C] ; CRASH if RAX is NULL +// +// Fix: Add null check after loading RAX, skip to safe exit if NULL +namespace MovementPathFollowerCrashFix +{ + // Address Library ID for MovementAgentPathFollowerVirtual::sub_1115090 + constexpr REL::RelocationID FuncID(92556, 92556); + + // Offsets from function start to hook point (MOV RAX, [RDI+0x48]) + // SE: 0x428, AE: 0x41F, VR: 0x428 + constexpr REL::VariantOffset HookOffset(0x428, 0x41F, 0x428); + + // Offsets from function start to skip point (safe exit label) + // SE: 0x518, AE: 0x50F, VR: 0x518 + constexpr REL::VariantOffset SkipOffset(0x518, 0x50F, 0x518); + + // Hook size: MOV (4 bytes) + LEA (4 bytes) = 8 bytes + constexpr std::size_t HookSize = 8; + + inline void Install() + { + const REL::Relocation funcBase{ FuncID }; + const std::uintptr_t hookAddr = funcBase.address() + HookOffset.offset(); + const std::uintptr_t skipAddr = funcBase.address() + SkipOffset.offset(); + const std::uintptr_t returnAddr = hookAddr + HookSize; + + // Generate the patch code using xbyak + struct PatchCode : Xbyak::CodeGenerator + { + PatchCode(std::uintptr_t a_skipAddr, std::uintptr_t a_returnAddr) + { + // Original: MOV RAX, [RDI+0x48] + mov(rax, qword[rdi + 0x48]); + + // Add null check + test(rax, rax); + jz("skip_label"); + + // Original: LEA RDX, [RAX+0x58] + lea(rdx, qword[rax + 0x58]); + + // Jump back to original code after LEA (continue with MOVSS) + jmp(ptr[rip]); + dq(a_returnAddr); + + // Skip label - jump to safe exit point + L("skip_label"); + jmp(ptr[rip]); + dq(a_skipAddr); + } + }; + + PatchCode patchCode(skipAddr, returnAddr); + patchCode.ready(); + + // Allocate trampoline space and copy our patch code + SKSE::AllocTrampoline(patchCode.getSize() + 14); + auto& trampoline = SKSE::GetTrampoline(); + + void* codeCave = trampoline.allocate(patchCode.getSize()); + std::memcpy(codeCave, patchCode.getCode(), patchCode.getSize()); + + // Write jump from original location to our patch code + trampoline.write_branch<5>(hookAddr, reinterpret_cast(codeCave)); + + // NOP remaining bytes + if (HookSize > 5) { + REL::safe_fill(hookAddr + 5, REL::NOP, HookSize - 5); + } + + const char* version = REL::Module::IsVR() ? "VR" : + (REL::Module::get().version() <= REL::Version(1, 5, 97, 0) ? "SE" : "AE"); + logger::info("MovementPathFollowerCrashFix: Patched {} at 0x{:X}, code cave at 0x{:X}", + version, hookAddr, reinterpret_cast(codeCave)); + } +}