WIP: feat(coop): add relay rooms, accepted progress sync and a Co-op tab #68

Draft
mooki wants to merge 29 commits from mooki/Longshot:coop-relay-wire into main
First-time contributor

Why

WIP: this waits for the module/core decision in Longshot issue 42, and for the shared text fields in Longshot pull request 51. The module proposal is open with no replies at this check; no implementing native-module pull request was found in the complete public PR title/body inventory. The text-field parent is still open/WIP.

Friends should be able to play separate games and saves while sharing explicitly accepted progress. This working draft supplies the relay, session, consent and progress layer, not other players' Links, enemies, combat, PvP or warp. It remains a core implementation while the proposal to make co-op the first native module is decided.

What changed

  • Stack: 23 carried co-op commits above the parent's four original text-field patches and its separate Longshot evidence commit; one new test-only integration fix follows. This opens against main because the forge cannot take a branch of a fork as the base: until pull request 51 is merged, its commits appear here too, and only the commits after the parent's head are this pull request's. The refreshed parent is d5b2ff117905fa9ad4a5def759efa52f7a5c1078; do not duplicate its four feature patches. After it merges, replay only this child range onto main.
  • host/coop_proto.c, coop_state.c, coop.c and coop_net.c: sealed UDP room/session traffic, signed progress/rules, acceptance tied to member/session identity, bounded state and shutdown. The guest game's sockets remain offline. Co-op is off by default.
  • host/oot_coop.c: runtime classification and getter checks, explicit acceptance and whole-save backup before applying accepted progress through the game's setters. Ordinary area changes do not masquerade as save loads; real loads require acceptance/backup again.
  • gpu/vk/vk_menu.cpp / host/coop_menu.c: name/relay text fields, masked room secret, Make/Join/Accept/Leave, maker remedies and Room rules. Main's Auto persistence and live/Dev controls remain. API 23 appends coop_view/coop_command after the parent's text API22; no numeric option ID is added.
  • Bounded events: eight queued events, at most16KiB each,1024-byte fragments, freshly sealed retries and whole FIFO callbacks. ACK body v2/27 bytes needs matching peers. An ACK means callback delivery, not gameplay authorization or durable delivery. Callbacks borrow bytes; layers must copy bounded work and recheck session, consent, signed rules, area/incarnation, freshness and receiver authority on the game thread.
  • Optional maker-PC hosting is off by default; no router/firewall automation. The relay still needs a reachable UDP port and can fail behind CGNAT. Windows port exclusivity remains; a secret holder can forge ordinary events/ACKs, while durable progress/rules retain individual signatures.
  • New third_party/monocypher is the original unmodified4.0.2 source with licence/provenance, used under BSD-2-Clause. Kuba's Windows binder fix and all original author/trailer records remain.
  • Migration fix: Auto's source test now recognizes its unchanged saved-key predicate continuing with co-op keys instead of ending at auto_steps; all Auto key assertions remain.

Protocol boundary, not a cosmetic rename: the earlier port deliberately selected Longshot-domain room/message keys, signatures, offers and schema. Its79 vectors were regenerated and the exact nine domains pinned. longshot-coop-sync/1 includes its final ASCII space. These are now fixed wire constants: a future branding change must not rename them. Matching Longshot protocol/game/schema builds can interoperate; Hookshot-domain and Longshot-domain peers cannot, even with the same secret/relay. That is codec/source proof, not a new two-game Longshot session.

Flags and settings

The environment overrides below are deliberately refused from the shareable longshot_env.txt; they require the real process environment. Settings and input are documented in docs/coop.md, linked from the development guide, and the README's menu documentation. Shared progress forces items/songs/quest shared; own progress forces keys own.

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
HOS_COOP New: Enable co-op session Off Off by default: explicit opt-in/test override Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
HOS_COOP_NAME New: Name shown in room Random Player-xxxx Off by default: explicit opt-in/test override Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
HOS_COOP_SERVER New: Relay host/address; default relay port24680 Unset Off by default: explicit opt-in/test override Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
HOS_COOP_PORT New: Local client UDP port 0, free port Off by default: explicit opt-in/test override Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
HOS_COOP_HOST New: Make hosts a relay on this PC; Join never hosts Off Off by default: explicit opt-in/test override Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
HOS_COOP_AUTO New: Test startup create or join Off Off by default: explicit opt-in/test override Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
HOS_COOP_ROOM New: Test room secret for startup or Make Unset Off by default: explicit opt-in/test override Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
HOS_COOP_DROP New: Test receive loss percentage 0 Off by default: explicit opt-in/test override Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
HOS_COOP_TRACE New: Packet/progress diagnostics and relay address Off Off by default: explicit opt-in/test override Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
HOS_COOP_DEMO New: Made-up menu room, no network session Off Off by default: explicit opt-in/test override Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
HOS_COOP_ACCEPT New: Test-only always acceptance Unset; manual consent Off by default: explicit opt-in/test override Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
HOS_COOP_PROBE New: Read-only flag/getter diagnostics Off Off by default: explicit opt-in/test override Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
HOS_COOP_TEST_SET New: Test grants through game routines at selected play frames Unset Off by default: explicit opt-in/test override Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
HOS_COOP_PROGRESS New: Room progress rule Shared Off by default: explicit opt-in/test override Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
HOS_COOP_ITEMS New: Room items rule Shared Off by default: explicit opt-in/test override Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
HOS_COOP_SONGS New: Room songs rule Shared Off by default: explicit opt-in/test override Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
HOS_COOP_QUEST New: Room quest rule Shared Off by default: explicit opt-in/test override Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
HOS_COOP_KEYS New: Room keys rule Pool Off by default: explicit opt-in/test override Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
HOS_COOP_RUPEES New: Room rupees rule Own Off by default: explicit opt-in/test override Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
HOS_COOP_AMMO New: Room ammo rule Own Off by default: explicit opt-in/test override Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
coop New ini key: Enable co-op session Off Off by default: for enable/hosting; rules apply only after consent Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
coop_name New ini key: Name shown in room Random Player-xxxx Off by default: for enable/hosting; rules apply only after consent Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
coop_server New ini key: Relay host/address; default relay port24680 Unset Off by default: for enable/hosting; rules apply only after consent Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
coop_port New ini key: Local client UDP port 0, free port Off by default: for enable/hosting; rules apply only after consent Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
coop_host New ini key: Make hosts a relay on this PC; Join never hosts Off Off by default: for enable/hosting; rules apply only after consent Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
coop_progress New ini key: Room progress rule Shared Off by default: for enable/hosting; rules apply only after consent Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
coop_items New ini key: Room items rule Shared Off by default: for enable/hosting; rules apply only after consent Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
coop_songs New ini key: Room songs rule Shared Off by default: for enable/hosting; rules apply only after consent Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
coop_quest New ini key: Room quest rule Shared Off by default: for enable/hosting; rules apply only after consent Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
coop_keys New ini key: Room keys rule Pool Off by default: for enable/hosting; rules apply only after consent Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
coop_rupees New ini key: Room rupees rule Own Off by default: for enable/hosting; rules apply only after consent Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
coop_ammo New ini key: Room ammo rule Own Off by default: for enable/hosting; rules apply only after consent Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
Co-op / Your name New menu row: Your name through the bounded co-op session/consent API Random Player-xxxx Off by default: player choice Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
Co-op / Relay New menu row: Relay through the bounded co-op session/consent API Empty Off by default: player choice Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
Co-op / Make a room New menu row: Make a room through the bounded co-op session/consent API Inactive until pressed Off by default: player choice Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
Co-op / Join a room New menu row: Join a room through the bounded co-op session/consent API Inactive until pressed Off by default: player choice Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
Co-op / Room secret New menu row: Room secret through the bounded co-op session/consent API Masked Off by default: player choice Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
Co-op / Accept the room New menu row: Accept the room through the bounded co-op session/consent API Not accepted; confirm twice Off by default: player choice Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
Co-op / Leave the room New menu row: Leave the room through the bounded co-op session/consent API Inactive; confirm twice Off by default: player choice Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
Co-op / Remove a player New menu row: Remove a player through the bounded co-op session/consent API Maker only; confirm twice Off by default: player choice Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
Co-op / Reset the pools New menu row: Reset the pools through the bounded co-op session/consent API Maker only; confirm twice Off by default: player choice Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
Co-op / Room rules New menu row: Room rules through the bounded co-op session/consent API Closed Off by default: player choice Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
Co-op / Co-op at start New menu row: Co-op at start through the bounded co-op session/consent API Off Off by default: player choice Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
Room rules / Progress New menu row: Select shared/own or pool/own rule Shared Only in an accepted opt-in room Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
Room rules / Items New menu row: Select shared/own or pool/own rule Shared Only in an accepted opt-in room Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
Room rules / Songs New menu row: Select shared/own or pool/own rule Shared Only in an accepted opt-in room Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
Room rules / Quest New menu row: Select shared/own or pool/own rule Shared Only in an accepted opt-in room Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
Room rules / Keys New menu row: Select shared/own or pool/own rule Pool Only in an accepted opt-in room Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
Room rules / Rupees New menu row: Select shared/own or pool/own rule Own Only in an accepted opt-in room Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
Room rules / Ammo New menu row: Select shared/own or pool/own rule Own Only in an accepted opt-in room Matching consenting co-op peers Solo play Current Longshot game; cross-PC/Windows
coop_relay.py --host New: Standalone Relay listening address 0.0.0.0 Off by default: automatic relay launch; operator-configured Relay operators No game connection by itself Public deployment/cross-PC
coop_relay.py --port New: Standalone Relay UDP port, 0 selects free 24680 Off by default: automatic relay launch; operator-configured Relay operators No game connection by itself Public deployment/cross-PC
coop_relay.py --log-addrs New: Standalone Include peer addresses in logs Off Off by default: automatic relay launch; operator-configured Relay operators No game connection by itself Public deployment/cross-PC
coop_relay.py --max-rooms New: Standalone Bound total rooms 1000 Off by default: automatic relay launch; operator-configured Relay operators No game connection by itself Public deployment/cross-PC
coop_relay.py --rooms-per-network New: Standalone Bound rooms per /24 or /48 8 Off by default: automatic relay launch; operator-configured Relay operators No game connection by itself Public deployment/cross-PC
coop_relay.py --log-joins New: Standalone Log join-budget use Off Off by default: automatic relay launch; operator-configured Relay operators No game connection by itself Public deployment/cross-PC
coop_relay.py --member-copies Changed: Standalone Per-member forwarded-copy ceiling 480/s, burst960 Off by default: automatic relay launch; operator-configured Relay operators No game connection by itself Public deployment/cross-PC
coop_relay.py --member-bytes Changed: Standalone Per-member forwarded-byte ceiling 262144 bytes/s, burst524288 Off by default: automatic relay launch; operator-configured Relay operators No game connection by itself Public deployment/cross-PC
Custom output size Parent change: Type output dimensions; parent text-field change None Off by default: optional control Custom monitor sizes/test input No co-op gameplay by itself Physical input/Windows
output_res Parent change: Persist dimensions rather than dynamic list index Game 2048x1152 Off by default: optional control Custom monitor sizes/test input No co-op gameplay by itself Physical input/Windows
RX_VK_MENU_SCRIPT Adds TYPE and text-edit keys; parent change Unset Off by default: optional control Custom monitor sizes/test input No co-op gameplay by itself Physical input/Windows
co-op tests --cc New test-driver flag: Select compiler/source or emit sealed reference vectors Driver default; --sealed off Off by default: developer checks Fixture developers Not a game feature Native Windows execution
co-op tests --cxx New test-driver flag: Select compiler/source or emit sealed reference vectors Driver default; --sealed off Off by default: developer checks Fixture developers Not a game feature Native Windows execution
co-op tests --source New test-driver flag: Select compiler/source or emit sealed reference vectors Driver default; --sealed off Off by default: developer checks Fixture developers Not a game feature Native Windows execution
co-op tests --sealed New test-driver flag: Select compiler/source or emit sealed reference vectors Driver default; --sealed off Off by default: developer checks Fixture developers Not a game feature Native Windows execution

