Skip to content

feat(coop): co-op rooms: a relay, progress sync, and a Co-op tab to make, join and accept - #2274

Draft
mooki000 wants to merge 20 commits into
NotRexed:mainfrom
mooki000:coop-relay-wire
Draft

mooki000 wants to merge 20 commits into
NotRexed:mainfrom
mooki000:coop-relay-wire

Conversation

@mooki000

@mooki000 mooki000 commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

What this is for: co-op (#2269): each friend plays their own game/save, sharing accepted progress through the game's setters. This draft adds bounded events and optional maker-PC hosting, not other players' Links, enemies, combat or warp. Measured: 2be28f893fa378a28de2a110caf491970be7b32e on de866017e1584372fc75a1caab3784b3ba1d9f98: game-free focused 60 pass / 2 fail, suite 467 pass / 15 fail / 20 skip, Windows-target syntax below. Every failure is reproduced on untouched base. Not measured: no game build/play, current Windows execution/link, LAN/cross-OS/controller or performance proof on this head. All game pictures below are historical.

Draft: 18 commits. Rebase recorded 2026-11-11T22:57:08-06:00; evidence assembled 2026-11-11T23:52:40-06:00. The socket decision remains open.

Problem

  • The game's own nn::socket stays offline; there was no host path for players' games to find a room or exchange progress. host/coop_net.c and host/coop.c keep that networking outside the guest game.
  • Games behind different routers need a reachable relay. scripts/coop_relay.py forwards sealed messages; host/coop_proto.c derives separate locator and message keys so the relay need not know the room secret or open progress.
  • Sharing raw save files would bypass player consent and the game's rules. host/coop_state.c merges accepted, signed progress; host/oot_coop.c checks the live flag table and calls the game's setters only after acceptance and a save copy.
  • Players need to do this without environment switches or hand-editing settings. Text fields and the Co-op tab in gpu/vk/vk_menu.cpp expose the same session and progress commands while the game runs.
    Additional defects: Windows name/printf/CRT conflicts; unstable fixture task counts; area resets mistaken for save loads; cancelled event fragments blocking later work or combining ACK epochs; localized UI helpers translating typed data.

What changed

Additions above the original six

Part / commit Adds
7 / 1c60356f Kuba's bind → bind_tables rename removes the Winsock declaration collision; IN → COOP_FLAG_IN also avoids the Windows annotation macro. Existing fixture callers/extraction markers migrate with their checks. Co-authored with Kuba.
8 / 68088064 Wait for a joined fixture helper's specific Linux TID to disappear before taking the exact task-count baseline. Five-second bound; no runtime behavior or thread/socket assertion removed.
9 / 184cceb0 Three bounded runtime packet-size traces and one codec diagnostic use %u plus unsigned, preserving format attributes and warning errors; no global ANSI-stdio override.
10 / 1cd02fc4 Declare secure-CRT guards before generated fixture headers, including the screensaver fixture; preserve runtime code and behavioral checks.
11 / 1f1d0a72 Use saved total play time rather than the per-area clock for progress save identity. Ordinary area-changing doors/loading zones keep acceptance and the existing backup; real save loads still require both anew.
12 / d2fc0f7f Raise configurable Python-relay member ceilings to 480 forwarded copies/s and 256 KiB/s, with double bursts; charge every broadcast copy to both budgets.
13 / 35931fa8 Add bounded session events, fragmentation, freshly sealed retries, whole callbacks and per-sender/recipient FIFO, separate from replaceable state datagrams.
14 / b8d0cbed Publish session-bound progress consent through coop_member_accepted(id); a roster entry or recycled member number is not acceptance.
15 / c517e746 Document message ownership, state/event calls, consent, payload and shared traffic budgets, receiver-side gameplay checks, and the layer integration example.
16 / 97f56483 Optional maker-PC relay (coop_host / HOS_COOP_HOST): its own socket/thread, local maker authorization, cookie proof, sealed forwarding, bounded state and joined shutdown; no router/firewall automation.
17 / dcf84430 Cache only the latest refused join answer. A changed reason is charged again, including when it changes back; preserve Python/C relay parity.
18 / 2be28f89 Retire cancelled incomplete events by recipient delivery floor; ACK assembly epochs prevent combining partial receipts and reject stale ACKs. Record revoke/reaccept transitions between socket steps. ACK body v2 requires peer updates.

Layer interface: coop_send_event(member, kind, bytes, n), coop_on_event(kind, callback, user), coop_member_accepted(id); contract/example in docs/coop.md#adding-a-layer. EVENT/ACK stay kinds16/17, but ACK body v2/27 bytes requires all peers to update. Eight queued events, 16 KiB each, 1024-byte fragments; one fragment/32 ms, retry250 ms, ACK50 ms. Whole FIFO callbacks are not gameplay authorization or durable delivery. Handle queue-full; validate signed rules, current session, authority/context/age/bounds and recheck bounded work on the game thread. Consent alone is not the completed-backup gate.

Rebase resolutions

Part5 met #1930's API bump: retain ocarina bindings, then text API20 and Co-op API21 (base API19). Part6 met #1864: retain game_lang exclusion and row_key(...), saved_value(...). #2435 keyboard prompts, #2574 DLSS failure hook, #2541 aspect migration, #2116 POSIX identity and Bug report/Restart/Quit remain. Part5 additionally keeps typed values/caret widths literal and migrates 13 custom-size translations; labels/placeholders stay localized. Replaying CRT guards conflicted with that new fixture: both retained, no old assertion removed. Other commits had no textual rebase conflicts.

Original six parts

Historical tested hashes versus current counterparts; per-prefix tests retain their tested identities:

Original part Published / historical tested commit Rebased counterpart
1 e27f1a92 ae4c5a3b
2 52c471bb 5ed79bfd
3 2f34da9a 67662ccb
4 841492c4 b8a6fabc
5 b4ec89b7 6b9ab189
6 71d4ab0f eb904f9c
# Commit What it adds Its tests (all run at that commit)
1 e27f1a92 Standard-library UDP relay and C codec (keyed BLAKE2b, XChaCha20-Poly1305, EdDSA), Monocypher4.0.2 and docs; no game access. test_coop_proto.py (gcc and clang), test_coop_relay.py, test_appimage_source_headers.py
2 52c471bb Game-free flag registers, per-player key/rupee/ammunition pools, acceptance by key and maker-signed rules. test_coop_sync.py (gcc and clang)
3 2f34da9a Opt-in UDP thread/socket: join/rejoin, roster, sealed messages, leave before counter reuse, graceful goodbye; no game-memory access. test_coop_session.py (gcc and clang), test_coop_env_file.py, test_coop_options_text.py, test_output_resolution.py, test_env_file.py, test_control.py (gcc and clang), test_display_startup_failure.py, test_env_on.py
4 841492c4 Runtime classification from game groups, no extracted flag list; SetBool/SetInt only raise values after acceptance/backup. Includes the Lens item-count fix. test_oot_coop.py (gcc and clang), test_save_copy.py (gcc and clang), test_env_file.py, test_present_trace.py, test_coop_env_file.py, test_env_on.py
5 b4ec89b7 Typed rows, caret/paste and window text queue; take_text/API20, first used by Custom output size. test_menu_text_field.py, test_menu_restart_slots.py, test_screensaver.py, test_menu_env_overrides.py, test_preset_exempt.py, test_menu_keys.py, test_menu_sections.py; vk_menu.cpp with -Wall -Wextra: 2 warnings, both main's
6 71d4ab0f — feat(menu): a Co-op tab to make, join, accept and leave a co-op room the Insert menu's last tab: Your name and Relay (text fields), Make a room, Join a room, the room's secret (masked), the members with their key fingerprints, Accept the room, Leave the room, the maker's Remove a player and Reset the pools, a Room rules page saved under the options file's coop_* keys (no numeric option ids), MKWVK_API_VERSION 21. 12 files, +1321 -42 test_menu_coop_tab.py, test_menu_text_field.py, test_env_on.py, test_menu_env_overrides.py, test_preset_exempt.py, test_control.py, test_menu_keys.py, test_menu_sections.py; vk_menu.cpp as at 5

How to try it

You need this branch built (docs/development.md), from your own copy of the game as always.

On two PCs:

  1. One of you runs the relay where both games can reach it by UDP (a forwarded port, or a small server): python -I scripts/coop_relay.py --port 24680 (--help lists its limits).
  2. Each of you starts the game and opens the Insert menu (Insert, or the menu shortcut you chose); the Co-op tab is the last one.
  3. Your name: Enter, type a name, Enter. Relay: Enter, type the relay's address (host or host:port; the port is 24680 if you leave it out), Enter. Both are saved in hookshot_options.ini.
  4. The first player chooses Make a room. Room secret: Enter shows it, Enter hides it. Give it to your friend privately: whoever has it can join.
  5. The second player chooses Join a room, types or pastes (Ctrl+V) the secret, Enter.
  6. Load your save. When the tab shows Accept the room (it shows once a save is in play), read the key fingerprints to each other, then press it twice. Your whole save folder is copied to coop-backup/ first; docs/coop.md says how to go back to it.
  7. Play. What one of you gets reaches the other's game through the game's own flags, so it comes without the game's "you got" message and pose. Shown so far between two games: items (given with the debug tools, and the Kokiri Sword from a real chest on an earlier head of this stack), a chest that one player opened showing open in the other's game, and a song marker.
  8. Leave the room (press twice), or just close the game: it says goodbye to the room.

On one PC, two folders: copy the built game folder twice, start the relay with --host 127.0.0.1, give each game its own user folder (./hookshot --user <folder> on Linux), use 127.0.0.1:24680 as the Relay in both, and follow steps 2 to 8.

Optional maker hosting: set coop_host=1 in hookshot_options.ini or real-environment HOS_COOP_HOST=1 before startup, then Make. Off by default; Join never hosts. Empty Relay uses UDP24680; 127.0.0.1:<port> chooses another; foreign hosts are refused. coop_port still means the client socket. Friends use the maker's reachable address and receive the secret privately. Internet needs UDP forwarding/firewall permission and may fail behind CGNAT. No UPnP/firewall changes or hosting UI row are added. Leaving/switching off/closing the maker ends its hosted room.

How it was tested

Current head — game-free

Arch/Linux 7.2.15-1-omarchy, i9-13900K; GCC 16.2.1, Clang 23.1.3, the project's pinned Python. Focused 2026-11-11T22:59:51-06:00 → 2026-11-11T23:08:53-06:00. 31 files in both compiler environments: 60 pass/2 fail; pure Python checks are not two native compilations. Loads are one-minute endpoints, not timing/performance claims.

Focused invocations: exit/expected and load
Check (tests/) GCC: exit; load Clang: exit; load
test_bug_report.py 0; 10.77 → 12.15 0; 40.08 → 40.16
test_coop_env_file.py 0; 12.15 → 12.15 0; 40.16 → 40.16
test_coop_events.py 0; 12.15 → 14.27 0; 40.16 → 40.89
test_coop_host_relay.py 0; 14.27 → 21.65 0; 40.89 → 16.82
test_coop_options_text.py 0; 21.65 → 21.65 0; 16.82 → 16.82
test_coop_proto.py 0; 21.65 → 21.84 0; 16.82 → 16.82
test_coop_relay.py 0; 21.84 → 21.84 0; 16.82 → 16.19
test_coop_session.py 0; 21.84 → 33.59 0; 16.19 → 9.18
test_coop_sync.py 0; 33.59 → 41.34 0; 9.18 → 8.55
test_env_file.py 0; 41.34 → 41.48 0; 8.55 → 8.55
test_menu_coop_tab.py 0; 41.48 → 41.48 0; 8.55 → 8.55
test_menu_env_on.py 0; 41.48 → 41.20 0; 8.55 → 8.55
test_menu_env_overrides.py 0; 41.20 → 41.20 0; 8.55 → 8.55
test_menu_hit.py 0; 41.20 → 41.20 0; 8.55 → 8.55
test_menu_hooks.py 0; 41.20 → 41.20 0; 8.55 → 8.55
test_menu_i18n.py 1; 41.20 → 41.20 1; 8.55 → 8.55
test_menu_keys.py 0; 41.20 → 41.20 0; 8.55 → 8.55
test_menu_mouse_scroll.py 0; 41.20 → 41.20 0; 8.55 → 8.35
test_menu_restart.py 0; 41.20 → 41.20 0; 8.35 → 8.35
test_menu_restart_slots.py 0; 41.20 → 41.20 0; 8.35 → 8.35
test_menu_sections.py 0; 41.20 → 41.20 0; 8.35 → 8.35
test_menu_setting_walk.py 0; 41.20 → 41.20 0; 8.35 → 8.35
test_menu_size.py 0; 41.20 → 41.20 0; 8.35 → 8.35
test_menu_text_field.py 0; 41.20 → 41.20 0; 8.35 → 8.35
test_menu_tune.py 0; 41.20 → 41.20 0; 8.35 → 8.35
test_menu_utf8.py 0; 41.20 → 40.46 0; 8.35 → 8.35
test_oot_coop.py 0; 40.46 → 39.55 0; 8.35 → 8.24
test_present_trace.py 0; 39.55 → 39.55 0; 8.24 → 8.24
test_save_copy.py 0; 39.55 → 40.08 0; 8.24 → 7.43
test_screensaver.py 0; 40.08 → 40.08 0; 7.43 → 7.43
test_vcore_fs_open.py 0; 40.08 → 40.08 0; 7.43 → 7.43

One whole suite: 467 passed, 15 failed, 20 skipped in 143 s (linux, 32 at once); exit1; load 11.65 → 39.26; 2026-11-11T23:11:01-06:00 → 2026-11-11T23:13:23-06:00. CC=gcc CXX=clang++ python -I -B tests/run_all.py. Only failing configurations were run once on untouched de866017e1584372fc75a1caab3784b3ba1d9f98; all failed there too. No zero-test baseline or whole-main-suite pass is claimed. The suite remains red.

Configuration Inherited check Stack/main exit Main 1-min load Observed on both
focused/gcc test_menu_i18n.py 1 / 1 10.28 → 10.28 A translated Gamepad prompt key no longer exists in vk_menu.cpp.
focused/clang test_menu_i18n.py 1 / 1 10.28 → 10.28 A translated Gamepad prompt key no longer exists in vk_menu.cpp.
suite/suite test_audio_aux.py 1 / 1 39.26 → 6.36 Extracted audio fixture lacks the env_on declaration used by reverb_frame.
suite/suite test_audio_biquad.py 1 / 1 6.36 → 6.36 Extracted audio fixture lacks the env_on declaration used by reverb_frame.
suite/suite test_display_startup_failure.py 1 / 1 6.36 → 18.10 Extracted display startup fixture lacks window_title.
suite/suite test_dlss_checks.py 1 / 1 18.10 → 6.33 The under-a-third-wide policy case fails at fixture line40 on both trees.
suite/suite test_env_on.py 1 / 1 6.33 → 6.33 RX_VK_CAPTURE_KEEP_OLD and RX_VK_CAP_BLOCKS still use set-means-on reads in vk_memory.cpp.
suite/suite test_framegen_preset.py 1 / 1 6.33 → 6.33 Menu extraction lacks the temporal_reset_failure stand-in.
suite/suite test_hdr_encode.py 1 / 1 6.33 → 6.33 The DLSS engine/chain HDR-colour wiring assertion fails.
suite/suite test_linux_docker_packages.py 1 / 1 6.33 → 6.33 The PPA sets disagree: ubuntu-toolchain-r/test versus the parsed --no-update token.
suite/suite test_menu_i18n.py 1 / 1 6.33 → 6.33 A translated Gamepad prompt key no longer exists in vk_menu.cpp.
suite/suite test_mods_size_table.py 1 / 1 6.33 → 6.33 The routing fixture fails to link hos_xinput_pads from prompts.c.
suite/suite test_mouse_native.py 1 / 1 6.33 → 6.33 The camera fixture expects yaw -1 but observes yaw4 (pitch10).
suite/suite test_readback_part.py 1 / 1 6.33 → 8.47 The extracted readback fixture has an extra closing brace and out-of-scope timeline/completed.
suite/suite test_rumble_menu.py 1 / 1 8.47 → 8.47 Menu extraction lacks the temporal_reset_failure stand-in.
suite/suite test_texel_buffer_ownership.py 1 / 1 8.47 → 8.47 The extracted texel fixture lacks dead_import_bytes, also breaking std::min deduction.
suite/suite test_wayland_libdecor_hint.py 1 / 1 8.47 → 8.47 Extracted display-init fixture lacks window_title.

Behavioral before/after proofs

Every ordinary area-changing loading zone was affected by the old per-area clock, not just warp: Players s2 B logged 2 false loads going to the field/back, hidden by HOS_COOP_ACCEPT=always. The current real-engine fixture preserves accepted offers/copies across area changes, but requires both anew for another slot, same-slot reload and new game. No corrected-head game proof yet.

All 14 event cases pass with ASan/UBSan, including loss/reordering and cancel, cancel-accept, reset-ack, delayed-ack, reaccept-step; the concurrent case also passes TSan. Those 5 new cases reject the previous helper. The Portuguese-table compiled value-cell fixture rejects renderer50bd1255 and passes literal Player/Off, clipping/caret and translated-placeholder checks; no screenshot proof is implied.

Family Old clock Old event helper Old renderer Current Before loads: clock; events; renderer
gcc exit 1; 9 checks failed 5 assertions rejected exit1: literal-value assertion all pass 7.43 → 7.82; 7.82 → 7.82; 7.82 → 7.82
clang exit 1; 9 checks failed 5 assertions rejected exit1: literal-value assertion all pass 7.82 → 8.48; 8.48 → 9.88; 9.88 → 9.88

Windows

17 Windows units + 1 native Linux-only unit; zero errors/name clashes. Clang --target=x86_64-w64-mingw32, llvm-mingw UCRT headers; generated headers from 71d4ab0 on fce5345 (existing actual generated headers, not a new game build). host/posix/display_sdl.c is Linux-only. Warnings retained: -Wdeprecated-enum-enum-conversion: 1, -Wmissing-field-initializers: 7, -Wunused-const-variable: 1, -Wunused-function: 3.

Syntax units, warnings and loads
Target / translation unit Exit; errors; name clashes Warnings by kind 1-min load
windows / host/control.c 0; 0; 0 none 10.77 → 10.77
windows / host/coop.c 0; 0; 0 none 10.77 → 12.15
windows / host/coop_menu.c 0; 0; 0 none 12.15 → 12.15
windows / host/coop_net.c 0; 0; 0 none 12.15 → 12.15
windows / host/coop_proto.c 0; 0; 0 none 12.15 → 12.15
windows / host/coop_relay.c 0; 0; 0 none 12.15 → 12.15
windows / host/coop_state.c 0; 0; 0 none 12.15 → 12.15
windows / host/display.c 0; 0; 0 none 12.15 → 12.15
windows / host/hos_main.c 0; 0; 0 -Wmissing-field-initializers: 1, -Wunused-const-variable: 1, -Wunused-function: 1 12.15 → 12.15
windows / host/hos_relaunch.c 0; 0; 0 none 12.15 → 12.15
windows / host/hos_sdl.c 0; 0; 0 none 12.15 → 12.15
windows / host/nn_applet.c 0; 0; 0 none 12.15 → 12.15
windows / host/nn_fs.c 0; 0; 0 none 12.15 → 12.15
windows / host/nvn_native.c 0; 0; 0 -Wunused-function: 1 12.15 → 12.15
windows / host/oot_coop.c 0; 0; 0 none 12.15 → 12.15
linux-only / host/posix/display_sdl.c 0; 0; 0 none 12.15 → 12.15
windows / host/rx_backend.c 0; 0; 0 -Wmissing-field-initializers: 6 12.15 → 13.42
windows / gpu/vk/vk_menu.cpp 0; 0; 0 -Wdeprecated-enum-enum-conversion: 1, -Wunused-function: 1 13.42 → 13.42

Windows CI compiles portable co-op fixtures but lacks generated headers for this full host/menu sweep. Syntax is not a Windows link/run. Kuba's Windows11 report, on 71d4ab0 plus his bind rename, established build/link/run, loopback Make/Join/Leave and physical typing. His fix is co-authored here. Accept/progress, LAN/cross-OS/controllers/suite were not tested there; he also reported a shader-start fault seen on releases, so it was not fault-free.

Historical fixture repair and stress proof — not a final-head stress rerun

On fe3d1eda (on published 71d4ab0f), 2026-11-11T18:22:54-06:00 to 2026-11-11T19:58:10-06:00: 40 complete session invocations, 320 modes passed, no session failure. Each compiler ran the complete test 20 times, not a reduced menu-only selection. Every start overlapped an active gated whole suite; endpoint one-minute loads were 5.93–181.58. These are correctness checks under load, not timing measurements.

The separate TSan lifetime probe observed a joined helper TID still in /proc/self/task after 624/1000 joins. The repair waits for that specific joined TID to disappear, bounded at five seconds, before the fixture counts its baseline. It changes no production lifetime rule and removes no exact thread/socket check. The deliberate still-live production-worker mutation was caught: exit 1, load 19.98 → 51.99, with FAIL coop_session: and the co-op thread has ended (threads 3, before co-op was on 2).

The 18 load-generating whole suites were not all green. All retained test_env_on.py; additional compiler failures are named in the table. The logs record compiler SIGKILLs, a killed cc1plus, and a shaderbatch compile failure without a diagnostic; their causes were not established. They were not rerun to erase failures. The later fixture fix does not turn the older published suite into a pass.

Every load suite, including all failures
Load suite Pass/fail/skip Exit 1-min load Every failure
1 459/1/20 1 36.56 → 34.04 tests/test_env_on.py
2 458/2/20 1 34.04 → 33.18 nx2/test_depth_sample_types.py, tests/test_env_on.py
3 459/1/20 1 33.18 → 34.26 tests/test_env_on.py
4 459/1/20 1 34.26 → 31.73 tests/test_env_on.py
5 459/1/20 1 31.73 → 26.91 tests/test_env_on.py
6 459/1/20 1 8.04 → 98.67 tests/test_env_on.py
7 459/1/20 1 98.67 → 36.21 tests/test_env_on.py
8 459/1/20 1 8.64 → 29.26 tests/test_env_on.py
9 459/1/20 1 29.26 → 26.97 tests/test_env_on.py
10 458/2/20 1 5.93 → 38.94 tests/test_bake_mimo.py, tests/test_env_on.py
11 458/2/20 1 38.94 → 49.81 tests/test_env_on.py, tests/test_lower_immediate_not_register.py
12 458/2/20 1 9.43 → 54.87 nx2/test_unsigned_image_types.py, tests/test_env_on.py
13 459/1/20 1 54.87 → 53.18 tests/test_env_on.py
14 459/1/20 1 53.18 → 30.87 tests/test_env_on.py
15 459/1/20 1 7.10 → 34.41 tests/test_env_on.py
16 458/2/20 1 34.41 → 30.34 nx2/test_unsigned_image_types.py, tests/test_env_on.py
17 459/1/20 1 30.34 → 25.31 tests/test_env_on.py
18 459/1/20 1 8.74 → 21.72 tests/test_env_on.py

Historical Windows format/CRT proof

The real build uses UCRT, ANSI stdio0 and vsnprintf, not GNU's wrapper. Explicit Microsoft-format cross-GCC rejects the old traces and passes %u/unsigned; unchanged coop_net passes. Clang emits both COFF objects; Linux GCC/Clang sessions pass8/8 each and codecs79 vectors/400000 fuzz inputs each. All19 steps match expected exits; attributes/-Werror unchanged. Secure-CRT guards precede fixture headers. Actual Windows fixture execution remains pending.

Every recorded format-proof command outcome and load
Step Exit / expected 1-min load start → end
gcc-version 0 / 0 not retained in cached preflight
gcc-target 0 / 0 not retained in cached preflight
clang-version 0 / 0 not retained in cached preflight
real-build-stdio-macros 0 / 0 not retained in cached preflight
before-gcc-ms-coop 1 / 1 not retained in cached preflight
before-gcc-ms-coop_net 0 / 0 not retained in cached preflight
before-gcc-ms-protocol-fixture-with-headers 1 / 1 9.43 → 9.43
after-gcc-ms-coop 0 / 0 9.43 → 9.43
after-gcc-ms-coop_net 0 / 0 9.43 → 36.94
after-gcc-ms-protocol-fixture-with-headers 0 / 0 36.94 → 36.94
after-clang-windows-coop 0 / 0 36.94 → 85.48
object-kind-coop 0 / 0 85.48 → 85.48
after-clang-windows-coop_net 0 / 0 85.48 → 107.14
object-kind-coop_net 0 / 0 107.14 → 107.14
real-build-stdio-symbols 0 / 0 107.14 → 107.14
linux-gcc-test_coop_session 0 / 0 107.14 → 126.28
linux-gcc-test_coop_proto 0 / 0 126.28 → 118.25
linux-clang-test_coop_session 0 / 0 118.25 → 95.67
linux-clang-test_coop_proto 0 / 0 95.67 → 97.17

Previous integration 94cb598f on55b976c0: 60 focused passes; 465 passed, 1 failed, 20 skipped in 221 s (linux, 32 at once), only env_on failed. Those results are not relabelled as this head.

Historical build and game evidence

The retained account below belongs only to its named heads. “Current/head/not pushed/queued” inside dated additions is historical, not this rebase. Other-player figures in follow-on experiments are outside this pull request. Pictures and both dated additions/chest are unchanged.

Linux (Arch, kernel 7.2.15-1-omarchy, i9-13900K), RTX 4090 with NVIDIA 615.78.08, clang 23.1.3, gcc 16.2.1, Python 3.12.16. Head 71d4ab0 on main fce5345, 2026-11-11 (US Central). The final session was headless, output 2048x1152, screenshots 1280x720.

Full make_game.py build: exit 0 and its completion line; the two game copies have matching executable / renderer SHA-256 prefixes 92986f4c15a12e84 / 65e42fecc682aed9, both stamped 71d4ab0 on fce5345. One-minute load at build request / completion: 127.77 / 16.94. No build-speed or frame-time claim.

Each published commit on its own (historical)

Each of the six commits checked out alone, and the tests it adds or touches run there, each with the compiler tests/run_all.py gives it (and the C tests that default to gcc once more with clang): 39 runs, 5 failed. The failures: test_env_on.py at commits 3, 4, 6 fails on main fce5345 itself, on HOS_FPS_LOG (host/oot_high_fps.c:15) (main's #2470 reads that switch as set-means-on; nothing of this stack); test_coop_session.py at commit 3 (--cc gcc and --cc clang) failed its menu fixture's process-thread counts at load 74.46 and 102.99; the complete standalone test then passed all 8 runs with each compiler after starting at load 39.72 (GCC ended at 29.20, Clang at 14.26). The repeated code and fixture were unchanged at the then-final 71d4ab0 head. This is evidence of a load-sensitive fixture, not an all-green suite or proof that the fixture is repaired. Besides what they compile and run, these tests check in the source where each part is wired in (the build compiles Monocypher; the game starts co-op beside the Discord status and says goodbye on both ways out; the present ends with progress sync's call and the save mount's writes take its lock; the interface's new members sit in their places in the host's table; the tab, its page and its keys in the options file).

Part / tested commit Test invocations passed / failed Renderer syntax
1 / e27f1a92 4 / 0 not a renderer part
2 / 52c471bb 2 / 0 not a renderer part
3 / 2f34da9a 7 / 3 (test_coop_session, test_env_on) not a renderer part
4 / 841492c4 7 / 1 (test_env_on) not a renderer part
5 / b4ec89b7 7 / 0 exit 0; 2 warnings
6 / 71d4ab0f 7 / 1 (test_env_on) exit 0; 2 warnings

Per-commit one-minute loads: 69.79 to 145.67. The two renderer syntax passes each retain the same enum-arithmetic and unused-function warnings recorded on main.

The whole suite on 71d4ab0 (historical)

python -I -B tests/run_all.py on 71d4ab0: 458 passed, 2 failed, 20 skipped in 227 s (linux, 32 at once) (one-minute load 125.19 → 84.33). Failures: tests/test_env_on.py: the isolated test fails on main fce5345 itself at HOS_FPS_LOG (host/oot_high_fps.c:15), introduced by #2470; tests/test_coop_session.py: the ThreadSanitizer menu fixture's thread-count assertions failed under load (including one thread more). Main has no co-op session test, so the main-tree selection ran zero tests and is not a passing baseline. The complete standalone GCC and Clang repeats described above pass on the unchanged session code. The suite was not rerun and remains failed.

Two games on 71d4ab0, from the Co-op tab alone (historical)

Two headless games on the RTX 4090, separate user folders and copies of the same child Kokiri save; Python relay on loopback. RX_VK_MENU_SCRIPT typed names/relay/secret and drove Make/Join/Accept; no automatic co-op enable/join/accept. Declared aids: HOS_COOP_ROOM fixes A's test secret, HOS_COOP_TRACE logs, HOS_COOP_TEST_SET sets B's song through the game routine, debug tools supply A's sword, and HOS_CONTROL quits gracefully. This is scripted menu input, not physical keyboard/controller proof.

Session s1 Session s2 Session s3 (71d4ab0)
Head / actual base 67d2de81 / b744b1ec f7a9640d / 940eb9ef 71d4ab0f / fce53453
Games' version line v2.1.0-rc.3-76-g67d2de81 v2.1.0-rc.3-138-gf7a9640d v2.1.0-rc.3-191-g71d4ab0f
One-minute load A start → end; B start → end 6.79 → 10.98; 6.63 → 10.98 1.35 → 10.45; 4.69 → 10.45 91.55 → 106.27; 45.17 → 106.27
The one hold on the card (UTC) 2026-11-11 17:30:03 to 2026-11-11 17:34:52 (289 s) 2026-11-11 22:13:03 to 2026-11-11 22:17:52 (289 s) 2026-11-11 23:55:45 to 2026-11-12 00:00:37 (292 s)
B started when (A's seconds; the card's free MiB; memory available) 76 s; 17369 MiB; 68 GB 75 s; 17445 MiB; 69 GB 75 s; 20589 MiB; 56 GB
Memory pressure before asking (full avg10) 0.47 0.00 0.00
Checks: stack / session / tab (PASS, FAIL, N/A) 25, 0, 0 / 29, 0, 3 / 17, 0, 0 25, 0, 0 / 29, 0, 3 / 17, 0, 0 25, 0, 0 / 29, 0, 3 / 17, 0, 0
Both left the room (relay's log, US Central) Bob 11:34:48, Alice2 11:34:51 Bob 16:17:48, Alice2 16:17:51 Bob 18:00:32, Alice2 18:00:36
Presents A / B; exit A / B 15600 / 10800; 0 / 0 15360 / 10560; 0 / 0 9960 / 7200; 0 / 0
Screenshots A / B 260 / 180 257 / 176 166 / 121
Fatal, crash, device lost, core dumps (both) 0 0 0
Kernel driver fault lines (Xid, kmemsysDoCacheOp) / all NVRM lines over the hold 0 / 0 0 / 0 0 / 0

The legacy session checker's N/A rows are the other-player figures (outside this stack), menu pictures and renaming; the tab checker covers the latter two. Its old descriptions mentioning automatic join/accept are not the test setup: the quoted log lines and the separate environment checks prove that this session used the tab.

The checks establish tab-only setup/acceptance; each backup equals its starting save (14 files); A's sword reaches B and B's song reaches A. Readback: A 1 applied/1 getter agreement; B 12 applied/11 getter agreements plus 1 Int readback; none missing. Six stats lines report no discarded datagram. Typed settings persist; both games send goodbye on the bridge's quit. The table retains launch guards, loads, counters and kernel results.

Historical 71d4ab0 session panel: A makes the room, B types the join secret, both accept, and B receives the item (game-log evidence).

  1. A's tab after Make a room: “In a room, 1 player”, Alice marked as the maker, and the room secret masked by the menu.
  2. B entering the room secret in Join a room. This input field was redacted for publication; the successful join is established by B's log and the later two-player roster, not by the “Co-op is off” status still visible in this input frame.
  3. A after accepting Bob: Bob's fingerprint is gold and the maker's Remove a player / Reset the pools rows are present.
  4. B after accepting Alice: Alice's fingerprint is gold and Accept the room has disappeared. These are each game's acceptance frames, not a claim that the pictures were simultaneous.
  5. B's game just after the item values arrived: Link remains in normal play. This image alone does not show the sword in the inventory, and no pickup animation is claimed.
  6. A separate, explicitly labelled excerpt from B's game log: the item values were applied and read back, and the sword's count was already 1 before the debug inventory read. The 71d4ab0 session did not open B's Equipment page; that visual proof belongs to the earlier chest session below.

Historical 71d4ab0f session panel: A makes the room, B types the join secret, both accept, and B receives the item (game-log evidence).

The room's secret is masked in every panel, and it is in neither game's log (searched).

Earlier sessions

The two-game sessions made on this stack's predecessor branches (the first Co-op tab, the chest and Lens sessions, the sessions with later parts on top that are not in this pull request) are in this description's edit history (its versions of 2026-11-11) and in the comments below. They are evidence for those commits, not for this head, and are left out here to keep this description to what this head carries.

Not tested

No current-head game build/play, GPU/two-game/manual-consent/loading-zone/hosted-game proof; no current Windows execution/link. No two-PC/LAN/internet/NAT/cross-OS, more than two real games, AMD/Deck/controller or Linux physical typing proof. No corrected Lens crossing, new chest/rupee chest/boss/door behavior, remote-area chest, live Remove/Reset/rule change or manual backup restoration. No performance/bandwidth proof or gameplay layer built on events.

Risks

Progress affects real saves: acceptance, whole-folder backup, classification/getter checks and game setters remain mandatory. Wrong classification can import wrong progress. Co-op is off by default with no socket/thread/table scan; hosting adds its own joined socket/thread. Relay loss, shared bandwidth ceilings, maker departure and NAT can prevent delivery.

Secret holders can forge ordinary events/ACKs; receipt is not gameplay approval. Durable progress/rules keep individual signatures; protocol/nonce/signature use still needs review. ACKv2 is incompatible with v1; callbacks borrow bytes, queue-full must be handled, session/consent changes cancel work. ABI callback order must move together. Literal-field/translation tests are not all-language playtests. A restarted maker can retain its key but bring no rules: question6 remains open. This is a substantial draft, not an all-green claim.

Overlaps

Saved git merge-tree trial at 2026-11-11T23:51:26-06:00, against main 87991bc8b21c271f2dee3daf7f06fd006faa865c: exit 0, clean at that moment, tree c23d9a4f44ee7c06ec654e273b40d24b08b1277c. This is not a continuing mergeability claim.
Standalone snapshot 2026-11-11T23:05:49-06:00: 88 PRs touch 68 files; 21 conflict entries, all hunks read. #2453 needs journal tree/commit synchronization reconciled with backup barriers; #2578's TCP gameplay must be ported deliberately into the UDP/session/consent/backup contract, not concatenated. #2600 must preserve packed-file dispatch plus save-writer accounting. Other host-ABI additions need matching declarations/positional tables/version; fixture-only conflicts retain both tests. #882's options-present flag precedes co-op's continue; #2371 retains retake persistence/cancel prompts; #2566 retains dynamic sizes plus custom editing/translations. Independent option/context tail additions are not evidence of gameplay incompatibility.

Conflicting PRs and exact paths
PR / recorded head Conflicting files Text blocks
#882 / 990febcb gpu/vk/vk_menu.cpp 1
#1521 / 4f6ccdb9 gpu/vk/vk_menu.h 1
#1543 / 2ef23f2f tests/test_present_trace.py 1
#1560 / 1fc9986f docs/development.md, gpu/vk/vk_menu.h 2
#1590 / 6fb092bb tests/test_menu_env_overrides.py, tests/test_menu_keys.py 3
#1872 / 5cf62486 host/hos_sdl.c, host/nn_fs.c, host/posix/display_sdl.c 3
#1882 / 5f85fe70 gpu/vk/vk_ui.h 1
#2151 / b5af3839 tests/test_vcore_fs_open.py 3
#2261 / 919ba0ee host/rx_backend.c 1
#2317 / 26793b60 gpu/vk/vk_api.h, host/rx_backend.c, tests/test_env_file.py 4
#2371 / a2bed6b2 gpu/vk/menu_i18n.h, gpu/vk/vk_menu.cpp 15
#2430 / d827111d tests/test_env_file.py 3
#2453 / 92ea6643 host/nn_fs.c, readme.md 10
#2459 / 9bd89259 gpu/vk/vk_menu.h, host/rx_backend.c 2
#2478 / f654ef20 host/rx_backend.c 2
#2479 / 44e48391 host/rx_backend.c 2
#2530 / 4c276b88 gpu/vk/vk_menu.h 1
#2566 / e0cedba7 gpu/vk/menu_i18n.h, gpu/vk/vk_api.h, gpu/vk/vk_menu.cpp, host/rx_backend.c, readme.md 19
#2578 / c5a0c1d3 docs/coop.md, host/oot_coop.c, readme.md, tests/test_oot_coop.py 8
#2600 / 041c9f2b gpu/vk/vk_api.h, host/nn_fs.c, host/rx_backend.c, tests/test_vcore_fs_open.py 8
#2604 / 6ecc6a79 gpu/vk/vk_api.h 1

All 9 unchanged co-op lane heads conflict in wholesale trial merges; rewritten-base add/add blocks inflate counts. Rebase topic ranges preserving ABI, Windows binder, clock, epochs, accepted-session binding, hosting and localization. No helper/lane branch moved.

Questions for you

  1. Merge this as one pull request, or split it back into one per part? The original six parts and the later repair/helper/hosting commits are separated above; the per-prefix tests remain historical, not reruns of every rebased commit.
  2. The socket itself (question 1 of Co-op: friends share progress across their own games (proposal and claim, before any pull request) #2269): is a UDP socket in the host acceptable at all?
  3. Where should the relay live: in this repository, as here (it and the game must speak the same version), or in one of its own? (Question 3 of Co-op: friends share progress across their own games (proposal and claim, before any pull request) #2269.)
  4. Is Monocypher under third_party/ acceptable, or would you rather have another library behind the same few functions?
  5. Is the Co-op tab, the strip's last, where you want it, and are coop_name, coop_server, coop the names you want in hookshot_options.ini?
  6. A room's maker whose game started again: should it sign the rules again with its kept key when no rules are in force, or require a returning player who accepted them? In CoopGame4's two-game run on f7a9640d, the restarted maker kept its key but neither game obtained the rules or shared progress.

What comes after it

Other players' Links (coop-players) and the tab's additions (coop-tab-more, coop-tab-where) are separate follow-on lanes.
Warp (coop-warp), hits and duels (coop-hits), and the shared world and area state (coop-world, coop-area) build above them; none of these lanes is in this pull request.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra noreply@openai.com

@mooki000 mooki000 changed the title feat(coop): a relay and an encrypted wire format for co-op rooms feat(coop): co-op rooms: a relay, progress sync, and a Co-op tab to make, join and accept Nov 12, 2026
@mooki000

Copy link
Copy Markdown
Contributor Author

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

Copy link
Copy Markdown
Contributor Author

@NotRexed @hckrman101 an update that bears on the questions in this draft's description, and one new question.

Two co-op drafts are becoming one. itzdaface opened #2578 the same evening as this stack: Windows only, a direct link over a VPN, and well ahead of mine on what players see (the other players as animated Links, shared enemies, bosses, PvP, played with real friends). We agreed there to put them together under this pull request: this stack as the link, the session, consent and progress sync, and his bodies, enemies, bosses and PvP on top as his own commits, ported to this stack's sealed messages. He is building this branch on Windows first, which nobody has done yet. Both drafts stay drafts until you have answered question 2 here: whether a socket in the host is acceptable at all.

Stages (question 1 here, now larger). Together this is a lot to review. Would you take it in this order, each stage off by default and reviewable alone?

  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 feat(menu): text fields in the Insert menu, and a Custom output size typed in it #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

Copy link
Copy Markdown

@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

mooki000 commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor Author

@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 #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

Copy link
Copy Markdown

@mooki000 sounds good on all of it. Pull requests against coop-relay-wire works for me, and I'm happy with #2274 staying the one. I'll wait for the new head, rebuild, and test Accept, sync and "Host on this PC" on Windows, including what the firewall asks.

mooki000 and others added 18 commits November 11, 2026 22:26
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 <noreply@anthropic.com>
…ted games

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 <noreply@anthropic.com>
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:
  hookshot_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 hookshot_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: hookshot_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 <noreply@anthropic.com>
Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
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 hookshot_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 <noreply@anthropic.com>
Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
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
hookshot_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.
tests/test_menu_keys.py (main's), which cuts menu_tick's Esc block, ends it at
the field's branch, the first of the keys' branches now.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
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 hookshot_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 <noreply@anthropic.com>
Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
…ock's bind

Adopt Kuba’s bind_tables rename from his Windows build/play report on NotRexed#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 <noreply@openai.com>

Co-authored-by: Kuba <255278380+itzdaface@users.noreply.github.com>
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 <noreply@openai.com>
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 <noreply@openai.com>
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 NotRexed#2594.

Assisted-by: GPT-6 Astra

Co-Authored-By: GPT-6 Astra <noreply@openai.com>
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 <noreply@openai.com>
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 <noreply@openai.com>
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 <noreply@openai.com>
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 <noreply@openai.com>
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 <noreply@openai.com>
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 <noreply@openai.com>
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 <noreply@openai.com>
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 <noreply@openai.com>
@mooki000

Copy link
Copy Markdown
Contributor Author

@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 and others added 2 commits November 12, 2026 00:31
…indows

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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@mooki000

Copy link
Copy Markdown
Contributor Author

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

Copy link
Copy Markdown
Contributor Author

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

Copy link
Copy Markdown
Contributor Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants