Skip to content
Yharnam Together

The server netcode

Why Yharnam Together replaced peer-to-peer with a server that owns the team's world: the game's native netcode, the UDP protocol, the reliability layer, team worlds and the native team lobby.

Why a server

Bloodborne's own netcode assumes one host's world, a short visit and peer-to-peer links. Seamless co-op breaks every one of those assumptions, and patching around them took about 120 patch sites and a set of side channels. The rewrite replaces it: every game keeps one UDP connection to a server, and nothing goes game to game.

Before: peer to peer

6 links for 4 hunters; the world lives in the host's game

After: one server

4 links for 4 hunters; the world lives on the server

Four hunters: six peer-to-peer links and a host who owns the world, or four links to a server that owns it.

Links per team size

  • Peer to peer
  • Server
02462 players, peer to peer: 1 link12 players, server: 2 links22 players3 players, peer to peer: 3 links33 players, server: 3 links33 players4 players, peer to peer: 6 links64 players, server: 4 links44 players
Links to keep alive as the team grows: every pair of players with peer to peer, one per player with a server.
QuestionDecisionWhy
Peer to peer or server?ServerPlayers reach each other over a VPN anyway, so P2P's one advantage (NAT traversal) is gone. The world survives the host leaving, one place orders every change, and each game has one link instead of a mesh. The cost is one extra hop between guests: milliseconds on a VPN.
Who owns the world?The server owns its state; the games still play itThe server records and orders kills, flags, doors, lamps, shortcuts, items, enemy HP and map state, keeps them across sessions and crashes, and hands them to joiners. It runs no game.
Rewrite movement sync?NoThe game's own player and enemy sync keeps running; its packets just travel through the server.
Wire format?UDP; protobuf for our messages, raw bytes for the game'sA schema ends hand-written frames breaking older readers. Game packets are many per second and the server never reads them.

What the game does on its own

Reverse engineering of the 1.09 eboot, checked with a packet trace on the test rig:

  • Every game packet goes through one send and one receive point. Packets travel on 68 numbered channels (about 60 in use), some unreliable (movement) and some reliable (everything that must arrive). Hooking those two points lets the co-op code see, drop, rewrite or inject any packet.
  • The native model is one host's world. The host runs every enemy and sends their positions and HP; a guest's hit on an enemy goes to the host, which applies it. A joining guest gets the host's flags for the host's current map only, once.
  • Players sync themselves. Each game sends its own player: position, heading, HP and the animation state. A hit on a player goes to the victim's game, which decides its HP.

When members are on different maps, those assumptions produce double damage, late health bars and joins that only cover one map. That is where the old design's bugs came from.

Architecture

Hunter 1

  • Bloodborne PC, the 1.09 game code
  • co-op hooks at the packet choke point

Hunter 2

  • Bloodborne PC, the 1.09 game code
  • co-op hooks at the packet choke point

Co-op server

  • accounts, presence, the team lobby
  • the session: members, places, one enemy owner per map
  • the world log: ordered, saved to disk, snapshots for joiners
  • the relay: game packets to the right members

Hunter 3

  • Bloodborne PC, the 1.09 game code
  • co-op hooks at the packet choke point

Hunter 4

  • Bloodborne PC, the 1.09 game code
  • co-op hooks at the packet choke point
One UDP link per game, with its own resends and ordering. No game talks to another.
The server in the middle: what it keeps and what it relays.
TrafficServer's role
Team and session: lobby, members, who runs which map's enemiesowns
World state: kills, flags, doors and levers, lamps, shortcuts, items, enemy HP, map stateowns: orders, stores, settles conflicts, builds snapshots for joiners
Player movement, animation and HPrelays, unread, to members on the same map
Hits, throws, bullets, forced animations, character datarelays; drops what is meant for members on other maps
Enemy movement and state from the map's ownerrelays to the others on that map
Enemy damage and HPreplaced by world-log HP entries, ordered by the server
Join snapshotsbuilt from the world log, for any map, not only the host's current one

Enemies' AI runs in one game per map, chosen by the server: the first member there, handed on when they leave, mid-fight if needed. Each player's character runs in its own game.

The protocol

  1. 01

    Game sends

    one send point sees every packet

  2. 02

    Channel policy

    pass, same map only, ours, or drop

  3. 03

    Our UDP link

    sequence, ack, resend, in order for reliable channels

  4. 04

    Server relays

    to the members who need it; it never parses game packets

  5. 05

    Game receives

    injected into the receiver's per-member queue

