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:
+2
-1
@@ -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).
|
||||||
|
|||||||
Binary file not shown.
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user