Measurements

Branch state: on main d725b3631982ed0035ea3ac9cbe1677e9fad260c; head cca2aebb03efac4d4e070469fd0690800931e2db; whole game-free suite on this head: 522 passed, 1 failed, 20 skipped (main d725b3631982ed0035ea3ac9cbe1677e9fad260c: 510 passed, 1 failed, 20 skipped); the commits are unchanged by rebases apart from the results table.

The only failure on both heads is tests/test_updater_asset_digest.py: local .NET NU1100, missing Microsoft.AspNetCore.App.Ref (=10.0.13). The base suite was run once for all moved pull requests of this day; no extra main suite was run for this draft.

main: d725b36319 (2026-11-12)
this PR: coop-relay-wire @ cca2aebb03
Hardware: Core i9-13900K,96GB RAM, Arch Linux7.2.15-1-omarchy; repository-default compiler selection. No GPU used for the new checks.
Scene: authored game-free protocol/session/relay/progress/menu fixtures, not a new game session.
Method: whole suite on the exact refreshed head after the last commit; the main suite is shared across today's moved PRs. Windows-target syntax captures are separate from native execution. No current-game/off-cost measurement or paired speed claim.
How to reproduce: python -I -B tests/run_all.py --jobs 6 --strict; focused protocol checks are python -I -B tests/test_coop_proto.py and python -I -B tests/test_coop_relay.py. A real two-game proof requires matching Longshot builds, separate disposable saves, reachable relay, manual room acceptance and checked backups; it was not run during this move.

main @ d725b3631 this PR @ cca2aebb
Current co-op game run None None; WIP
Windows-target fixture syntax Not a current Windows execution 39 captured units,0 failures,2 explicitly unjudged drivers

The Windows 39-unit result is carried from a6dcac8f (the same patches before the rebase), not a new comparison.

INCOMPLETE: 39 captured translation units, 0 failures, 2 unjudged drivers/files; Windows execution NOT TESTED

The earlier suite on the d4a69456-based co-op head was519/2/20: that error plus Auto's stale terminal-semicolon expectation. The latter was repaired, and the full suite repeated on the exact final head. Main d725b363 differs from d4a69456 only by scripts, docs and tests, not game code; earlier labelled functional evidence is not invalidated, but this does not turn old Hookshot co-op runs into Longshot protocol proof.

All co-op drivers passed in the final whole suite. The old-domain negative test stays literal. The retained port proof reproduced the old vector file byte-for-byte before generating the new family; current vector SHA-256 is ac00e565141b5c85300dd66f694e17b413d5074abc1010be555c7349eaf3506c. The original79-vector/66-relay-test proof is historical port evidence, not an extra fresh game run.

The two unjudged entries are the Python relay's execution and the POSIX-only save-copy driver. Windows runtime-dependent variants were not run. A small Linux empty-environment contract probe passed; it is not Windows networking. The original posted record says GitHub head 9ba4ee007d87112569c47067b8140a7c498d80a9 passed Windows and Ubuntu, every job, in fork run43291054852. This historical success is not a check of the new Longshot domains/integration. These checks have not yet been run on a forge runner.

Historical game evidence and its limits, retained: Hookshot binaries 2be28f89, Linux/RTX4090/NVIDIA615.78.08,2026-11-12, two windowless games on one PC with separate saves.

  • The tab-only session used manual scripted Make/Join/Accept, not automatic consent. Both backups matched their14-file starting saves. Sword/song values crossed; rename caused a new offer and reacceptance; both quit with relay Leave. It did not show a loading zone after acceptance, Windows, a controller, more than two games or acceptance/progress over the in-game relay.
  • The in-game relay showed Make/Join. Its first stop proof failed because the wrappers, not the games, stopped; that result remains failed. The corrected run ended the actual maker and the joiner logged relay loss/reconnecting. It did not show useful recovery after the relay disappeared, acceptance/progress over that relay or tab Leave.
  • Floor-switch flag SHOWN: sharing on gave A/B1/1, off1/0 with the same transfer assertion failing. This is the permanent flag, not the live switch/platform timer; nothing visible changed on B's screen.
  • Lens of Truth SHOWN: A received a debug grant, not a natural chest pickup; B received no local grant before the decisive Equipment pictures. Sharing on gave both count/icon1, off A1/B0. Getter probes followed the pictures; the negative receiver probe then grants locally, and is not synchronization. This proves acquisition/display, not Lens use, hidden-object rendering or shared magic consumption.
  • Clear-room sharing NOT SHOWN by this method: the sender never cleared its room with sharing on or off. Response to a real clear was never tested; this is a proof-route gap, not a product failure. No shared enemy AI/damage/death claim.

Historical log excerpts, not from this Longshot head:

[hos host] coop: sync: sent progress 6fb4f455 = 1 to all
[hos Presentation Thread] coop: sync: the game shows progress 6fb4f455 = 1 (its getter agrees)
[hos Presentation Thread] coop: sync: applied items 49d1c374 = 1
[hos BaseProcExecutor3] cheats: inventory Item_GlassTruth +1 (had 1, now 1, event status 2)
[hos BaseProcExecutor4] cheats: inventory Item_GlassTruth +1 (had 0, now 0, event status 2)

These ordinary correctness runs are not a performance/bandwidth comparison. The earlier71d4ab0f tab panel did not open B's Equipment screen and its item proof was a log, not a visible sword pickup. Kuba's Windows71d4ab0f-plus-bind-fix report showed build/link/run, loopback Make/Join/Leave and typing, but not Accept/progress/LAN/cross-OS/controllers/suite; it also included a shader-start fault seen on releases.

Testing

Tested: On this PC: Arch Linux, RTX 4090, NVIDIA 615.78.08, Core i9-13900K. No GPU was used for the new game-free checks: the current-head suite covered co-op protocol/session/relay/sync/events/menu/save drivers. The carried Windows-target check compiled 39 translation units, with no compilation failures and two unjudged drivers; not run on Windows. Historical Hookshot 2be28f89 runs on this PC used two windowless games with separate saves: manual scripted Make/Join/Accept, checked whole-save backups, item/song exchange, rename/reacceptance and the corrected maker-exit observation. Their floor-switch and debug-granted Lens checks retain the limits stated above; none is a new Longshot game session or proof of the new protocol's two-game interoperability.

Tested but not measured: historical Hookshot menu/progress/hosted-link correctness above; no fresh Longshot gameplay. New source/fixture checks are not gameplay or performance.

Not tested: Current Longshot game build/play, two-game protocol interoperability, off-cost, public relay deployment and the current-runtime cases below. This move stayed source-only while the module/core decision and shared text-field parent remain open. No test request was posted during this move.

The original draft's remaining limitations (the platform/network checks are requested below; the later historical flag/Lens evidence above does not establish a fresh Longshot session):

No current-head game build/play, GPU/two-game/manual-consent/loading-zone/hosted-game proof. No 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.

Needs other testers:

  • Windows: build the host and execute the changed co-op fixtures, expecting passing fixtures and exclusive UDP relay binding (request to open)
  • Separate Windows/Linux PCs on a real LAN or internet path, including NAT: Make/Join/Accept with matching Longshot builds and a reachable relay, expecting progress only after acceptance/backups and relay loss to be reported (request to open)

Screenshots

No current Longshot co-op screenshots: main @ d725b3631 has no co-op, and this PR @ cca2aebb was not run in the game. Both retained pictures below were inspected and are explicitly historical.

Historical Hookshot2be28f89,2026-11-12: Make, manual Accept, rename/new offer/reaccept. Secret masked in every panel; not current Longshot proof.

Historical Hookshot2be28f89,2026-11-12: A left/B right; sharing on above/off below. The receiver's Lens icon is present only in the positive, before its later getter probe; not actual Lens use.

Risk and rollback

Wrong classification or authorization can affect saves. Acceptance, checked whole-folder backup, getters and game setters remain mandatory; being on the roster or receiving an ACK is not consent or completed-backup permission. Network loss/NAT/shared budgets and maker departure can prevent delivery. A restarted maker retaining a key but lacking room rules remains an open design question.

Leave the room or set coop=0/HOS_COOP=0 to disable the session; leave coop_host=0/HOS_COOP_HOST=0 to avoid hosting. Turning off stops further sharing but does not undo already merged progress: recovery uses the pre-room backup, whose manual restoration is not freshly tested. Reverting the child commits removes co-op code but likewise cannot undo an existing save. No firewall/router automation or public relay is enabled by this PR.

Reviewer notes

Start with the protocol/domain boundary, coop_state.c consent/session/backup gates, oot_coop.c classification/setters and event cancellation/ACK epochs. Review shared text fields in parent51, not twice. The module/core choice is unresolved in issue42; no loadable-module interface is implemented here. Bodies/enemies/bosses/PvP from itzdaface remain separate work, not silently included. The original questions about a host UDP socket, stage splitting, relay/library ownership, tab/settings names and maker restart remain open.

Issues

Original draft: https://github.com/NotRexed/Hookshot/issues/2274
Original proposal: https://github.com/NotRexed/Hookshot/issues/2269
Original combined gameplay proposal: https://github.com/NotRexed/Hookshot/issues/2578
Original module direction: https://github.com/NotRexed/Hookshot/issues/2892
The module-direction issue now exists at #42 . The text-field parent is #51 . See #42 for the module direction and See #83 for stopping a windowless game cleanly on Windows, which a tester raised here and which this does not settle. Old GitHub numbers are not Forgejo closing references.

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main. Through the shared text-field parent.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. Current-game/off-cost evidence remains unrun; WIP.
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead). New Monocypher folder only.
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Flags noted, and the switch reference updated (docs/development.md, the README).
  • Screenshots for a change that shows on screen. Historical, labelled; no current-game claim.
  • MKWVK_API_VERSION bumped (gpu/vk/vk_api.h's MkwVkHost or MkwVkRenderer changed).
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. No current game builds; syntax scope above.
  • Docs updated.

Moved from NotRexed/Hookshot#2274.

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]

