Fix trampoline crash by replacing MenuCursor hooks

The previous MenuCursor detour hooks crashed at startup with 'REL/Trampoline.cpp: Failed to handle allocation request' because the plugin never allocated F4SE trampoline memory. Replaced detours with a MainMenu::AdvanceMovie vtable hook (which requires no trampoline) that re-expands cursor bounds immediately after vanilla title-screen parallax runs. Added UIAdvanceMenusFunctionCompleteEvent sink as additional safety net. Changed main menu disableFocusMenu to true to avoid fighting vanilla cursor/parallax state.
This commit is contained in:
2026-07-07 14:44:40 +12:00
parent f1635948ee
commit c957c117c0
5 changed files with 139 additions and 10 deletions
+85 -8
View File
@@ -50,6 +50,12 @@ namespace F4T::PrismaUI
bool g_hasLastCursorPos = false;
POINT g_lastCursorPos{};
using MainMenuAdvanceMovieFn = void(RE::IMenu*, float, std::uint64_t);
std::uintptr_t g_originalMainMenuAdvanceMovie = 0;
bool g_mainMenuAdvanceHookInstalled = false;
bool g_menuAdvanceSinkRegistered = false;
// Forward declarations (definitions appear later in this anonymous namespace;
// several functions call each other out of definition order).
void HideMainMenuInternal();
@@ -60,6 +66,46 @@ namespace F4T::PrismaUI
void CreateServerBrowserView();
void SyncBrowserHover();
void WatchMainMenuState();
void InstallMainMenuCursorReleaseHook();
void RegisterMenuAdvanceSink();
void ReleaseMenuCursorConstraints();
[[nodiscard]] bool ShouldOverrideMenuCursorConstraints()
{
return g_mainMenuVisible || g_browserVisible;
}
void MainMenuAdvanceMovieHook(RE::IMenu* a_this, float a_timeDelta, std::uint64_t a_time)
{
if (g_originalMainMenuAdvanceMovie != 0) {
reinterpret_cast<MainMenuAdvanceMovieFn*>(g_originalMainMenuAdvanceMovie)(a_this, a_timeDelta, a_time);
}
// MainMenu parallax updates MenuCursor bounds during AdvanceMovie; re-expand after vanilla runs.
if (ShouldOverrideMenuCursorConstraints()) {
ReleaseMenuCursorConstraints();
}
}
class MenuAdvanceSink :
public RE::BSTEventSink<RE::UIAdvanceMenusFunctionCompleteEvent>
{
public:
RE::BSEventNotifyControl ProcessEvent(
const RE::UIAdvanceMenusFunctionCompleteEvent&,
RE::BSTEventSource<RE::UIAdvanceMenusFunctionCompleteEvent>* a_source) override
{
(void)a_source;
if (ShouldOverrideMenuCursorConstraints()) {
ReleaseMenuCursorConstraints();
}
return RE::BSEventNotifyControl::kContinue;
}
};
MenuAdvanceSink g_menuAdvanceSink;
std::string GetLogPrefix()
{
@@ -78,6 +124,35 @@ namespace F4T::PrismaUI
REX::WARN(std::string_view{ message });
}
void InstallMainMenuCursorReleaseHook()
{
if (g_mainMenuAdvanceHookInstalled) {
return;
}
REL::Relocation<std::uintptr_t> vtable{ RE::MainMenu::VTABLE[0] };
g_originalMainMenuAdvanceMovie = vtable.write_vfunc(0x04, &MainMenuAdvanceMovieHook);
if (g_originalMainMenuAdvanceMovie != 0) {
g_mainMenuAdvanceHookInstalled = true;
LogInfo("Installed MainMenu AdvanceMovie hook for MenuCursor release.");
} else {
LogWarning("MainMenu AdvanceMovie hook failed to install.");
}
}
void RegisterMenuAdvanceSink()
{
if (g_menuAdvanceSinkRegistered) {
return;
}
if (auto* source = RE::UIAdvanceMenusFunctionCompleteEvent::GetEventSource()) {
source->RegisterSink(&g_menuAdvanceSink);
g_menuAdvanceSinkRegistered = true;
LogInfo("Registered UIAdvanceMenusFunctionCompleteEvent sink for MenuCursor release.");
}
}
void SuppressNativeGameCursor()
{
auto* ui = RE::UI::GetSingleton();
@@ -116,12 +191,15 @@ namespace F4T::PrismaUI
return;
}
// MainMenu keeps a tight MenuCursor box for title-screen parallax; clear it every frame
// while our overlay owns input so the OS pointer can reach the full window.
// MainMenu keeps a tight MenuCursor box for title-screen parallax; expand to the full
// window while our overlay owns input so the OS pointer can reach every edge.
menuCursor->SetCursorConstraintsRaw(0, 0, static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height));
menuCursor->minCursorX = 0;
menuCursor->minCursorY = 0;
menuCursor->maxCursorX = width;
menuCursor->maxCursorY = height;
::ClipCursor(nullptr);
}
void ApplyBrowserPointerMode(bool a_mouseCursorVisible)
@@ -136,12 +214,9 @@ namespace F4T::PrismaUI
if (g_mainMenuVisible && g_api && g_api->IsValid(g_mainMenuView)) {
activeView = g_mainMenuView;
namespace_obj = "window.CO_App";
// The main menu is the primary/standalone title-screen UI, not a panel opened
// on top of another menu's cursor. Let PrismaUI open its own FocusMenu (kUsesCursor /
// kCursor input context) instead of relying on vanilla MainMenu's cursor handling,
// which re-centers the OS cursor each frame for its background camera parallax and
// otherwise pins the pointer to the middle of the screen.
disableFocusMenu = false;
// MainMenu is already open with a working title-screen cursor. Keep PrismaUI's
// FocusMenu overlay suppressed so we do not fight vanilla cursor/parallax state.
disableFocusMenu = true;
} else if (g_browserVisible && g_api && g_api->IsValid(g_serverBrowserView)) {
activeView = g_serverBrowserView;
namespace_obj = "window.CO_App";
@@ -674,6 +749,8 @@ namespace F4T::PrismaUI
void OnGameDataReady()
{
F4T::MainMenuInject::Install();
InstallMainMenuCursorReleaseHook();
RegisterMenuAdvanceSink();
g_api = PRISMA_UI_API::RequestPluginAPI<PRISMA_UI_API::IVPrismaUI4>();
if (!g_api) {