feat(coop): co-op rooms: a relay, progress sync, and a Co-op tab to make, join and accept #2274
Open
mooki000 wants to merge 6 commits from
mooki000/coop-relay-wire into main
pull from: mooki000/coop-relay-wire
merge into: MagicBeanDev:main
MagicBeanDev:main
MagicBeanDev:github/pr/2893
MagicBeanDev:github/pr/2890
MagicBeanDev:github/pr/2887
MagicBeanDev:github/pr/2878
MagicBeanDev:github/pr/2875
MagicBeanDev:github/pr/2874
MagicBeanDev:github/pr/2873
MagicBeanDev:github/pr/2871
MagicBeanDev:github/pr/2756
MagicBeanDev:github/pr/2897
MagicBeanDev:github/pr/2896
MagicBeanDev:github/pr/2718
MagicBeanDev:github/pr/2842
MagicBeanDev:github/pr/2691
MagicBeanDev:github/pr/2895
MagicBeanDev:github/pr/2743
MagicBeanDev:github/pr/2765
MagicBeanDev:github/pr/2708
MagicBeanDev:github/pr/2732
MagicBeanDev:github/pr/2755
MagicBeanDev:github/pr/2817
MagicBeanDev:github/pr/2876
MagicBeanDev:release-please--branches--main
MagicBeanDev:release-please--branches--main--release-notes
MagicBeanDev:github/pr/2778
MagicBeanDev:github/pr/2888
MagicBeanDev:github/pr/2866
MagicBeanDev:github/pr/2877
MagicBeanDev:github/pr/2800
MagicBeanDev:github/pr/2829
MagicBeanDev:github/pr/2850
MagicBeanDev:github/pr/2889
MagicBeanDev:github/pr/2882
MagicBeanDev:github/pr/2860
MagicBeanDev:github/pr/2849
MagicBeanDev:github/pr/2886
MagicBeanDev:github/pr/2794
MagicBeanDev:github/pr/2840
MagicBeanDev:github/pr/2830
MagicBeanDev:github/pr/2834
MagicBeanDev:github/pr/2719
MagicBeanDev:github/pr/2742
MagicBeanDev:github/pr/2741
MagicBeanDev:github/pr/2820
MagicBeanDev:github/pr/2771
MagicBeanDev:github/pr/2859
MagicBeanDev:github/pr/2858
MagicBeanDev:github/pr/2823
MagicBeanDev:github/pr/2757
MagicBeanDev:github/pr/2769
MagicBeanDev:github/pr/2844
MagicBeanDev:github/pr/2843
MagicBeanDev:github/pr/2673
MagicBeanDev:github/pr/2855
MagicBeanDev:github/pr/2739
MagicBeanDev:github/pr/2807
MagicBeanDev:github/pr/2851
MagicBeanDev:github/pr/2789
MagicBeanDev:github/pr/2814
MagicBeanDev:github/pr/2818
MagicBeanDev:github/pr/2774
MagicBeanDev:github/pr/2772
MagicBeanDev:github/pr/2750
MagicBeanDev:github/pr/2786
MagicBeanDev:github/pr/2785
MagicBeanDev:github/pr/2782
MagicBeanDev:github/pr/2779
MagicBeanDev:github/pr/2775
MagicBeanDev:github/pr/2706
MagicBeanDev:github/pr/2766
MagicBeanDev:github/pr/2747
MagicBeanDev:github/pr/2759
MagicBeanDev:github/pr/2745
MagicBeanDev:github/pr/2740
MagicBeanDev:github/pr/2733
MagicBeanDev:github/pr/2725
MagicBeanDev:github/pr/2727
MagicBeanDev:github/pr/2728
MagicBeanDev:github/pr/2729
MagicBeanDev:github/pr/2721
MagicBeanDev:github/pr/2722
MagicBeanDev:github/pr/2715
MagicBeanDev:github/pr/2714
MagicBeanDev:github/pr/2707
MagicBeanDev:github/pr/2703
MagicBeanDev:github/pr/2705
MagicBeanDev:github/pr/2697
MagicBeanDev:github/pr/2679
MagicBeanDev:github/pr/2676
MagicBeanDev:github/pr/2669
MagicBeanDev:docs/what-hookshot-connects-to
MagicBeanDev:fix/hdr-encode-test
MagicBeanDev:fix/main-tests
MagicBeanDev:fix/play-cache-files
MagicBeanDev:perf/game-barrier-order
MagicBeanDev:feat/heap-lazy
MagicBeanDev:feat/texture-detail
MagicBeanDev:perf/astc-opaque-bc1
MagicBeanDev:fix/fs-dir-lock
MagicBeanDev:fix/framegen-native-kept
MagicBeanDev:fix/update-no-downgrade
MagicBeanDev:port/k10-gcn-f16-mirrors
MagicBeanDev:fix/framegen-mult-cap
MagicBeanDev:chore/release-as-2.1.1
MagicBeanDev:perf/pcache-load-switch
MagicBeanDev:fix/prepare-progress-log
MagicBeanDev:fix/update-closes-stale-game
MagicBeanDev:fix/hdr-scaled-sampled
MagicBeanDev:perf/first-start-preset
MagicBeanDev:perf/zero-upload
MagicBeanDev:fix/vcore-escape-2s
MagicBeanDev:test/fake-gh-atomic
MagicBeanDev:perf/batch-idle-flush
MagicBeanDev:test/finish-stub-a
MagicBeanDev:chore/record-2.1.0
MagicBeanDev:fix/main-red-tests
MagicBeanDev:fix/test-display-startup-driver-cache
MagicBeanDev:feat/mesh-shader-mode
MagicBeanDev:fix/menu-probes-at-startup
MagicBeanDev:fix/fps-counter-classic
MagicBeanDev:fix/test-log-batch-flake
MagicBeanDev:fix/narrow-screen-hud
MagicBeanDev:ci/prereleases
MagicBeanDev:release/v2.0.15
MagicBeanDev:hotfix/v2.0.15
MagicBeanDev:fix/present-ahead-replay
MagicBeanDev:release/v2.0.13
MagicBeanDev:fix/bracket-paths-2013
MagicBeanDev:fix/hdr-overlays-2013
MagicBeanDev:fix/fps-counter-scale-2013
MagicBeanDev:fix/stick-seen-at-rest
MagicBeanDev:hotfix/menu-fits-screen
MagicBeanDev:fix/f1-always-opens-menu
MagicBeanDev:fix/ini-keep-lines
MagicBeanDev:claude/cranky-pike-7cf247
MagicBeanDev:perf/texture-answer-flat
MagicBeanDev:fix/stamp-test-modules
MagicBeanDev:fix/appimage-fonts
MagicBeanDev:fix/display-awake
MagicBeanDev:release/v2.0.12
MagicBeanDev:fix/discord-windows
MagicBeanDev:rc/v2.0.12-tests
MagicBeanDev:fix/dlss-keep-feature
MagicBeanDev:rc/v2.0.12
MagicBeanDev:button-icons
No reviewers
Labels
Clear labels
accessibility
Barrier affecting people with disabilities
autorelease: pending
autorelease: tagged
bug
Something isn't working
dependencies
Pull requests that update a dependency file
documentation
Improvements or additions to documentation
duplicate
This issue or pull request already exists
enhancement
New feature or request
github_actions
Pull requests that update GitHub Actions code
good first issue
Good for newcomers
help wanted
Extra attention is needed
invalid
This doesn't seem right
question
Further information is requested
testing wanted
wontfix
This will not be worked on
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
Milestone
Clear milestone
No items
No milestone
Assignees
Clear assignees
No assignees
2 participants
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!2274
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki000/coop-relay-wire"
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?
What this is for: co-op (#2269): each friend plays their own game/save, sharing accepted progress through the game's setters. This draft adds bounded events and optional maker-PC hosting, not other players' Links, enemies, combat or warp. Measured:
2be28f893fa378a28de2a110caf491970be7b32eonde866017e1584372fc75a1caab3784b3ba1d9f98: game-free focused 60 pass / 2 fail, suite 467 pass / 15 fail / 20 skip, Windows-target syntax below. Every failure is reproduced on untouched base. Not measured: no game build/play, current Windows execution/link, LAN/cross-OS/controller or performance proof on this head. All game pictures below are historical.Draft: 18 commits. Rebase recorded 2026-11-11T22:57:08-06:00; evidence assembled 2026-11-11T23:52:40-06:00. The socket decision remains open.
Problem
nn::socketstays offline; there was no host path for players' games to find a room or exchange progress.host/coop_net.candhost/coop.ckeep that networking outside the guest game.scripts/coop_relay.pyforwards sealed messages;host/coop_proto.cderives separate locator and message keys so the relay need not know the room secret or open progress.host/coop_state.cmerges accepted, signed progress;host/oot_coop.cchecks the live flag table and calls the game's setters only after acceptance and a save copy.gpu/vk/vk_menu.cppexpose the same session and progress commands while the game runs.Additional defects: Windows name/printf/CRT conflicts; unstable fixture task counts; area resets mistaken for save loads; cancelled event fragments blocking later work or combining ACK epochs; localized UI helpers translating typed data.
What changed
Additions above the original six
1c60356fbind→bind_tablesrename removes the Winsock declaration collision;IN→COOP_FLAG_INalso avoids the Windows annotation macro. Existing fixture callers/extraction markers migrate with their checks. Co-authored with Kuba.68088064184cceb0%uplusunsigned, preserving format attributes and warning errors; no global ANSI-stdio override.1cd02fc41f1d0a72d2fc0f7f35931fa8b8d0cbedcoop_member_accepted(id); a roster entry or recycled member number is not acceptance.c517e74697f56483coop_host/HOS_COOP_HOST): its own socket/thread, local maker authorization, cookie proof, sealed forwarding, bounded state and joined shutdown; no router/firewall automation.dcf844302be28f89Layer interface:
coop_send_event(member, kind, bytes, n),coop_on_event(kind, callback, user),coop_member_accepted(id); contract/example indocs/coop.md#adding-a-layer. EVENT/ACK stay kinds16/17, but ACK body v2/27 bytes requires all peers to update. Eight queued events, 16 KiB each, 1024-byte fragments; one fragment/32 ms, retry250 ms, ACK50 ms. Whole FIFO callbacks are not gameplay authorization or durable delivery. Handle queue-full; validate signed rules, current session, authority/context/age/bounds and recheck bounded work on the game thread. Consent alone is not the completed-backup gate.Rebase resolutions
Part5 met #1930's API bump: retain ocarina bindings, then text API20 and Co-op API21 (base API19). Part6 met #1864: retain
game_langexclusion androw_key(...), saved_value(...). #2435 keyboard prompts, #2574 DLSS failure hook, #2541 aspect migration, #2116 POSIX identity and Bug report/Restart/Quit remain. Part5 additionally keeps typed values/caret widths literal and migrates 13 custom-size translations; labels/placeholders stay localized. Replaying CRT guards conflicted with that new fixture: both retained, no old assertion removed. Other commits had no textual rebase conflicts.Original six parts
Historical tested hashes versus current counterparts; per-prefix tests retain their tested identities:
e27f1a92ae4c5a3b52c471bb5ed79bfd2f34da9a67662ccb841492c4b8a6fabcb4ec89b76b9ab18971d4ab0feb904f9ce27f1a92test_coop_proto.py(gcc and clang),test_coop_relay.py,test_appimage_source_headers.py52c471bbtest_coop_sync.py(gcc and clang)2f34da9atest_coop_session.py(gcc and clang),test_coop_env_file.py,test_coop_options_text.py,test_output_resolution.py,test_env_file.py,test_control.py(gcc and clang),test_display_startup_failure.py,test_env_on.py841492c4test_oot_coop.py(gcc and clang),test_save_copy.py(gcc and clang),test_env_file.py,test_present_trace.py,test_coop_env_file.py,test_env_on.pyb4ec89b7test_menu_text_field.py,test_menu_restart_slots.py,test_screensaver.py,test_menu_env_overrides.py,test_preset_exempt.py,test_menu_keys.py,test_menu_sections.py;vk_menu.cppwith-Wall -Wextra: 2 warnings, both main's71d4ab0f—feat(menu): a Co-op tab to make, join, accept and leave a co-op roomcoop_*keys (no numeric option ids),MKWVK_API_VERSION21. 12 files, +1321 -42test_menu_coop_tab.py,test_menu_text_field.py,test_env_on.py,test_menu_env_overrides.py,test_preset_exempt.py,test_control.py,test_menu_keys.py,test_menu_sections.py;vk_menu.cppas at 5How to try it
You need this branch built (docs/development.md), from your own copy of the game as always.
On two PCs:
python -I scripts/coop_relay.py --port 24680(--helplists its limits).hostorhost:port; the port is 24680 if you leave it out), Enter. Both are saved inhookshot_options.ini.coop-backup/first; docs/coop.md says how to go back to it.On one PC, two folders: copy the built game folder twice, start the relay with
--host 127.0.0.1, give each game its own user folder (./hookshot --user <folder>on Linux), use127.0.0.1:24680as the Relay in both, and follow steps 2 to 8.Optional maker hosting: set
coop_host=1inhookshot_options.inior real-environmentHOS_COOP_HOST=1before startup, then Make. Off by default; Join never hosts. Empty Relay uses UDP24680;127.0.0.1:<port>chooses another; foreign hosts are refused.coop_portstill means the client socket. Friends use the maker's reachable address and receive the secret privately. Internet needs UDP forwarding/firewall permission and may fail behind CGNAT. No UPnP/firewall changes or hosting UI row are added. Leaving/switching off/closing the maker ends its hosted room.How it was tested
Current head — game-free
Arch/Linux 7.2.15-1-omarchy, i9-13900K; GCC 16.2.1, Clang 23.1.3, the project's pinned Python. Focused 2026-11-11T22:59:51-06:00 → 2026-11-11T23:08:53-06:00. 31 files in both compiler environments: 60 pass/2 fail; pure Python checks are not two native compilations. Loads are one-minute endpoints, not timing/performance claims.
Focused invocations: exit/expected and load
One whole suite:
467 passed, 15 failed, 20 skipped in 143 s (linux, 32 at once); exit1; load 11.65 → 39.26; 2026-11-11T23:11:01-06:00 → 2026-11-11T23:13:23-06:00.CC=gcc CXX=clang++ python -I -B tests/run_all.py. Only failing configurations were run once on untouchedde866017e1584372fc75a1caab3784b3ba1d9f98; all failed there too. No zero-test baseline or whole-main-suite pass is claimed. The suite remains red.test_menu_i18n.pytest_menu_i18n.pytest_audio_aux.pytest_audio_biquad.pytest_display_startup_failure.pytest_dlss_checks.pytest_env_on.pytest_framegen_preset.pytest_hdr_encode.pytest_linux_docker_packages.pytest_menu_i18n.pytest_mods_size_table.pytest_mouse_native.pytest_readback_part.pytest_rumble_menu.pytest_texel_buffer_ownership.pytest_wayland_libdecor_hint.pyBehavioral before/after proofs
Every ordinary area-changing loading zone was affected by the old per-area clock, not just warp: Players s2 B logged 2 false loads going to the field/back, hidden by
HOS_COOP_ACCEPT=always. The current real-engine fixture preserves accepted offers/copies across area changes, but requires both anew for another slot, same-slot reload and new game. No corrected-head game proof yet.All 14 event cases pass with ASan/UBSan, including loss/reordering and cancel, cancel-accept, reset-ack, delayed-ack, reaccept-step; the concurrent case also passes TSan. Those 5 new cases reject the previous helper. The Portuguese-table compiled value-cell fixture rejects renderer50bd1255 and passes literal Player/Off, clipping/caret and translated-placeholder checks; no screenshot proof is implied.
Windows
17 Windows units + 1 native Linux-only unit; zero errors/name clashes. Clang
--target=x86_64-w64-mingw32, llvm-mingw UCRT headers; generated headers from71d4ab0fonfce53453(existing actual generated headers, not a new game build).host/posix/display_sdl.cis Linux-only. Warnings retained: -Wdeprecated-enum-enum-conversion: 1, -Wmissing-field-initializers: 7, -Wunused-const-variable: 1, -Wunused-function: 3.Syntax units, warnings and loads
host/control.chost/coop.chost/coop_menu.chost/coop_net.chost/coop_proto.chost/coop_relay.chost/coop_state.chost/display.chost/hos_main.chost/hos_relaunch.chost/hos_sdl.chost/nn_applet.chost/nn_fs.chost/nvn_native.chost/oot_coop.chost/posix/display_sdl.chost/rx_backend.cgpu/vk/vk_menu.cppWindows CI compiles portable co-op fixtures but lacks generated headers for this full host/menu sweep. Syntax is not a Windows link/run. Kuba's Windows11 report, on
71d4ab0fplus his bind rename, established build/link/run, loopback Make/Join/Leave and physical typing. His fix is co-authored here. Accept/progress, LAN/cross-OS/controllers/suite were not tested there; he also reported a shader-start fault seen on releases, so it was not fault-free.Historical fixture repair and stress proof — not a final-head stress rerun
On
fe3d1eda(on published71d4ab0f), 2026-11-11T18:22:54-06:00 to 2026-11-11T19:58:10-06:00: 40 complete session invocations, 320 modes passed, no session failure. Each compiler ran the complete test 20 times, not a reduced menu-only selection. Every start overlapped an active gated whole suite; endpoint one-minute loads were 5.93–181.58. These are correctness checks under load, not timing measurements.The separate TSan lifetime probe observed a joined helper TID still in
/proc/self/taskafter 624/1000 joins. The repair waits for that specific joined TID to disappear, bounded at five seconds, before the fixture counts its baseline. It changes no production lifetime rule and removes no exact thread/socket check. The deliberate still-live production-worker mutation was caught: exit 1, load 19.98 → 51.99, withFAIL coop_session: and the co-op thread has ended (threads 3, before co-op was on 2).The 18 load-generating whole suites were not all green. All retained
test_env_on.py; additional compiler failures are named in the table. The logs record compiler SIGKILLs, a killedcc1plus, and a shaderbatch compile failure without a diagnostic; their causes were not established. They were not rerun to erase failures. The later fixture fix does not turn the older published suite into a pass.Every load suite, including all failures
tests/test_env_on.pynx2/test_depth_sample_types.py,tests/test_env_on.pytests/test_env_on.pytests/test_env_on.pytests/test_env_on.pytests/test_env_on.pytests/test_env_on.pytests/test_env_on.pytests/test_env_on.pytests/test_bake_mimo.py,tests/test_env_on.pytests/test_env_on.py,tests/test_lower_immediate_not_register.pynx2/test_unsigned_image_types.py,tests/test_env_on.pytests/test_env_on.pytests/test_env_on.pytests/test_env_on.pynx2/test_unsigned_image_types.py,tests/test_env_on.pytests/test_env_on.pytests/test_env_on.pyHistorical Windows format/CRT proof
The real build uses UCRT, ANSI stdio0 and
vsnprintf, not GNU's wrapper. Explicit Microsoft-format cross-GCC rejects the old traces and passes%u/unsigned; unchanged coop_net passes. Clang emits both COFF objects; Linux GCC/Clang sessions pass8/8 each and codecs79 vectors/400000 fuzz inputs each. All19 steps match expected exits; attributes/-Werror unchanged. Secure-CRT guards precede fixture headers. Actual Windows fixture execution remains pending.Every recorded format-proof command outcome and load
Previous integration 94cb598f on55b976c0: 60 focused passes;
465 passed, 1 failed, 20 skipped in 221 s (linux, 32 at once), only env_on failed. Those results are not relabelled as this head.Historical build and game evidence
The retained account below belongs only to its named heads. “Current/head/not pushed/queued” inside dated additions is historical, not this rebase. Other-player figures in follow-on experiments are outside this pull request. Pictures and both dated additions/chest are unchanged.
Linux (Arch, kernel 7.2.15-1-omarchy, i9-13900K), RTX 4090 with NVIDIA 615.78.08, clang 23.1.3, gcc 16.2.1, Python 3.12.16. Head
71d4ab0fon mainfce53453, 2026-11-11 (US Central). The final session was headless, output 2048x1152, screenshots 1280x720.Full
make_game.pybuild: exit 0 and its completion line; the two game copies have matching executable / renderer SHA-256 prefixes92986f4c15a12e84/65e42fecc682aed9, both stamped71d4ab0fonfce53453. One-minute load at build request / completion: 127.77 / 16.94. No build-speed or frame-time claim.Each published commit on its own (historical)
Each of the six commits checked out alone, and the tests it adds or touches run there, each with the compiler
tests/run_all.pygives it (and the C tests that default to gcc once more with clang): 39 runs, 5 failed. The failures:test_env_on.pyat commits 3, 4, 6 fails on mainfce53453itself, onHOS_FPS_LOG (host/oot_high_fps.c:15)(main's #2470 reads that switch as set-means-on; nothing of this stack);test_coop_session.pyat commit 3 (--cc gcc and --cc clang) failed its menu fixture's process-thread counts at load 74.46 and 102.99; the complete standalone test then passed all 8 runs with each compiler after starting at load 39.72 (GCC ended at 29.20, Clang at 14.26). The repeated code and fixture were unchanged at the then-final71d4ab0fhead. This is evidence of a load-sensitive fixture, not an all-green suite or proof that the fixture is repaired. Besides what they compile and run, these tests check in the source where each part is wired in (the build compiles Monocypher; the game starts co-op beside the Discord status and says goodbye on both ways out; the present ends with progress sync's call and the save mount's writes take its lock; the interface's new members sit in their places in the host's table; the tab, its page and its keys in the options file).e27f1a9252c471bb2f34da9a841492c4b4ec89b771d4ab0fPer-commit one-minute loads: 69.79 to 145.67. The two renderer syntax passes each retain the same enum-arithmetic and unused-function warnings recorded on main.
The whole suite on
71d4ab0f(historical)python -I -B tests/run_all.pyon71d4ab0f:458 passed, 2 failed, 20 skipped in 227 s (linux, 32 at once)(one-minute load 125.19 → 84.33). Failures:tests/test_env_on.py: the isolated test fails on mainfce53453itself atHOS_FPS_LOG (host/oot_high_fps.c:15), introduced by #2470;tests/test_coop_session.py: the ThreadSanitizer menu fixture's thread-count assertions failed under load (includingone thread more). Main has no co-op session test, so the main-tree selection ran zero tests and is not a passing baseline. The complete standalone GCC and Clang repeats described above pass on the unchanged session code. The suite was not rerun and remains failed.Two games on
71d4ab0f, from the Co-op tab alone (historical)Two headless games on the RTX 4090, separate user folders and copies of the same child Kokiri save; Python relay on loopback. RX_VK_MENU_SCRIPT typed names/relay/secret and drove Make/Join/Accept; no automatic co-op enable/join/accept. Declared aids: HOS_COOP_ROOM fixes A's test secret, HOS_COOP_TRACE logs, HOS_COOP_TEST_SET sets B's song through the game routine, debug tools supply A's sword, and HOS_CONTROL quits gracefully. This is scripted menu input, not physical keyboard/controller proof.
71d4ab0f)67d2de81/b744b1ecf7a9640d/940eb9ef71d4ab0f/fce53453v2.1.0-rc.3-76-g67d2de81v2.1.0-rc.3-138-gf7a9640dv2.1.0-rc.3-191-g71d4ab0fThe legacy session checker's N/A rows are the other-player figures (outside this stack), menu pictures and renaming; the tab checker covers the latter two. Its old descriptions mentioning automatic join/accept are not the test setup: the quoted log lines and the separate environment checks prove that this session used the tab.
The checks establish tab-only setup/acceptance; each backup equals its starting save (14 files); A's sword reaches B and B's song reaches A. Readback: A 1 applied/1 getter agreement; B 12 applied/11 getter agreements plus 1 Int readback; none missing. Six stats lines report no discarded datagram. Typed settings persist; both games send goodbye on the bridge's quit. The table retains launch guards, loads, counters and kernel results.
Historical
71d4ab0fsession panel: A makes the room, B types the join secret, both accept, and B receives the item (game-log evidence).71d4ab0fsession did not open B's Equipment page; that visual proof belongs to the earlier chest session below.The room's secret is masked in every panel, and it is in neither game's log (searched).
Earlier sessions
The two-game sessions made on this stack's predecessor branches (the first Co-op tab, the chest and Lens sessions, the sessions with later parts on top that are not in this pull request) are in this description's edit history (its versions of 2026-11-11) and in the comments below. They are evidence for those commits, not for this head, and are left out here to keep this description to what this head carries.
Not tested
No current-head game build/play, GPU/two-game/manual-consent/loading-zone/hosted-game proof; no current Windows execution/link. No two-PC/LAN/internet/NAT/cross-OS, more than two real games, AMD/Deck/controller or Linux physical typing proof. No corrected Lens crossing, new chest/rupee chest/boss/door behavior, remote-area chest, live Remove/Reset/rule change or manual backup restoration. No performance/bandwidth proof or gameplay layer built on events.
Risks
Progress affects real saves: acceptance, whole-folder backup, classification/getter checks and game setters remain mandatory. Wrong classification can import wrong progress. Co-op is off by default with no socket/thread/table scan; hosting adds its own joined socket/thread. Relay loss, shared bandwidth ceilings, maker departure and NAT can prevent delivery.
Secret holders can forge ordinary events/ACKs; receipt is not gameplay approval. Durable progress/rules keep individual signatures; protocol/nonce/signature use still needs review. ACKv2 is incompatible with v1; callbacks borrow bytes, queue-full must be handled, session/consent changes cancel work. ABI callback order must move together. Literal-field/translation tests are not all-language playtests. A restarted maker can retain its key but bring no rules: question6 remains open. This is a substantial draft, not an all-green claim.
Overlaps
Saved
git merge-treetrial at 2026-11-11T23:51:26-06:00, against main87991bc8b21c271f2dee3daf7f06fd006faa865c: exit 0, clean at that moment, treec23d9a4f44ee7c06ec654e273b40d24b08b1277c. This is not a continuing mergeability claim.Standalone snapshot 2026-11-11T23:05:49-06:00: 88 PRs touch 68 files; 21 conflict entries, all hunks read. #2453 needs journal tree/commit synchronization reconciled with backup barriers; #2578's TCP gameplay must be ported deliberately into the UDP/session/consent/backup contract, not concatenated. #2600 must preserve packed-file dispatch plus save-writer accounting. Other host-ABI additions need matching declarations/positional tables/version; fixture-only conflicts retain both tests. #882's options-present flag precedes co-op's continue; #2371 retains retake persistence/cancel prompts; #2566 retains dynamic sizes plus custom editing/translations. Independent option/context tail additions are not evidence of gameplay incompatibility.
Conflicting PRs and exact paths
990febcbgpu/vk/vk_menu.cpp4f6ccdb9gpu/vk/vk_menu.h2ef23f2ftests/test_present_trace.py1fc9986fdocs/development.md,gpu/vk/vk_menu.h6fb092bbtests/test_menu_env_overrides.py,tests/test_menu_keys.py5cf62486host/hos_sdl.c,host/nn_fs.c,host/posix/display_sdl.c5f85fe70gpu/vk/vk_ui.hb5af3839tests/test_vcore_fs_open.py919ba0eehost/rx_backend.c26793b60gpu/vk/vk_api.h,host/rx_backend.c,tests/test_env_file.pya2bed6b2gpu/vk/menu_i18n.h,gpu/vk/vk_menu.cppd827111dtests/test_env_file.py92ea6643host/nn_fs.c,readme.md9bd89259gpu/vk/vk_menu.h,host/rx_backend.cf654ef20host/rx_backend.c44e48391host/rx_backend.c4c276b88gpu/vk/vk_menu.he0cedba7gpu/vk/menu_i18n.h,gpu/vk/vk_api.h,gpu/vk/vk_menu.cpp,host/rx_backend.c,readme.mdc5a0c1d3docs/coop.md,host/oot_coop.c,readme.md,tests/test_oot_coop.py041c9f2bgpu/vk/vk_api.h,host/nn_fs.c,host/rx_backend.c,tests/test_vcore_fs_open.py6ecc6a79gpu/vk/vk_api.hAll 9 unchanged co-op lane heads conflict in wholesale trial merges; rewritten-base add/add blocks inflate counts. Rebase topic ranges preserving ABI, Windows binder, clock, epochs, accepted-session binding, hosting and localization. No helper/lane branch moved.
Questions for you
third_party/acceptable, or would you rather have another library behind the same few functions?coop_name,coop_server,coopthe names you want inhookshot_options.ini?What comes after it
Other players' Links (
coop-players) and the tab's additions (coop-tab-more,coop-tab-where) are separate follow-on lanes.Warp (
coop-warp), hits and duels (coop-hits), and the shared world and area state (coop-world,coop-area) build above them; none of these lanes is in this pull request.Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 [email protected]
Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra [email protected]
This draft now carries the whole stack as six commits (it was the first part only): the relay and wire format, the merge engine, the runtime session, progress sync, text fields in the Insert menu, and the Co-op tab. Head
71d4ab0fon mainfce53453; it merges withef81aee7without a conflict.The description is rewritten for it: what each commit adds and its tests, how to try it on two PCs or on one, the two-game session on this head, what failed in the test runs and why, what is not tested (Windows first of all: I have no Windows PC and would be glad of a tester), and six questions at the end. It stays a draft until the first of them, a socket in the host at all, is answered.
@NotRexed @hckrman101 an update that bears on the questions in this draft's description, and one new question.
Two co-op drafts are becoming one. itzdaface opened #2578 the same evening as this stack: Windows only, a direct link over a VPN, and well ahead of mine on what players see (the other players as animated Links, shared enemies, bosses, PvP, played with real friends). We agreed there to put them together under this pull request: this stack as the link, the session, consent and progress sync, and his bodies, enemies, bosses and PvP on top as his own commits, ported to this stack's sealed messages. He is building this branch on Windows first, which nobody has done yet. Both drafts stay drafts until you have answered question 2 here: whether a socket in the host is acceptable at all.
Stages (question 1 here, now larger). Together this is a lot to review. Would you take it in this order, each stage off by default and reviewable alone?
The relay (question 3 here, now more pressing). His players asked why co-op needs a VPN, and a relay that someone has to run is the same hurdle moved. I am adding "host on this PC": the relay inside the game for a room's maker, so that on one home network there is nothing else to start, and across the internet the maker forwards one UDP port. Beyond that: would the project consider hosting a public relay? It cannot read or change what it forwards (the room's secret never reaches it), and it has limits per member, per network and in all.
@mooki000 Windows results for
71d4ab0f, as promised. Short version: one compile error, a two-line fix, and after that everything I tried works.Setup: Windows 11, Ryzen 7 7700X, RTX 3060 12 GB, 32 GB RAM. Built with
nx2/make_game.py --updateinto its own game folder, using the Python and llvm-mingw (clang 23.1.5) that ship with the v2.1.0 Hookshot Builder. The relay ran under Python 3.14.5 on the same PC.1. Does it build? Not as it is, by one name
host/oot_coop.chas a static function calledbind. On Windowscoop_net.hpulls inwinsock2.h, which declaresbind(SOCKET, ...):It was the only file in the whole build that failed. The fix is a rename:
With that, everything compiles and links, Monocypher and
-lws2_32 -lbcryptincluded. I did not run the test suite, so I have nothing on the two tests you said don't build on the Windows job.2. Does it run? Yes
First start prepared 38,809 shader programs and the pipelines and ended cleanly. My NVIDIA driver faulted on one pipeline during that, which the game skipped. That also happens on this PC with release builds, so I don't think it is yours.
3. Make a room and Join through a relay on 127.0.0.1? Yes, both ways
Two games on one PC, two folders, from the environment (
HOS_COOP_AUTO=createandjoin, a fixedHOS_COOP_ROOM, both headless):the relay answered in 3 ms,room 234… created (member 1), thenSecond (member 2) joined the roomthe relay answered in 2 ms,Kuba (member 1) joined the room,joined room 234… as member 2silent 15 s, as the doc saysBy hand, from the Co-op tab, in a window, with a second headless game waiting in a room:
WM_CHARis fine)room XHM… created (member 1)in the game,createdandjoined as member 1in the relayKuba (member 1) leftand closed the room at oncejoined room 234… as member 2, and the tab listed the other player4. What does the firewall ask? Nothing
No Windows Firewall prompt appeared, and there is no firewall rule for either test exe afterwards. That is with the relay on loopback only, so it says nothing yet about a relay on another machine.
Not tested yet
Two small notes
HOS_HEADLESS) does not close when Windows asks it to (taskkillwithout/f; there is no window to take the request), so I had to force it. That is why the headless runs showsilent 15 sand not a goodbye. From the window, Leave says goodbye properly.os.link) costs no space and worked fine, since the game only reads them. Might be worth a line in the doc.The fix
It is one commit on my branch (
coop-win, on top of your head). If you add me (itzdaface) as a collaborator on your fork I'll push it straight ontocoop-relay-wire, and later the body, enemy and PvP commits the same way. If you'd rather review each one, I'll open pull requests against your branch. Or just take the two lines above; it's faster than either.@itzdaface thank you, this is exactly the report this draft was missing.
The fix goes in as you wrote it (
bindtobind_tables), as a commit with you as co-author, in the next push. I owe you an admission: a Windows-target syntax pass on my side showed this very error earlier today and it was misread as a fault of the check. That pass now runs over every co-op file before a push, and its errors count.The head is about to move, so please wait for it before the next test. The next push replaces
71d4ab0fwith the same six commits on current main (the Bug report row that merged today changes the same menu code as the Co-op tab), and adds:71d4ab0fthe room is offered again, and the save copied again, at every loading zone (the clock I watched to notice "another save was loaded" restarts with every area). Accept and sync work, but you would see the offer come back each time you change area;The helper for messages that must arrive comes a little later: review found two defects in it (a cancelled half-sent event blocked later ones; acknowledgements of two attempts could add up), and it goes in when its tests pass.
Your commits: pull requests against
coop-relay-wireon my fork, please. I merge them without squashing, so they stay yours, and I look at each the same day. I rebase that branch on main now and then and will say so here before I do.Or the other way round, if you prefer: what matters to me is that this base ends up under your players, enemies and PvP, not which pull request carries it. If you would rather keep #2578 as the one, say so, and I open the base as a pull request against your
feat/coopinstead; this draft then stays the place where the base alone can be read and tested.Your two notes: the hard-link tip goes into the doc. The windowless game that ignores a polite close on Windows is not co-op's alone; I will look at a console handler for it separately. And thank you for the firewall check: with the relay inside the game, the maker's game listens on a UDP port, so Windows will ask then; I will write down what it asks once one of us has seen it.
Edited: the change merged today is a Bug report row in the Gameplay tab, not a tab of its own; I had the wrong word.
@mooki000 sounds good on all of it. Pull requests against
coop-relay-wireworks for me, and I'm happy with #2274 staying the one. I'll wait for the new head, rebuild, and test Accept, sync and "Host on this PC" on Windows, including what the firewall asks.WIP: feat(coop): co-op rooms: a relay, progress sync, and a Co-op tab to make, join and acceptto feat(coop): co-op rooms: a relay, progress sync, and a Co-op tab to make, join and accept@itzdaface the new head is up:
2be28f89, 18 commits on mainde866017(it merges cleanly with main as of now). The branch was force-pushed once, as announced; rebase yourcoop-winon it and send your commits as pull requests againstcoop-relay-wire.What is above the six commits you built:
1c60356fyourbindrename, with you as co-author; the macroINin that file is renamed too (it shadowed a Windows header's).184cceb0,1cd02fc4,68088064: the two tests that did not build on the Windows job, and a fixture's thread count.1f1d0a72the loading-zone fix: the room is no longer offered again and the save no longer copied again at every area change (it watched a clock that restarts in each area; it now watches the save's total play time). A real load of another save still asks again.d2fc0f7fthe relay's limits: 480 forwarded copies and 256 KiB a second per member (twice that as burst), as options ofscripts/coop_relay.py.35931fa8,2be28f89messages that must arrive:coop_send_event(member, kind, bytes, n)andcoop_on_event(kind, callback, user). Up to eight queued events of at most 16 KiB each, sent in 1024-byte parts, sealed anew at every resend until acknowledged, delivered whole, once and in order per sender; leaving, a new session or withdrawn consent cancels. An acknowledgement means "delivered to the callback", not "the game accepted it".b8d0cbedcoop_member_accepted(id): whether this player has accepted that member in this session. Being on the roster is not consent.c517e746docs/coop.md, "Adding a layer": kinds, the calls above, pacing, and what a receiver checks.97f56483,dcf84430Host on this PC: withcoop_host=1inhookshot_options.ini(orHOS_COOP_HOST=1), Make a room starts the relay inside the maker's game (UDP 24680 unless the Relay field names another port). Off by default.Tested here: game-free only, with GCC 16.2.1 and Clang 23.1.3: the protocol, session, relay, event, hosted-relay and text-field checks pass with both (I ran those six myself on this head); the loading-zone and the event fixes each fail on the old code. A Windows-target syntax pass over the 17 host files that are built on Windows shows no error and no clash with a Windows header name. Not done on this head: no game has been built or run from it yet, on Linux or Windows; a two-game session here is queued for tonight and I will post what it shows.
What would help most from Windows, in this order: does it build; Make, Join and Leave through
python -I scripts/coop_relay.py --host 127.0.0.1 --port 24680as before; then Accept and progress sync across a loading zone (the offer should come once, not at every door); then the same with the script stopped andcoop_host=1on the maker, and what the firewall asks.The two-game session on the new line, as promised. Built from
2be28f89; the head is now2f412422, which adds two test-only commits for the Windows checks (a fixture helper namedacceptbeside winsock's, and an emptyHOS_COOP_HOST, which a Windows process cannot hold), so the game code is the same.Setup: Arch Linux, RTX 4090 (NVIDIA 615.78.08), one full build (11 min 24 s) and two copies of it, two games on this PC with their own saves, the relay script on loopback, no window. Everything co-op was set in the Co-op tab by scripted input: no
HOS_COOP_AUTO, noHOS_COOP_ACCEPT.What it showed, in order (2026-11-12 02:04:26 to 02:09:14 local, 288 s; both games exited 0):
No fatal line, crash or device loss in either log, and no kernel or driver message during the run. The run's three checklists (the stack's, the tab's and the older session's) pass with no failure.
Not shown by this run: the relay inside the maker's game (a link-only run of it is next on this PC tonight), a loading zone after acceptance (the fix is shown by its game-free test only), two PCs, Windows, a controller, more than two players.
The in-game relay, two games on this PC, same build (
2be28f89), 02:14:39 to 02:16:41 local:coop_host=1on the maker only and no relay script running, Alice's game logshosting on this PC, makes the room, and its own relay admits Bob; each game sees the other in the room.I am repairing the stop step and running that part once more tonight; the result goes here.
The repaired rerun of the in-game relay's last step (same build
2be28f89, 02:33:34 to 02:34:58 local): passes.coop: relay lost; rejoining, thencoop: connecting to the relayand repeated connection attempts. So the joiner notices that the maker and its relay are gone.Not claimed: that Bob can do anything useful after the loss (he keeps trying to reconnect to a relay that is gone; what the tab should say then is a design question for this pull request), acceptance or progress over the in-game relay, a Leave from the tab.
First, a correction to my reply on #2578: one “damage taken so far, highest wins” total loses simultaneous hits from different players. Keep a cumulative component per member/session and enemy incarnation, merge each component by maximum, then sum and clamp to the agreed life cap. Repeated state adds nothing; received damage must not be counted again as a local hit. The pushed “Adding a layer” docs carried the same wrong sentence; it is corrected in
a4c080e0.@itzdaface, the base is now
a4c080e0. The game code is still that of2be28f89: the intervening commits are two test fixes and this documentation correction. The two-game tab result therefore covers the same game code: acceptance, save backups and progress exchange through the separate relay. The in-game relay has shown make/join and noticing the maker's exit, not yet acceptance/progress or tab Leave over that relay.For your port, I'd keep the split we agreed:
coop_send_event/coop_on_eventfor PvP requests and boss hand-over, rather than another retry loop. OnlyCOOP_EVENT_QUEUEDmeans it queued; handle a full queue. An ACK means callback delivery, not accepted damage.Callbacks borrow their bytes on the co-op thread: copy bounded work for the game update, then recheck accepted member/session, signed feature permission, area/incarnation and freshness there. Keep mutual PvP opt-in and your cutscene/recovery guard; let the receiver adjudicate a hit rather than trusting a requested heart subtraction. Retries have no time limit while the same accepted sessions remain, so stale hits must still be rejected.
Your planned Windows Accept/sync/hosting checks remain useful, especially a loading zone after acceptance. The base does not yet contain the body, enemy or PvP implementation; those remain your gameplay commits, through pull requests against
coop-relay-wireas agreed.Three proofs from two games running together, as promised for this draft.
Floor-switch flag SHOWN; Lens of Truth SHOWN; clear-room sharing NOT SHOWN by this method.
Two windowless games on one Linux PC (RTX 4090), separate saves, automatic room acceptance and a local relay; not a Windows or two-PC test.
Floor-switch flag — binaries built from
2be28f89, with identical game code to this PR'sa4c080e0.A walked onto the real switch; B stood still. With progress sharing on, both players' flag changed from 0 to 1.
Extracted sender log:
[hos host] coop: sync: sent progress 6fb4f455 = 1 to all.Extracted receiver log:
[hos Presentation Thread] coop: sync: the game shows progress 6fb4f455 = 1 (its getter agrees).With progress sharing off, the same steps set A's flag to 1 while B stayed at 0; the shared-flag assertion correctly failed.
Nothing visible changes on B's screen for this one, so the evidence is the flag in both games (the log lines above and each game's getter), not a picture. The permanent flag transfers; the live switch and its platform timer are not shared.
Lens of Truth — binaries built from
2be28f89, with identical game code to this PR'sa4c080e0.A received the Lens through a debug aid, not from a chest. B received no local grant before the inventory pictures.
Extracted receiver log:
[hos Presentation Thread] coop: sync: applied items 49d1c374 = 1.Receiver's later getter:
[hos BaseProcExecutor3] cheats: inventory Item_GlassTruth +1 (had 1, now 1, event status 2).Both Equipment pages start with an empty Lens slot and then show the goggles, including B's unselected icon.
With item sharing off, A still gets the goggles but B has neither the count nor the Equipment icon; the same transfer assertion fails.
Negative receiver getter:
[hos BaseProcExecutor4] cheats: inventory Item_GlassTruth +1 (had 0, now 0, event status 2).Getter probes happen after the pictures; their
hadvalue is the prior count, and the negative probe itself then grants the item locally.The Equipment page after the grant, player A left and player B right; sharing on above, off below:
Clear room: NOT SHOWN by this method. The scripted sender never clears its room with sharing on or off, so the response to a real clear was not tested. This is a gap in the test, not a finding about the feature; it needs a working clear route or a player to perform the clear.
All games in these four shown-proof runs exited normally, without logged runtime or driver faults. No performance claim or actual Lens-use claim is made.
A note on direction, so nobody is surprised: in #2892 I have proposed that co-op become the first module on a small mod interface (the same one a randomizer or a restoration mod would use), instead of landing in the core as this draft has it. That delays co-op, and I think it is the better shape for the project. Whether native modules are acceptable at all is the maintainers' call, and it is one of the questions there.
Until that is answered this draft stays the working branch: it is being rebased over today's main, with a fix for the Windows check that fails here (two test modes shared one UDP port; on Windows a wildcard socket and a loopback one can both bind it, so the fix takes the port exclusively).
@itzdaface this touches what we agreed about combining our work: the link, the session and your layers on top would stay as planned, only where they live would change. Your view is welcome in #2892.