## Why **WIP: this waits for the module/core decision in [Longshot issue 42](https://git.magicbean.dev/MagicBeanDev/Longshot/issues/42), and for the shared text fields in [Longshot pull request 51](https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/51).** The module proposal is open with no replies at this check; no implementing native-module pull request was found in the complete public PR title/body inventory. The text-field parent is still open/WIP. Friends should be able to play separate games and saves while sharing explicitly accepted progress. This working draft supplies the relay, session, consent and progress layer, not other players' Links, enemies, combat, PvP or warp. It remains a core implementation while the proposal to make co-op the first native module is decided. ## What changed - **Stack:** 23 carried co-op commits above the parent's four original text-field patches and its separate Longshot evidence commit; one new test-only integration fix follows. This opens against `main` because the forge cannot take a branch of a fork as the base: until pull request 51 is merged, its commits appear here too, and only the commits after the parent's head are this pull request's. The refreshed parent is `d5b2ff117905fa9ad4a5def759efa52f7a5c1078`; do not duplicate its four feature patches. After it merges, replay only this child range onto main. - `host/coop_proto.c`, `coop_state.c`, `coop.c` and `coop_net.c`: sealed UDP room/session traffic, signed progress/rules, acceptance tied to member/session identity, bounded state and shutdown. The guest game's sockets remain offline. Co-op is off by default. - `host/oot_coop.c`: runtime classification and getter checks, explicit acceptance and whole-save backup before applying accepted progress through the game's setters. Ordinary area changes do not masquerade as save loads; real loads require acceptance/backup again. - `gpu/vk/vk_menu.cpp` / `host/coop_menu.c`: name/relay text fields, masked room secret, Make/Join/Accept/Leave, maker remedies and Room rules. Main's Auto persistence and live/Dev controls remain. API **23** appends `coop_view`/`coop_command` after the parent's text API22; no numeric option ID is added. - Bounded events: eight queued events, at most16KiB each,1024-byte fragments, freshly sealed retries and whole FIFO callbacks. ACK body v2/27 bytes needs matching peers. An ACK means callback delivery, **not gameplay authorization or durable delivery**. Callbacks borrow bytes; layers must copy bounded work and recheck session, consent, signed rules, area/incarnation, freshness and receiver authority on the game thread. - Optional maker-PC hosting is off by default; no router/firewall automation. The relay still needs a reachable UDP port and can fail behind CGNAT. Windows port exclusivity remains; a secret holder can forge ordinary events/ACKs, while durable progress/rules retain individual signatures. - New `third_party/monocypher` is the original unmodified4.0.2 source with licence/provenance, used under BSD-2-Clause. Kuba's Windows binder fix and all original author/trailer records remain. - Migration fix: Auto's source test now recognizes its unchanged saved-key predicate continuing with co-op keys instead of ending at `auto_steps`; all Auto key assertions remain. **Protocol boundary, not a cosmetic rename:** the earlier port deliberately selected Longshot-domain room/message keys, signatures, offers and schema. Its79 vectors were regenerated and the exact nine domains pinned. `longshot-coop-sync/1 ` includes its final ASCII space. These are now fixed wire constants: a future branding change must not rename them. Matching Longshot protocol/game/schema builds can interoperate; **Hookshot-domain and Longshot-domain peers cannot**, even with the same secret/relay. That is codec/source proof, not a new two-game Longshot session. ## Flags and settings The environment overrides below are deliberately refused from the shareable `longshot_env.txt`; they require the real process environment. Settings and input are documented in `docs/coop.md`, linked from the development guide, and the README's menu documentation. Shared progress forces items/songs/quest shared; own progress forces keys own. | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | `HOS_COOP` | New: Enable co-op session | Off | Off by default: explicit opt-in/test override | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `HOS_COOP_NAME` | New: Name shown in room | Random Player-xxxx | Off by default: explicit opt-in/test override | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `HOS_COOP_SERVER` | New: Relay host/address; default relay port24680 | Unset | Off by default: explicit opt-in/test override | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `HOS_COOP_PORT` | New: Local client UDP port | 0, free port | Off by default: explicit opt-in/test override | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `HOS_COOP_HOST` | New: Make hosts a relay on this PC; Join never hosts | Off | Off by default: explicit opt-in/test override | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `HOS_COOP_AUTO` | New: Test startup create or join | Off | Off by default: explicit opt-in/test override | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `HOS_COOP_ROOM` | New: Test room secret for startup or Make | Unset | Off by default: explicit opt-in/test override | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `HOS_COOP_DROP` | New: Test receive loss percentage | 0 | Off by default: explicit opt-in/test override | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `HOS_COOP_TRACE` | New: Packet/progress diagnostics and relay address | Off | Off by default: explicit opt-in/test override | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `HOS_COOP_DEMO` | New: Made-up menu room, no network session | Off | Off by default: explicit opt-in/test override | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `HOS_COOP_ACCEPT` | New: Test-only always acceptance | Unset; manual consent | Off by default: explicit opt-in/test override | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `HOS_COOP_PROBE` | New: Read-only flag/getter diagnostics | Off | Off by default: explicit opt-in/test override | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `HOS_COOP_TEST_SET` | New: Test grants through game routines at selected play frames | Unset | Off by default: explicit opt-in/test override | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `HOS_COOP_PROGRESS` | New: Room progress rule | Shared | Off by default: explicit opt-in/test override | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `HOS_COOP_ITEMS` | New: Room items rule | Shared | Off by default: explicit opt-in/test override | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `HOS_COOP_SONGS` | New: Room songs rule | Shared | Off by default: explicit opt-in/test override | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `HOS_COOP_QUEST` | New: Room quest rule | Shared | Off by default: explicit opt-in/test override | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `HOS_COOP_KEYS` | New: Room keys rule | Pool | Off by default: explicit opt-in/test override | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `HOS_COOP_RUPEES` | New: Room rupees rule | Own | Off by default: explicit opt-in/test override | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `HOS_COOP_AMMO` | New: Room ammo rule | Own | Off by default: explicit opt-in/test override | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `coop` | New ini key: Enable co-op session | Off | Off by default: for enable/hosting; rules apply only after consent | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `coop_name` | New ini key: Name shown in room | Random Player-xxxx | Off by default: for enable/hosting; rules apply only after consent | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `coop_server` | New ini key: Relay host/address; default relay port24680 | Unset | Off by default: for enable/hosting; rules apply only after consent | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `coop_port` | New ini key: Local client UDP port | 0, free port | Off by default: for enable/hosting; rules apply only after consent | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `coop_host` | New ini key: Make hosts a relay on this PC; Join never hosts | Off | Off by default: for enable/hosting; rules apply only after consent | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `coop_progress` | New ini key: Room progress rule | Shared | Off by default: for enable/hosting; rules apply only after consent | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `coop_items` | New ini key: Room items rule | Shared | Off by default: for enable/hosting; rules apply only after consent | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `coop_songs` | New ini key: Room songs rule | Shared | Off by default: for enable/hosting; rules apply only after consent | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `coop_quest` | New ini key: Room quest rule | Shared | Off by default: for enable/hosting; rules apply only after consent | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `coop_keys` | New ini key: Room keys rule | Pool | Off by default: for enable/hosting; rules apply only after consent | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `coop_rupees` | New ini key: Room rupees rule | Own | Off by default: for enable/hosting; rules apply only after consent | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `coop_ammo` | New ini key: Room ammo rule | Own | Off by default: for enable/hosting; rules apply only after consent | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `Co-op / Your name` | New menu row: Your name through the bounded co-op session/consent API | Random Player-xxxx | Off by default: player choice | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `Co-op / Relay` | New menu row: Relay through the bounded co-op session/consent API | Empty | Off by default: player choice | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `Co-op / Make a room` | New menu row: Make a room through the bounded co-op session/consent API | Inactive until pressed | Off by default: player choice | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `Co-op / Join a room` | New menu row: Join a room through the bounded co-op session/consent API | Inactive until pressed | Off by default: player choice | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `Co-op / Room secret` | New menu row: Room secret through the bounded co-op session/consent API | Masked | Off by default: player choice | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `Co-op / Accept the room` | New menu row: Accept the room through the bounded co-op session/consent API | Not accepted; confirm twice | Off by default: player choice | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `Co-op / Leave the room` | New menu row: Leave the room through the bounded co-op session/consent API | Inactive; confirm twice | Off by default: player choice | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `Co-op / Remove a player` | New menu row: Remove a player through the bounded co-op session/consent API | Maker only; confirm twice | Off by default: player choice | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `Co-op / Reset the pools` | New menu row: Reset the pools through the bounded co-op session/consent API | Maker only; confirm twice | Off by default: player choice | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `Co-op / Room rules` | New menu row: Room rules through the bounded co-op session/consent API | Closed | Off by default: player choice | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `Co-op / Co-op at start` | New menu row: Co-op at start through the bounded co-op session/consent API | Off | Off by default: player choice | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `Room rules / Progress` | New menu row: Select shared/own or pool/own rule | Shared | Only in an accepted opt-in room | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `Room rules / Items` | New menu row: Select shared/own or pool/own rule | Shared | Only in an accepted opt-in room | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `Room rules / Songs` | New menu row: Select shared/own or pool/own rule | Shared | Only in an accepted opt-in room | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `Room rules / Quest` | New menu row: Select shared/own or pool/own rule | Shared | Only in an accepted opt-in room | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `Room rules / Keys` | New menu row: Select shared/own or pool/own rule | Pool | Only in an accepted opt-in room | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `Room rules / Rupees` | New menu row: Select shared/own or pool/own rule | Own | Only in an accepted opt-in room | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `Room rules / Ammo` | New menu row: Select shared/own or pool/own rule | Own | Only in an accepted opt-in room | Matching consenting co-op peers | Solo play | Current Longshot game; cross-PC/Windows | | `coop_relay.py --host` | New: Standalone Relay listening address | 0.0.0.0 | Off by default: automatic relay launch; operator-configured | Relay operators | No game connection by itself | Public deployment/cross-PC | | `coop_relay.py --port` | New: Standalone Relay UDP port, 0 selects free | 24680 | Off by default: automatic relay launch; operator-configured | Relay operators | No game connection by itself | Public deployment/cross-PC | | `coop_relay.py --log-addrs` | New: Standalone Include peer addresses in logs | Off | Off by default: automatic relay launch; operator-configured | Relay operators | No game connection by itself | Public deployment/cross-PC | | `coop_relay.py --max-rooms` | New: Standalone Bound total rooms | 1000 | Off by default: automatic relay launch; operator-configured | Relay operators | No game connection by itself | Public deployment/cross-PC | | `coop_relay.py --rooms-per-network` | New: Standalone Bound rooms per /24 or /48 | 8 | Off by default: automatic relay launch; operator-configured | Relay operators | No game connection by itself | Public deployment/cross-PC | | `coop_relay.py --log-joins` | New: Standalone Log join-budget use | Off | Off by default: automatic relay launch; operator-configured | Relay operators | No game connection by itself | Public deployment/cross-PC | | `coop_relay.py --member-copies` | Changed: Standalone Per-member forwarded-copy ceiling | 480/s, burst960 | Off by default: automatic relay launch; operator-configured | Relay operators | No game connection by itself | Public deployment/cross-PC | | `coop_relay.py --member-bytes` | Changed: Standalone Per-member forwarded-byte ceiling | 262144 bytes/s, burst524288 | Off by default: automatic relay launch; operator-configured | Relay operators | No game connection by itself | Public deployment/cross-PC | | `Custom output size` | Parent change: Type output dimensions; parent text-field change | None | Off by default: optional control | Custom monitor sizes/test input | No co-op gameplay by itself | Physical input/Windows | | `output_res` | Parent change: Persist dimensions rather than dynamic list index | Game 2048x1152 | Off by default: optional control | Custom monitor sizes/test input | No co-op gameplay by itself | Physical input/Windows | | `RX_VK_MENU_SCRIPT` | Adds TYPE and text-edit keys; parent change | Unset | Off by default: optional control | Custom monitor sizes/test input | No co-op gameplay by itself | Physical input/Windows | | `co-op tests --cc` | New test-driver flag: Select compiler/source or emit sealed reference vectors | Driver default; --sealed off | Off by default: developer checks | Fixture developers | Not a game feature | Native Windows execution | | `co-op tests --cxx` | New test-driver flag: Select compiler/source or emit sealed reference vectors | Driver default; --sealed off | Off by default: developer checks | Fixture developers | Not a game feature | Native Windows execution | | `co-op tests --source` | New test-driver flag: Select compiler/source or emit sealed reference vectors | Driver default; --sealed off | Off by default: developer checks | Fixture developers | Not a game feature | Native Windows execution | | `co-op tests --sealed` | New test-driver flag: Select compiler/source or emit sealed reference vectors | Driver default; --sealed off | Off by default: developer checks | Fixture developers | Not a game feature | Native Windows execution | ## Measurements <!-- branch-state --> Branch state: on main `d725b3631982ed0035ea3ac9cbe1677e9fad260c`; head `cca2aebb03efac4d4e070469fd0690800931e2db`; whole game-free suite on this head: `522 passed, 1 failed, 20 skipped` (main `d725b3631982ed0035ea3ac9cbe1677e9fad260c`: `510 passed, 1 failed, 20 skipped`); the commits are unchanged by rebases apart from the results table. <!-- /branch-state --> The only failure on both heads is `tests/test_updater_asset_digest.py`: local .NET `NU1100`, missing `Microsoft.AspNetCore.App.Ref (=10.0.13)`. The base suite was run once for all moved pull requests of this day; no extra main suite was run for this draft. main: d725b3631982ed0035ea3ac9cbe1677e9fad260c (2026-11-12) this PR: coop-relay-wire @ cca2aebb03efac4d4e070469fd0690800931e2db Hardware: Core i9-13900K,96GB RAM, Arch Linux7.2.15-1-omarchy; repository-default compiler selection. No GPU used for the new checks. Scene: authored game-free protocol/session/relay/progress/menu fixtures, not a new game session. Method: whole suite on the exact refreshed head after the last commit; the main suite is shared across today's moved PRs. Windows-target syntax captures are separate from native execution. No current-game/off-cost measurement or paired speed claim. How to reproduce: `python -I -B tests/run_all.py --jobs 6 --strict`; focused protocol checks are `python -I -B tests/test_coop_proto.py` and `python -I -B tests/test_coop_relay.py`. A real two-game proof requires matching Longshot builds, separate disposable saves, reachable relay, manual room acceptance and checked backups; it was not run during this move. | | main @ d725b3631 | this PR @ cca2aebb | |---|---|---| | Current co-op game run | None | None; WIP | | Windows-target fixture syntax | Not a current Windows execution | 39 captured units,0 failures,2 explicitly unjudged drivers | The Windows 39-unit result is carried from `a6dcac8f` (the same patches before the rebase), not a new comparison. ```text INCOMPLETE: 39 captured translation units, 0 failures, 2 unjudged drivers/files; Windows execution NOT TESTED ``` The earlier suite on the d4a69456-based co-op head was519/2/20: that error plus Auto's stale terminal-semicolon expectation. The latter was repaired, and the full suite repeated on the exact final head. Main `d725b363` differs from `d4a69456` only by scripts, docs and tests, not game code; earlier labelled functional evidence is not invalidated, but this does not turn old Hookshot co-op runs into Longshot protocol proof. All co-op drivers passed in the final whole suite. The old-domain negative test stays literal. The retained port proof reproduced the old vector file byte-for-byte before generating the new family; current vector SHA-256 is `ac00e565141b5c85300dd66f694e17b413d5074abc1010be555c7349eaf3506c`. The original79-vector/66-relay-test proof is historical port evidence, not an extra fresh game run. The two unjudged entries are the Python relay's execution and the POSIX-only save-copy driver. Windows runtime-dependent variants were not run. A small Linux empty-environment contract probe passed; it is not Windows networking. The original posted record says GitHub head `9ba4ee007d87112569c47067b8140a7c498d80a9` passed Windows and Ubuntu, every job, in [fork run43291054852](https://github.com/mooki000/Hookshot/actions/runs/43291054852). This historical success is not a check of the new Longshot domains/integration. These checks have not yet been run on a forge runner. **Historical game evidence and its limits, retained:** Hookshot binaries `2be28f89`, Linux/RTX4090/NVIDIA615.78.08,2026-11-12, two windowless games on one PC with separate saves. - The tab-only session used manual scripted Make/Join/Accept, not automatic consent. Both backups matched their14-file starting saves. Sword/song values crossed; rename caused a new offer and reacceptance; both quit with relay Leave. It did not show a loading zone after acceptance, Windows, a controller, more than two games or acceptance/progress over the in-game relay. - The in-game relay showed Make/Join. Its first stop proof failed because the wrappers, not the games, stopped; that result remains failed. The corrected run ended the actual maker and the joiner logged relay loss/reconnecting. It did not show useful recovery after the relay disappeared, acceptance/progress over that relay or tab Leave. - **Floor-switch flag SHOWN:** sharing on gave A/B1/1, off1/0 with the same transfer assertion failing. This is the permanent flag, **not** the live switch/platform timer; nothing visible changed on B's screen. - **Lens of Truth SHOWN:** A received a debug grant, not a natural chest pickup; B received no local grant before the decisive Equipment pictures. Sharing on gave both count/icon1, off A1/B0. Getter probes followed the pictures; the negative receiver probe then grants locally, and is not synchronization. This proves acquisition/display, not Lens use, hidden-object rendering or shared magic consumption. - **Clear-room sharing NOT SHOWN by this method:** the sender never cleared its room with sharing on or off. Response to a real clear was never tested; this is a proof-route gap, not a product failure. No shared enemy AI/damage/death claim. Historical log excerpts, not from this Longshot head: ```text [hos host] coop: sync: sent progress 6fb4f455 = 1 to all [hos Presentation Thread] coop: sync: the game shows progress 6fb4f455 = 1 (its getter agrees) [hos Presentation Thread] coop: sync: applied items 49d1c374 = 1 [hos BaseProcExecutor3] cheats: inventory Item_GlassTruth +1 (had 1, now 1, event status 2) [hos BaseProcExecutor4] cheats: inventory Item_GlassTruth +1 (had 0, now 0, event status 2) ``` These ordinary correctness runs are not a performance/bandwidth comparison. The earlier71d4ab0f tab panel did not open B's Equipment screen and its item proof was a log, not a visible sword pickup. Kuba's Windows71d4ab0f-plus-bind-fix report showed build/link/run, loopback Make/Join/Leave and typing, but not Accept/progress/LAN/cross-OS/controllers/suite; it also included a shader-start fault seen on releases. ## Testing **Tested:** On this PC: Arch Linux, RTX 4090, NVIDIA 615.78.08, Core i9-13900K. No GPU was used for the new game-free checks: the current-head suite covered co-op protocol/session/relay/sync/events/menu/save drivers. The carried Windows-target check compiled 39 translation units, with no compilation failures and two unjudged drivers; not run on Windows. Historical Hookshot `2be28f89` runs on this PC used two windowless games with separate saves: manual scripted Make/Join/Accept, checked whole-save backups, item/song exchange, rename/reacceptance and the corrected maker-exit observation. Their floor-switch and debug-granted Lens checks retain the limits stated above; none is a new Longshot game session or proof of the new protocol's two-game interoperability. Tested but not measured: historical Hookshot menu/progress/hosted-link correctness above; no fresh Longshot gameplay. New source/fixture checks are not gameplay or performance. **Not tested:** Current Longshot game build/play, two-game protocol interoperability, off-cost, public relay deployment and the current-runtime cases below. This move stayed source-only while the module/core decision and shared text-field parent remain open. No test request was posted during this move. The original draft's remaining limitations (the platform/network checks are requested below; the later historical flag/Lens evidence above does not establish a fresh Longshot session): No current-head game build/play, GPU/two-game/manual-consent/loading-zone/hosted-game proof. No 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. **Needs other testers:** - Windows: build the host and execute the changed co-op fixtures, expecting passing fixtures and exclusive UDP relay binding (request to open) - Separate Windows/Linux PCs on a real LAN or internet path, including NAT: Make/Join/Accept with matching Longshot builds and a reachable relay, expecting progress only after acceptance/backups and relay loss to be reported (request to open) ## Screenshots No current Longshot co-op screenshots: main @ d725b3631 has no co-op, and this PR @ cca2aebb was not run in the game. Both retained pictures below were inspected and are explicitly historical. ![Historical Hookshot2be28f89,2026-11-12: Make, manual Accept, rename/new offer/reaccept. Secret masked in every panel; not current Longshot proof.](https://git.magicbean.dev/attachments/2b3f5da6-0263-4b3c-93e1-56c7d8d3e95c) ![Historical Hookshot2be28f89,2026-11-12: A left/B right; sharing on above/off below. The receiver's Lens icon is present only in the positive, before its later getter probe; not actual Lens use.](https://git.magicbean.dev/attachments/c5977869-0de6-472e-b90f-69368a73e54b) ## Risk and rollback Wrong classification or authorization can affect saves. Acceptance, checked whole-folder backup, getters and game setters remain mandatory; being on the roster or receiving an ACK is not consent or completed-backup permission. Network loss/NAT/shared budgets and maker departure can prevent delivery. A restarted maker retaining a key but lacking room rules remains an open design question. Leave the room or set `coop=0`/`HOS_COOP=0` to disable the session; leave `coop_host=0`/`HOS_COOP_HOST=0` to avoid hosting. Turning off stops further sharing but does **not undo** already merged progress: recovery uses the pre-room backup, whose manual restoration is not freshly tested. Reverting the child commits removes co-op code but likewise cannot undo an existing save. No firewall/router automation or public relay is enabled by this PR. ## Reviewer notes Start with the protocol/domain boundary, `coop_state.c` consent/session/backup gates, `oot_coop.c` classification/setters and event cancellation/ACK epochs. Review shared text fields in parent51, not twice. The module/core choice is unresolved in issue42; no loadable-module interface is implemented here. Bodies/enemies/bosses/PvP from itzdaface remain separate work, not silently included. The original questions about a host UDP socket, stage splitting, relay/library ownership, tab/settings names and maker restart remain open. ## Issues Original draft: https://github.com/NotRexed/Hookshot/issues/2274 Original proposal: https://github.com/NotRexed/Hookshot/issues/2269 Original combined gameplay proposal: https://github.com/NotRexed/Hookshot/issues/2578 Original module direction: https://github.com/NotRexed/Hookshot/issues/2892 The module-direction issue now exists at https://git.magicbean.dev/MagicBeanDev/Longshot/issues/42 . The text-field parent is https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/51 . See #42 for the module direction and See #83 for stopping a windowless game cleanly on Windows, which a tester raised here and which this does not settle. Old GitHub numbers are not Forgejo closing references. ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. Through the shared text-field parent. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [ ] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. Current-game/off-cost evidence remains unrun; WIP. - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). New Monocypher folder only. - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### If applicable - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] Flags noted, and the switch reference updated (docs/development.md, the README). - [x] Screenshots for a change that shows on screen. Historical, labelled; no current-game claim. - [x] `MKWVK_API_VERSION` bumped (`gpu/vk/vk_api.h`'s `MkwVkHost` or `MkwVkRenderer` changed). - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [ ] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. No current game builds; syntax scope above. - [x] Docs updated. --- Moved from NotRexed/Hookshot#2274. 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]>
The Insert menu could not take typed text: a setting held as text, such as an
output size of the player's own, could only be set by editing
longshot_options.ini.

