WIP: feat(coop): online co-op for up to eight players (Network tab) #2578

Draft
itzdaface wants to merge 8 commits from itzdaface/feat/coop into main
itzdaface commented 2026-11-12 00:04:06 +00:00 (Migrated from github.com)

Friends who each build Hookshot from their own copy have no way to play it together. This adds online co-op: up to eight players in one Hyrule, each on their own PC with their own copy of the game, from a new Network tab in the menu.

At a glance

What it is Online co-op for two to eight players. One hosts, the others join. It is off, and costs nothing, until Connect is pressed in the Network tab
What you see The other players as Link in your game, with their own animation, sword and shield, a name over their head, and a line in the corner saying who is where
What is shared Lasting items, heart containers and pieces, save flags, chests and pickups, small keys, rupees and ammunition picked up. A player who joins later is levelled with the rest
Enemies One enemy for everyone: one life (a hit from any player counts in every game, and it dies for all) and one place (the nearest player's game leads it, the others' copies follow and play the same animation)
Bosses One leader, the player who started the fight. A second player's hit that takes no life hands the boss over, so a stun works from either side
PvP Optional, only between two players who both turn it on. The other player becomes a real target for the game's own combat: lock-on, hit flash, sparks, knockback, hearts lost
How friends link up Radmin VPN (free) for friends in different homes, TCP port 43400. The full steps are below and in docs/coop.md
Platform Windows. On Linux the tab is there and the entry points do nothing yet
Size 18 files, about 4,600 lines added. Nearly all of it is one new file, host/oot_coop.c (3,101 lines), plus the Network tab, two data scripts, a test file and the docs
Game files None in this PR. Two scripts make the other players' actors and the list of shared flags on each PC, from that player's own dump
How it was made About sixty small builds on a private branch across v2.0.13, v2.0.14 and v2.1.0, each played before the next. Directed and played by Kuba, written with Claude Code. The whole account, with what did not work, is under "How it was made"
Played by real players Friends over the internet on the v2.0.14 build: shared items, flags and keys, seeing each other, the first PvP
Tried with one game and a scripted second player On v2.1.0: PvP through the game's own combat, shared enemy life, enemies following their leader, the lead passing to the nearer player, bosses. Nine bosses fought as the leader with no crash; Gohma also fought from the second player's side by a person
Not done yet Not built into a game or run on main: this branch is the port, and it has only been compiled file by file and unit-tested. Enemies and bosses have not been played by two real games. Every limit is listed in docs/coop.md
Status Draft. Six questions for the maintainers are at the bottom

Pictures and video

All of these are from the earlier builds this branch was ported from, not from main.

Two real players, each on their own PC, linked over Radmin VPN (42 seconds, no sound):

https://github.com/user-attachments/assets/0f8a8716-4c07-42ef-b1a8-0b95f62f2cc2

The other player in Hyrule Field and in the Market, with the name tag and the line in the corner:

Two players in Hyrule Field Two players in the Market

PvP: lock-on on the other player and the game's own hit effects (four frames of one sword hit; the second player here is the scripted one):

A sword hit on another player

The Network tab (an older build: today the first row is Play as, and there are Connection, Go to player and PvP rows too):

The Network tab

Pictures from a build of main will follow once there is one.

What it does

  • Network tab: Host or Join, a name, the host's address (typed, kept off the screen), port (43400), PvP on or off. Nothing starts until Connect is pressed; with co-op off it costs nothing.
  • The other players are drawn as Link with their own animation, sword and shield, a name tag, and a line in the corner saying who is where.
  • Shared progress: lasting items, save flags, chests and keys, and counted pickups, levelled when a player joins.
  • PvP (when both players turn it on): the other player is a real target for the game's own combat: lock-on, the game's hit effects, hearts lost on the other side.
  • Enemies are one for everyone: a hit from any player takes the same life off that enemy in every game, and it dies for all. The game of the nearest player leads each enemy; the others' copies are moved to the same place and play the same animation.
  • Bosses: one leader (whoever started the fight); a second player's hit that takes no life hands the boss over to them, so a stun works from either side.
  • Linking up needs Radmin VPN for friends in different homes: the game only opens a plain TCP link, with no relay. The whole process is below.
  • Switches: HOS_COOP_ENEMY_PLACE=0, HOS_COOP_BOSS=0, HOS_COOP_TEST=1. All documented in docs/coop.md.

This is the way every session so far was played: friends in different homes, joined through Radmin VPN.

Once, on every player's PC

  1. Build Hookshot from your own copy of the game. Everyone must be on the same version of Hookshot.
  2. Make co-op's two data files (nx2/coop_flags.py and nx2/coop_bodies.py, run once against the game folder; the commands are in docs/coop.md). Do it again after a rebuild that replaces the game folder.
  3. Install Radmin VPN (free, from radmin-vpn.com), start it, and switch it on with the large button.

Once, for the group

  1. The host, in Radmin VPN: Network > Create Network. Pick a network name and a password and send both to your
    friends in private.
  2. Everyone else, in Radmin VPN: Network > Join Network, and type that name and password. Each player now sees the
    others in the Radmin window.
  3. Every player has an address in the Radmin window, a number that starts with 26.. The one that matters is the
    host's. The joiners right-click the host's name there and copy the address. Treat it like a password: do not
    show it on a stream. Hookshot keeps it off the screen for that reason.

Every time you play

  1. Everyone starts Radmin VPN first and checks that the host shows as online in it. Then start Hookshot and load your
    save (a slot you can afford to lose). With a randomizer, everyone must be on the same seed.
  2. The host goes first. Open the menu (Insert or F1), go to Network, set Play as to Host, set
    Your name, then press Enter on Connection. The first time, Windows Firewall may ask about Hookshot: allow
    it, or nobody can reach you.
  3. Then each joiner. In Network, set Play as to Join. On Host address press Enter, paste the
    host's Radmin address with Ctrl+V, and press Enter again. Leave Port at 43400 unless the host changed theirs.
    Set Your name, then press Enter on Connection.
  4. Status turns to Connected and names who is there, and the top left corner of the screen says the same. After a
    moment everyone holds the same items and progress. The other players are Links with their name over their head;
    if you are in different maps, press Enter on a player's Go to player row.
  5. PvP is off until two players both turn PvP on.
  6. To stop, press Enter on Connection again, or close the game. A game always starts not connected, so steps 8
    and 9 are done each time; the name, the address and the port are remembered.

If it does not connect

  • Status says why. Read it first.
  • Is the host connected before the joiners try? A joiner who was early presses Enter on Connection again.
  • Does Radmin VPN show the host as online for the joiner? If not, the trouble is in Radmin, not in Hookshot.
  • Did the joiner paste the host's address, not their own?
  • Is Port the same number for everyone?
  • Did the host allow Hookshot when Windows Firewall asked? If the question was closed or refused, allow Hookshot under
    "Allow an app through Windows Firewall". Do not turn the firewall or the antivirus off.
  • Is everyone on the same version of Hookshot?

