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
+2 -1
View File
@@ -28,7 +28,8 @@ For testing notes, milestone summaries, known issues, and next steps, see [`docs
- `main-menu/script.js` now includes `window.CO_App` object with `setPointerMode()` for C++ to invoke, and `handleControllerInput()` stub for gamepad dispatch (wired in v1.1 when dispatch is complete). - `main-menu/script.js` now includes `window.CO_App` object with `setPointerMode()` for C++ to invoke, and `handleControllerInput()` stub for gamepad dispatch (wired in v1.1 when dispatch is complete).
### Fixed ### Fixed
- Mouse cursor could not move away from the center of the screen while the custom main menu overlay was shown. Root causes: (1) overlay could appear during Please Stand By / intro Binks; (2) vanilla `RE::MenuCursor` min/max bounds were never cleared. Fixed by gating show on intro completion (`PlayBinkMenu` closed, main panel/list visible, `userEngaged`, or short fallback) plus strict `MAIN_STATE` when GFx publishes it; and clearing `MenuCursor` constraints to full window size each frame while the custom menu owns input. - Game crashed on load with `REL/Trampoline.cpp: Failed to handle allocation request` because `MenuCursor` detour hooks required trampoline memory the plugin never allocated. Replaced detours with a `MainMenu::AdvanceMovie` vtable hook (no trampoline) that re-expands cursor bounds after vanilla parallax runs.
- Mouse cursor could not move away from the center of the screen while the custom main menu overlay was shown. Root causes: (1) overlay could appear during Please Stand By / intro Binks; (2) vanilla `RE::MenuCursor` min/max bounds were re-applied each frame by title-screen parallax. Fixed by gating show on intro completion, clearing constraints each frame, releasing again after `MainMenu::AdvanceMovie` and on `UIAdvanceMenusFunctionCompleteEvent`, and using `disableFocusMenu=true` for the main menu overlay.
### Changed ### Changed
- Pause menu settings panel fades and slides in subtly on open and animates out on back (opacity + 10px vertical offset, 220ms ease-out). - Pause menu settings panel fades and slides in subtly on open and animates out on back (opacity + 10px vertical offset, 220ms ease-out).
+52
View File
@@ -7,6 +7,58 @@ failed experiments, successful tests, and next steps.
--- ---
---
## 2026-07-07 - Fix: Trampoline Crash on Load (MenuCursor Hook Replaced)
### Summary
The previous `MenuCursor::SetCursorConstraintsRaw` / `CenterCursor` detour hooks crashed at startup with `REL/Trampoline.cpp: Failed to handle allocation request` (Allocate Size: 14, Free Size: 0) because the plugin never allocated F4SE trampoline memory. Removed the detours and instead hook `MainMenu::AdvanceMovie` via vtable (same pattern as existing MainMenu input hooks) to re-expand cursor bounds immediately after vanilla title-screen parallax runs. Kept the `UIAdvanceMenusFunctionCompleteEvent` sink and per-frame `ReleaseMenuCursorConstraints()` as additional safety.
### Files Changed
- `plugin/src/F4TPrismaUI.cpp`
- `changelog.md`
- `docs/dev-log.md`
### Testing
- Built via `xmake build`.
- Launch game: should no longer show trampoline error dialog; custom main menu should appear after intro.
- Verify mouse reaches all screen edges on title screen.
### Known Issues
- Cursor snap-back fix needs in-game re-verification with the vtable-hook approach.
### Next Steps
- User verification after deploy.
## 2026-07-06 - Fix: Main Menu Cursor Snap-Back (MenuCursor Hooks)
### Summary
The custom main menu overlay still trapped the mouse in a central box even after per-frame `ReleaseMenuCursorConstraints()`. Vanilla `MainMenu` title-screen parallax calls `MenuCursor::SetCursorConstraintsRaw` and `CenterCursor` every frame **after** our task cleared bounds, immediately re-tightening them. Fixed by hooking both functions to expand constraints to the full window and skip re-centering while a custom overlay is visible, registering a `UIAdvanceMenusFunctionCompleteEvent` sink as a post-menu safety net, and using `disableFocusMenu=true` for the main menu (vanilla `MainMenu` already owns a working cursor).
### Files Changed
- `plugin/src/F4TPrismaUI.cpp`
- `ui/views/CommonwealthOnline/main-menu/shell.html` (removed stray viewport meta)
- `changelog.md`
- `docs/dev-log.md`
### Details
- `SetCursorConstraintsRaw` hook redirects to `(0, 0, windowWidth, windowHeight)` when main menu or server browser overlay is active.
- `CenterCursor` hook becomes a no-op while overlays are active.
- `ReleaseMenuCursorConstraints()` now also calls `SetCursorConstraintsRaw` with full-window dimensions.
- Main menu `Focus()` now passes `disableFocusMenu=true` so PrismaUI does not open its own FocusMenu on top of vanilla title-screen cursor handling.
### Testing
- Build via `build-all.bat`.
- Launch to title screen; wait for custom main menu overlay.
- Move mouse to all four corners — cursor should not snap back to center.
- Click each menu row; open MULTIPLAYER and confirm server browser cursor also spans full window.
### Known Issues
- None currently known for this fix.
### Next Steps
- User in-game verification.
## 2026-07-06 - Fix: MenuCursor Constraints + Bink Gating (cursor jolt + early overlay) ## 2026-07-06 - Fix: MenuCursor Constraints + Bink Gating (cursor jolt + early overlay)
### Summary ### Summary
+85 -8
View File
@@ -50,6 +50,12 @@ namespace F4T::PrismaUI
bool g_hasLastCursorPos = false; bool g_hasLastCursorPos = false;
POINT g_lastCursorPos{}; 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; // Forward declarations (definitions appear later in this anonymous namespace;
// several functions call each other out of definition order). // several functions call each other out of definition order).
void HideMainMenuInternal(); void HideMainMenuInternal();
@@ -60,6 +66,46 @@ namespace F4T::PrismaUI
void CreateServerBrowserView(); void CreateServerBrowserView();
void SyncBrowserHover(); void SyncBrowserHover();
void WatchMainMenuState(); 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() std::string GetLogPrefix()
{ {
@@ -78,6 +124,35 @@ namespace F4T::PrismaUI
REX::WARN(std::string_view{ message }); 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() void SuppressNativeGameCursor()
{ {
auto* ui = RE::UI::GetSingleton(); auto* ui = RE::UI::GetSingleton();
@@ -116,12 +191,15 @@ namespace F4T::PrismaUI
return; return;
} }
// MainMenu keeps a tight MenuCursor box for title-screen parallax; clear it every frame // MainMenu keeps a tight MenuCursor box for title-screen parallax; expand to the full
// while our overlay owns input so the OS pointer can reach the full window. // 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->minCursorX = 0;
menuCursor->minCursorY = 0; menuCursor->minCursorY = 0;
menuCursor->maxCursorX = width; menuCursor->maxCursorX = width;
menuCursor->maxCursorY = height; menuCursor->maxCursorY = height;
::ClipCursor(nullptr);
} }
void ApplyBrowserPointerMode(bool a_mouseCursorVisible) void ApplyBrowserPointerMode(bool a_mouseCursorVisible)
@@ -136,12 +214,9 @@ namespace F4T::PrismaUI
if (g_mainMenuVisible && g_api && g_api->IsValid(g_mainMenuView)) { if (g_mainMenuVisible && g_api && g_api->IsValid(g_mainMenuView)) {
activeView = g_mainMenuView; activeView = g_mainMenuView;
namespace_obj = "window.CO_App"; namespace_obj = "window.CO_App";
// The main menu is the primary/standalone title-screen UI, not a panel opened // MainMenu is already open with a working title-screen cursor. Keep PrismaUI's
// on top of another menu's cursor. Let PrismaUI open its own FocusMenu (kUsesCursor / // FocusMenu overlay suppressed so we do not fight vanilla cursor/parallax state.
// kCursor input context) instead of relying on vanilla MainMenu's cursor handling, disableFocusMenu = true;
// 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;
} else if (g_browserVisible && g_api && g_api->IsValid(g_serverBrowserView)) { } else if (g_browserVisible && g_api && g_api->IsValid(g_serverBrowserView)) {
activeView = g_serverBrowserView; activeView = g_serverBrowserView;
namespace_obj = "window.CO_App"; namespace_obj = "window.CO_App";
@@ -674,6 +749,8 @@ namespace F4T::PrismaUI
void OnGameDataReady() void OnGameDataReady()
{ {
F4T::MainMenuInject::Install(); F4T::MainMenuInject::Install();
InstallMainMenuCursorReleaseHook();
RegisterMenuAdvanceSink();
g_api = PRISMA_UI_API::RequestPluginAPI<PRISMA_UI_API::IVPrismaUI4>(); g_api = PRISMA_UI_API::RequestPluginAPI<PRISMA_UI_API::IVPrismaUI4>();
if (!g_api) { if (!g_api) {
@@ -2,7 +2,6 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Commonwealth Online — Main Menu Shell</title> <title>Commonwealth Online — Main Menu Shell</title>
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
<style> <style>