A text field is a row whose value is typed. Characters come from the window
(WM_CHAR on Windows, SDL's text input on Linux, turned on only while a field
has the keyboard) through a new MkwVkHost member, take_text, appended last
(MKWVK_API_VERSION 20, from 18; 19 is claimed by an open pull request). Left,
Right, Home, End, Backspace and Delete edit,
Ctrl+V and Shift+Insert paste the clipboard's first line, Enter sets the
text, Esc puts it back; a controller's confirm and back are Enter and Esc,
its D-pad moves the caret, a free face button pastes. A field holds printable
ASCII, as the menu's glyphs are baked; other characters are left out. While a
field has the keyboard no key does anything else, and the game gets none, as
for the whole menu. RX_VK_MENU_SCRIPT gains TYPE:<text> and the editing keys.

The first field is Custom output size on the Graphics tab: the Output
resolution of the player's own (output_res=), typed and checked by the same
rules, with the reason shown when a size is refused. As the row itself, it
leaves the Quality preset alone, and a size typed is the player's own from
then on, also in a run whose HOS_OUTPUT_RES holds the row (env_release).

Main's prompts_live (version 17) stays before take_text in both the interface
and the host table. tests/test_menu_text_field.py compiles the editor and the
host's queue cut from the source, and checks the wiring in the source: the
interface's take_text in its place in the host's table, WM_CHAR and SDL's text
input, the menu telling the host whether a field has the keyboard, TYPE.

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]>
Windows clang with Microsoft headers rejected getenv from hos_wide_size.h and sscanf from the extracted output parser as deprecated under -Werror. Put _CRT_SECURE_NO_WARNINGS on the first line of both generated C and C++ sources, matching the existing capture fixture convention. The screensaver fixture also needs the guard: its injected window procedure calls getenv twice. Runtime code, warning flags and every behavioral check stay unchanged.