On one home network Radmin is not needed: the joiners type the host's address on that network instead. Other virtual
networks (Tailscale, ZeroTier) or a port forwarded to the host work the same way, but have not been tried.

What changed

  • host/oot_coop.c (new): the whole runtime: the link (plain text lines over TCP), the players' bodies, what is shared, enemies and bosses.
  • nx2/oot_patches.py, host/hos_main.c, host/nn_vi.c, host/oot_menu.c: the calls into it (Link's update, the frame, pickups, the Life component's update).
  • gpu/vk/vk_menu.cpp, vk_menu.h, oot_coop_opts.h, vk_replay.cpp: the Network tab and what is drawn over the game.
  • nx2/coop_bodies.py, nx2/coop_flags.py, nx2/byml_rw.py: two scripts, run by hand, that make the other players' actors and the list of shared flags from the player's own dump into a mod folder. Nothing made from the game is in this PR.
  • tests/test_oot_coop.py, docs/coop.md, the README and docs/development.md.

How it was made

Co-op was built on a private branch in about sixty small builds, first on v2.0.13, then v2.0.14, then v2.1.0, and each build was tried in the game before the next one was started. Kuba set what it had to do, played every build (alone, with friends over Radmin VPN, and in a test copy) and judged it on the screen. The code was written with Claude Code. Several parts below exist because something looked wrong to him while playing, not because a check failed.

How things were found

  • The lifted C and a disassembly of the game image. A string (a script node's name, an actor's name) leads to the code that uses it. A script node's run function sits 0xa0 after its name function in its vtable, which is how the game's own "get life", "play animation" and "create actor" functions were found.
  • The game's own data files. Most of what the other players' bodies can do is data, not code. Dark Link is a Link-shaped enemy, so his actor pack was the model: what he has that the pause menu's Link lacks is what makes a body something the game's combat can hit and lock on to.
  • Reading the running game's memory from outside, to see which fields change when Link is hit, picks something up or opens a chest.
  • Logging first. For shared enemies the first build only logged every actor that has a life, to learn what names an enemy the same in two games, before anything was sent.

How it was tested while being built

  • A test copy of the game on a copy of a save, with its own user folder, usually with no window, driven by scripted pad input and judged from the game's own frame dumps, read as runs of consecutive frames (a single still hides a sliding or frozen body).
  • A stand-in second player: a script that joins over the link and says what a second game would. It can stand, run in a circle, mirror the real player, hit back, and replay a recording.
  • Recorded fights. A game that leads an enemy can write its Q lines to a file. Kuba fought the bosses in a playable test copy while it recorded; the recording is then played to a second game, which must show the same fight.
  • The test lines behind HOS_COOP_TEST=1 (travel to a map, stand at a place, make an actor beside Link) put Link next to a given enemy without playing there.
  • Real sessions with friends on the v2.0.14 build. Their logs found bugs no script did (below).

In the order it was built, with what did not work

  1. The link and shared items. Text lines over TCP, the host passing them on. Every pickup goes through the pouch's add by name (f_8112d570), so one call there sees them all. Heart containers are given through the pouch too: the max-hearts value in the game's data turned out to be a mirror the game rewrites every frame, so writing it does nothing.
  2. Shared flags. The first rule was "a flag that went from off to on is shared". The first night with three players showed it was wrong: 54 of the shared flags start on, and a character who had moved aside came back for everyone. The rule became "a flag that has left the value a new file starts with". A flag written without the game's changed-since-save mark is also not saved, which cost another build. Per-object flags are shared only where the game never resets them: pots, enemies and shop stock come back by design and are left alone.
  3. Seeing the other players. The first body was the pause menu's Link, made with the game's own create-actor call and moved by its place request. He slid about in a stiff pose, because his animation graph only has menu poses. Writing a pose straight into the animation result buffer was tried for three builds and never moved the drawn body. What works: a new actor, made on each PC from the player's own files, that is the same body with Link's own animation graph. Each game says which command each of Link's nine animation slots is playing and the animation inputs that changed, and the body plays the same.
  4. Sword and shield. Calling the game's equip function put the sword at the body's feet, and a second call into a used slot crashed. It became data instead: an equipment table and two names, as Dark Link's pack has them, and the sword and shield then follow the mirrored animation by themselves.
  5. PvP. The first version went by reach: the attacker's game decided a swing within 1.9 m had landed. A session with friends showed hits landing during a cutscene, which broke the scene for the player being moved by it, so hits are now ignored while a player's game is in an event and for 1.5 s after. Then the body was made a real target: Dark Link's life, hit, affiliation and target parts, on the body's own physics with one added sensor. Taking Dark Link's whole physics gave the body a mind of its own: it walked away from where its player stood. The slash flash, sparks, sound and lock-on arrows came from three more parts of data. Dark Link's own lock-on part could not be used, because it checks a value only his fight sets.
  6. One life for an enemy. The Life component's update (f_804bd8c0) is where any actor's life goes down, in two compare-and-swap writes. Nothing inside an actor names it the same in two games, so an enemy is matched by its name and the place it was first seen. Writing life 0 by hand does not kill: the enemy stands there at zero. The damage is given through the game's own life message instead, and the enemy then dies as it does to a real hit. One early build crashed at every save load: an address of 0 passed the range check, so every read of game memory is now checked.
  7. One place for an enemy. The place request that moves a player's body does nothing on an enemy; the game's own move request, which goes through physics, does. The leader was at first whichever game updated the enemy first. That made an enemy ignore the player standing next to it, so the nearest player's game leads now, with a margin so the lead does not flap. For two builds every player stood frozen: animation command names live in memory above the heap, and a check that had been made too narrow refused to read them.
  8. Bosses. A boss fight is a string of states only the leader's copy is in, so a second player's seed in Gohma's eye was blocked by his own copy. Putting the hit back into the game's hit list was tried and does nothing: damage and reactions happen at the real hit, not later. The answer was the hand-over: a hit that takes no life passes the boss to the game that made it. It was found and proven with Kuba playing the second player against a recorded leader. He then fought Gohma, King Dodongo, Barinade, Phantom Ganon, Volvagia, Morpha, Bongo Bongo, Twinrova and Ganondorf as the leader with this code on, with no crash in any fight.
  9. The menu. The Network tab was written for the old menu and fitted again when v2.1.0 rewrote the menu on Dear ImGui. Its option ids are in a header of their own so the shader cache stamp does not move.
  10. The port to main. The co-op parts were lifted out of the private branch on their own, renamed to the project's oot_* names, and everything else from that branch left behind. This is the step that has only been compiled and unit-tested so far.