One game packet, from the sending game to the receiving one.
  • One UDP socket per game to the server, on a fixed port next to the TCP one. Each datagram holds messages; each message has a one-byte kind and a length.
  • Kinds: Game (a small fixed header {channel, reliable bit, sender member, map, visit} followed by the game's packet untouched), Proto (one protobuf message), Ack and Ping.
  • Our own reliability layer, per member and per class: sequence numbers, acks, resends and in-order delivery for reliable traffic (the game's reliable channels, world-log entries, snapshots). Unreliable traffic such as movement is never resent; the newest wins. Who is connected, outages and timeouts come from this layer, not from the game's own timers.
  • Protobuf messages (server/proto/coop.proto, shared by the game and the server): Hello (account, build, protocol), the lobby (TeamCreate, Invite, InviteAnswer, TeamState, Ready, Start), the session (Place, MemberJoined, MemberLeft, EnemyOwner), the world log (Propose, Entries, SnapshotRequest, Snapshot), vitals, party death and portraits.
  • One build for everyone. Hello carries the build, and a different build is refused at the lobby with a clear message. There are no compatibility fallbacks.

What a game sends

Measured on the test rig: two players at 30 FPS on one PC, from the server's per-kind counters and the games' own logs. The game sends each packet once per teammate, and the server forwards it only to teammates on the same map, so a player on another map gets almost nothing from the others.

Packets per second, to each teammate

  • Enemy owner, with a teammate
  • Teammate on the same map
  • Alone on another map
010203040506070Event syncEvent sync, enemy owner, with a teammate: 62 per second62/sEvent sync, teammate on the same map: 0.5 per secondEvent sync, alone on another map: 0.3 per secondEnemy positionsEnemy positions, enemy owner, with a teammate: 9.5 per secondEnemy positions, teammate on the same map: 0 per secondEnemy positions, alone on another map: 9.1 per secondEnemy HPEnemy HP, enemy owner, with a teammate: 7.5 per secondEnemy HP, teammate on the same map: 0 per secondEnemy HP, alone on another map: 7.5 per secondPlayer statePlayer state, enemy owner, with a teammate: 6 per secondPlayer state, teammate on the same map: 6 per secondPlayer state, alone on another map: 6 per secondEnemy AI recordsEnemy AI records, enemy owner, with a teammate: 1.5 per secondEnemy AI records, teammate on the same map: 0 per secondEnemy AI records, alone on another map: 1.6 per secondForced animationsForced animations, enemy owner, with a teammate: 0.1 per secondForced animations, teammate on the same map: 0 per secondForced animations, alone on another map: 1 per secondHits on enemiesHits on enemies, enemy owner, with a teammate: 0 per secondHits on enemies, teammate on the same map: 0.2 per secondHits on enemies, alone on another map: 0 per secondEverything elseEverything else, enemy owner, with a teammate: 0.5 per secondEverything else, teammate on the same map: 1.5 per secondEverything else, alone on another map: 0.6 per second
Game packets a game hands to each teammate per second, by its role. The follower's total is measured; its split is partly inferred.
KindEnemy owner, with a teammateTeammate on the same mapAlone on another map
Event sync620.50.3
Enemy positions9.509.1
Enemy HP7.507.5
Player state666
Enemy AI records1.501.6
Forced animations0.101
Hits on enemies00.20
Everything else0.51.50.6
Total~87~8~26
  • Event sync is most of the owner's packets. It is reliable, so every packet is acknowledged. It ran at about 2 a second in an earlier trace, so the current rate is being looked into.
  • Frame rate sets most of the rates. Enemy HP goes every 4th frame and player state every 5th, so at the 60 FPS used online both double.
  • Bandwidth is not the limit. Under 10 KB/s per game with two players; with three, the host's upload peaks around 13 KB/s. The rig link had 2 ms round trips and no resends.

Upload to the server, KB/s

  • Host, enemy owner
  • Guest
guest on another map024681003691215minutes since the guest joinedMinute 0.0: host 0.3 KB/s, guest 0 KB/sMinute 0.2: host 5.4 KB/s, guest 0.7 KB/sMinute 0.3: host 4.6 KB/s, guest 1.7 KB/sMinute 0.5: host 5.8 KB/s, guest 2 KB/sMinute 0.7: host 6.6 KB/s, guest 2.2 KB/sMinute 0.8: host 6.6 KB/s, guest 2.3 KB/sMinute 1.0: host 6.2 KB/s, guest 2.2 KB/sMinute 1.2: host 6.6 KB/s, guest 2.7 KB/sMinute 1.4: host 6.5 KB/s, guest 3.3 KB/sMinute 1.5: host 6.9 KB/s, guest 2.2 KB/sMinute 1.7: host 6.3 KB/s, guest 2 KB/sMinute 1.9: host 6.6 KB/s, guest 2.1 KB/sMinute 2.0: host 6.4 KB/s, guest 2 KB/sMinute 2.2: host 6.7 KB/s, guest 2.2 KB/sMinute 2.4: host 6.4 KB/s, guest 2.2 KB/sMinute 2.5: host 6.7 KB/s, guest 2.2 KB/sMinute 2.7: host 6.8 KB/s, guest 2.3 KB/sMinute 2.9: host 6.5 KB/s, guest 2.3 KB/sMinute 3.1: host 6.6 KB/s, guest 2.3 KB/sMinute 3.2: host 6.3 KB/s, guest 2.2 KB/sMinute 3.4: host 6.5 KB/s, guest 2.3 KB/sMinute 3.6: host 6.1 KB/s, guest 2.1 KB/sMinute 3.8: host 6.5 KB/s, guest 2.1 KB/sMinute 3.9: host 6.4 KB/s, guest 2.2 KB/sMinute 4.1: host 6.7 KB/s, guest 2.2 KB/sMinute 4.3: host 6.7 KB/s, guest 2.2 KB/sMinute 4.5: host 6.4 KB/s, guest 2.2 KB/sMinute 4.6: host 6.5 KB/s, guest 2.3 KB/sMinute 4.8: host 6.6 KB/s, guest 2.3 KB/sMinute 5.0: host 6.1 KB/s, guest 2.4 KB/sMinute 5.1: host 6 KB/s, guest 2.1 KB/sMinute 5.3: host 5.9 KB/s, guest 2.1 KB/sMinute 5.5: host 6.5 KB/s, guest 2.7 KB/sMinute 5.6: host 8.2 KB/s, guest 4.1 KB/sMinute 5.8: host 8.2 KB/s, guest 4.4 KB/sMinute 6.0: host 8.5 KB/s, guest 4.5 KB/sMinute 6.1: host 8 KB/s, guest 4.3 KB/sMinute 6.3: host 8 KB/s, guest 4.2 KB/sMinute 6.5: host 8.1 KB/s, guest 4.2 KB/sMinute 6.6: host 8.2 KB/s, guest 4.3 KB/sMinute 6.8: host 8.6 KB/s, guest 4.3 KB/sMinute 7.0: host 8.6 KB/s, guest 4.3 KB/sMinute 7.2: host 9.1 KB/s, guest 4.4 KB/sMinute 7.3: host 9.2 KB/s, guest 4.3 KB/sMinute 7.5: host 9.2 KB/s, guest 4.2 KB/sMinute 7.7: host 9 KB/s, guest 4.3 KB/sMinute 7.8: host 8.3 KB/s, guest 3.7 KB/sMinute 8.0: host 8.1 KB/s, guest 3.6 KB/sMinute 8.2: host 7.7 KB/s, guest 2.9 KB/sMinute 8.3: host 6.6 KB/s, guest 2.1 KB/sMinute 8.5: host 6.9 KB/s, guest 2.2 KB/sMinute 8.7: host 8.1 KB/s, guest 3.2 KB/sMinute 8.8: host 6.6 KB/s, guest 2.3 KB/sMinute 9.0: host 7.3 KB/s, guest 2.1 KB/sMinute 9.2: host 7.2 KB/s, guest 2.2 KB/sMinute 9.3: host 7 KB/s, guest 2.1 KB/sMinute 9.5: host 6.6 KB/s, guest 2.1 KB/sMinute 9.7: host 6.9 KB/s, guest 2.8 KB/sMinute 9.8: host 7.2 KB/s, guest 3.8 KB/sMinute 10.0: host 6.5 KB/s, guest 2.5 KB/sMinute 10.2: host 6.5 KB/s, guest 2 KB/sMinute 10.3: host 6.2 KB/s, guest 2.2 KB/sMinute 10.5: host 5.7 KB/s, guest 2.1 KB/sMinute 10.7: host 5.7 KB/s, guest 2 KB/sMinute 10.8: host 5.9 KB/s, guest 2.8 KB/sMinute 11.0: host 5.8 KB/s, guest 2.6 KB/sMinute 11.2: host 5.5 KB/s, guest 1.5 KB/sMinute 11.3: host 5.6 KB/s, guest 5.1 KB/sMinute 11.5: host 5.5 KB/s, guest 4.5 KB/sMinute 11.7: host 5.3 KB/s, guest 4.3 KB/sMinute 11.8: host 4 KB/s, guest 4.4 KB/sMinute 12.0: host 5.5 KB/s, guest 4.3 KB/sMinute 12.2: host 5.5 KB/s, guest 4.3 KB/sMinute 12.3: host 5.7 KB/s, guest 5 KB/sMinute 12.5: host 5.6 KB/s, guest 4.4 KB/sMinute 12.7: host 5.5 KB/s, guest 4 KB/sMinute 12.8: host 5.5 KB/s, guest 4.5 KB/sMinute 13.0: host 5.5 KB/s, guest 4.5 KB/sMinute 13.2: host 6.4 KB/s, guest 4.3 KB/sMinute 13.3: host 6.4 KB/s, guest 4.2 KB/sMinute 13.5: host 3.5 KB/s, guest 3.6 KB/sMinute 13.7: host 1.9 KB/s, guest 2.5 KB/sMinute 13.8: host 2.8 KB/s, guest 1.5 KB/sMinute 14.0: host 5.9 KB/s, guest 2 KB/sMinute 14.2: host 6 KB/s, guest 2.1 KB/sMinute 14.3: host 5.9 KB/s, guest 2.1 KB/sMinute 14.5: host 5.9 KB/s, guest 2 KB/s
Bytes each game sent to the server per second, from the server's 10-second link counters. While the guest was on another map it ran that map's enemies, so its upload rose; the server forwarded none of the host's same-map packets to it.

Where the server sends each kind

KindRoute
Player state, event sync, enemy AI records, forced animations, script callsteammates on the same map
Enemy positions, HP and catch-up for joinersfrom the map's enemy owner to the others on that map
A teammate's hits on enemies, enemy near liststo the map's enemy owner only
Player hits, throws, bullets, objectsteammates on the same map
Join handshake, character dataevery teammate
Map items, door corrections, the boss-room kicknever forwarded

Our own messages

Besides the game's packets, each game and the server exchange protobuf messages on their own reliable stream: about five a second each way when nothing changes.

MessageDirectionHow often
Pinggame and serveronce a second
Map and enemy owners boardgame to server to teamonce a second, and at once on a map change
Vitals (HP, downed)game to server to teamonce a second; 4 a second while someone is down
HP proposalsgame to servertwice a second, and at once on a change
Enemy hitsgame to server to teamone per hit that takes HP
World-log entriesserver to teamheartbeat once a second, at once on news
Flags (doors, lamps, levers)game to serverat once, then every half second for 10 seconds
Map state reporthost to serverabout every 6 seconds, ~1.25 KB
Portraitgame to server to teamevery 15 seconds, 16 KB

Team worlds

Your save, on your PC

the character

  • level and stats
  • gear and inventory
  • Insight and echoes
  • appearance
  • NPC gifts you received

The team world, on the server

one per team, saved to disk

  • bosses beaten
  • lamps, doors, levers, shortcuts
  • items picked up
  • NPC and quest states
  • kills and map state
The save splits in two: the character stays yours, the world is the team's.
  • The server stores the worlds itself, with atomic writes and backups. Every accepted world-log entry is written at once, so a crash, a quit or a host change loses nothing, and any member can be the game-level host.
  • A new team world starts from the leader's progress.
  • Every save keeps its personal world. Loading into a team world puts the save's own world aside and applies the team's; playing solo again brings it back. The character is never touched by this.
  • Bosses beaten solo stay in that save's personal world; in a team only the team world counts.
  • Up to four hunters per team: the game's remote player slots.

The team lobby

Continue, Load Game or New Game opens the team screen, where the server keeps the team: its members, the hunters online and the invitations. When the leader begins the hunt, every member's game loads its own save; the first to load becomes the game-level host (the server still owns the world) and the others join its session as they finish loading. After a crash or a quit, starting the game again rejoins the team. Using it is on Playing together.

  1. 01 Continue, Load Game or New Game
  2. 02 Team screen: create a team or accept an invite
  3. 03 Everyone picks a character and marks Ready
  4. 04 The leader begins the hunt
  5. 05 Loading holds until the team is together
  6. 06 One world, each hunter where they stand
From the title screen to one world, with no bell and no signs.

Status

The rewrite was built in phases on a branch, each with tests first and runs on the multi-game rig: a packet trace, a channel policy table, one UDP link to the server, the world log on the server, enemy owners from the server, the native team lobby, the server owning the session (faster joins, host migration, resuming after a drop), one owner per map with a single HP path, server routing with the old peer-to-peer paths deleted, and crash-safe world saves with a server that restarts without ending sessions. It is in main and ships in the pre-releases.

The research and the plan with every phase are in the repository's design notes: NATIVE_NETCODE.md and SERVER_COOP_PLAN.md.