Linux g++ and clang++ execute all three text-field checks; GCC and Clang pass all 18 mocked-window-procedure cases. Native LLVM-MinGW 23.1.5 Windows-target syntax with -Wall -Wextra -Werror -Wdeprecated-declarations and Microsoft-style getenv/sscanf annotations fails before and passes after. The three generated co-op tab fixtures pass that audit unchanged; restart-slot checks generate no C or C++. This is not a Windows runtime test. Refs #2594.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Main took version 20 for screen_size and version 21 for nvn_program_created while this rebase was being prepared. Append take_text after screen_size at version 22 and keep its fixture version check in step. Preserve both merged callbacks and their history comments. Reuse the SDL2 SDL_free lookup now supplied by the merged controller diagnostics instead of loading the same callback twice.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Record the two windowless runs built at 4914fdec on main 8cf9a524. The native build reused a completed lift only after both actual source-tree lift keys matched the recorded donor key. Keep the tested commit named explicitly: the later clean rebase onto main 15d4ae8c is checked game-free, not represented as another game run.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
docs(menu): record Longshot text-field proof and noisy closed-menu pairs
Some checks are pending
PR description / Description follows the template (pull_request_target) Waiting to run
d5b2ff1179
Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Co-op needs a way for the games of several players to find each other and talk without anyone on the path, the relay
included, reading or forging what they send. This is the game-free first part: the relay and the wire format both
ends speak. The game does not use it yet.

- scripts/coop_relay.py: a UDP relay, standard library only. Rooms of up to 8 players named by a locator derived from
  a secret the relay never sees; a cookie challenge before it answers anything larger than it got; rejoin tokens;
  limits per member, per network, per room and in all.
- host/coop_proto.c/.h: the same format in C: room secrets (18 characters, 85 bits), keys derived with keyed BLAKE2b,
  datagram tags (keyed BLAKE2b-64), the client and relay messages, and the players' messages sealed with
  XChaCha20-Poly1305; EdDSA for what only one player may make; the POS message for showing other players later.
- third_party/monocypher: Monocypher 4.0.2, unmodified (BSD-2-Clause / CC0), compiled with the runtime by
  nx2/build.py and shipped in the AppImage's sources.
- docs/coop.md: running a relay, rooms and secrets, the wire.

Tests: tests/test_coop_relay.py (60 cases: the codec, the relay's rooms and limits on a fake clock, hostile clients,
real UDP); tests/test_coop_proto.py (the C codec against every vector of tests/coop_vectors.txt, which the Python is
checked against too, known answers, sealing, signatures, 400000 fuzzed datagrams under ASan/UBSan, the POS counter).

Carried from the author's own co-op for another port of this game (files he wrote; nothing of that port's code), with
the domain strings renamed and the datagram tag changed from HMAC-SHA256 to keyed BLAKE2b, so that Monocypher is the
only cryptography.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Progress sync needs one rule for how the players' states combine that holds under lost, repeated and reordered
messages and against a player who sends what they should not. This is that engine, game-free; the game does not use
it yet.

- host/coop_state.c/.h: flags as (category, 32-bit hash, value) registers merged by keeping the highest value; small
  keys, rupees and ammunition as signed per-player records in shared pools; digests and sections between the players;
  identities, acceptance by key, the room maker's signed charter (rules, admissions, removals, pool resets). Every
  value is checked against the game's own table before it is stored, passed on or applied.
- tests/coop_sync_test.c with tests/test_coop_sync.py: the engine between simulated games on a simulated network
  (merging in any order, 30 % loss, late joiners, rules, pools, carry-over between rooms and saves, hostile members and
  an attacker with the room's secret, a randomised run against a ledger), under ASan (leaks too) and UBSan on Linux.
  The sanitizers found the engine's apply queue never freed; coop_sync_free frees it now.

Carried from the author's own co-op for another port of this game (files he wrote, no code of that port), with the
signing roles renamed to hookshot-coop and the test's old-engine shim removed.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-op needs the game itself to reach a relay and stay in a room: this adds the
session layer, one UDP socket on one host thread, off unless switched on, that
the menu's Co-op tab and progress sync build on. It never touches game memory.

- host/coop.c, host/coop.h: the session (the relay's cookie exchange, CONNECT
  bursts with backoff, keep-alive PINGs, rejoin after the relay goes silent or
  restarts, the roster, sealed player messages with replay windows, removal by
  the room's maker, BYE and LEAVE when leaving) and the process's session:
  longshot_options.ini's coop, coop_name, coop_server and coop_port, with
  HOS_COOP, HOS_COOP_NAME, HOS_COOP_SERVER and HOS_COOP_PORT over them;
  HOS_COOP_AUTO=create|join with HOS_COOP_ROOM, HOS_COOP_DROP and
  HOS_COOP_TRACE for tests (HOS_COOP and HOS_COOP_TRACE are on/off switches by
  env_on's rule: =0 is off); coop_configure for the menu. The thread exists
  only while co-op is on: switching it off, or the game closing, asks it to
  end and waits for it at most 0.4 s, with the socket closed; switching it on
  starts it again. The room's secret and the relay's address stay out of the
  log. A sender that has used up its 32-bit message counter leaves the room
  rather than reuse a nonce; a new join draws a new session.
- host/coop_net.c, host/coop_net.h: the operating system's part: a dual-stack
  UDP socket, the name lookup, random bytes (BCryptGenRandom, getrandom), a
  lock, an event, a once and a thread that is waited for, on Winsock and on
  BSD sockets.
- host/hos_main.c: coop_init() beside hos_discord_start() on Windows and
  Linux; no name that begins HOS_COOP is taken from longshot_env.txt, a file
  players pass around.
- host/hos_relaunch.c, host/control.c: BYE and LEAVE when the window closes,
  and when the control bridge's quit ends the game. Co-op installs no
  signal handler: a game that is killed says nothing, and the relay drops it
  after 15 s of silence (docs/coop.md says what the others see).
- host/nn_applet.c: hos_ini_text, the options file's text values; its line
  buffer is 256 bytes, for a relay address of up to 159.
- nx2/build.py: -lws2_32 -lbcrypt in the Windows host libraries.
- tests/coop_session_test.c, tests/test_coop_session.py: the sockets on
  loopback, coop_server_parse, co-op off (no thread, no socket), the menu
  switching it on and off (off, on, off cycles with a socket open: no thread
  and no socket left), and two sessions in one process through a hostile
  relay of the test's own and through scripts/coop_relay.py (restarted
  midway), at 0 % and 30 % loss, with the process's session and, on Linux, a
  child ended by SIGTERM that the relay drops for its silence; ASan and UBSan
  on Linux, the two unit runs also under ThreadSanitizer; send_sealed cut
  from the source and run with the real sealing at the counter's end; and the
  wiring in the source (the start beside the Discord status, both ways out,
  the Windows libraries, hos_ini_text).
- tests/test_coop_env_file.py: longshot_env.txt sets no HOS_COOP name.
- tests/test_coop_options_text.py: a long coop_server= line is read whole.
- tests/display_startup_failure_test.cpp: an inert coop_init beside the
  inert hos_discord_start (its fixture compiles main()).
- docs/coop.md: the settings, the log lines, a game that ends without a
  goodbye, the tests; docs/development.md: the options file's co-op keys.

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]>
Players in a co-op room should share a chest, a boss, a song or an item one of them gets, in their own games with their
own saves. This is the game side of that, on the merge engine of the commit before: the game's flags read between frames,
the room's values given to the game through its own set routines, and the save copied before anything of a room reaches
it.

- host/oot_coop.c/.h: at the end of each present (host/nvn_native.c, one call, on the presenting game thread with its
  core held), the game's flag database (GameDataMgr) read in quarters and its shared flags classified at run time from
  each entry's reset kinds and save file and the game's own groups (item counts, first-get markers, dungeon keys,
  capacities), with a few dozen hashes for what the groups cannot tell; no list of the game's flags in the source. The
  table is checked against the game's own getter before anything is shared. Values are applied through SetBool and
  SetInt (never a write of the table), only raised, within the game's own table, and only after the player accepted the
  room and its save was copied. Nothing is read or set on the title screen, while a save loads, a scene changes or the
  play clock stands. With co-op off the call reads one value and returns. Sync is on whenever co-op is: from the start
  with HOS_COOP=1 or coop=1 in the options file, or from the session's first callback when co-op is switched on later
  (the session's hooks are registered either way). The offer, acceptance and the maker's remedies for the Co-op tab:
  hos_oot_coop_offer/accept/is_maker/reset_pools/remove_key/contributors. HOS_COOP, HOS_COOP_PROBE and
  HOS_COOP_TRACE are on/off switches by env_on's rule (=0 is off).
- host/nn_fs.c: hos_save_copy, the save folder copied to <user>/coop-backup/ (first-<time> kept, the newest eight
  others), under a lock that the save mount's changes take, only when no save file is open for writing and nothing
  changed for half a second; a save waits for a copy under way. The save folder's path is kept once, by the
  save mounts under that lock, and the control bridge's hos_fs_save_root reads it there.
- tests/test_env_file.py: HOS_COOP_ACCEPT, HOS_COOP_TEST_SET and HOS_COOP_NAME in longshot_env.txt set nothing
  (host/hos_main.c is not changed here: the commit before refuses every HOS_COOP name from that file).
- Tests: tests/test_oot_coop.py (the reader, classification, apply rules, guest calls, table check and gate on a made-up
  flag database, and the switch), tests/test_save_copy.py (a writer racing the copies, ThreadSanitizer; the bridge's save root).
- docs/coop.md: what joining a room means for your save, going back, progress sync, what is shared, the room's rules.

Carried from the author's own co-op for another port of this game (his coop_game.c: the logic; every call into that
port's runtime rewritten against Hookshot's).

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]>
Co-op could only be started from the environment (HOS_COOP_AUTO and HOS_COOP_ROOM) or the options file, and a room
could only be accepted from a test switch; a player needs to make or join a room, see who is in it and accept it from
the game.

The Insert menu gets a Co-op tab, the strip's last. Your name and Relay are text fields saved as longshot_options.ini's
coop_name= and coop_server= (which host/coop.c reads at the start); Make a room and Join a room (the secret typed or
pasted) switch co-op on for this run with them (coop_configure, then coop_command). In a room the tab shows its secret
(masked until Enter), a status box with the state, the player count, the relay's round trip and the session's own
words, and a line per member: number, name, you, maker, away, and its identity key's fingerprint. Accept the room
(press twice) takes progress sync's offer (hos_oot_coop_accept: the save is copied first); the game holding the
maker's key also gets Remove a player (Left and Right choose) and Reset the pools (hos_oot_coop_remove_key,
hos_oot_coop_reset_pools). Leave the room, press twice. Rows come and go with the room's state and the selection stays
on its row. While the room waits to be accepted the remedies and Room rules stand back, and the members' lines move
closer together when the room is full, so that every key being accepted shows above the prompts where it can.

Room rules opens a page of its own, as Motion does on Controls: what a room this game makes shares. Progress, Items,
Songs, Quest items (Shared or Per player), Small keys, Rupees, Ammunition (One pool or Per player) are the options
file's coop_progress .. coop_ammo, which host/oot_coop.c reads when the room's maker signs its rules, with the
engine's values (0 per player, 1 shared, 2 one pool) and defaults; under a row the page says what a room cannot have
(with progress shared, items, songs and quest items are shared; with progress per player, small keys are). Co-op at
start is the file's coop=, which host/coop.c reads at the start. The rows' keys are not their names, so they are read
and written through coop_options beside ini_key. The rows take no numeric option id (rx_option): the runtime reads
them by their keys, so they need none of the ids main and other pull requests use.