How it was tested

  • This branch, on main: every C and C++ file touched passes a syntax-only compile with llvm-mingw; python -I -B tests/run_all.py gives 427 passed, 6 failed, 38 skipped on Windows 11, and the same 6 fail on an untouched main on this PC. Not built into a game, not started. Nothing compiled for Linux: co-op is Windows-only here, and elsewhere its entry points do nothing.
  • Two real players over the internet (v2.0.14 build): shared items, flags and keys, the other players' bodies, and the first PvP (by reach).
  • One game plus a scripted second player (v2.1.0, RTX 3060): PvP through the game's own combat, shared enemy life, enemies following their leader, the lead passing to the nearer player, and bosses. A person played the second player's side of the Gohma fight against a recorded leader: following it, being hurt by it, stunning and killing it. Gohma is the only boss tried from that side.
  • Not tested: enemies and bosses with two real games; any boss but Gohma as a follower; enemies the game spawns on the fly (each game still has its own).

Questions for the maintainers

  1. Is a feature this size welcome as one PR, or would you rather have it in pieces (runtime, menu, builders)?
  2. The option ids sit in their own header (oot_coop_opts.h) so the shader cache stamp does not move, and the options file keys are play_as, port, pvp. Should they move to vk_api.h and become coop_*?
  3. Should the build make the co-op data files itself, perhaps behind an opt-in, instead of the two scripts being run by hand?
  4. oot_coop.c carries copies of two static pouch functions from hos_main.c. Would you export those instead?
  5. Linux: stand-ins for now, or sockets through host/posix first?
  6. The link is plain text with no password. Is that acceptable for a first version?

Co-op by Kuba. Written with Claude Code; the trailers are on each commit.

🤖 Generated with Claude Code

