Repository navigation
Conversation
Two to four players, each in their own copy of the game, share one adventure over a TCP link one of them hosts (port 43400): items, save flags, chests and pickups, small keys, where each player is, and enemies (one life for all, one place for all, led by the nearest player's game; a boss keeps its leader, and a follower's own hit that takes no life takes the boss over). host/oot_coop.c holds all of it: the link's thread, what is shared, the other players' bodies, PvP, go to a player. Nothing calls it yet. It talks to the menu through the option ids in gpu/vk/oot_coop_opts.h, read by both sides. Beside it, the two things it asks of the runtime: - hos_oot_travel (host/hos_main.c): the debug tools' teleport by a map's own name, for going to a player on another map. - hos_oot_menu_in_front (host/oot_menu.c): whether one of the game's own menus is in front, so co-op's corner and the names are not drawn over it. Windows only for now: the sockets are Winsock's. Elsewhere the file builds to entry points that do nothing. Written and played on v2.1.0; this is its port to main. Not built into a game or run on main: see docs/coop.md. Assisted-by: Claude Code Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
- host/nn_vi.c: oot_coop_frame with every frame shown (the Network tab's rows, the link's comings and goings; at the title screen too). - host/hos_main.c: oot_coop_player from Link's own update (where he stands, and everything that reads or changes the game). - nx2/oot_patches.py, the supported build only: patch_coop_pickup puts oot_coop_pouch_add first in the pouch's add by name (f_8112d570), so a pickup can be told to the other players; patch_coop_life puts oot_coop_life_seen at the head of the Life component's update (f_804bd8c0) and oot_coop_life_write before each of its two writes of the life, after the heart protection's guard, so an enemy's life is one for all. Both are counted in report(), as the other hooks are. Each returns at once while no link is up. tests/test_present_trace.py pastes hos_vi_native_presented into its fixture: it gets a stand-in for oot_coop_frame. Assisted-by: Claude Code Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
A sixth tab: Play as (Host or Join), Host address, Port, Your name, Connection, Status, a Go to player row for each other player, and PvP. The name and the address are typed in their rows (Enter, the text, Enter; Ctrl+V pastes; the address shows only its last letter). The mouse and the compact bar work on the rows as on any other. While connected, the top left corner shows the status, the other players and where they are, and a line for a few seconds when something happens; each other player has their name over their head. Neither is drawn over the game's own menus. The host and the menu talk through the HOS_OPT_COOP_* ids (gpu/vk/oot_coop_opts.h, not vk_api.h: that file is part of the shader cache's stamp): vk_replay.cpp's option hands that range to menu_coop_option, and menu_option_set takes the host's texts a letter at a time. Play as, Port and PvP are saved in hookshot_options.ini by their names, as every row (play_as, port, pvp), and no preset sets them. The tab strip steps its text and padding down where six tabs do not fit a narrow window. The control bridge names the tab "network". tests/test_fps_overlay_order.py pastes menu_record into its fixture: it now checks co-op's corner too (under the menu, not over the start-up screen). Assisted-by: Claude Code Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
Two scripts, run by hand on a built game folder. Everything they write is made from the player's own game files and stays in the game folder. - nx2/coop_flags.py writes coop/flags.bin: the hashes of the save flags the players share, chosen from the game's own flag list (a Bool kept in the save that the game never resets, less the ones that are each player's own). Without it items are shared and flags are not. - nx2/coop_bodies.py writes the mod mods/00-coop: CoopPlayer_Child/_Adult, the pause menu's Link with Link's own animation graph, sounds and equipment, and CoopFoe_Child/_Adult, the same body as something the game's combat can hit (PvP), with the size table entries and actor table rows new actors need. Without it the other players are drawn as the pause menu's Link and there is no PvP. - nx2/byml_rw.py: the game's little-endian BYML, read and written without loss. make_game.py does not run them: a mod that carries the resource size table and the actor tables in every game folder is a choice for the maintainers. They need zstandard, as the pipeline does. Assisted-by: Claude Code Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
tests/test_oot_coop.py: the three calls nx2/oot_patches.py makes (once, declared, first or before each write, after the heart protection's guard, left alone when patched before, only in the supported build's lift); every entry point defined for Windows and stood in for elsewhere; the runtime's calls; the option ids one run of numbers that are no other row's, each answered or taken by the menu; the Network tab's rows; and of the builders what needs no game: BYML and actor packs written and read back the same, the size table's entries kept in order and only ever raised. Assisted-by: Claude Code Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
docs/coop.md: what co-op is, what it needs, the Network tab, what is shared, how it works (the link's lines, the bodies, items and flags, one enemy for everyone, bosses), the switches, and what is not done or not tested. The README's menu table gets the Network tab's rows; the development guide the HOS_COOP_* switches and the three rows' keys in the options file. Assisted-by: Claude Code Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
The doc listed Radmin as one of three virtual networks. It is the one every session so far was played over, so it is now named as what is needed, with the other ways (another virtual network, a forwarded port) after it. Assisted-by: Claude Code Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
A new section in docs/coop.md: what each player does once (build, the two data files, Radmin VPN), what the group does once (one Radmin network, the host's address), what is done every session in the Network tab, host first, and what to check when it does not connect. Assisted-by: Claude Code Co-authored-by: Claude Fable 5.2 <noreply@anthropic.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 (
What combining would give, under the four things I'd judge either pull request by: Simpler
Faster
More for players
Safer (your question 6)
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; |
|
@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 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 Four things I'd like to sort out first, because they decide how my side gets ported:
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 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. |
|
@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 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
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:
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 Windows: yes please, that is the most useful thing right now. The branch is #2274's head ( 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. |
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
docs/coop.mdhost/oot_coop.c(3,101 lines), plus the Network tab, two data scripts, a test file and the docsmain: 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 indocs/coop.mdPictures 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):
coop-two-players.mp4
The other player in Hyrule Field and in the Market, with the name tag and the line in the corner:
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):
The Network tab (an older build: today the first row is Play as, and there are Connection, Go to player and PvP rows too):
Pictures from a build of
mainwill follow once there is one.What it does
HOS_COOP_ENEMY_PLACE=0,HOS_COOP_BOSS=0,HOS_COOP_TEST=1. All documented indocs/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
nx2/coop_flags.pyandnx2/coop_bodies.py, run once against the game folder; the commands are indocs/coop.md). Do it again after a rebuild that replaces the game folder.Once, for the group
friends in private.
others in the Radmin window.
26.. The one that matters is thehost'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
save (a slot you can afford to lose). With a randomizer, everyone must be on the same seed.
Your name, then press Enter on Connection. The first time, Windows Firewall may ask about Hookshot: allow
it, or nobody can reach you.
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.
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.
and 9 are done each time; the name, the address and the port are remembered.
If it does not connect
"Allow an app through Windows Firewall". Do not turn the firewall or the antivirus off.
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 anddocs/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
How it was tested while being built
Qlines 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.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.In the order it was built, with what did not work
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.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.main. The co-op parts were lifted out of the private branch on their own, renamed to the project'soot_*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
main: every C and C++ file touched passes a syntax-only compile with llvm-mingw;python -I -B tests/run_all.pygives 427 passed, 6 failed, 38 skipped on Windows 11, and the same 6 fail on an untouchedmainon 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.Questions for the maintainers
oot_coop_opts.h) so the shader cache stamp does not move, and the options file keys areplay_as,port,pvp. Should they move tovk_api.hand becomecoop_*?oot_coop.ccarries copies of two static pouch functions fromhos_main.c. Would you export those instead?host/posixfirst?Co-op by Kuba. Written with Claude Code; the trailers are on each commit.
🤖 Generated with Claude Code