Co-op: friends share progress across their own games (proposal and claim, before any pull request) #2269

Closed
opened 2026-11-11 12:55:50 +00:00 by mooki000 · 4 comments
mooki000 commented 2026-11-11 12:55:50 +00:00 (Migrated from github.com)

The idea

Co-op: two to eight friends each play their own game with their own save, and progress is shared. A chest, a boss, a song or an item one of them gets is in everyone's game too; whoever makes the room sets which parts are shared (progress, items, songs, quest items, keys, rupees, ammunition). It is co-operative play of the single-player game. It is not a versus mode (#1522) and not emulator netplay: nobody's game drives anyone else's controls or camera. Showing the other players' Links in your world could come later, off by default.

I would like to claim this, and I am asking before opening any pull request because it adds something Hookshot does not have today: a network socket. The code exists and is tested as far as described below; the questions at the end are the ones I cannot answer for you.

What it adds to the host

  • One UDP socket, opened only while the player is in a room, to a relay the players choose and run themselves (scripts/coop_relay.py, Python standard library, 869 lines). Nothing listens on the player's machine and there is no default server. The game's own nn::socket stays offline as it is.
  • On the wire: flag values (a 32-bit hash and a number), names, and the room's bookkeeping. No game files and no game code.
  • A room is named by an 18-character secret the players pass on themselves; the relay never learns it. Everything between players is sealed with a key derived from that secret (XChaCha20-Poly1305), and what one player may change in another's game is signed with that player's key (EdDSA), so the relay, or someone who sees the traffic, can neither read nor forge it. The cryptography is Monocypher 4.0.2 (BSD-2-Clause or CC0, two files, unmodified, under third_party/monocypher/).
  • With co-op off, which is the default, nothing runs: no socket and no thread.

What it does to saves

Nothing from a room reaches a player's game until that player accepts the room on a Co-op tab, which lists the players and their key fingerprints. At that moment the whole save folder is copied to coop-backup/ beside it, and nothing is applied before the copy exists. Values are applied through the game's own setter routines, called from the host, and only raised (a flag set, a count up to its maximum), never lowered; a value outside the game's own table is refused.

The pull requests I have in mind, each building and passing tests/run_all.py alone:

# Title What Touches the game
1 feat(coop): a relay and an encrypted wire format for co-op rooms the relay, the codec in C (683 lines), Monocypher, the guide no
2 feat(menu): text fields in the Insert menu typed text and paste in the menu; useful for any text setting menu only
3 feat(coop): join a room from the runtime the socket and one thread, for Windows and Linux; settings; log lines no; off by default
4 feat(menu): the Co-op tab make, join, accept, leave menu only
5 feat(coop): progress sync the merge engine, the game's flags read and set at the end of a present, the save copy yes, after Accept
6 feat(coop): show the other players later yes, off by default

Where the code stands (on my fork, not proposed for merge yet: coop has 1, 3 and the merge engine of 5; menu-text-fields has 2):

  • Game-free tests: the relay on a fake clock and over real UDP (60 cases); the C codec against byte vectors the Python relay is checked against too, and 400,000 fuzzed datagrams under ASan and UBSan; the merge engine between simulated games with loss and hostile members; the session layer against a hostile mock relay and against the real relay at 0 % and 30 % loss, with a relay restart midway.
  • In the game, on Linux (Arch, RTX 4090, NVIDIA 615.78.08, headless): a game made a room on a relay on loopback, a scripted second member joined it, the two kept the room alive for the two minutes of the run, and the game said goodbye when it closed.
  • Windows: the session layer and its test compile and link with the pinned llvm-mingw. Nothing of it has run on Windows; I have none.
  • Not done yet: two real games syncing progress (pull request 5 is written and has its game-free tests; in the game it has so far only read the flag store, without writing), and the tab bound to a real session (it is drawn against a made-up room).