Friends who each build Hookshot from their own copy have no way to play it together. This adds online co-op: up to eight players in one Hyrule, each on their own PC with their own copy of the game, from a new **Network** tab in the menu. ## At a glance | | | |---|---| | **What it is** | Online co-op for two to eight players. One hosts, the others join. It is off, and costs nothing, until Connect is pressed in the Network tab | | **What you see** | The other players as Link in your game, with their own animation, sword and shield, a name over their head, and a line in the corner saying who is where | | **What is shared** | Lasting items, heart containers and pieces, save flags, chests and pickups, small keys, rupees and ammunition picked up. A player who joins later is levelled with the rest | | **Enemies** | One enemy for everyone: one life (a hit from any player counts in every game, and it dies for all) and one place (the nearest player's game leads it, the others' copies follow and play the same animation) | | **Bosses** | One leader, the player who started the fight. A second player's hit that takes no life hands the boss over, so a stun works from either side | | **PvP** | Optional, only between two players who both turn it on. The other player becomes a real target for the game's own combat: lock-on, hit flash, sparks, knockback, hearts lost | | **How friends link up** | Radmin VPN (free) for friends in different homes, TCP port 43400. The full steps are below and in `docs/coop.md` | | **Platform** | Windows. On Linux the tab is there and the entry points do nothing yet | | **Size** | 18 files, about 4,600 lines added. Nearly all of it is one new file, `host/oot_coop.c` (3,101 lines), plus the Network tab, two data scripts, a test file and the docs | | **Game files** | None in this PR. Two scripts make the other players' actors and the list of shared flags on each PC, from that player's own dump | | **How it was made** | About sixty small builds on a private branch across v2.0.13, v2.0.14 and v2.1.0, each played before the next. Directed and played by Kuba, written with Claude Code. The whole account, with what did not work, is under "How it was made" | | **Played by real players** | Friends over the internet on the v2.0.14 build: shared items, flags and keys, seeing each other, the first PvP | | **Tried with one game and a scripted second player** | On v2.1.0: PvP through the game's own combat, shared enemy life, enemies following their leader, the lead passing to the nearer player, bosses. Nine bosses fought as the leader with no crash; Gohma also fought from the second player's side by a person | | **Not done yet** | **Not built into a game or run on `main`**: this branch is the port, and it has only been compiled file by file and unit-tested. Enemies and bosses have not been played by two real games. Every limit is listed in `docs/coop.md` | | **Status** | Draft. Six questions for the maintainers are at the bottom | ## Pictures and video All of these are from the earlier builds this branch was ported from, not from `main`. Two real players, each on their own PC, linked over Radmin VPN (42 seconds, no sound): https://github.com/user-attachments/assets/0f8a8716-4c07-42ef-b1a8-0b95f62f2cc2 The other player in Hyrule Field and in the Market, with the name tag and the line in the corner: <img width="2322" height="1424" alt="Two players in Hyrule Field" src="https://github.com/user-attachments/assets/56f1cd49-c52b-4939-ac49-717531551b3b" /> <img width="2322" height="1424" alt="Two players in the Market" src="https://github.com/user-attachments/assets/c57a5683-ae5d-49bf-9946-8b69d61e6207" /> PvP: lock-on on the other player and the game's own hit effects (four frames of one sword hit; the second player here is the scripted one): <img width="1024" height="708" alt="A sword hit on another player" src="https://github.com/user-attachments/assets/da2c04eb-f913-4e30-bec5-65f65cb82c4e" /> The Network tab (an older build: today the first row is Play as, and there are Connection, Go to player and PvP rows too): <img width="820" height="880" alt="The Network tab" src="https://github.com/user-attachments/assets/1845e02d-855c-4ff3-92e2-46fe20802726" /> Pictures from a build of `main` will follow once there is one. ## What it does - **Network tab:** Host or Join, a name, the host's address (typed, kept off the screen), port (43400), PvP on or off. Nothing starts until Connect is pressed; with co-op off it costs nothing. - **The other players are drawn** as Link with their own animation, sword and shield, a name tag, and a line in the corner saying who is where. - **Shared progress:** lasting items, save flags, chests and keys, and counted pickups, levelled when a player joins. - **PvP (when both players turn it on):** the other player is a real target for the game's own combat: lock-on, the game's hit effects, hearts lost on the other side. - **Enemies are one for everyone:** a hit from any player takes the same life off that enemy in every game, and it dies for all. The game of the nearest player leads each enemy; the others' copies are moved to the same place and play the same animation. - **Bosses:** one leader (whoever started the fight); a second player's hit that takes no life hands the boss over to them, so a stun works from either side. - **Linking up needs Radmin VPN** for friends in different homes: the game only opens a plain TCP link, with no relay. The whole process is below. - **Switches:** `HOS_COOP_ENEMY_PLACE=0`, `HOS_COOP_BOSS=0`, `HOS_COOP_TEST=1`. All documented in `docs/coop.md`. ## How friends link up (Radmin VPN), step by step This is the way every session so far was played: friends in different homes, joined through Radmin VPN. **Once, on every player's PC** 1. Build Hookshot from your own copy of the game. Everyone must be on the same version of Hookshot. 2. Make co-op's two data files (`nx2/coop_flags.py` and `nx2/coop_bodies.py`, run once against the game folder; the commands are in `docs/coop.md`). Do it again after a rebuild that replaces the game folder. 3. Install **Radmin VPN** (free, from radmin-vpn.com), start it, and switch it on with the large button. **Once, for the group** 4. The host, in Radmin VPN: **Network > Create Network**. Pick a network name and a password and send both to your friends in private. 5. Everyone else, in Radmin VPN: **Network > Join Network**, and type that name and password. Each player now sees the others in the Radmin window. 6. Every player has an address in the Radmin window, a number that starts with `26.`. The one that matters is the **host's**. The joiners right-click the host's name there and copy the address. Treat it like a password: do not show it on a stream. Hookshot keeps it off the screen for that reason. **Every time you play** 7. Everyone starts Radmin VPN first and checks that the host shows as online in it. Then start Hookshot and load your save (a slot you can afford to lose). With a randomizer, everyone must be on the same seed. 8. **The host goes first.** Open the menu (Insert or F1), go to **Network**, set **Play as** to **Host**, set **Your name**, then press Enter on **Connection**. The first time, Windows Firewall may ask about Hookshot: allow it, or nobody can reach you. 9. **Then each joiner.** In **Network**, set **Play as** to **Join**. On **Host address** press Enter, paste the host's Radmin address with Ctrl+V, and press Enter again. Leave **Port** at 43400 unless the host changed theirs. Set **Your name**, then press Enter on **Connection**. 10. **Status** turns to Connected and names who is there, and the top left corner of the screen says the same. After a moment everyone holds the same items and progress. The other players are Links with their name over their head; if you are in different maps, press Enter on a player's **Go to player** row. 11. PvP is off until two players both turn **PvP** on. 12. To stop, press Enter on **Connection** again, or close the game. A game always starts not connected, so steps 8 and 9 are done each time; the name, the address and the port are remembered. **If it does not connect** - **Status** says why. Read it first. - Is the host connected before the joiners try? A joiner who was early presses Enter on **Connection** again. - Does Radmin VPN show the host as online for the joiner? If not, the trouble is in Radmin, not in Hookshot. - Did the joiner paste the **host's** address, not their own? - Is **Port** the same number for everyone? - Did the host allow Hookshot when Windows Firewall asked? If the question was closed or refused, allow Hookshot under "Allow an app through Windows Firewall". Do not turn the firewall or the antivirus off. - Is everyone on the same version of Hookshot? On one home network Radmin is not needed: the joiners type the host's address on that network instead. Other virtual networks (Tailscale, ZeroTier) or a port forwarded to the host work the same way, but have not been tried. ## What changed - `host/oot_coop.c` (new): the whole runtime: the link (plain text lines over TCP), the players' bodies, what is shared, enemies and bosses. - `nx2/oot_patches.py`, `host/hos_main.c`, `host/nn_vi.c`, `host/oot_menu.c`: the calls into it (Link's update, the frame, pickups, the Life component's update). - `gpu/vk/vk_menu.cpp`, `vk_menu.h`, `oot_coop_opts.h`, `vk_replay.cpp`: the Network tab and what is drawn over the game. - `nx2/coop_bodies.py`, `nx2/coop_flags.py`, `nx2/byml_rw.py`: two scripts, run by hand, that make the other players' actors and the list of shared flags **from the player's own dump** into a mod folder. Nothing made from the game is in this PR. - `tests/test_oot_coop.py`, `docs/coop.md`, the README and `docs/development.md`. ## How it was made Co-op was built on a private branch in about sixty small builds, first on v2.0.13, then v2.0.14, then v2.1.0, and each build was tried in the game before the next one was started. Kuba set what it had to do, played every build (alone, with friends over Radmin VPN, and in a test copy) and judged it on the screen. The code was written with Claude Code. Several parts below exist because something looked wrong to him while playing, not because a check failed. ### How things were found - **The lifted C and a disassembly of the game image.** A string (a script node's name, an actor's name) leads to the code that uses it. A script node's run function sits 0xa0 after its name function in its vtable, which is how the game's own "get life", "play animation" and "create actor" functions were found. - **The game's own data files.** Most of what the other players' bodies can do is data, not code. Dark Link is a Link-shaped enemy, so his actor pack was the model: what he has that the pause menu's Link lacks is what makes a body something the game's combat can hit and lock on to. - **Reading the running game's memory** from outside, to see which fields change when Link is hit, picks something up or opens a chest. - **Logging first.** For shared enemies the first build only logged every actor that has a life, to learn what names an enemy the same in two games, before anything was sent. ### How it was tested while being built - **A test copy of the game** on a copy of a save, with its own user folder, usually with no window, driven by scripted pad input and judged from the game's own frame dumps, read as runs of consecutive frames (a single still hides a sliding or frozen body). - **A stand-in second player:** a script that joins over the link and says what a second game would. It can stand, run in a circle, mirror the real player, hit back, and replay a recording. - **Recorded fights.** A game that leads an enemy can write its `Q` lines to a file. Kuba fought the bosses in a playable test copy while it recorded; the recording is then played to a second game, which must show the same fight. - **The test lines** behind `HOS_COOP_TEST=1` (travel to a map, stand at a place, make an actor beside Link) put Link next to a given enemy without playing there. - **Real sessions with friends** on the v2.0.14 build. Their logs found bugs no script did (below). ### In the order it was built, with what did not work 1. **The link and shared items.** Text lines over TCP, the host passing them on. Every pickup goes through the pouch's add by name (`f_8112d570`), so one call there sees them all. Heart containers are given through the pouch too: the max-hearts value in the game's data turned out to be a mirror the game rewrites every frame, so writing it does nothing. 2. **Shared flags.** The first rule was "a flag that went from off to on is shared". The first night with three players showed it was wrong: 54 of the shared flags start *on*, and a character who had moved aside came back for everyone. The rule became "a flag that has left the value a new file starts with". A flag written without the game's changed-since-save mark is also not saved, which cost another build. Per-object flags are shared only where the game never resets them: pots, enemies and shop stock come back by design and are left alone. 3. **Seeing the other players.** The first body was the pause menu's Link, made with the game's own create-actor call and moved by its place request. He slid about in a stiff pose, because his animation graph only has menu poses. Writing a pose straight into the animation result buffer was tried for three builds and never moved the drawn body. What works: a new actor, made on each PC from the player's own files, that is the same body with Link's own animation graph. Each game says which command each of Link's nine animation slots is playing and the animation inputs that changed, and the body plays the same. 4. **Sword and shield.** Calling the game's equip function put the sword at the body's feet, and a second call into a used slot crashed. It became data instead: an equipment table and two names, as Dark Link's pack has them, and the sword and shield then follow the mirrored animation by themselves. 5. **PvP.** The first version went by reach: the attacker's game decided a swing within 1.9 m had landed. A session with friends showed hits landing during a cutscene, which broke the scene for the player being moved by it, so hits are now ignored while a player's game is in an event and for 1.5 s after. Then the body was made a real target: Dark Link's life, hit, affiliation and target parts, on the body's own physics with one added sensor. Taking Dark Link's whole physics gave the body a mind of its own: it walked away from where its player stood. The slash flash, sparks, sound and lock-on arrows came from three more parts of data. Dark Link's own lock-on part could not be used, because it checks a value only his fight sets. 6. **One life for an enemy.** The Life component's update (`f_804bd8c0`) is where any actor's life goes down, in two compare-and-swap writes. Nothing inside an actor names it the same in two games, so an enemy is matched by its name and the place it was first seen. Writing life 0 by hand does not kill: the enemy stands there at zero. The damage is given through the game's own life message instead, and the enemy then dies as it does to a real hit. One early build crashed at every save load: an address of 0 passed the range check, so every read of game memory is now checked. 7. **One place for an enemy.** The place request that moves a player's body does nothing on an enemy; the game's own move request, which goes through physics, does. The leader was at first whichever game updated the enemy first. That made an enemy ignore the player standing next to it, so the nearest player's game leads now, with a margin so the lead does not flap. For two builds every player stood frozen: animation command names live in memory above the heap, and a check that had been made too narrow refused to read them. 8. **Bosses.** A boss fight is a string of states only the leader's copy is in, so a second player's seed in Gohma's eye was blocked by his own copy. Putting the hit back into the game's hit list was tried and does nothing: damage and reactions happen at the real hit, not later. The answer was the hand-over: a hit that takes no life passes the boss to the game that made it. It was found and proven with Kuba playing the second player against a recorded leader. He then fought Gohma, King Dodongo, Barinade, Phantom Ganon, Volvagia, Morpha, Bongo Bongo, Twinrova and Ganondorf as the leader with this code on, with no crash in any fight. 9. **The menu.** The Network tab was written for the old menu and fitted again when v2.1.0 rewrote the menu on Dear ImGui. Its option ids are in a header of their own so the shader cache stamp does not move. 10. **The port to `main`.** The co-op parts were lifted out of the private branch on their own, renamed to the project's `oot_*` names, and everything else from that branch left behind. This is the step that has only been compiled and unit-tested so far. ## How it was tested - **This branch, on `main`:** every C and C++ file touched passes a syntax-only compile with llvm-mingw; `python -I -B tests/run_all.py` gives 427 passed, 6 failed, 38 skipped on Windows 11, and the same 6 fail on an untouched `main` on this PC. **Not built into a game, not started.** Nothing compiled for Linux: co-op is Windows-only here, and elsewhere its entry points do nothing. - **Two real players over the internet (v2.0.14 build):** shared items, flags and keys, the other players' bodies, and the first PvP (by reach). - **One game plus a scripted second player (v2.1.0, RTX 3060):** PvP through the game's own combat, shared enemy life, enemies following their leader, the lead passing to the nearer player, and bosses. A person played the second player's side of the Gohma fight against a recorded leader: following it, being hurt by it, stunning and killing it. Gohma is the only boss tried from that side. - **Not tested:** enemies and bosses with two real games; any boss but Gohma as a follower; enemies the game spawns on the fly (each game still has its own). ## Questions for the maintainers 1. Is a feature this size welcome as one PR, or would you rather have it in pieces (runtime, menu, builders)? 2. The option ids sit in their own header (`oot_coop_opts.h`) so the shader cache stamp does not move, and the options file keys are `play_as`, `port`, `pvp`. Should they move to `vk_api.h` and become `coop_*`? 3. Should the build make the co-op data files itself, perhaps behind an opt-in, instead of the two scripts being run by hand? 4. `oot_coop.c` carries copies of two static pouch functions from `hos_main.c`. Would you export those instead? 5. Linux: stand-ins for now, or sockets through `host/posix` first? 6. The link is plain text with no password. Is that acceptable for a first version? Co-op by Kuba. Written with Claude Code; the trailers are on each commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
mooki000 commented 2026-11-12 00:35:20 +00:00 (Migrated from github.com)