The interface: vk_api.h version 21, MkwVkCoopView and MkwVkHost.coop_view and coop_command after take_text (version 20).
host/coop_menu.c is the host's side: the session's view and progress sync's offer (keys, token, acceptance) and pool
list (the player's own key) in the tab's terms, the tab's commands into the session's and progress sync's calls. Make
a room uses HOS_COOP_ROOM's secret when the environment has one (test sessions whose second game types it).
HOS_COOP_DEMO=1 runs the tab on a made-up room that walks through every state (members with and without keys, an
offer, accepted, a member away, removed, left) without starting the session.

RX_VK_MENU_SCRIPT gains ROW:<name>: the shown tab's or page's row of that name is selected, if it shows. The tab's
rows come and go with the room's state (and the Graphics tab's with the GPU), so a scripted run that counted DOWN
presses pressed the wrong row; a session driven from the tab chooses Accept the room by name.

The control bridge (host/control.c) lists and sets the Room rules rows like the other settings, under their
coop_ keys (row_key), and names the Co-op tab's and the Room rules page's rows' tabs (co-op, room rules).

save() writes the Room rules rows under their keys with saved_value, as every row, so a run's environment does
not stay in the file; HOS_COOP_DEMO is an on/off switch (env_on). tests/test_menu_env_overrides.py, which compiles
save() cut from the source, gets the tab's part of it as stand-ins; so does tests/test_menu_keys.py for the tab's
parts of menu_tick's Esc block and of menu_init's reading of the options file.

tests/test_menu_coop_tab.py drives the tab's code cut from vk_menu.cpp (rows per state, presses, fields, selection,
the remedies, what the rules page says under a row), compiles host/coop_menu.c against the real secret code with the
session and progress sync stubbed (and the made-up room on a made-up clock), checks the rules page's rows against
host/coop_state.c itself (keys, modes, defaults), and checks the wiring.

docs/coop.md says what the tab does (a section of its own; the lines that said the tab was to come), the readme's menu
table has its rows, docs/development.md has ROW:<name>, HOS_COOP_DEMO and the options file's co-op keys.

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]>
Adopt Kuba’s bind_tables rename from his Windows build/play report on #2274, avoiding the bind declaration imported by winsock2.h. Rename the internal IN macro to COOP_FLAG_IN as well, because minwindef.h defines IN. Migrate the existing fixture callers and extraction markers without removing a check. Document the reported Windows read-only hard-link and headless forced-stop behavior.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>

Co-authored-by: Kuba <[email protected]>
A pthread join can finish before the joined Linux TID disappears from /proc/self/task, especially with ThreadSanitizer. The warm-up helper then inflates the baseline from two tasks to three and every exact menu-thread check uses the wrong baseline. Retain each fixture helper’s TID until its join, wait at most five seconds for that specific task to disappear, and only then let the baseline be counted. Keep every existing exact thread/socket-count check and all production code unchanged.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
MinGW GCC checks the session logger’s format(printf) attribute against Microsoft’s format dialect, which rejects %zu even when the fixture requests MinGW ANSI stdio. Print the three wire byte counts with %u and explicit unsigned casts; all are bounded by COOP_MAX_DGRAM. Apply the same portable format to the protocol fixture’s maximum-payload diagnostic. Keep format attributes and -Werror intact, with no build-wide stdio override or runtime behavior change.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
This is a progress-sync fix found by warp, independent of the warp and feature-rule changes. It is intended to be cherry-picked onto the six-commit stack by itself.

Use the save total-play-time counter rather than seconds spent in the current area. The old counter resets in the common area-change completion path, so ordinary doors and loading zones that change area spuriously re-offer an accepted room and require another backup on reacceptance. The Players s2 field-and-back session and Warp s1 silently reaccepted under HOS_COOP_ACCEPT=always; the manual-menu stack proof and other stationary proofs did not exercise that boundary.

Keep all existing load detection unchanged. Independent area and total counters now exercise each transition flag through the real merge engine: area changes preserve the accepted offer and existing copy; another slot, the same slot loaded again with unchanged time, and a new game each invalidate the old offer and require a new acceptance and copy before application. No existing checks removed or weakened. GCC and Clang pass; the extended fixture fails against the prior binding with both compilers. No new GPU run.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Raise the default member limits to 480 forwarded copies and 256 KiB per second, with double bursts and command-line options. Charge every broadcast copy to both budgets.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Reseal missing event fragments until receipt, bind them to accepted sender and recipient sessions, and dispatch each whole event once in per-sender FIFO order. Keep state datagrams independent.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Publish the progress engine consent decision to the session without a game dependency. Refresh it after identity, removal, roster and save changes, and reject bindings for another member session.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
List allocated and reserved kinds, state and event calls, traffic limits, receiver checks, and a ten-line integration example.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Run the single-room relay on its own socket and thread. Make connects the local session over loopback; friends use the maker’s reachable UDP address. Keep cookie proof, sealed forwarding, bounded budgets, and joined shutdown.

Exercise the shared relay cases, session scenarios, settings and lifecycle without the game. Document the single-room limits and the port-forwarding requirement.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Replace a cached refusal after a charged change of reason, as the Python relay does. A request that changes back must pay again. The new test fails before this change and passes against both relays.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <[email protected]>
Treat each sent event id as the oldest live id for its recipient, so a cancelled partial cannot block later events. Bind acknowledgement masks to the receiver assembly epoch and retain consent transitions between socket steps. Keep completed receipts and reject delayed older-epoch acknowledgements.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
The settings fixture sets HOS_COOP_HOST to an empty value and expects
"off". A Windows process cannot hold an empty variable: _putenv_s(name,
"") removes it, so the game reads the ini value there, as it should,
and the Windows checks failed on "host switch environment precedence"
and "off override does not host" for that one mode. On Windows the
fixture now expects the unset behaviour for the empty mode; elsewhere
the empty value is still set and still means off.

Tested: Linux, GCC 16.2.1 and Clang 23.1.3: tests/test_coop_host_relay.py
passes as before. The fixture compiled for the Windows target (clang
--target=x86_64-w64-mingw32, syntax only): no error, no warning.
Not tested: run on Windows (the project's Windows job does that; it is
the job that showed the failure).

Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <[email protected]>
The fixture's helper was named accept. On Windows the host's network
header brings in winsock2.h, which declares accept(), so the Windows
checks could not compile the fixture ("conflicting types for 'accept'")
and the event tests never ran there. The helper is named grant.

