Game menus: point and click with the mouse in the title, pause menu, panel pages, options and dialogs #1500
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!1500
Loading…
Reference in a new issue
No description provided.
Delete branch "ingame-mouse"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Point-and-click mouse support in Ocarina of Time's own menus (Windows):
How it works
nx2/oot_patches.pyadds two calls at the heads of lifted functions, in the supported build only (protect_hearts):f_802ed660)f_802f1380)A lift that is patched again is left as it is.
host/oot_menu.c:button+0x58) in the layout's 3840×2160 units.f_8088ffc0f_833fda00)The pad is used only where the game has no setter: A/B, L/R, the map's floors.
host/display.c: frees the mouse while a game menu runs (300 ms without a frame captures it again) and hides the cursor until the mouse is used.host/nn_hid.c: host-injected presses (hos_hid_tap, in the input script's syntax), a held stick, and a drag mapped like the camera's mouse (HOS_MENU_DRAG_GAIN, default 6).Guest reads are bounds-checked
memcpy(commit_on_touchcovers reserved pages), notReadProcessMemory.One lock serialises the mouse state across the game's worker threads. The focus hook never waits for it: when it is busy, the change is kept and applied at the next tick.
Tests
tests/test_oot_menu_mouse.py(Windows): compilesoot_menu.cagainst a fake guest memory. It covers:tests/test_oot_menu_mouse.pyalso covers:tests/test_menu_hooks.py: each hook is inserted first and once, re-applying is harmless, and only the supported build's lift gets the hooks.tests/test_input_background.pyandtests/screensaver_test.c: stand-ins for the new code inside the parts ofnn_hid.canddisplay.cthey compile.tests/run_all.py: no new failures compared withmainon the same machine.Since the review
pause_screenis read and written atomically.Known limits / follow-ups
Addresses are this build's. The hooks and setters are fixed addresses, gated to the supported build. A follow-up will find them by signature at lift time and fail closed.
Positions measured from screenshots: some positions are measured in English:
Other languages' layouts haven't been checked yet.
Native mouse camera: it doesn't respond with the slingshot or boomerang out. That's an existing issue, separate from this PR.
🤖 Generated with Claude Code
Nice work. The way it's built (hooks at the heads of
f_802ed660/f_802f1380, gated to the supported build, then driving the game's own focus setter and Key Items refresh instead of faking pad input everywhere) is the right call, and the Ctx save/restore around the guest calls follows the patternhos_main.calready uses. I also checked the plumbing:donelike the other protect_hearts patches.oot_menu.c's#elsestubs keep the link happy, andnn_hid.c's new SRW lock and 64-bit interlocked calls map ontohost/posix/windows.h.script_end_ms, soHOS_INPUT_EXCLUSIVE/ scripted runs behave as before. HOS_INPUT_BACKGROUND is unaffected because the pointer is only read while the window is in front.run_all.py's glob.Things worth a look, none blocking:
ptr_x/ptr_y/...globals with aHosPointersample. This PR reads and writes those globals directly, so whichever lands second will need a rework there, not just a rebase.pause_screenandticksare written undermouse_lockbut read inhos_oot_menu_tickunderseen_lockonly. That's harmless on x64 with aligned 64-bit fields, but it's worth a comment or an atomic read.mouse_capture(w, 0)callsShowCursor(TRUE), and the cursor stays visible until the next WM_SETCURSOR or the 250 ms timer hides it. A controller player may see a brief flash. CallingSetCursor(NULL)right inmenus_mousewould avoid that.Optional simplification:
hos_hid_stick(held stick until a deadline) andhos_hid_drag(decaying stick) are two separate host-stick injectors, andhos_hid_tapis a third input path into nn_hid.c. #926 (control bridge) and #1025 (MCP server) also inject input there. One small "host input" entry point (taps plus a stick sample with an optional decay) would keep that surface smaller. It's fine as it is if you'd rather not.CI was still pending when I looked.
@ -0,0 +1,817 @@/* The mouse in the game's own menus: the title and its lists, the pause menu, the panel ofThis is a blocking acquire inside a guest function (the pause menu's focus handler). Meanwhile
mouse_tickcan hold the same lock on another game thread while it runs guest code (set_focus,guest_call). If that guest code ever waits on something the thread blocked here holds (a job, a guest mutex), the two deadlock. I doubt it happens with today's call graph, butfocus_lockedonly writes a handful of words. Making those plain atomics, or usingTryAcquirehere and stashing the pane for the next tick, would remove the risk completely.@ -0,0 +395,4 @@const int item_model = pause && page_now == 2, pan = page >= 0 || item_model;static int ldown, ldrag, lx0, ly0, llx, lly;if (pan) {const int held = (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0;GetAsyncKeyStatereports the physical buttons, butWM_LBUTTONDOWNis the logical one. With "swap primary and secondary buttons" on (left-handed setups), the press arrives from the physical right button,heldreads 0 on the next tick, and every drag turns into an immediate click. You could track the logical left-button state in display.c (WM_LBUTTONDOWN/WM_LBUTTONUPare already handled there) and hand it out throughhos_mouse_game_pointer. Or checkGetSystemMetrics(SM_SWAPBUTTON)and readVK_RBUTTONinstead.Thanks for the review. Follow-ups are pushed:
mouse_tickholds it the focus change is kept and applied at the next tick. A test covers it: a focus change reported from another thread while the setter holds the lock.pause_screen: now read and written atomically, with a comment.ticksis counted the same way.Since then I've also added a drag to turn the item on the Items page and a wheel for the R item row (one D-pad press per notch, with the mouse kept for the camera). The wheel now works while the cursor is hidden, and the mouse returns to the camera about 0.1 s after a menu closes. Main is merged in, and the branch is conflict-free again.
@ -0,0 +1,817 @@/* The mouse in the game's own menus: the title and its lists, the pause menu, the panel ofFixed: the hook only tries the lock now, and keeps the change for the next tick when it's busy.
tests/test_oot_menu_mouse.pyreproduces the two-thread case.Re-reviewed the new commits (ba20577..891d6a6, plus the main merge). Looks good to me.
pause_screen/ticksatomics and hiding the cursor on open both look right.hos_hid_tapscript syntax and the 256-byte buffer) and the 50 ms "menus gone" hand-back all read correctly. Hidden-cursor mode returning 2 (wheel only, no presses or pointing) makes sense.GetAsyncKeyState(VK_LBUTTON), so it has the same left-handed caveat.CI: the 8 failures from before are gone after the merge. The one left,
test_shader_stamp_cache.py(NameError: logo_rgba), comes from main at6add2f1(#1567) and is already fixed on main by #1575. It isn't caused by this PR, and merging main again should turn both Python jobs green.