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
pull from: itzdaface/feat/coop
merge into: MagicBeanDev:main
MagicBeanDev:main
MagicBeanDev:github/pr/2893
MagicBeanDev:github/pr/2890
MagicBeanDev:github/pr/2887
MagicBeanDev:github/pr/2878
MagicBeanDev:github/pr/2875
MagicBeanDev:github/pr/2874
MagicBeanDev:github/pr/2873
MagicBeanDev:github/pr/2871
MagicBeanDev:github/pr/2756
MagicBeanDev:github/pr/2897
MagicBeanDev:github/pr/2896
MagicBeanDev:github/pr/2718
MagicBeanDev:github/pr/2842
MagicBeanDev:github/pr/2691
MagicBeanDev:github/pr/2895
MagicBeanDev:github/pr/2743
MagicBeanDev:github/pr/2765
MagicBeanDev:github/pr/2708
MagicBeanDev:github/pr/2732
MagicBeanDev:github/pr/2755
MagicBeanDev:github/pr/2817
MagicBeanDev:github/pr/2876
MagicBeanDev:release-please--branches--main
MagicBeanDev:release-please--branches--main--release-notes
MagicBeanDev:github/pr/2778
MagicBeanDev:github/pr/2888
MagicBeanDev:github/pr/2866
MagicBeanDev:github/pr/2877
MagicBeanDev:github/pr/2800
MagicBeanDev:github/pr/2829
MagicBeanDev:github/pr/2850
MagicBeanDev:github/pr/2889
MagicBeanDev:github/pr/2882
MagicBeanDev:github/pr/2860
MagicBeanDev:github/pr/2849
MagicBeanDev:github/pr/2886
MagicBeanDev:github/pr/2794
MagicBeanDev:github/pr/2840
MagicBeanDev:github/pr/2830
MagicBeanDev:github/pr/2834
MagicBeanDev:github/pr/2719
MagicBeanDev:github/pr/2742
MagicBeanDev:github/pr/2741
MagicBeanDev:github/pr/2820
MagicBeanDev:github/pr/2771
MagicBeanDev:github/pr/2859
MagicBeanDev:github/pr/2858
MagicBeanDev:github/pr/2823
MagicBeanDev:github/pr/2757
MagicBeanDev:github/pr/2769
MagicBeanDev:github/pr/2844
MagicBeanDev:github/pr/2843
MagicBeanDev:github/pr/2673
MagicBeanDev:github/pr/2855
MagicBeanDev:github/pr/2739
MagicBeanDev:github/pr/2807
MagicBeanDev:github/pr/2851
MagicBeanDev:github/pr/2789
MagicBeanDev:github/pr/2814
MagicBeanDev:github/pr/2818
MagicBeanDev:github/pr/2774
MagicBeanDev:github/pr/2772
MagicBeanDev:github/pr/2750
MagicBeanDev:github/pr/2786
MagicBeanDev:github/pr/2785
MagicBeanDev:github/pr/2782
MagicBeanDev:github/pr/2779
MagicBeanDev:github/pr/2775
MagicBeanDev:github/pr/2706
MagicBeanDev:github/pr/2766
MagicBeanDev:github/pr/2747
MagicBeanDev:github/pr/2759
MagicBeanDev:github/pr/2745
MagicBeanDev:github/pr/2740
MagicBeanDev:github/pr/2733
MagicBeanDev:github/pr/2725
MagicBeanDev:github/pr/2727
MagicBeanDev:github/pr/2728
MagicBeanDev:github/pr/2729
MagicBeanDev:github/pr/2721
MagicBeanDev:github/pr/2722
MagicBeanDev:github/pr/2715
MagicBeanDev:github/pr/2714
MagicBeanDev:github/pr/2707
MagicBeanDev:github/pr/2703
MagicBeanDev:github/pr/2705
MagicBeanDev:github/pr/2697
MagicBeanDev:github/pr/2679
MagicBeanDev:github/pr/2676
MagicBeanDev:github/pr/2669
MagicBeanDev:docs/what-hookshot-connects-to
MagicBeanDev:fix/hdr-encode-test
MagicBeanDev:fix/main-tests
MagicBeanDev:fix/play-cache-files
MagicBeanDev:perf/game-barrier-order
MagicBeanDev:feat/heap-lazy
MagicBeanDev:feat/texture-detail
MagicBeanDev:perf/astc-opaque-bc1
MagicBeanDev:fix/fs-dir-lock
MagicBeanDev:fix/framegen-native-kept
MagicBeanDev:fix/update-no-downgrade
MagicBeanDev:port/k10-gcn-f16-mirrors
MagicBeanDev:fix/framegen-mult-cap
MagicBeanDev:chore/release-as-2.1.1
MagicBeanDev:perf/pcache-load-switch
MagicBeanDev:fix/prepare-progress-log
MagicBeanDev:fix/update-closes-stale-game
MagicBeanDev:fix/hdr-scaled-sampled
MagicBeanDev:perf/first-start-preset
MagicBeanDev:perf/zero-upload
MagicBeanDev:fix/vcore-escape-2s
MagicBeanDev:test/fake-gh-atomic
MagicBeanDev:perf/batch-idle-flush
MagicBeanDev:test/finish-stub-a
MagicBeanDev:chore/record-2.1.0
MagicBeanDev:fix/main-red-tests
MagicBeanDev:fix/test-display-startup-driver-cache
MagicBeanDev:feat/mesh-shader-mode
MagicBeanDev:fix/menu-probes-at-startup
MagicBeanDev:fix/fps-counter-classic
MagicBeanDev:fix/test-log-batch-flake
MagicBeanDev:fix/narrow-screen-hud
MagicBeanDev:ci/prereleases
MagicBeanDev:release/v2.0.15
MagicBeanDev:hotfix/v2.0.15
MagicBeanDev:fix/present-ahead-replay
MagicBeanDev:release/v2.0.13
MagicBeanDev:fix/bracket-paths-2013
MagicBeanDev:fix/hdr-overlays-2013
MagicBeanDev:fix/fps-counter-scale-2013
MagicBeanDev:fix/stick-seen-at-rest
MagicBeanDev:hotfix/menu-fits-screen
MagicBeanDev:fix/f1-always-opens-menu
MagicBeanDev:fix/ini-keep-lines
MagicBeanDev:claude/cranky-pike-7cf247
MagicBeanDev:perf/texture-answer-flat
MagicBeanDev:fix/stamp-test-modules
MagicBeanDev:fix/appimage-fonts
MagicBeanDev:fix/display-awake
MagicBeanDev:release/v2.0.12
MagicBeanDev:fix/discord-windows
MagicBeanDev:rc/v2.0.12-tests
MagicBeanDev:fix/dlss-keep-feature
MagicBeanDev:rc/v2.0.12
MagicBeanDev:button-icons
No reviewers
Labels
Clear labels
accessibility
Barrier affecting people with disabilities
autorelease: pending
autorelease: tagged
bug
Something isn't working
dependencies
Pull requests that update a dependency file
documentation
Improvements or additions to documentation
duplicate
This issue or pull request already exists
enhancement
New feature or request
github_actions
Pull requests that update GitHub Actions code
good first issue
Good for newcomers
help wanted
Extra attention is needed
invalid
This doesn't seem right
question
Further information is requested
testing wanted
wontfix
This will not be worked on
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
Milestone
Clear milestone
No items
No milestone
Assignees
Clear assignees
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!2578
Loading…
Reference in a new issue
No description provided.
Delete branch "itzdaface/feat/coop"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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):
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:
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
@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:mainWhat combining would give, under the four things I'd judge either pull request by:
Simpler
host/coop_net.calready has the sockets for Winsock and POSIX, which answers your question 5.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.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
sscanfthirty times a second for each player.More for players
Safer (your question 6)
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.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;
KandNnamed for what they are.@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-byline. 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:
POSalready 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 besidePOSthan 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
mainhas only been compiled, not run. Getting it running onmainwas 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.
@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-byfor 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.pywithout touching the router, but it is still a second thing to start. What I would do, in this order: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:
POSruns at that rate; if it looks choppy we smooth on the receiving side before we raise it);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.POSstays 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 myPOSgo 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 intodocs/coop.mdso 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-wireon 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 (%zuinhost/coop.cunder 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 on127.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.