What I am asking you to decide

  1. A socket in the host at all. Is a UDP client in the runtime acceptable under the conditions above? #882's sandbox allows no TCP; this would need UDP to the address the player typed.
  2. Text entry and the menu's interface. The tab needs typed text and strings crossing from the runtime to the renderer library: one new MkwVkHost member and an MKWVK_API_VERSION bump. Would you take text fields as a pull request of their own first?
  3. Where the relay lives. In this repository (my proposal: it and the game must speak the same version), or in one of its own?
  4. A save copy in host/nn_fs.c. The copy takes a lock that the save mount's writes also take, because there is no transaction to hook onto. Acceptable in that file?

If the answer to 1 is no, I will keep it out of your way as a fork and say so here.

Anything else

  • About #1027 ("the community multiplayer mod", collected in #1007): as far as I can find, that is a video of a mod running on an emulator, with no published source. Hookshot cannot load an emulator's code, so this proposal is something else: native to the host, with a relay anyone can run.
  • What I can test: Linux with an RTX 4090 on NVIDIA's driver, and an Intel UHD 770 on Mesa in the same machine (two games at once, one on each). What I cannot: Windows, AMD, a Steam Deck. From pull request 3 on I would ask for a Windows tester.
  • Priority by docs/workflow.md: S3, a feature.
  • The work is AI-assisted (Claude); commits carry the trailers. I read what I send and run its tests.
### The idea **Co-op: two to eight friends each play their own game with their own save, and progress is shared.** A chest, a boss, a song or an item one of them gets is in everyone's game too; whoever makes the room sets which parts are shared (progress, items, songs, quest items, keys, rupees, ammunition). It is co-operative play of the single-player game. It is not a versus mode (#1522) and not emulator netplay: nobody's game drives anyone else's controls or camera. Showing the other players' Links in your world could come later, off by default. I would like to claim this, and I am asking before opening any pull request because it adds something Hookshot does not have today: a network socket. The code exists and is tested as far as described below; the questions at the end are the ones I cannot answer for you. **What it adds to the host** - One UDP socket, opened only while the player is in a room, to a relay the players choose and run themselves (`scripts/coop_relay.py`, Python standard library, 869 lines). Nothing listens on the player's machine and there is no default server. The game's own `nn::socket` stays offline as it is. - On the wire: flag values (a 32-bit hash and a number), names, and the room's bookkeeping. No game files and no game code. - A room is named by an 18-character secret the players pass on themselves; the relay never learns it. Everything between players is sealed with a key derived from that secret (XChaCha20-Poly1305), and what one player may change in another's game is signed with that player's key (EdDSA), so the relay, or someone who sees the traffic, can neither read nor forge it. The cryptography is Monocypher 4.0.2 (BSD-2-Clause or CC0, two files, unmodified, under `third_party/monocypher/`). - With co-op off, which is the default, nothing runs: no socket and no thread. **What it does to saves** Nothing from a room reaches a player's game until that player accepts the room on a Co-op tab, which lists the players and their key fingerprints. At that moment the whole save folder is copied to `coop-backup/` beside it, and nothing is applied before the copy exists. Values are applied through the game's own setter routines, called from the host, and only raised (a flag set, a count up to its maximum), never lowered; a value outside the game's own table is refused. **The pull requests I have in mind**, each building and passing `tests/run_all.py` alone: | # | Title | What | Touches the game | |---|---|---|---| | 1 | `feat(coop): a relay and an encrypted wire format for co-op rooms` | the relay, the codec in C (683 lines), Monocypher, the guide | no | | 2 | `feat(menu): text fields in the Insert menu` | typed text and paste in the menu; useful for any text setting | menu only | | 3 | `feat(coop): join a room from the runtime` | the socket and one thread, for Windows and Linux; settings; log lines | no; off by default | | 4 | `feat(menu): the Co-op tab` | make, join, accept, leave | menu only | | 5 | `feat(coop): progress sync` | the merge engine, the game's flags read and set at the end of a present, the save copy | yes, after Accept | | 6 | `feat(coop): show the other players` | later | yes, off by default | **Where the code stands** (on my fork, not proposed for merge yet: [`coop`](https://github.com/mooki000/Hookshot/tree/coop) has 1, 3 and the merge engine of 5; [`menu-text-fields`](https://github.com/mooki000/Hookshot/tree/menu-text-fields) has 2): - Game-free tests: the relay on a fake clock and over real UDP (60 cases); the C codec against byte vectors the Python relay is checked against too, and 400,000 fuzzed datagrams under ASan and UBSan; the merge engine between simulated games with loss and hostile members; the session layer against a hostile mock relay and against the real relay at 0 % and 30 % loss, with a relay restart midway. - In the game, on Linux (Arch, RTX 4090, NVIDIA 615.78.08, headless): a game made a room on a relay on loopback, a scripted second member joined it, the two kept the room alive for the two minutes of the run, and the game said goodbye when it closed. - Windows: the session layer and its test compile and link with the pinned llvm-mingw. Nothing of it has run on Windows; I have none. - Not done yet: two real games syncing progress (pull request 5 is written and has its game-free tests; in the game it has so far only read the flag store, without writing), and the tab bound to a real session (it is drawn against a made-up room). **What I am asking you to decide** 1. **A socket in the host at all.** Is a UDP client in the runtime acceptable under the conditions above? #882's sandbox allows no TCP; this would need UDP to the address the player typed. 2. **Text entry and the menu's interface.** The tab needs typed text and strings crossing from the runtime to the renderer library: one new `MkwVkHost` member and an `MKWVK_API_VERSION` bump. Would you take text fields as a pull request of their own first? 3. **Where the relay lives.** In this repository (my proposal: it and the game must speak the same version), or in one of its own? 4. **A save copy in `host/nn_fs.c`.** The copy takes a lock that the save mount's writes also take, because there is no transaction to hook onto. Acceptable in that file? If the answer to 1 is no, I will keep it out of your way as a fork and say so here. ### Anything else - About #1027 ("the community multiplayer mod", collected in #1007): as far as I can find, that is a video of a mod running on an emulator, with no published source. Hookshot cannot load an emulator's code, so this proposal is something else: native to the host, with a relay anyone can run. - What I can test: Linux with an RTX 4090 on NVIDIA's driver, and an Intel UHD 770 on Mesa in the same machine (two games at once, one on each). What I cannot: Windows, AMD, a Steam Deck. From pull request 3 on I would ask for a Windows tester. - Priority by `docs/workflow.md`: S3, a feature. - The work is AI-assisted (Claude); commits carry the trailers. I read what I send and run its tests.
mooki000 commented 2026-11-11 13:08:55 +00:00 (Migrated from github.com)