@itzdaface this is great to see, and you are well ahead of me where it counts for players: animated Links with sword and shield, name tags, shared enemies, bosses, PvP, and real sessions with friends. I opened a co-op draft the same evening, #2274, and the two collide file for file (host/oot_coop.c, docs/coop.md, tests/test_oot_coop.py, a new menu tab each), so the maintainers could only take one as they stand. I'd rather put them together than have anyone pick, because they are strong in different places:

#2578 (yours) #2274 (mine)
What players see and do bodies with animation, names, go to player, enemies, bosses, PvP shared progress and a Co-op tab; the rest only started
On main ported, compiled file by file built and played: two games in one room
Platform Windows Linux; Windows written (Winsock, BCrypt) but never run
The link TCP text lines to a host; Radmin VPN UDP through a small relay, sealed with a key from the room's secret
Before a save is changed nothing the player accepts the room, and the save folder is copied first
Tests hooks, ids, builders the C compiled and run under sanitizers, a hostile relay, two-game sessions

What combining would give, under the four things I'd judge either pull request by:

Simpler

  • One link instead of two, and no VPN step: Host/Join/address/port become "make a room" and "join with its secret". host/coop_net.c already has the sockets for Winsock and POSIX, which answers your question 5.
  • Your 3,100-line file splits along lines that exist: the session (coop.c), the wire (coop_proto.c), the progress merge (coop_state.c), and the game side, where bodies, enemies, bosses and PvP would each be a file that sends and takes typed messages.
  • The typed name and address rows can use the menu's text fields (a commit of #2274, and on its own as #2594).
  • Your questions 2 to 4 get one answer each: coop_* keys, the pouch functions exported once, and the body's actor made by the build instead of a script run by hand (I have a build-time generator for it on a branch, not yet proven in the game; yours is the part that makes it move).

Faster

  • Place and animation as datagrams. Over TCP one lost packet holds back everything behind it, which shows as a body that freezes and jumps; over UDP the next state simply replaces the lost one.
  • Fixed binary messages instead of text and base64 parsed with sscanf thirty times a second for each player.
  • A relay passes the messages on, not the host's game.
  • On a join every game says everything again. #2274 merges values by "the highest wins" and catches a joiner up by digests: a sum for each of 64 sections every 2 s and at a join, and only the sections that differ are sent.
  • A game that shows nobody, because the others are in other maps, can say its place slowly (a commit on my players branch does that).
  • Neither of us has measured the cost per frame. I can: I have a harness that runs two games on one PC without a window and logs their frame times.

