Repository navigation
Conversation
7b7ece9 to
71d4ab0
Compare
|
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 The description is rewritten for it: what each commit adds and its tests, how to try it on two PCs or on one, the two-game session on this head, what failed in the test runs and why, what is not tested (Windows first of all: I have no Windows PC and would be glad of a tester), and six questions at the end. It stays a draft until the first of them, a socket in the host at all, is answered. |
|
@NotRexed @hckrman101 an update that bears on the questions in this draft's description, and one new question. Two co-op drafts are becoming one. itzdaface opened #2578 the same evening as this stack: Windows only, a direct link over a VPN, and well ahead of mine on what players see (the other players as animated Links, shared enemies, bosses, PvP, played with real friends). We agreed there to put them together under this pull request: this stack as the link, the session, consent and progress sync, and his bodies, enemies, bosses and PvP on top as his own commits, ported to this stack's sealed messages. He is building this branch on Windows first, which nobody has done yet. Both drafts stay drafts until you have answered question 2 here: whether a socket in the host is acceptable at all. Stages (question 1 here, now larger). Together this is a lot to review. Would you take it in this order, each stage off by default and reviewable alone?
The relay (question 3 here, now more pressing). His players asked why co-op needs a VPN, and a relay that someone has to run is the same hurdle moved. I am adding "host on this PC": the relay inside the game for a room's maker, so that on one home network there is nothing else to start, and across the internet the maker forwards one UDP port. Beyond that: would the project consider hosting a public relay? It cannot read or change what it forwards (the room's secret never reaches it), and it has limits per member, per network and in all. |
|
@mooki000 Windows results for Setup: Windows 11, Ryzen 7 7700X, RTX 3060 12 GB, 32 GB RAM. Built with 1. Does it build? Not as it is, by one name
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 2. Does it run? YesFirst 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 waysTwo games on one PC, two folders, from the environment (
By hand, from the Co-op tab, in a window, with a second headless game waiting in a room:
4. What does the firewall ask? NothingNo Windows Firewall prompt appeared, and there is no firewall rule for either test exe afterwards. That is with the relay on loopback only, so it says nothing yet about a relay on another machine. Not tested yet
Two small notes
The fixIt is one commit on my branch ( |
|
@itzdaface thank you, this is exactly the report this draft was missing. The fix goes in as you wrote it ( The head is about to move, so please wait for it before the next test. The next push replaces
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 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 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. |
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>
71d4ab0 to
2be28f8
Compare
|
@itzdaface the new head is up: What is above the six commits you built:
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 |
…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>
|
The two-game session on the new line, as promised. Built from 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 What it showed, in order (2026-11-12 02:04:26 to 02:09:14 local, 288 s; both games exited 0):
No fatal line, crash or device loss in either log, and no kernel or driver message during the run. The run's three checklists (the stack's, the tab's and the older session's) pass with no failure. Not shown by this run: the relay inside the maker's game (a link-only run of it is next on this PC tonight), a loading zone after acceptance (the fix is shown by its game-free test only), two PCs, Windows, a controller, more than two players. |
|
The in-game relay, two games on this PC, same build (
I am repairing the stop step and running that part once more tonight; the result goes here. |
|
The repaired rerun of the in-game relay's last step (same build
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. |

What this is for: co-op (#2269): each friend plays their own game/save, sharing accepted progress through the game's setters. This draft adds bounded events and optional maker-PC hosting, not other players' Links, enemies, combat or warp. Measured:
2be28f893fa378a28de2a110caf491970be7b32eonde866017e1584372fc75a1caab3784b3ba1d9f98: game-free focused 60 pass / 2 fail, suite 467 pass / 15 fail / 20 skip, Windows-target syntax below. Every failure is reproduced on untouched base. Not measured: no game build/play, current Windows execution/link, LAN/cross-OS/controller or performance proof on this head. All game pictures below are historical.Draft: 18 commits. Rebase recorded 2026-11-11T22:57:08-06:00; evidence assembled 2026-11-11T23:52:40-06:00. The socket decision remains open.
Problem
nn::socketstays offline; there was no host path for players' games to find a room or exchange progress.host/coop_net.candhost/coop.ckeep that networking outside the guest game.scripts/coop_relay.pyforwards sealed messages;host/coop_proto.cderives separate locator and message keys so the relay need not know the room secret or open progress.host/coop_state.cmerges accepted, signed progress;host/oot_coop.cchecks the live flag table and calls the game's setters only after acceptance and a save copy.gpu/vk/vk_menu.cppexpose the same session and progress commands while the game runs.Additional defects: Windows name/printf/CRT conflicts; unstable fixture task counts; area resets mistaken for save loads; cancelled event fragments blocking later work or combining ACK epochs; localized UI helpers translating typed data.
What changed
Additions above the original six
1c60356fbind→bind_tablesrename removes the Winsock declaration collision;IN→COOP_FLAG_INalso avoids the Windows annotation macro. Existing fixture callers/extraction markers migrate with their checks. Co-authored with Kuba.68088064184cceb0%uplusunsigned, preserving format attributes and warning errors; no global ANSI-stdio override.1cd02fc41f1d0a72d2fc0f7f35931fa8b8d0cbedcoop_member_accepted(id); a roster entry or recycled member number is not acceptance.c517e74697f56483coop_host/HOS_COOP_HOST): its own socket/thread, local maker authorization, cookie proof, sealed forwarding, bounded state and joined shutdown; no router/firewall automation.dcf844302be28f89Layer interface:
coop_send_event(member, kind, bytes, n),coop_on_event(kind, callback, user),coop_member_accepted(id); contract/example indocs/coop.md#adding-a-layer. EVENT/ACK stay kinds16/17, but ACK body v2/27 bytes requires all peers to update. Eight queued events, 16 KiB each, 1024-byte fragments; one fragment/32 ms, retry250 ms, ACK50 ms. Whole FIFO callbacks are not gameplay authorization or durable delivery. Handle queue-full; validate signed rules, current session, authority/context/age/bounds and recheck bounded work on the game thread. Consent alone is not the completed-backup gate.Rebase resolutions
Part5 met #1930's API bump: retain ocarina bindings, then text API20 and Co-op API21 (base API19). Part6 met #1864: retain
game_langexclusion androw_key(...), saved_value(...). #2435 keyboard prompts, #2574 DLSS failure hook, #2541 aspect migration, #2116 POSIX identity and Bug report/Restart/Quit remain. Part5 additionally keeps typed values/caret widths literal and migrates 13 custom-size translations; labels/placeholders stay localized. Replaying CRT guards conflicted with that new fixture: both retained, no old assertion removed. Other commits had no textual rebase conflicts.Original six parts
Historical tested hashes versus current counterparts; per-prefix tests retain their tested identities:
e27f1a92ae4c5a3b52c471bb5ed79bfd2f34da9a67662ccb841492c4b8a6fabcb4ec89b76b9ab18971d4ab0feb904f9ce27f1a92test_coop_proto.py(gcc and clang),test_coop_relay.py,test_appimage_source_headers.py52c471bbtest_coop_sync.py(gcc and clang)2f34da9atest_coop_session.py(gcc and clang),test_coop_env_file.py,test_coop_options_text.py,test_output_resolution.py,test_env_file.py,test_control.py(gcc and clang),test_display_startup_failure.py,test_env_on.py841492c4test_oot_coop.py(gcc and clang),test_save_copy.py(gcc and clang),test_env_file.py,test_present_trace.py,test_coop_env_file.py,test_env_on.pyb4ec89b7test_menu_text_field.py,test_menu_restart_slots.py,test_screensaver.py,test_menu_env_overrides.py,test_preset_exempt.py,test_menu_keys.py,test_menu_sections.py;vk_menu.cppwith-Wall -Wextra: 2 warnings, both main's71d4ab0f—feat(menu): a Co-op tab to make, join, accept and leave a co-op roomcoop_*keys (no numeric option ids),MKWVK_API_VERSION21. 12 files, +1321 -42test_menu_coop_tab.py,test_menu_text_field.py,test_env_on.py,test_menu_env_overrides.py,test_preset_exempt.py,test_control.py,test_menu_keys.py,test_menu_sections.py;vk_menu.cppas at 5How to try it
You need this branch built (docs/development.md), from your own copy of the game as always.
On two PCs:
python -I scripts/coop_relay.py --port 24680(--helplists its limits).hostorhost:port; the port is 24680 if you leave it out), Enter. Both are saved inhookshot_options.ini.coop-backup/first; docs/coop.md says how to go back to it.On one PC, two folders: copy the built game folder twice, start the relay with
--host 127.0.0.1, give each game its own user folder (./hookshot --user <folder>on Linux), use127.0.0.1:24680as the Relay in both, and follow steps 2 to 8.Optional maker hosting: set
coop_host=1inhookshot_options.inior real-environmentHOS_COOP_HOST=1before startup, then Make. Off by default; Join never hosts. Empty Relay uses UDP24680;127.0.0.1:<port>chooses another; foreign hosts are refused.coop_portstill means the client socket. Friends use the maker's reachable address and receive the secret privately. Internet needs UDP forwarding/firewall permission and may fail behind CGNAT. No UPnP/firewall changes or hosting UI row are added. Leaving/switching off/closing the maker ends its hosted room.How it was tested
Current head — game-free
Arch/Linux 7.2.15-1-omarchy, i9-13900K; GCC 16.2.1, Clang 23.1.3, the project's pinned Python. Focused 2026-11-11T22:59:51-06:00 → 2026-11-11T23:08:53-06:00. 31 files in both compiler environments: 60 pass/2 fail; pure Python checks are not two native compilations. Loads are one-minute endpoints, not timing/performance claims.
Focused invocations: exit/expected and load
One whole suite:
467 passed, 15 failed, 20 skipped in 143 s (linux, 32 at once); exit1; load 11.65 → 39.26; 2026-11-11T23:11:01-06:00 → 2026-11-11T23:13:23-06:00.CC=gcc CXX=clang++ python -I -B tests/run_all.py. Only failing configurations were run once on untouchedde866017e1584372fc75a1caab3784b3ba1d9f98; all failed there too. No zero-test baseline or whole-main-suite pass is claimed. The suite remains red.test_menu_i18n.pytest_menu_i18n.pytest_audio_aux.pytest_audio_biquad.pytest_display_startup_failure.pytest_dlss_checks.pytest_env_on.pytest_framegen_preset.pytest_hdr_encode.pytest_linux_docker_packages.pytest_menu_i18n.pytest_mods_size_table.pytest_mouse_native.pytest_readback_part.pytest_rumble_menu.pytest_texel_buffer_ownership.pytest_wayland_libdecor_hint.pyBehavioral before/after proofs
Every ordinary area-changing loading zone was affected by the old per-area clock, not just warp: Players s2 B logged 2 false loads going to the field/back, hidden by
HOS_COOP_ACCEPT=always. The current real-engine fixture preserves accepted offers/copies across area changes, but requires both anew for another slot, same-slot reload and new game. No corrected-head game proof yet.All 14 event cases pass with ASan/UBSan, including loss/reordering and cancel, cancel-accept, reset-ack, delayed-ack, reaccept-step; the concurrent case also passes TSan. Those 5 new cases reject the previous helper. The Portuguese-table compiled value-cell fixture rejects renderer50bd1255 and passes literal Player/Off, clipping/caret and translated-placeholder checks; no screenshot proof is implied.
Windows
17 Windows units + 1 native Linux-only unit; zero errors/name clashes. Clang
--target=x86_64-w64-mingw32, llvm-mingw UCRT headers; generated headers from 71d4ab0 on fce5345 (existing actual generated headers, not a new game build).host/posix/display_sdl.cis Linux-only. Warnings retained: -Wdeprecated-enum-enum-conversion: 1, -Wmissing-field-initializers: 7, -Wunused-const-variable: 1, -Wunused-function: 3.Syntax units, warnings and loads
host/control.chost/coop.chost/coop_menu.chost/coop_net.chost/coop_proto.chost/coop_relay.chost/coop_state.chost/display.chost/hos_main.chost/hos_relaunch.chost/hos_sdl.chost/nn_applet.chost/nn_fs.chost/nvn_native.chost/oot_coop.chost/posix/display_sdl.chost/rx_backend.cgpu/vk/vk_menu.cppWindows CI compiles portable co-op fixtures but lacks generated headers for this full host/menu sweep. Syntax is not a Windows link/run. Kuba's Windows11 report, on 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 published71d4ab0f), 2026-11-11T18:22:54-06:00 to 2026-11-11T19:58:10-06:00: 40 complete session invocations, 320 modes passed, no session failure. Each compiler ran the complete test 20 times, not a reduced menu-only selection. Every start overlapped an active gated whole suite; endpoint one-minute loads were 5.93–181.58. These are correctness checks under load, not timing measurements.The separate TSan lifetime probe observed a joined helper TID still in
/proc/self/taskafter 624/1000 joins. The repair waits for that specific joined TID to disappear, bounded at five seconds, before the fixture counts its baseline. It changes no production lifetime rule and removes no exact thread/socket check. The deliberate still-live production-worker mutation was caught: exit 1, load 19.98 → 51.99, withFAIL coop_session: and the co-op thread has ended (threads 3, before co-op was on 2).The 18 load-generating whole suites were not all green. All retained
test_env_on.py; additional compiler failures are named in the table. The logs record compiler SIGKILLs, a killedcc1plus, and a shaderbatch compile failure without a diagnostic; their causes were not established. They were not rerun to erase failures. The later fixture fix does not turn the older published suite into a pass.Every load suite, including all failures
tests/test_env_on.pynx2/test_depth_sample_types.py,tests/test_env_on.pytests/test_env_on.pytests/test_env_on.pytests/test_env_on.pytests/test_env_on.pytests/test_env_on.pytests/test_env_on.pytests/test_env_on.pytests/test_bake_mimo.py,tests/test_env_on.pytests/test_env_on.py,tests/test_lower_immediate_not_register.pynx2/test_unsigned_image_types.py,tests/test_env_on.pytests/test_env_on.pytests/test_env_on.pytests/test_env_on.pynx2/test_unsigned_image_types.py,tests/test_env_on.pytests/test_env_on.pytests/test_env_on.pyHistorical Windows format/CRT proof
The real build uses UCRT, ANSI stdio0 and
vsnprintf, not GNU's wrapper. Explicit Microsoft-format cross-GCC rejects the old traces and passes%u/unsigned; unchanged coop_net passes. Clang emits both COFF objects; Linux GCC/Clang sessions pass8/8 each and codecs79 vectors/400000 fuzz inputs each. All19 steps match expected exits; attributes/-Werror unchanged. Secure-CRT guards precede fixture headers. Actual Windows fixture execution remains pending.Every recorded format-proof command outcome and load
Previous integration 94cb598f on55b976c0: 60 focused passes;
465 passed, 1 failed, 20 skipped in 221 s (linux, 32 at once), only env_on failed. Those results are not relabelled as this head.Historical build and game evidence
The retained account below belongs only to its named heads. “Current/head/not pushed/queued” inside dated additions is historical, not this rebase. Other-player figures in follow-on experiments are outside this pull request. Pictures and both dated additions/chest are unchanged.
Linux (Arch, kernel 7.2.15-1-omarchy, i9-13900K), RTX 4090 with NVIDIA 615.78.08, clang 23.1.3, gcc 16.2.1, Python 3.12.16. Head 71d4ab0 on main fce5345, 2026-11-11 (US Central). The final session was headless, output 2048x1152, screenshots 1280x720.
Full
make_game.pybuild: exit 0 and its completion line; the two game copies have matching executable / renderer SHA-256 prefixes92986f4c15a12e84/65e42fecc682aed9, both 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.pygives it (and the C tests that default to gcc once more with clang): 39 runs, 5 failed. The failures:test_env_on.pyat commits 3, 4, 6 fails on main fce5345 itself, onHOS_FPS_LOG (host/oot_high_fps.c:15)(main's #2470 reads that switch as set-means-on; nothing of this stack);test_coop_session.pyat commit 3 (--cc gcc and --cc clang) failed its menu fixture's process-thread counts at load 74.46 and 102.99; the complete standalone test then passed all 8 runs with each compiler after starting at load 39.72 (GCC ended at 29.20, Clang at 14.26). The repeated code and fixture were unchanged at the then-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).e27f1a9252c471bb2f34da9a841492c4b4ec89b771d4ab0fPer-commit one-minute loads: 69.79 to 145.67. The two renderer syntax passes each retain the same enum-arithmetic and unused-function warnings recorded on main.
The whole suite on 71d4ab0 (historical)
python -I -B tests/run_all.pyon 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 atHOS_FPS_LOG (host/oot_high_fps.c:15), introduced by #2470;tests/test_coop_session.py: the ThreadSanitizer menu fixture's thread-count assertions failed under load (includingone thread more). Main has no co-op session test, so the main-tree selection ran zero tests and is not a passing baseline. The complete standalone GCC and Clang repeats described above pass on the unchanged session code. The suite was not rerun and remains failed.Two games on 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.
67d2de81/b744b1ecf7a9640d/940eb9ef71d4ab0f/fce53453v2.1.0-rc.3-76-g67d2de81v2.1.0-rc.3-138-gf7a9640dv2.1.0-rc.3-191-g71d4ab0fThe legacy session checker's N/A rows are the other-player figures (outside this stack), menu pictures and renaming; the tab checker covers the latter two. Its old descriptions mentioning automatic join/accept are not the test setup: the quoted log lines and the separate environment checks prove that this session used the tab.
The checks establish tab-only setup/acceptance; each backup equals its starting save (14 files); A's sword reaches B and B's song reaches A. Readback: A 1 applied/1 getter agreement; B 12 applied/11 getter agreements plus 1 Int readback; none missing. Six stats lines report no discarded datagram. Typed settings persist; both games send goodbye on the bridge's quit. The table retains launch guards, loads, counters and kernel results.
Historical 71d4ab0 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-treetrial at 2026-11-11T23:51:26-06:00, against main87991bc8b21c271f2dee3daf7f06fd006faa865c: exit 0, clean at that moment, treec23d9a4f44ee7c06ec654e273b40d24b08b1277c. This is not a continuing mergeability claim.Standalone snapshot 2026-11-11T23:05:49-06:00: 88 PRs touch 68 files; 21 conflict entries, all hunks read. #2453 needs journal tree/commit synchronization reconciled with backup barriers; #2578's TCP gameplay must be ported deliberately into the UDP/session/consent/backup contract, not concatenated. #2600 must preserve packed-file dispatch plus save-writer accounting. Other host-ABI additions need matching declarations/positional tables/version; fixture-only conflicts retain both tests. #882's options-present flag precedes co-op's continue; #2371 retains retake persistence/cancel prompts; #2566 retains dynamic sizes plus custom editing/translations. Independent option/context tail additions are not evidence of gameplay incompatibility.
Conflicting PRs and exact paths
990febcbgpu/vk/vk_menu.cpp4f6ccdb9gpu/vk/vk_menu.h2ef23f2ftests/test_present_trace.py1fc9986fdocs/development.md,gpu/vk/vk_menu.h6fb092bbtests/test_menu_env_overrides.py,tests/test_menu_keys.py5cf62486host/hos_sdl.c,host/nn_fs.c,host/posix/display_sdl.c5f85fe70gpu/vk/vk_ui.hb5af3839tests/test_vcore_fs_open.py919ba0eehost/rx_backend.c26793b60gpu/vk/vk_api.h,host/rx_backend.c,tests/test_env_file.pya2bed6b2gpu/vk/menu_i18n.h,gpu/vk/vk_menu.cppd827111dtests/test_env_file.py92ea6643host/nn_fs.c,readme.md9bd89259gpu/vk/vk_menu.h,host/rx_backend.cf654ef20host/rx_backend.c44e48391host/rx_backend.c4c276b88gpu/vk/vk_menu.he0cedba7gpu/vk/menu_i18n.h,gpu/vk/vk_api.h,gpu/vk/vk_menu.cpp,host/rx_backend.c,readme.mdc5a0c1d3docs/coop.md,host/oot_coop.c,readme.md,tests/test_oot_coop.py041c9f2bgpu/vk/vk_api.h,host/nn_fs.c,host/rx_backend.c,tests/test_vcore_fs_open.py6ecc6a79gpu/vk/vk_api.hAll 9 unchanged co-op lane heads conflict in wholesale trial merges; rewritten-base add/add blocks inflate counts. Rebase topic ranges preserving ABI, Windows binder, clock, epochs, accepted-session binding, hosting and localization. No helper/lane branch moved.
Questions for you
third_party/acceptable, or would you rather have another library behind the same few functions?coop_name,coop_server,coopthe names you want inhookshot_options.ini?What comes after it
Other players' Links (
coop-players) and the tab's additions (coop-tab-more,coop-tab-where) are separate follow-on lanes.Warp (
coop-warp), hits and duels (coop-hits), and the shared world and area state (coop-world,coop-area) build above them; none of these lanes is in this pull request.Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com
Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra noreply@openai.com