feat(control): drive a running game over a local socket, with a CLI #926
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!926
Loading…
Reference in a new issue
No description provided.
Delete branch "control-bridge"
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?
Tests and tools that drive a running game today write presses into a file the game polls and scrape
hookshot_log.txtfor its state, timed in wall-clock seconds. This adds a local, opt-in control socket so a script can press buttons, wait for the game, take screenshots, teleport and change settings over one connection, with replies timed in frames.What it is
HOS_CONTROL=<socket path>starts a small bridge inside the game process; without it nothing listens. One JSON request a line, one reply a line (with the request'sidand the current frame); subscription events on the same connection.scripts/hookshot_ctl.pyis a standard-library client with alaunchhelper. The protocol, the security model, examples and the known limits are indocs/control.md(linked fromdocs/development.md).statuspress/stick/sequenceHOS_INPUT_SCRIPT's syntax accepted;exclusivekeeps the player's devices out; the reply when the input is over (or at once with"wait": false)release"all": truefor every client'swaitpresent(N frames),frame,in_play,state,loading_start,loading_end,change,mapshotoption_get/option_sethookshot_options.inikey, through the menu's own setterteleport(OoT)savessubscribemem_read/mem_read_many/mem_writeHOS_CONTROL_MEM=roorrwquitHooks outside the bridge are small:
host/nn_hid.c(the bridge's input in the controller read),host/hos_main.c(start; the current map, the state flags, and the teleport request and its outcome beside the existing debug teleport),host/hos_thread.c(log lines),host/hos_mem.c(bounds for guest memory),host/nn_fs.c(the save folder),host/rx_backend.candgpu/vk/vk_api.h(four renderer functions:frame_info,shot,setting,setting_set;MKWVK_API_VERSION14 -> 15),gpu/vk/vk_replay.cpp(shots at a present, with their own image and readback, never waited for),gpu/vk/vk_menu.cpp(settings by key, under a lockmenu_tickalso holds).Design choices
hookshot_options.inikey and changed through the menu's own setter (presets, restart notice and the options file behave as with the arrow keys).wait maprunning to its timeout. A teleport the game does not take in time (not in play) is withdrawn, so it does not happen later behind the script's back.releaselets go of the caller's own inputs (and of those left by clients that are gone), not another script's. An input sent without waiting stays after its client leaves, so a one-shothookshot_ctl.py press --no-waitworks, and is freed once over.Security model
HOS_CONTROLis set. Guest memory is a second opt-in (HOS_CONTROL_MEM=ro|rw); anything else leaves it off. Every write and setting change is logged.bind, in a folder that must be owned by the user and writable by no one else (a shared/tmpis refused), andSO_PEERCREDchecked on each client. A file in the way that is not a socket is never replaced; a live socket is left alone. Windows: a named pipe with a DACL for the current user alone,FILE_FLAG_FIRST_PIPE_INSTANCEandPIPE_REJECT_REMOTE_CLIENTS.0xhex digits.slots --thumbs), shows the game's control characters as?in plain output, andlaunch --saverefuses source saves holding links or special files.TSYNC); on older kernels it would be outside the sandbox, like the window's main thread there. If both land, the bridge should start after the restriction (its folder allowed) or stay off without ABI 8. This PR does not depend on #882.Tested
Linux (CachyOS, NVIDIA).
364d1994merged in (headless,HOS_NO_SDL=1, a copy of one save slot made bylaunch --save --slot 04,HOS_CONTROL_MEM=rw), 24 of 24 checks:option_getbefore the renderer was up:unavailable; right after the first frame: answered at once.statuswithmapandgame(event,loading,age).input.queuedback to 0.releasefrom one client left another client's held input alone;releasewithalllet it go.teleport 1:sent, toHyruleField_Child,accepted. A second teleport at once:refused, "another teleport is still in progress (one every 8 s)".wait mapwith the map's name in lower case answered on arrival. In an earlier run, a teleport sent while the arrival event played was answeredrefused, "an event is playing".teleport 0 --arrive 120(the client): sent, and in Kokiri Forest 182 frames later.rw: the heap's mapped end found by reads (one page past it:out_of_range), 8 bytes written there, read back, then the old bytes written back and read back; an address string"-1"refusedbad_request;mem readof the image as u32.option set fps_counteron and off; shots (named, and the default folder); a client that shut its sending side after awaitgot its reply, then the close;watchwith present events;quit: the game gone 1.5 s later, the socket removed. The source save was not changed.cancelledfor a teleport the game then sent. The fake game in the tests now takes and finishes in two steps, as the game does.tests/test_control.py: the bridge built with a fake game and driven over a real socket. Covered: every command, malformed and oversized input, numbers past int64, too-long ids, memory address strings, disconnects mid-press, too many clients, a client that never reads, a half-closed client, the socket's mode, a file in the way, a shared folder refused, map/teleport (sent, refused, not taken, replaced)/state, presses without waiting (more than the queue holds), release per client and for all, memory at each level, quit. Also clean under AddressSanitizer and UndefinedBehaviorSanitizer (clang).tests/test_hookshot_ctl.py: the client against a fake bridge. Covered: every command's request and output, exit codes, watch, launch, teleport (--arrive,--no-wait, refused),release --all, how long the client waits for a long input,--savewith a planted link refused, a hostile slot listing refused.tests/run_all.pyon the final head (main2e62a977merged in): 358 passed, 4 failed, 13 skipped. The same four fail on clean main here, for reasons of this PC:test_nup_read_nameandtest_bake_portability(nozstandardmodule),test_builder_log_read_only(no Tk library),test_wayland_libdecor_hint(clang 23's-Wunused-but-set-globalin its own harness). The bridge's two checks pass.hookshot-build: a full game build (the one the live runs used), andhookshot_vk.soof the final head with the ABI check (no undefinedmkwvksymbols). The final merge of main (5 commits: release, builder, HDR/FSR, vcores, tests) had no conflicts and was not run in the game again.host/control.candtests/control_test.ccompile and link with llvm-mingw 20261104 at-Wall -Wextra -Werror.Known limits
host/control.candtests/control_test.ccompile and link with llvm-mingw 20261104 (clang 23) at-Wall -Wextra -Werror, and the pipe tests run in the windows-2022 CI job, but they were not run here. Untested: the bridge inside a Windows game build,launchon Windows, and refusing remote pipe clients.status'sgameflags andmapare the game's own values.acceptedis the game's event manager's answer to the request;falsewas seen once, when the previous teleport's arrival event was still playing and the map did not change. Whetherfalsealways means no move is not established, so it is reported, not turned into an error.HOS_CONTROL_MEM=rwwas checked against the game's memory (a write read back, then put back), not against what the GPU draws from a written value.mem(Link's position, health, ...) are not in this PR.Thank you for this! It changes how the game runs at a level that needs a maintainer design decision first, so I'm closing it to keep the queue clear: needs maintainer design decision, reopen after discussion. (A local control socket that can read and write guest memory is a new interface: needs a design discussion first.)
github-sync referenced this pull request2026-11-12 06:22:54 +00:00