More for players

  • Linux, the Steam Deck and Windows from one code path.
  • Friends in different homes without a VPN: one relay everyone can reach (a small server, or one UDP port forwarded where it runs); the players' own games need no forwarded port.
  • Nobody is the host: any player can leave and come back, and a room stays for 10 minutes after the last one left. The maker can remove a player and reset the pools from the tab.
  • Consent and a way back: nothing from a room is applied before the player accepts it, and the whole save folder is copied first.
  • Room rules the maker signs: each kind of progress shared, pooled or kept per player.
  • And on top, everything yours has that mine lacks. I'd port yours rather than finish my own versions.

Safer (your question 6)

  • Today whoever can reach the host's port joins with H 1 <name>. From then on their lines give items (I, up to 99 a line), set shared flags (F) and per-object flags such as opened chests (K), change small-key counts (N), take life off any enemy (U) and, with PvP on, hearts off a player (D); the items and flags are then saved by each game. With Radmin the network's password is the only gate; on a home network or a forwarded port the text travels in the clear.
  • In #2274 a room is a secret. Every message is sealed under a key derived from it, so the relay can neither read nor change one; the room's rules are signed by its maker's key; each player has a key whose fingerprint the others read aloud before they accept; and values are only raised, only inside the game's own table, only through its own setters.
  • For your messages that would mean: enemy and PvP lines taken only from members this player accepted, only while a signed room rule allows them, with a limit per member, and a hit decided by the receiver's game wherever it can be.

How I'd go about it, if you like the idea: your gameplay commits on top of #2274's six, with you as their author, ported to typed messages in the order bodies, enemies, PvP. You test on Windows with your friends, which I cannot; I test on Linux with two games on one PC, which you have not had. Or the other way round, my base as commits into your branch, if you'd rather keep #2578 as the one. Either way the first question of both drafts, whether a socket in the host is acceptable at all, is the maintainers', and I'd leave both drafts open until they answer.

What do you think?

Edited once, before any reply: #2594 named for the text fields; #2274's digest catch-up stated as existing rather than proposed; the relay's need for a reachable port said exactly; K and N named for what they are.

@itzdaface this is great to see, and you are well ahead of me where it counts for players: animated Links with sword and shield, name tags, shared enemies, bosses, PvP, and real sessions with friends. I opened a co-op draft the same evening, #2274, and the two collide file for file (`host/oot_coop.c`, `docs/coop.md`, `tests/test_oot_coop.py`, a new menu tab each), so the maintainers could only take one as they stand. I'd rather put them together than have anyone pick, because they are strong in different places: | | #2578 (yours) | #2274 (mine) | |---|---|---| | What players see and do | bodies with animation, names, go to player, enemies, bosses, PvP | shared progress and a Co-op tab; the rest only started | | On `main` | ported, compiled file by file | built and played: two games in one room | | Platform | Windows | Linux; Windows written (Winsock, BCrypt) but never run | | The link | TCP text lines to a host; Radmin VPN | UDP through a small relay, sealed with a key from the room's secret | | Before a save is changed | nothing | the player accepts the room, and the save folder is copied first | | Tests | hooks, ids, builders | the C compiled and run under sanitizers, a hostile relay, two-game sessions | What combining would give, under the four things I'd judge either pull request by: **Simpler** - One link instead of two, and no VPN step: Host/Join/address/port become "make a room" and "join with its secret". `host/coop_net.c` already has the sockets for Winsock and POSIX, which answers your question 5. - Your 3,100-line file splits along lines that exist: the session (`coop.c`), the wire (`coop_proto.c`), the progress merge (`coop_state.c`), and the game side, where bodies, enemies, bosses and PvP would each be a file that sends and takes typed messages. - The typed name and address rows can use the menu's text fields (a commit of #2274, and on its own as #2594). - Your questions 2 to 4 get one answer each: `coop_*` keys, the pouch functions exported once, and the body's actor made by the build instead of a script run by hand (I have a build-time generator for it on a branch, not yet proven in the game; yours is the part that makes it move). **Faster** - Place and animation as datagrams. Over TCP one lost packet holds back everything behind it, which shows as a body that freezes and jumps; over UDP the next state simply replaces the lost one. - Fixed binary messages instead of text and base64 parsed with `sscanf` thirty times a second for each player. - A relay passes the messages on, not the host's game. - On a join every game says everything again. #2274 merges values by "the highest wins" and catches a joiner up by digests: a sum for each of 64 sections every 2 s and at a join, and only the sections that differ are sent. - A game that shows nobody, because the others are in other maps, can say its place slowly (a commit on my players branch does that). - Neither of us has measured the cost per frame. I can: I have a harness that runs two games on one PC without a window and logs their frame times. **More for players** - Linux, the Steam Deck and Windows from one code path. - Friends in different homes without a VPN: one relay everyone can reach (a small server, or one UDP port forwarded where it runs); the players' own games need no forwarded port. - Nobody is the host: any player can leave and come back, and a room stays for 10 minutes after the last one left. The maker can remove a player and reset the pools from the tab. - Consent and a way back: nothing from a room is applied before the player accepts it, and the whole save folder is copied first. - Room rules the maker signs: each kind of progress shared, pooled or kept per player. - And on top, everything yours has that mine lacks. I'd port yours rather than finish my own versions. **Safer** (your question 6) - Today whoever can reach the host's port joins with `H 1 <name>`. From then on their lines give items (`I`, up to 99 a line), set shared flags (`F`) and per-object flags such as opened chests (`K`), change small-key counts (`N`), take life off any enemy (`U`) and, with PvP on, hearts off a player (`D`); the items and flags are then saved by each game. With Radmin the network's password is the only gate; on a home network or a forwarded port the text travels in the clear. - In #2274 a room is a secret. Every message is sealed under a key derived from it, so the relay can neither read nor change one; the room's rules are signed by its maker's key; each player has a key whose fingerprint the others read aloud before they accept; and values are only raised, only inside the game's own table, only through its own setters. - For your messages that would mean: enemy and PvP lines taken only from members this player accepted, only while a signed room rule allows them, with a limit per member, and a hit decided by the receiver's game wherever it can be. How I'd go about it, if you like the idea: your gameplay commits on top of #2274's six, with you as their author, ported to typed messages in the order bodies, enemies, PvP. You test on Windows with your friends, which I cannot; I test on Linux with two games on one PC, which you have not had. Or the other way round, my base as commits into your branch, if you'd rather keep #2578 as the one. Either way the first question of both drafts, whether a socket in the host is acceptable at all, is the maintainers', and I'd leave both drafts open until they answer. What do you think? *Edited once, before any reply: #2594 named for the text fields; #2274's digest catch-up stated as existing rather than proposed; the relay's need for a reachable port said exactly; `K` and `N` named for what they are.*
itzdaface commented 2026-11-12 00:56:07 +00:00 (Migrated from github.com)