Two updates:

  • This is now claimed in Discord (#dev-work) as well.
  • The first part is open as a draft, so the questions above can be read against real code: #2274 (the relay and the wire format; no contact with the game, and nothing in it opens a socket in the game). It is a draft on purpose: please don't merge it before question 1 is settled.

I'm watching this thread and will answer here.

Two updates: - This is now claimed in Discord (#dev-work) as well. - The first part is open as a draft, so the questions above can be read against real code: #2274 (the relay and the wire format; no contact with the game, and nothing in it opens a socket in the game). It is a draft on purpose: please don't merge it before question 1 is settled. I'm watching this thread and will answer here.
mooki000 commented 2026-11-11 13:42:31 +00:00 (Migrated from github.com)

Progress: two games in one room now share progress. The code is on my fork, branch coop-sync: the three commits listed above and a fourth, feat(coop): progress sync (the game side and the save copy).

The session: two builds of that branch on one PC under Linux, game A on an RTX 4090 (NVIDIA 615.78.08), game B on an Intel UHD 770 (Mesa 26.2.5), both without a window, each with its own user folder and its own copy of the same save (Kokiri Forest, no sword), and the relay on loopback.

step what the logs show
A makes the room, B joins A: coop: room … created (member 1); B: coop: joined room … as member 2; the relay saw both
B's save is copied before anything of the room reaches it B: the save as it was before this room is in …/coop-backup/first-…; the copy equals B's starting save file by file (14 files)
A gets the Kokiri Sword (debug tools) A sends the two item values and ten progress flags that changed
B applies them through the game's own set routines, only after accepting the room 12 applied on B; all 12 read back from the game afterwards (11 through the game's getter, 1 Int from its table)
the sword is in B's inventory the game's own count for the Kokiri Sword on B: 0 at the start, 1 after (QueryGetPouchContentNumByActorName)
the other direction B sets a song marker through the game's SetBool; A receives it, applies it, and A's getter reads it set
the network no datagram thrown away on either side (every stats line)

The session script's checks: 28 pass, 0 fail, 4 not applicable (puppets and the menu are later parts). No fatal line, no device lost, and no driver fault line in the kernel log for either GPU.

One thing it settled for the design: an item arrives through the flags the game itself keeps (SetBool/SetInt), not through the game's item routine. So it comes without the "you got an item" message and pose, and what Link wears stays each player's choice.

Not shown yet:

  • a long session: the longest stretch with both games in play together was 2 minutes 23 seconds (see the addition below);
  • two PCs and a real network (loopback only so far);
  • Windows (it compiles and links with llvm-mingw; it has never run there);
  • the other player's puppet (part 6) and the Co-op tab (part 4: until then a room is accepted through a test switch in the environment);
  • more than two players in a game (the relay's own tests go to 8).

On the Intel GPU game B's picture has false colours for a reason that is not co-op's (known on that device, being fixed separately); these checks read the logs and the save, not that picture.

Added 13:46 UTC: items both ways while both games play. A further session on the same machine and build: the Deku Shield and the Boomerang given on B, the Fairy Slingshot and the Fairy Ocarina given on A, one every 17 seconds. At the end each game's own inventory count read all four, and the Kokiri Sword, as 1 in both games. B applied 16 values and A applied 5, and all 21 read back from the game afterwards (16 on/off flags through the game's getter, 5 counts from its table). No datagram was thrown away. 29 checks pass, 0 fail, 4 not applicable; no fatal line, no device lost, no driver fault line in the kernel log for either GPU.

**Progress: two games in one room now share progress.** The code is on my fork, branch [`coop-sync`](https://github.com/mooki000/Hookshot/tree/coop-sync): the three commits listed above and a fourth, `feat(coop): progress sync` (the game side and the save copy). The session: two builds of that branch on one PC under Linux, game A on an RTX 4090 (NVIDIA 615.78.08), game B on an Intel UHD 770 (Mesa 26.2.5), both without a window, each with its own user folder and its own copy of the same save (Kokiri Forest, no sword), and the relay on loopback. | step | what the logs show | |---|---| | A makes the room, B joins | A: `coop: room … created (member 1)`; B: `coop: joined room … as member 2`; the relay saw both | | B's save is copied before anything of the room reaches it | B: `the save as it was before this room is in …/coop-backup/first-…`; the copy equals B's starting save file by file (14 files) | | A gets the Kokiri Sword (debug tools) | A sends the two item values and ten progress flags that changed | | B applies them through the game's own set routines, only after accepting the room | 12 applied on B; all 12 read back from the game afterwards (11 through the game's getter, 1 Int from its table) | | the sword is in B's inventory | the game's own count for the Kokiri Sword on B: 0 at the start, 1 after (`QueryGetPouchContentNumByActorName`) | | the other direction | B sets a song marker through the game's `SetBool`; A receives it, applies it, and A's getter reads it set | | the network | no datagram thrown away on either side (every stats line) | The session script's checks: 28 pass, 0 fail, 4 not applicable (puppets and the menu are later parts). No fatal line, no device lost, and no driver fault line in the kernel log for either GPU. One thing it settled for the design: an item arrives through the flags the game itself keeps (`SetBool`/`SetInt`), not through the game's item routine. So it comes without the "you got an item" message and pose, and what Link wears stays each player's choice. Not shown yet: - a long session: the longest stretch with both games in play together was 2 minutes 23 seconds (see the addition below); - two PCs and a real network (loopback only so far); - Windows (it compiles and links with llvm-mingw; it has never run there); - the other player's puppet (part 6) and the Co-op tab (part 4: until then a room is accepted through a test switch in the environment); - more than two players in a game (the relay's own tests go to 8). On the Intel GPU game B's picture has false colours for a reason that is not co-op's (known on that device, being fixed separately); these checks read the logs and the save, not that picture. **Added 13:46 UTC: items both ways while both games play.** A further session on the same machine and build: the Deku Shield and the Boomerang given on B, the Fairy Slingshot and the Fairy Ocarina given on A, one every 17 seconds. At the end each game's own inventory count read all four, and the Kokiri Sword, as 1 in both games. B applied 16 values and A applied 5, and all 21 read back from the game afterwards (16 on/off flags through the game's getter, 5 counts from its table). No datagram was thrown away. 29 checks pass, 0 fail, 4 not applicable; no fatal line, no device lost, no driver fault line in the kernel log for either GPU.
NotRexed commented 2026-11-11 21:28:06 +00:00 (Migrated from github.com)

Thanks for the report. Reports of the same problem are now collected in #1007, which lists this one with your details and links back here, so one fix closes them together and the open list stays readable. Follow #1007 for the fix, and add new details there (or here). If this turns out to be a separate problem it will be reopened. Pull request #2274 works on this.

Thanks for the report. Reports of the same problem are now collected in #1007, which lists this one with your details and links back here, so one fix closes them together and the open list stays readable. Follow #1007 for the fix, and add new details there (or here). If this turns out to be a separate problem it will be reopened. Pull request #2274 works on this.
mooki000 commented 2026-11-12 00:13:28 +00:00 (Migrated from github.com)

Progress, here for those who follow this thread (the tracker for it is #1007, the code is in #2274):

Draft #2274 assembles the relay and encrypted format, merge engine, runtime session, progress sync, text fields and Co-op tab as six commits: head 71d4ab0f, on main fce53453. The description names the head used by each two-game session; an earlier head also delivered the Kokiri Sword from a real chest to the second game's inventory.

To try it, build the branch, run python -I scripts/coop_relay.py --port 24680 somewhere both games can reach by UDP, and use the Insert menu's Co-op tab to enter a name and relay address, make/join a room, then load a save and press Accept the room twice in each game. Read the key fingerprints to each other first; each save is copied before room values are applied. Invite only people you trust with your save.

This remains a draft, not an all-green or complete shared-world claim. The final-head suite was 458 passed, 2 failed, 20 skipped: main's isolated HOS_FPS_LOG environment-rule failure, and the co-op session fixture's thread-count failure under load. The complete standalone session test subsequently passed all 8 runs with GCC and all 8 with Clang on unchanged session code. Other players' Links, warp, combat and shared enemies are separate work, not in that draft; two PCs and Windows remain untested. The socket-policy question and the restarted maker's missing-rules case still need maintainer decisions.

Progress, here for those who follow this thread (the tracker for it is #1007, the code is in #2274): Draft #2274 assembles the relay and encrypted format, merge engine, runtime session, progress sync, text fields and Co-op tab as six commits: head `71d4ab0f`, on main `fce53453`. The description names the head used by each two-game session; an earlier head also delivered the Kokiri Sword from a real chest to the second game's inventory. To try it, build the branch, run `python -I scripts/coop_relay.py --port 24680` somewhere both games can reach by UDP, and use the Insert menu's Co-op tab to enter a name and relay address, make/join a room, then load a save and press Accept the room twice in each game. Read the key fingerprints to each other first; each save is copied before room values are applied. Invite only people you trust with your save. This remains a draft, not an all-green or complete shared-world claim. The final-head suite was 458 passed, 2 failed, 20 skipped: main's isolated `HOS_FPS_LOG` environment-rule failure, and the co-op session fixture's thread-count failure under load. The complete standalone session test subsequently passed all 8 runs with GCC and all 8 with Clang on unchanged session code. Other players' Links, warp, combat and shared enemies are separate work, not in that draft; two PCs and Windows remain untested. The socket-policy question and the restarted maker's missing-rules case still need maintainer decisions.
Commenting is not possible because the repository is archived.
No milestone
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Hookshot#2269
No description provided.