Tested: all eight C fixtures this branch adds or changes compiled for
the Windows target (clang --target=x86_64-w64-mingw32, -Wall -Wextra,
syntax only): this fixture had 15 errors before and has none now; the
four other co-op fixtures show none (three fixtures that take their
sources or defines from their Python drivers cannot be judged by that
pass). Linux, GCC 16.2.1 and Clang 23.1.3: tests/test_coop_events.py
passes as before.
Not tested: run on Windows (the project's Windows job does that).

Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <[email protected]>
"Adding a layer" told layer authors to heal damage state with one
running total merged by highest-wins. That loses a hit when two players
strike at once: totals of 2 and 2 merge to 2, not 4. The rule that
works is a running total per player and enemy, each merged by the
highest value seen, then the players' totals added together. The
sentence says so now.

Docs only.

Assisted-by: Claude Fable 5.2
Co-Authored-By: Claude Fable 5.2 <[email protected]>
Windows permits a same-user specific-address bind beside a default wildcard
bind, including the IPv4 half of a dual-stack socket. Another program could
receive datagrams meant for the game's session or hosted relay, or send from
an overlapping local endpoint and disturb the room. It cannot decrypt sealed
player messages without the room secret; relay-control metadata is not sealed.
The Windows session CI failure exposed this as cross-test Alice/Bob traffic,
not a reason to serialize the fixture or relax its roster/message checks.

Set SO_EXCLUSIVEADDRUSE before binding native Windows co-op sockets and the
Python relay. Close the socket and fail when exclusivity cannot be obtained.
A relay bound to a specific address owns that requested address, not unrelated
interfaces. Linux enables neither SO_REUSEADDR nor SO_REUSEPORT and already
rejects overlapping binds; no reuse option is added there.

Keep all concurrent session cases. Add real wildcard/specific IPv4 bind probes
in both orders, including a competing reuse request, on Windows and Linux.
Compile the actual Windows open function against a call recorder to check
option ordering and failure cleanup without a Windows machine. The recorder
and Python option check fail on the old source and pass on the fix; real
Windows kernel execution remains required in the fork workflow. Preserve
-Werror and add the fixture's secure-CRT guards before includes.

Binding rules: https://learn.microsoft.com/en-us/windows/win32/winsock/using-so-reuseaddr-and-so-exclusiveaddruse

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
The reviewed text-field base correctly put screen_size and take_text last.
Co-op appends its view and command after them, so the shared fixture's old
last-two-members assertion rejected the integrated interface with both
compilers even though typing and queue behavior passed.

Keep the pair adjacent and in its matching callback-table slots without
requiring it to end the extended interface. The existing take_text slot,
member-count and ABI-floor checks remain, as does the co-op fixture's exact
last-three-callback check. The actual source check accepts the legal co-op
tail and rejects a reversed pair, wrong slots, a missing screen callback and
an intervening slot. This adapts the co-op integration; the reviewed
text-field commits underneath remain byte-identical.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
The local Windows preflight found that several co-op drivers declared the
CRT policy in only their fixture, or not until after an included header.
Their separately compiled game sources and the forced environment header
therefore still produced secure-CRT deprecation diagnostics.

Set the existing project policy before those headers, use compile-wide
Windows flags for multi-unit fixtures, and let existing fixture
prefixes coexist with that flag. Cover all three Co-op menu compilations,
not just the first one reached before the capture stops at execution.
Keep the existing compiler warnings, assertions, and concurrent modes.
No game or protocol behavior changes here.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Keep the existing Longshot domains and pin their exact bytes against future branding changes. Regenerate sealed payloads with the C reference fixture and the vector file with test_coop_relay.py --write-vectors. Document the Hookshot/Longshot interoperability boundary.

The same reference with only the old domain inputs reproduces the original vector file byte-for-byte; the original port commits are unchanged.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
test(menu): retain Auto save-key checks beside co-op keys
Some checks are pending
PR description / Description follows the template (pull_request_target) Waiting to run
cca2aebb03
The shared predicate continues with the co-op keys instead of ending at auto_steps. Keep every Auto persistence key assertion.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Author
First-time contributor

Discussion carried over from NotRexed/Hookshot#2274

Every comment that was readable on the original thread when it was copied on 2026-11-12, in order, unchanged except that issue and pull request numbers are written as links to the old repository and names carry no @. Comments by an account that has since been closed there could no longer be read and are missing; where a reply quotes them, the quote remains.


mooki000, 2026-11-12 00:13 UTC (comment, original):

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 71d4ab0f on main fce53453; it merges with ef81aee7 without 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.


mooki000, 2026-11-12 01:02 UTC (comment, original):

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 NotRexed/Hookshot#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?

  1. the link and the session (commits 1 to 3 here: no contact with the game)
  2. progress sync and the Co-op tab (commits 4 to 6; the text fields are also alone as NotRexed/Hookshot#2594)
  3. the other players' bodies
  4. shared enemies and bosses
  5. PvP

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.


itzdaface, 2026-11-12 02:21 UTC (comment, original):

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 --update into 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.c has a static function called bind. On Windows coop_net.h pulls in winsock2.h, which declares bind(SOCKET, ...):

host/oot_coop.c:1061:13: error: conflicting types for 'bind'
host/oot_coop.c:1373:30: error: too few arguments to function call, expected 3, have 2

It was the only file in the whole build that failed. The fix is a rename:

-static void bind(Ctx *c, uint64_t mgr) {
+static void bind_tables(Ctx *c, uint64_t mgr) {
-    if (!G.bound) bind(c, mgr);
+    if (!G.bound) bind_tables(c, mgr);

With that, everything compiles and links, Monocypher and -lws2_32 -lbcrypt included. 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=create and join, a fixed HOS_COOP_ROOM, both headless):

  • maker: the relay answered in 3 ms, room 234… created (member 1), then Second (member 2) joined the room
  • joiner: the relay answered in 2 ms, Kuba (member 1) joined the room, joined room 234… as member 2
  • each game listed the other with its key fingerprint, and both got to "a room to accept"
  • stats on both: thrown away 0 (tag 0, sender 0, not co-op 0, replay 0), outbox full 0, round trip 1 ms
  • after I killed the games the relay dropped each one as silent 15 s, as the doc says

By hand, from the Co-op tab, in a window, with a second headless game waiting in a room:

  • typing in Your name and Relay works (so WM_CHAR is fine)
  • Make a room works: room XHM… created (member 1) in the game, created and joined as member 1 in the relay
  • Leave the room works: the relay logged Kuba (member 1) left and closed the room at once
  • Join a room with a typed secret works: joined room 234… as member 2, and the tab listed the other player

4. 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

  • Accept the room and real progress sync between the two games. That is next.
  • A relay on another PC or a server, and Windows with Linux in one room.
  • Controllers in the tab, and the test suite.

Two small notes

  • A headless game (HOS_HEADLESS) does not close when Windows asks it to (taskkill without /f; there is no window to take the request), so I had to force it. That is why the headless runs show silent 15 s and not a goodbye. From the window, Leave says goodbye properly.
  • For "copy the built game folder twice" on Windows: the romfs is 24 GB, and I was short on disk. Hard-linking the romfs files into the second folder (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 onto coop-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.


mooki000, 2026-11-12 02:27 UTC (comment, original):

itzdaface thank you, this is exactly the report this draft was missing.

The fix goes in as you wrote it (bind to bind_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 71d4ab0f with 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:

  • your fix, and the two test fixes for the Windows job;
  • a fix that matters for exactly what you test next: on 71d4ab0f the 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 relay's limits raised and made options (480 copies and 256 KB a second per member);
  • the call that says whether this player has accepted a member, and a page for layer authors: how a body, enemy or PvP message is sent, received and checked;
  • "Host on this PC": the relay inside the game, for the room's maker.

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-wire on 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 NotRexed/Hookshot#2578 as the one, say so, and I open the base as a pull request against your feat/coop instead; 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.


itzdaface, 2026-11-12 02:32 UTC (comment, original):

mooki000 sounds good on all of it. Pull requests against coop-relay-wire works for me, and I'm happy with NotRexed/Hookshot#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.


mooki000, 2026-11-12 06:03 UTC (comment, original):

itzdaface the new head is up: 2be28f89, 18 commits on main de866017 (it merges cleanly with main as of now). The branch was force-pushed once, as announced; rebase your coop-win on it and send your commits as pull requests against coop-relay-wire.

What is above the six commits you built:

  • 1c60356f your bind rename, with you as co-author; the macro IN in 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.
  • 1f1d0a72 the 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.
  • d2fc0f7f the relay's limits: 480 forwarded copies and 256 KiB a second per member (twice that as burst), as options of scripts/coop_relay.py.
  • 35931fa8, 2be28f89 messages that must arrive: coop_send_event(member, kind, bytes, n) and coop_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".
  • b8d0cbed coop_member_accepted(id): whether this player has accepted that member in this session. Being on the roster is not consent.
  • c517e746 docs/coop.md, "Adding a layer": kinds, the calls above, pacing, and what a receiver checks.
  • 97f56483, dcf84430 Host on this PC: with coop_host=1 in hookshot_options.ini (or HOS_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 24680 as 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 and coop_host=1 on the maker, and what the firewall asks.


mooki000, 2026-11-12 08:14 UTC (comment, original):

The two-game session on the new line, as promised. Built from 2be28f89; the head is now 2f412422, which adds two test-only commits for the Windows checks (a fixture helper named accept beside winsock's, and an empty HOS_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, no HOS_COOP_ACCEPT.

What it showed, in order (2026-11-12 02:04:26 to 02:09:14 local, 288 s; both games exited 0):

  1. Alice makes a room from the tab; Bob joins with the secret.
  2. Each accepts the other by hand (the second press confirms).
  3. Before anything is applied, each game copies its save: both copies are byte-equal to their 14-file starting saves.
  4. A sword granted to Alice arrives and is applied on Bob; a song marker set on Bob arrives and is applied on Alice.
  5. Alice renames herself Alice2: Bob gets a new offer and accepts again.
  6. Both quit through the control bridge; the relay logs a Leave for each, no timeout.

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.

1: Alice made the room from the Co-op tab. 2: Bob confirms acceptance. 3: accepted, the offer row gone. 4: Alice changes her name to Alice2. 5: Bob sees Alice2 as a new offer. 6: Bob accepts Alice2. The room secret is masked by the game in all six

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.


mooki000, 2026-11-12 08:21 UTC (comment, original):

The in-game relay, two games on this PC, same build (2be28f89), 02:14:39 to 02:16:41 local:

  • Shown: with coop_host=1 on the maker only and no relay script running, Alice's game logs hosting on this PC, makes the room, and its own relay admits Bob; each game sees the other in the room.
  • Not shown, and the run's own last check failed: the script then stops the maker and expects Bob to notice that the room ended. The stop did not reach the game process (it ended the wrappers around it; both games were still running and were ended afterwards from outside), so the check "B did not notice that its maker and relay ended" failed for a reason that says nothing about the game either way. No kernel or driver message during the run.
  • Still unshown over the in-game relay: acceptance and progress, a Leave from the tab, and what the joiner sees when the maker closes.

I am repairing the stop step and running that part once more tonight; the result goes here.


mooki000, 2026-11-12 08:38 UTC (comment, original):

The repaired rerun of the in-game relay's last step (same build 2be28f89, 02:33:34 to 02:34:58 local): passes.

  • The earlier stop had signalled the wrapper's process group; the game sat in another one. The repaired stop signals the game's own process: the maker's game ended 0.25 s after SIGTERM.
  • Only after that exit, Bob's game logs coop: relay lost; rejoining, then coop: connecting to the relay and repeated connection attempts. So the joiner notices that the maker and its relay are gone.
  • Both of the run's link checks pass (make and join through the maker's own relay; the loss noticed). No kernel or driver message.

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.


mooki000, 2026-11-12 12:46 UTC (comment, original):

First, a correction to my reply on NotRexed/Hookshot#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 of 2be28f89: 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:

  • Bodies: their own message beside the small POS message, keeping your animation and smoothing.
  • Enemies/bosses: aggregate state instead of one packet per enemy, keep your nearest-player hand-over, and carry the component damage history across leader changes. A missing record is not a death.
  • One-offs: use the base's coop_send_event / coop_on_event for PvP requests and boss hand-over, rather than another retry loop. Only COOP_EVENT_QUEUED means 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-wire as agreed.


mooki000, 2026-11-12 17:32 UTC (comment, original):

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's a4c080e0.
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's a4c080e0.
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 had value 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:

The Equipment page of both players after A got the Lens: with item sharing on B has the goggles too, with it off B's slot is empty

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.


mooki000, 2026-11-12 18:33 UTC (comment, original):

A note on direction, so nobody is surprised: in NotRexed/Hookshot#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 NotRexed/Hookshot#2892.


mooki000, 2026-11-12 19:42 UTC (comment, original):

Co-op rebase: 9ba4ee00 on 9b6072cd, API 23.
The four shared text-field patches retain identical changed lines; range-diff shows 25 identical entries and one context-only Co-op menu entry.
The save() join keeps co-op settings and drops the obsolete mesh_shaders key. Only the Mesh shaders row disappears; no Co-op row moves.

Windows now requests SO_EXCLUSIVEADDRUSE before binding native co-op and Python relay sockets. Without it, Winsock permits same-user wildcard/specific-address binds to coexist: another program could receive/inject raw datagrams and disturb rooms. Those packets do not reveal sealed player payloads without the room secret; addresses/control metadata are unencrypted. Linux sets neither SO_REUSEADDR nor SO_REUSEPORT and already rejects overlapping binds.

Checks: 38 drivers pass with GCC and Clang, including menu row/order and README checks; Linux full suite 518 passed, 0 failed, 20 skipped. PR preflight and three Windows-target menu syntax captures pass (not Windows execution).
The b7f5a87d fork run passes all eight co-op drivers on Windows and Ubuntu, including port-ownership probes and unchanged concurrent modes; its whole workflow remains red on old-main README and Windows shadow-fixture checks.
No game rerun: the prior two-game Linux evidence covers byte-identical co-op gameplay/update/sync code, supported by the range-diff; it does not prove the changed Windows UDP ownership behavior.
On this exact head a full run of the project's checks in my fork is green on Windows and Ubuntu, every job (run 43291054852): the first fully green Windows run this branch has had. No Windows game, firewall or cross-PC play claim is made.

Direction, as noted above: in NotRexed/Hookshot#2892 I have proposed that co-op become the first module on a mod interface rather than land in the core. This branch stays the working copy, kept current, until that is answered. It also carries NotRexed/Hookshot#2594's four text-field commits underneath; review of those belongs there.

## Discussion carried over from NotRexed/Hookshot#2274 Every comment that was readable on [the original thread](https://github.com/NotRexed/Hookshot/pull/2274) when it was copied on 2026-11-12, in order, unchanged except that issue and pull request numbers are written as links to the old repository and names carry no @. Comments by an account that has since been closed there could no longer be read and are missing; where a reply quotes them, the quote remains. --- **mooki000**, 2026-11-12 00:13 UTC (comment, [original](https://github.com/NotRexed/Hookshot/pull/2274#issuecomment-6810431871)): 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 `71d4ab0f` on main `fce53453`; it merges with `ef81aee7` without 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. --- **mooki000**, 2026-11-12 01:02 UTC (comment, [original](https://github.com/NotRexed/Hookshot/pull/2274#issuecomment-6811249615)): 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 [NotRexed/Hookshot#2578](https://github.com/NotRexed/Hookshot/issues/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? 1. the link and the session (commits 1 to 3 here: no contact with the game) 2. progress sync and the Co-op tab (commits 4 to 6; the text fields are also alone as [NotRexed/Hookshot#2594](https://github.com/NotRexed/Hookshot/issues/2594)) 3. the other players' bodies 4. shared enemies and bosses 5. PvP **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. --- **itzdaface**, 2026-11-12 02:21 UTC (comment, [original](https://github.com/NotRexed/Hookshot/pull/2274#issuecomment-6812494715)): 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 --update` into 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.c` has a static function called `bind`. On Windows `coop_net.h` pulls in `winsock2.h`, which declares `bind(SOCKET, ...)`: ``` host/oot_coop.c:1061:13: error: conflicting types for 'bind' host/oot_coop.c:1373:30: error: too few arguments to function call, expected 3, have 2 ``` It was the only file in the whole build that failed. The fix is a rename: ```diff -static void bind(Ctx *c, uint64_t mgr) { +static void bind_tables(Ctx *c, uint64_t mgr) { - if (!G.bound) bind(c, mgr); + if (!G.bound) bind_tables(c, mgr); ``` With that, everything compiles and links, Monocypher and `-lws2_32 -lbcrypt` included. 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=create` and `join`, a fixed `HOS_COOP_ROOM`, both headless): - maker: `the relay answered in 3 ms`, `room 234… created (member 1)`, then `Second (member 2) joined the room` - joiner: `the relay answered in 2 ms`, `Kuba (member 1) joined the room`, `joined room 234… as member 2` - each game listed the other with its key fingerprint, and both got to "a room to accept" - stats on both: thrown away 0 (tag 0, sender 0, not co-op 0, replay 0), outbox full 0, round trip 1 ms - after I killed the games the relay dropped each one as `silent 15 s`, as the doc says **By hand, from the Co-op tab,** in a window, with a second headless game waiting in a room: - typing in **Your name** and **Relay** works (so `WM_CHAR` is fine) - **Make a room** works: `room XHM… created (member 1)` in the game, `created` and `joined as member 1` in the relay - **Leave the room** works: the relay logged `Kuba (member 1) left` and closed the room at once - **Join a room** with a typed secret works: `joined room 234… as member 2`, and the tab listed the other player ### 4. 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 - **Accept the room** and real progress sync between the two games. That is next. - A relay on another PC or a server, and Windows with Linux in one room. - Controllers in the tab, and the test suite. ### Two small notes - A headless game (`HOS_HEADLESS`) does not close when Windows asks it to (`taskkill` without `/f`; there is no window to take the request), so I had to force it. That is why the headless runs show `silent 15 s` and not a goodbye. From the window, Leave says goodbye properly. - For "copy the built game folder twice" on Windows: the romfs is 24 GB, and I was short on disk. Hard-linking the romfs files into the second folder (`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 onto `coop-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. --- **mooki000**, 2026-11-12 02:27 UTC (comment, [original](https://github.com/NotRexed/Hookshot/pull/2274#issuecomment-6812576925)): itzdaface thank you, this is exactly the report this draft was missing. **The fix** goes in as you wrote it (`bind` to `bind_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 `71d4ab0f` with 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: - your fix, and the two test fixes for the Windows job; - a fix that matters for exactly what you test next: on `71d4ab0f` the 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 relay's limits raised and made options (480 copies and 256 KB a second per member); - the call that says whether this player has accepted a member, and a page for layer authors: how a body, enemy or PvP message is sent, received and checked; - "Host on this PC": the relay inside the game, for the room's maker. 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-wire` on 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 [NotRexed/Hookshot#2578](https://github.com/NotRexed/Hookshot/issues/2578) as the one, say so, and I open the base as a pull request against your `feat/coop` instead; 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.* --- **itzdaface**, 2026-11-12 02:32 UTC (comment, [original](https://github.com/NotRexed/Hookshot/pull/2274#issuecomment-6812659402)): mooki000 sounds good on all of it. Pull requests against `coop-relay-wire` works for me, and I'm happy with [NotRexed/Hookshot#2274](https://github.com/NotRexed/Hookshot/issues/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. --- **mooki000**, 2026-11-12 06:03 UTC (comment, [original](https://github.com/NotRexed/Hookshot/pull/2274#issuecomment-6816046193)): itzdaface the new head is up: `2be28f89`, 18 commits on main `de866017` (it merges cleanly with main as of now). The branch was force-pushed once, as announced; rebase your `coop-win` on it and send your commits as pull requests against `coop-relay-wire`. **What is above the six commits you built:** - `1c60356f` your `bind` rename, with you as co-author; the macro `IN` in 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. - `1f1d0a72` **the 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. - `d2fc0f7f` the relay's limits: 480 forwarded copies and 256 KiB a second per member (twice that as burst), as options of `scripts/coop_relay.py`. - `35931fa8`, `2be28f89` **messages that must arrive**: `coop_send_event(member, kind, bytes, n)` and `coop_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". - `b8d0cbed` `coop_member_accepted(id)`: whether this player has accepted that member in this session. Being on the roster is not consent. - `c517e746` `docs/coop.md`, "Adding a layer": kinds, the calls above, pacing, and what a receiver checks. - `97f56483`, `dcf84430` **Host on this PC**: with `coop_host=1` in `hookshot_options.ini` (or `HOS_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 24680` as 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 and `coop_host=1` on the maker, and what the firewall asks. --- **mooki000**, 2026-11-12 08:14 UTC (comment, [original](https://github.com/NotRexed/Hookshot/pull/2274#issuecomment-6818394613)): The two-game session on the new line, as promised. Built from `2be28f89`; the head is now `2f412422`, which adds two test-only commits for the Windows checks (a fixture helper named `accept` beside winsock's, and an empty `HOS_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`, no `HOS_COOP_ACCEPT`. **What it showed, in order** (2026-11-12 02:04:26 to 02:09:14 local, 288 s; both games exited 0): 1. Alice makes a room from the tab; Bob joins with the secret. 2. Each accepts the other by hand (the second press confirms). 3. Before anything is applied, each game copies its save: both copies are byte-equal to their 14-file starting saves. 4. A sword granted to Alice arrives and is applied on Bob; a song marker set on Bob arrives and is applied on Alice. 5. Alice renames herself Alice2: Bob gets a new offer and accepts again. 6. Both quit through the control bridge; the relay logs a Leave for each, no timeout. 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. ![1: Alice made the room from the Co-op tab. 2: Bob confirms acceptance. 3: accepted, the offer row gone. 4: Alice changes her name to Alice2. 5: Bob sees Alice2 as a new offer. 6: Bob accepts Alice2. The room secret is masked by the game in all six](https://gist.githubusercontent.com/mooki000/b86bd3f35969803fb032f71f035844f8/raw/21c6a230767de5e645ae95970cb065616f3679d8/hookshot-coop-next-2be28f89-tab.jpg) **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. --- **mooki000**, 2026-11-12 08:21 UTC (comment, [original](https://github.com/NotRexed/Hookshot/pull/2274#issuecomment-6818523676)): The in-game relay, two games on this PC, same build (`2be28f89`), 02:14:39 to 02:16:41 local: - **Shown:** with `coop_host=1` on the maker only and no relay script running, Alice's game logs `hosting on this PC`, makes the room, and its own relay admits Bob; each game sees the other in the room. - **Not shown, and the run's own last check failed:** the script then stops the maker and expects Bob to notice that the room ended. The stop did not reach the game process (it ended the wrappers around it; both games were still running and were ended afterwards from outside), so the check "B did not notice that its maker and relay ended" failed for a reason that says nothing about the game either way. No kernel or driver message during the run. - **Still unshown over the in-game relay:** acceptance and progress, a Leave from the tab, and what the joiner sees when the maker closes. I am repairing the stop step and running that part once more tonight; the result goes here. --- **mooki000**, 2026-11-12 08:38 UTC (comment, [original](https://github.com/NotRexed/Hookshot/pull/2274#issuecomment-6818855545)): The repaired rerun of the in-game relay's last step (same build `2be28f89`, 02:33:34 to 02:34:58 local): **passes.** - The earlier stop had signalled the wrapper's process group; the game sat in another one. The repaired stop signals the game's own process: the maker's game ended 0.25 s after SIGTERM. - Only after that exit, Bob's game logs `coop: relay lost; rejoining`, then `coop: connecting to the relay` and repeated connection attempts. So the joiner notices that the maker and its relay are gone. - Both of the run's link checks pass (make and join through the maker's own relay; the loss noticed). No kernel or driver message. 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. --- **mooki000**, 2026-11-12 12:46 UTC (comment, [original](https://github.com/NotRexed/Hookshot/pull/2274#issuecomment-6823991481)): First, a correction to my reply on [NotRexed/Hookshot#2578](https://github.com/NotRexed/Hookshot/issues/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 of `2be28f89`: the intervening commits are two test fixes and this documentation correction. The [two-game tab result](https://github.com/NotRexed/Hookshot/pull/2274#issuecomment-6818394613) 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](https://github.com/NotRexed/Hookshot/pull/2274#issuecomment-6818855545), not yet acceptance/progress or tab Leave over that relay. For your port, I'd keep the split we agreed: - **Bodies:** their own message beside the small POS message, keeping your animation and smoothing. - **Enemies/bosses:** aggregate state instead of one packet per enemy, keep your nearest-player hand-over, and carry the component damage history across leader changes. A missing record is not a death. - **One-offs:** use the base's `coop_send_event` / `coop_on_event` for PvP requests and boss hand-over, rather than another retry loop. Only `COOP_EVENT_QUEUED` means 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-wire` as agreed. --- **mooki000**, 2026-11-12 17:32 UTC (comment, [original](https://github.com/NotRexed/Hookshot/pull/2274#issuecomment-6831046415)): 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's `a4c080e0`.** 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's `a4c080e0`.** 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 `had` value 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: ![The Equipment page of both players after A got the Lens: with item sharing on B has the goggles too, with it off B's slot is empty](https://gist.githubusercontent.com/mooki000/b86bd3f35969803fb032f71f035844f8/raw/b8edfef39a99007125aeebd9fbdc09b7c3221955/hookshot-coop-proof-lens-equipment.jpg) **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. --- **mooki000**, 2026-11-12 18:33 UTC (comment, [original](https://github.com/NotRexed/Hookshot/pull/2274#issuecomment-6832470016)): A note on direction, so nobody is surprised: in [NotRexed/Hookshot#2892](https://github.com/NotRexed/Hookshot/issues/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 [NotRexed/Hookshot#2892](https://github.com/NotRexed/Hookshot/issues/2892). --- **mooki000**, 2026-11-12 19:42 UTC (comment, [original](https://github.com/NotRexed/Hookshot/pull/2274#issuecomment-6834065675)): **Co-op rebase: `9ba4ee00` on `9b6072cd`, API 23.** The four shared text-field patches retain identical changed lines; range-diff shows 25 identical entries and one context-only Co-op menu entry. The `save()` join keeps co-op settings and drops the obsolete `mesh_shaders` key. Only the Mesh shaders row disappears; no Co-op row moves. Windows now requests `SO_EXCLUSIVEADDRUSE` before binding native co-op and Python relay sockets. Without it, [Winsock permits same-user wildcard/specific-address binds to coexist](https://learn.microsoft.com/en-us/windows/win32/winsock/using-so-reuseaddr-and-so-exclusiveaddruse): another program could receive/inject raw datagrams and disturb rooms. Those packets do not reveal sealed player payloads without the room secret; addresses/control metadata are unencrypted. Linux sets neither `SO_REUSEADDR` nor `SO_REUSEPORT` and already rejects overlapping binds. Checks: 38 drivers pass with GCC and Clang, including menu row/order and README checks; Linux full suite 518 passed, 0 failed, 20 skipped. PR preflight and three Windows-target menu syntax captures pass (not Windows execution). The [b7f5a87d fork run](https://github.com/mooki000/Hookshot/actions/runs/43284031489) passes all eight co-op drivers on Windows and Ubuntu, including port-ownership probes and unchanged concurrent modes; its whole workflow remains red on old-main README and Windows shadow-fixture checks. No game rerun: the prior two-game Linux evidence covers byte-identical co-op gameplay/update/sync code, supported by the range-diff; it does not prove the changed Windows UDP ownership behavior. On this exact head a full run of the project's checks in my fork is green on Windows and Ubuntu, every job ([run 43291054852](https://github.com/mooki000/Hookshot/actions/runs/43291054852)): the first fully green Windows run this branch has had. No Windows game, firewall or cross-PC play claim is made. Direction, as noted above: in [NotRexed/Hookshot#2892](https://github.com/NotRexed/Hookshot/issues/2892) I have proposed that co-op become the first module on a mod interface rather than land in the core. This branch stays the working copy, kept current, until that is answered. It also carries [NotRexed/Hookshot#2594](https://github.com/NotRexed/Hookshot/issues/2594)'s four text-field commits underneath; review of those belongs there.
Author
First-time contributor

Added the three-part Testing section and updated the full checklist. Added one branch base/head/suite block under Measurements; apart from consolidating duplicate results and minor wording cleanup, nothing else changed.

Added the three-part Testing section and updated the full checklist. Added one branch base/head/suite block under Measurements; apart from consolidating duplicate results and minor wording cleanup, nothing else changed.
Some checks are pending
PR description / Description follows the template (pull_request_target) Waiting to run
This pull request has changes conflicting with the target branch.
  • docs/development.md
  • docs/tested-configurations.md
View command line instructions

Manual merge helper

Use this merge commit message when completing the merge manually.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'coop-relay-wire:mooki-coop-relay-wire'
git switch 'mooki-coop-relay-wire'
Sign in to join this conversation.
No description provided.