@mooki000 thanks for this, and for reading mine that closely. Yes, I'm interested, and the connection code most of all. I've read through coop_net, coop_proto, the session interface and the relay doc, so the questions below come from that.

The VPN step is the first thing people push back on with mine. Someone in Discord already asked why it needs Radmin, and my answer was that I want linking up built into the game. Your rooms are much closer to that than anything I have: a secret to share, no addresses passed around between players, and a relay that can't read or change what goes through it. You're also right about my link. Anyone who can reach the port can join and send items, flags and hits. I knew that was the weak part.

So in principle: one co-op, your link and session underneath, my bodies, enemies, bosses and PvP on top as typed messages. I'm fine with it landing under your pull request, #2274. On credit I'd keep it simple and mutual: we're both on it. My parts go in as my commits, yours as yours, and if the project squashes it into one commit, whichever of us isn't the PR author gets a Co-authored-by line. Your order (bodies, then enemies, then PvP) makes sense to me.

Four things I'd like to sort out first, because they decide how my side gets ported:

  1. Who runs the relay for ordinary players? The doc says the players run one, which means someone forwards a UDP port or rents a server. For most groups that's the same hurdle as a VPN, just moved. Is the idea that the project hosts a public one? And could two games on one home network talk without a relay at all?
  2. The per-member limit. 200 datagrams a second with a broadcast to seven counted seven times is about 28 broadcasts a second in a full room. Mine sends a player's place 30 times a second, animation changes on top, and 15 a second for every enemy a game leads, and a game can lead several at once. I can pack all the enemies a game leads into one message and drop the rates, but it would help to know what budget you'd want me to design to, or whether the limit can move.
  3. Messages that must arrive. Nothing is acknowledged, which is fine for place and animation: the next one replaces the lost one. But "my hit took this much life off that enemy" and a PvP hit are one-off events. I can turn enemy life into state ("this enemy's life is now X, lowest wins") so a lost message heals itself. PvP hits and the boss hand-over are harder. Would you take a small resend-until-acked helper in the session, or should each layer do its own?
  4. Size. A message tops out near 1,100 bytes. My animation inputs can be larger than that when a player first appears, so I'd split them. Your POS already carries four animation slots with a node name and a frame. Mine mirrors all nine of Link's slots plus the inputs that drive them, which is what makes the body move exactly as its player does. I'd rather extend or sit beside POS than fight it. Which would you prefer?

What I can do right away: you said the Windows side is written but never run. I'll build your branch on Windows and try two games through a relay, and tell you what happens. That seems like the most useful first thing from me.

Where mine stands, so there are no surprises: it was played by real players on v2.0.14 and tested with a scripted second player on v2.1.0, but the port to main has only been compiled, not run. Getting it running on main was my next step anyway, so doing that on top of your base costs me little.

I agree on leaving both drafts open until the maintainers say whether a socket in the host is acceptable at all. That also goes for the size of the whole thing: yours plus mine is a lot to review, so it might be worth asking them early whether they'd want it in stages.

@mooki000 thanks for this, and for reading mine that closely. Yes, I'm interested, and the connection code most of all. I've read through `coop_net`, `coop_proto`, the session interface and the relay doc, so the questions below come from that. The VPN step is the first thing people push back on with mine. Someone in Discord already asked why it needs Radmin, and my answer was that I want linking up built into the game. Your rooms are much closer to that than anything I have: a secret to share, no addresses passed around between players, and a relay that can't read or change what goes through it. You're also right about my link. Anyone who can reach the port can join and send items, flags and hits. I knew that was the weak part. So in principle: one co-op, your link and session underneath, my bodies, enemies, bosses and PvP on top as typed messages. I'm fine with it landing under your pull request, #2274. On credit I'd keep it simple and mutual: we're both on it. My parts go in as my commits, yours as yours, and if the project squashes it into one commit, whichever of us isn't the PR author gets a `Co-authored-by` line. Your order (bodies, then enemies, then PvP) makes sense to me. Four things I'd like to sort out first, because they decide how my side gets ported: 1. **Who runs the relay for ordinary players?** The doc says the players run one, which means someone forwards a UDP port or rents a server. For most groups that's the same hurdle as a VPN, just moved. Is the idea that the project hosts a public one? And could two games on one home network talk without a relay at all? 2. **The per-member limit.** 200 datagrams a second with a broadcast to seven counted seven times is about 28 broadcasts a second in a full room. Mine sends a player's place 30 times a second, animation changes on top, and 15 a second for every enemy a game leads, and a game can lead several at once. I can pack all the enemies a game leads into one message and drop the rates, but it would help to know what budget you'd want me to design to, or whether the limit can move. 3. **Messages that must arrive.** Nothing is acknowledged, which is fine for place and animation: the next one replaces the lost one. But "my hit took this much life off that enemy" and a PvP hit are one-off events. I can turn enemy life into state ("this enemy's life is now X, lowest wins") so a lost message heals itself. PvP hits and the boss hand-over are harder. Would you take a small resend-until-acked helper in the session, or should each layer do its own? 4. **Size.** A message tops out near 1,100 bytes. My animation inputs can be larger than that when a player first appears, so I'd split them. Your `POS` already carries four animation slots with a node name and a frame. Mine mirrors all nine of Link's slots plus the inputs that drive them, which is what makes the body move exactly as its player does. I'd rather extend or sit beside `POS` than fight it. Which would you prefer? What I can do right away: you said the Windows side is written but never run. I'll build your branch on Windows and try two games through a relay, and tell you what happens. That seems like the most useful first thing from me. Where mine stands, so there are no surprises: it was played by real players on v2.0.14 and tested with a scripted second player on v2.1.0, but the port to `main` has only been compiled, not run. Getting it running on `main` was my next step anyway, so doing that on top of your base costs me little. I agree on leaving both drafts open until the maintainers say whether a socket in the host is acceptable at all. That also goes for the size of the whole thing: yours plus mine is a lot to review, so it might be worth asking them early whether they'd want it in stages.
mooki000 commented 2026-11-12 01:00:49 +00:00 (Migrated from github.com)

@itzdaface good, then we do it that way: one co-op under #2274, my link and session underneath, your bodies, enemies, bosses and PvP on top as typed messages, your commits yours and mine mine, a Co-authored-by for whoever is not the author if it gets squashed. Your four questions are the right ones; none has a finished answer in my code, so here is where each stands and what I would do.

1. Who runs the relay. Today a player or a server does, and you are right that a forwarded port is the same hurdle as a VPN. On one home network any PC can run coop_relay.py without touching the router, but it is still a second thing to start. What I would do, in this order:

  • Host on this PC: the relay's loop inside the game, so that the maker's game is the relay. The session already has the socket and the format. On one network that is "Make a room" and nothing else; across the internet the maker forwards one UDP port, which is your Host/Join of today, sealed.
  • A public relay is the maintainers' call (question 3 of #2274). It cannot read or change what it forwards and has limits per member and per network, so it mostly costs bandwidth. I will ask them there.

2. The limit. It is a default in the relay, not a law: 200 forwarded copies and 64 KB a second per member, sized for progress sync, which paces itself at 32 KB a second. Bodies and enemies need more. Design to this, and I move the relay to fit it with room to spare and make the two numbers options:

  • one body message per player at 15 a second (my POS runs at that rate; if it looks choppy we smooth on the receiving side before we raise it);
  • one message for all the enemies a game leads, at 10 to 15 a second, not one per enemy;
  • events on top.
    That is about 30 datagrams a second per player, 210 copies in a full room, so the default becomes 480 copies and 256 KB.

3. Messages that must arrive. Enemy life as state is the right turn, and it fits what the progress layer already does: make it "damage taken so far, the highest wins" and a lost message heals itself. For the true one-offs (a PvP hit, the boss hand-over) I would rather have one helper in the session than one per layer: an id per sender for each event, the receiver's acknowledgements riding on its next message, a resend until acknowledged or the member is gone, repeats dropped by id, a bounded queue. One detail: the envelope refuses the same sealed datagram twice, so a resend is a new message carrying the same event id. I will write it, with a test at 30 % loss like the session's.

4. Size, and POS. Beside it, not inside it. POS stays the small "where" message every game sends, also one that shows nobody (map, place, facing, flags). Your body state is its own kind, with all nine slots and the inputs, at the body rate; the four slots in my POS go away when yours is in, since yours is the better body. For the first appearance, when the inputs are larger than one message, the helper from 3 can carry a larger piece in parts. Kinds are only numbers inside the sealed message; 1 to 7 are in #2274, and I will put the list into docs/coop.md so we take the next ones together.

Windows: yes please, that is the most useful thing right now. The branch is #2274's head (coop-relay-wire on my fork, 71d4ab0f); "On one PC, two folders" in its description is the quickest way to two games through a relay. Two things you would otherwise trip over: the project's Windows job shows two of my tests not building there (%zu in host/coop.c under the runner's gcc, and deprecation errors in the text-field test's fixture). Both are in how the tests compile, and fixes are on the way; I expect the game's own build is not affected, but that is exactly what I have not seen. What I would most like to hear: whether it builds, whether Make a room and Join work through a relay on 127.0.0.1, whether typing in the tab's fields works (WM_CHAR), and what the firewall asks.

For your layers I will add one call to the base that says whether this player has accepted a given member, so that body, enemy and PvP messages count only from accepted members.

And agreed on asking early about stages. I will put it to the maintainers on #2274 as: the link and session; progress sync and the tab; bodies; enemies and bosses; PvP.

@itzdaface good, then we do it that way: one co-op under #2274, my link and session underneath, your bodies, enemies, bosses and PvP on top as typed messages, your commits yours and mine mine, a `Co-authored-by` for whoever is not the author if it gets squashed. Your four questions are the right ones; none has a finished answer in my code, so here is where each stands and what I would do. **1. Who runs the relay.** Today a player or a server does, and you are right that a forwarded port is the same hurdle as a VPN. On one home network any PC can run `coop_relay.py` without touching the router, but it is still a second thing to start. What I would do, in this order: - **Host on this PC:** the relay's loop inside the game, so that the maker's game is the relay. The session already has the socket and the format. On one network that is "Make a room" and nothing else; across the internet the maker forwards one UDP port, which is your Host/Join of today, sealed. - **A public relay** is the maintainers' call (question 3 of #2274). It cannot read or change what it forwards and has limits per member and per network, so it mostly costs bandwidth. I will ask them there. **2. The limit.** It is a default in the relay, not a law: 200 forwarded copies and 64 KB a second per member, sized for progress sync, which paces itself at 32 KB a second. Bodies and enemies need more. Design to this, and I move the relay to fit it with room to spare and make the two numbers options: - one body message per player at 15 a second (my `POS` runs at that rate; if it looks choppy we smooth on the receiving side before we raise it); - one message for all the enemies a game leads, at 10 to 15 a second, not one per enemy; - events on top. That is about 30 datagrams a second per player, 210 copies in a full room, so the default becomes 480 copies and 256 KB. **3. Messages that must arrive.** Enemy life as state is the right turn, and it fits what the progress layer already does: make it "damage taken so far, the highest wins" and a lost message heals itself. For the true one-offs (a PvP hit, the boss hand-over) I would rather have one helper in the session than one per layer: an id per sender for each event, the receiver's acknowledgements riding on its next message, a resend until acknowledged or the member is gone, repeats dropped by id, a bounded queue. One detail: the envelope refuses the same sealed datagram twice, so a resend is a new message carrying the same event id. I will write it, with a test at 30 % loss like the session's. **4. Size, and `POS`.** Beside it, not inside it. `POS` stays the small "where" message every game sends, also one that shows nobody (map, place, facing, flags). Your body state is its own kind, with all nine slots and the inputs, at the body rate; the four slots in my `POS` go away when yours is in, since yours is the better body. For the first appearance, when the inputs are larger than one message, the helper from 3 can carry a larger piece in parts. Kinds are only numbers inside the sealed message; 1 to 7 are in #2274, and I will put the list into `docs/coop.md` so we take the next ones together. **Windows:** yes please, that is the most useful thing right now. The branch is #2274's head (`coop-relay-wire` on my fork, `71d4ab0f`); "On one PC, two folders" in its description is the quickest way to two games through a relay. Two things you would otherwise trip over: the project's Windows job shows two of my tests not building there (`%zu` in `host/coop.c` under the runner's gcc, and deprecation errors in the text-field test's fixture). Both are in how the tests compile, and fixes are on the way; I expect the game's own build is not affected, but that is exactly what I have not seen. What I would most like to hear: whether it builds, whether Make a room and Join work through a relay on `127.0.0.1`, whether typing in the tab's fields works (`WM_CHAR`), and what the firewall asks. For your layers I will add one call to the base that says whether this player has accepted a given member, so that body, enemy and PvP messages count only from accepted members. And agreed on asking early about stages. I will put it to the maintainers on #2274 as: the link and session; progress sync and the tab; bodies; enemies and bosses; PvP.
Commenting is not possible because the repository is archived.
No description provided.