Skip to content

Multiplayer

codingncaffeine edited this page Nov 7, 2026 · 3 revisions

Multiplayer

SiegeFX plays the campaign in co-op for up to 8 players: one machine hosts the world, everyone else joins it. This page describes how that works today: the ways to connect, the path from the menus into the game, who decides what, how the world reaches the other machines, what it costs on the wire, and how to test it.

Why Epic's backend

The original game found internet games through a GameSpy-era service that no longer exists. Instead of reverse engineering that service and standing up a replacement, SiegeFX uses Epic Online Services (EOS) for internet play: its lobby search lists games, and its peer-to-peer layer carries the traffic. Two things make it a good fit:

  • No accounts. The game signs in with an anonymous identity tied to the device. Nobody needs an Epic account or a launcher; the game sends Epic only the player's multiplayer name and the platform (for example "Linux X64"), nothing about the computer.
  • No IP addresses to share. Players pick a game from a list. Epic connects the machines directly when it can (NAT traversal) and through its own relays when it can't, so nobody forwards ports or reads out an address.

The LAN and direct-IP paths need no outside service at all, and they stay as the fallback: losing a vendor means writing a new provider, not touching the game (ISessionTransport in SiegeFX.Core/Net/Transport.cs is the seam).

Ways to connect

Screen How games are found Transport
Internet Epic's lobby search (the list refreshes every 2.5 s) EOS peer-to-peer, relayed when a direct connection fails
Internet, an IP address typed in the host box none: you know the address UDP to port 47778 (the host forwards it)
Network (LAN) a broadcast beacon on UDP 47777, once a second UDP to port 47778

Internet play comes with the official builds, which bundle Epic's SDK library and the game's EOS client credentials. A build from source has no EOS module (the SDK is not in the repository) and plays on the LAN and by direct IP; see Building with EOS.

The direct UDP transport has a small connection layer of its own: HELLO / WELCOME with a version byte and an assigned player id, a ping every 2 s, and a peer that is silent for 8 s counts as gone.

From the menu into the game

  1. Multiplayer → Internet or Network. Type a player name. HOST GAME starts a game; on the other machines it shows in the list, and CONNECT (JOIN GAME on the Network screen) joins it.
  2. The staging area, drawn from the original's own interface files and art. Everyone sees the same player list: the host keeps it (host first, always ready) and sends it to every machine, and its Character column shows the character each player picked. Players chat in the chat window and tick I'm Ready to Begin; a mark is sent again until the host's list shows it, so a dropped packet can't leave the host showing the old one. Start Game waits until everyone is ready.
  3. START. The host sends the region and difficulty to everyone. Each game then restarts itself straight into the region, carrying its multiplayer role (host or joiner), the host's address and the player's name, and the joiners connect and join again there.
  4. Join in progress, always. The host's game stays in the list, marked "(in progress)". Anyone who joins it from the menus lands in the staging area with the game marked as under way, picks a character, and clicks Enter Game (the original's join-in-progress step); they are placed beside the host. A player who left can come back the same way.

A joiner that loses its connection before the host has answered keeps asking (the join request repeats every second; over Epic, a connection that is not up after 20 s is dialed again). Every join request carries the protocol version, and a host refuses a different one with a message that both players need the same SiegeFX build.

Characters

Every player picks a co-op character before playing, as in the original. Your own row's Character cell opens Select Character: your co-op characters with portrait, class and the four skill levels. From there:

  • Create New Character... asks for a hero name and the look (Character, Head, Skin, Hair, Shirt, Pants, with the hero turning in a preview); the new hero starts with the starting kit.
  • Import Character... picks a single-player saved game from a drop-down and copies its hero (level, skills, pack, gear, gold, spells, look) into a new co-op character. The single-player game is not touched, and importing the same hero twice makes two characters, never overwrites one.
  • Delete Character asks first.

I'm Ready to Begin, Start Game and Enter Game need a chosen character ("You must select a character before you can start the game."). The choice is remembered, so next time it is already picked. In the game, each player is the character they chose; its progress is saved on their own machine (see Saving).

Each machine sends the others its character card (appearance, name, class, stats, skill levels, the weapon and shield in hand, and the fighting stance), so every player is drawn the way its owner built it, holding the same gear and swinging with the animations rigged for it.

Who decides what

The host runs the world. Each player's machine owns that player. This is the friend-trust model of the original: it is built for playing with people you know, not strangers.

Thing Owner How
Monsters and NPCs: AI, movement, life, death host sent to joiners 15 times a second (see below)
Monsters a generator spawns host each spawn is announced under the host's id, so all machines name it the same
Loot piles host piles carry a host-issued id; a joiner asks to take or drop, and only the host's answer changes a pile, so two players can never grab the same one
Breakable props whoever breaks one the host passes it on to the others
Hiring a companion host a joiner asks, the host grants it and tells everyone
Your movement and pose your machine sent to the host 15 times a second; the host passes it on
Damage you deal your machine you roll it, the host applies it to the monster; the monster's new life comes back with the next world update
Damage you take your machine a host monster's hit is sent to your machine, which owns your life
Items handed to another player the giver passed through the host to that player

In the game

  • The leash. If the host's camera can't see a player (judged a fifth of the screen past its edge), that player can't walk further away and is walked back toward the host, with "Too far from the host" on screen. It is off while a cutscene points the camera elsewhere.
  • Death is never permanent. The multiplayer death dialog offers Resume (close the dialog and stay where you fell; Esc brings the choices back), Respawn as Ghost and Exit Game. A ghost walks but can't fight, take things or be targeted, and is drawn pale and see-through on every machine. It lives again after a minute or at a Resurrection Shrine; those shrines exist only in multiplayer games and are registered when one starts. Items are kept on death.
  • Chat shows each line with the player's name, on every machine and in the session log ([chat] Name: text).

Saving

There are no manual saves or loads in a multiplayer game; everything saves itself. This follows the original, whose manual says: "If you or the host exits, character's attributes, skills, and Inventory items are saved."

What Where When
Your co-op characters (every player, host included): level, skills, attributes, pack, worn gear, gold, spells, name and look; not the journal or anything of the world your own machine, one file each in the save folder's Multiplayer folder, named after the hero (the previous version kept as .bak; selected.txt names the one picked last) the one being played: checked every 30 s and written when it changed; at checkpoints; on close; when the game ends
The world the host's machine, multiplayer_autosave.save in the save folder (never the single-player autosave) every 5 minutes ("Auto-saving..."); at checkpoints; on close

Every co-op game you host or join starts with the character you chose, so a joiner whose host quits keeps everything and brings it to the next game, with that host or another. Nothing is written until the saved character is on the player, so a load that fails can't overwrite it; a file that won't load is set aside under another name.

Closing the window, EXIT GAME, and on Linux and macOS a logout, a shutdown or Ctrl+C in the terminal all close the game the normal way, so the saves on close run.

When the host leaves

The game ends for everyone, as in the original ("If the host exits, the game ends"). It had no host migration and neither does SiegeFX: the world lives only on the host's machine.

  • The host's exit question says so: END THE GAME FOR EVERYONE?
  • On leaving, the host saves the world and tells every joiner the game is over. Each joiner saves its character and sees THE HOST HAS ENDED THE GAME. YOUR CHARACTER IS SAVED.; OK goes back to the main menu.
  • If the host disappears without a word (a crash, a lost connection), a joiner saves its character after 5 seconds of silence and ends the game after 30 (THE HOST STOPPED ANSWERING.). A hitch or a region load on the host lasts far less.
  • The host can host again later; the joiners join again with their characters.

How the world reaches the joiners

Two lanes

Epic carries at most 1170 bytes in a packet, and the game uses two of its channels:

  • Channel 0, reliable and ordered: joining, the roster, chat, character cards, loot, spawns, hits, START. A frame bigger than one packet goes in pieces, each tagged whole, more or last, and is joined again on arrival.
  • Channel 1, unreliable and unordered: the 15-a-second world updates and each joiner's pose. Each is one whole packet. A lost one is never sent again and never holds up anything behind it; the next one covers it.

On the LAN and direct IP everything is UDP, so both kinds of frame travel the same way.

World updates

Fifteen times a second the host writes one update for each joiner (Snapshot, protocol v8; SiegeFX.Core/Net/WorldSync.cs):

  • Against what the joiner confirmed. Every pose a joiner sends carries the number of the newest update it holds. The host writes the next update against that one (its baseline) and names only the players and world actors whose state differs from it. A lost update costs nothing: the next one is written against the same baseline and carries the change again. Anything standing still costs nothing at all. With nothing to say, a bare 15-byte header goes out every 8 updates.
  • Only what is near. For each joiner the host sends world actors within 45 m of that joiner's player in every update, those between 45 and 90 m in every third update (5 a second), and keeps following an actor until it is 98 m away. An actor that leaves stops walking on the joiner's machine; when it comes near again it arrives with its current state. An actor a joiner only gets later (a neighboring region streamed in, a spawn) takes the newest state the host sent for it. Players are always sent, except the joiner's own.
  • Packed small. Each entry is a key (as a gap from the previous key), a byte naming the fields that changed, and only those fields:
Field Encoding Error
position 2 cm grid: 16-bit offsets from an anchor cell (81.92 m) near the joiner, or 8-bit steps from the confirmed value at most 1 cm per axis
heading 1/256 of a turn about 1.2 cm at the edge of a character
life 16-bit whole points (0 = dead) none
flags walking, dead, ghost none
last chore a counter that moves on each swing or cast, the clip, its length none: the counter replays a missed swing once, never a stuck one

A walking monster costs about 6 bytes in an update; the old format sent 21 bytes for every monster in the region 15 times a second, whether it moved or not. An update bigger than one packet goes in parts, each with the full header; a joiner uses it once every part has arrived.

Monsters carry the host's own heading and walking state. Joiners draw each one moving smoothly toward its latest position.

Joiner to host

Each joiner sends ClientState 15 times a second on the lossy lane: a sequence number (an older one arriving late is dropped), the newest update it holds, and its pose (position, heading, life, flags, last chore).

Messages

Id Message Direction Lane
1 JoinRequest (protocol version, name) joiner → host reliable
2 Input joiner → host reliable
3 Chat joiner → host reliable
5 ClientState (sequence, acknowledged update, pose) joiner → host lossy
6 ClientHit (actor, damage) joiner → host reliable
7 ClientInfo (character card) joiner → host reliable
8 Ready joiner → host reliable
10 / 11 JoinAccept / JoinReject host → joiner reliable
13 / 14 PlayerJoined / PlayerLeft host → all reliable
15 ChatRelay host → all reliable
17 GameStart (region, difficulty) host → all reliable
18 PlayerInfo (a character card) host → all reliable
19–22 LootTake, LootDrop, LootSpawn, LootGone both reliable
23 ItemGive via the host reliable
24 / 25 RecruitRequest / RecruitGrant both reliable
26 PlayerHit (damage) host → one joiner reliable
27 PropBreak via the host reliable
28 ActorSpawn host → joiners reliable
29 Roster host → all reliable
30 Snapshot (world update) host → one joiner lossy
31 GameEnd (the host is leaving; the game is over) host → all reliable

Ids 12 and 16 (the per-frame StateDelta and PlayerDelta before v8) are retired and not reused.

What it costs

Measured with 8 players (a host and 7 joiners) in the first region:

Setup Host upload
The real game over Epic, joiners walking around the host 222 kbit/s in total, every byte the host sent (5 minutes)
The same with the host's uplink held to 1 Mbit/s, 2 % loss, 100 ms delay 226 kbit/s; updates reached joiners 83–221 ms after the host sent them, with no growth over 5 minutes
Synthetic worst case (--selftest-mp-load): 360 monsters all walking, 1 Mbit/s, 2 % loss, 100 ms about 330 kbit/s; updates 133 ms old at the 95th percentile; every monster within 30 m of a joiner within 1.7 cm of the host's position
The format before v8, same worst case 7.0 Mbit/s: the link fell further behind every second

In the real game the game's own data was about 85 of those 222 kbit/s (about 12 kbit/s per joiner). The rest is Epic's packet framing (about 154 bytes per packet on the wire) and Epic's own packets alongside the game's.

Security

  • Every frame is read with a bounds-checked reader: a truncated or malformed frame is dropped whole, never thrown on. The protocol reader survives 20,000 random frames and the world-update reader 5,000 in the self-test.
  • Optional encryption: set SIEGEFX_MP_PASSPHRASE to the same text on every machine and every frame is sealed with AES-GCM under a key derived from it; frames that don't open are dropped. Meant for the LAN and direct-IP paths.
  • Players own their own characters (see Who decides what), so a modified client can cheat for itself. Play with people you trust.

Testing and diagnostics

Logs. The session log (Linux ~/.local/share/SiegeFX/logs, Windows %LOCALAPPDATA%\SiegeFX\logs) has every connection event on a [net] line, and every 10 s a stats line with the bytes sent and received per player and per message type and the current update number. Saving shows as mp: co-op character restored/saved (why) and mp: the game saved itself (why), and a game that ends as in-region: the game is over (why). SIEGEFX_EOS_LOG=verbose adds Epic's own peer-to-peer log.

Self-tests (no window):

Command What it checks
SiegeFX --selftest-net the protocol over an in-process loopback: joining, the version check, the roster and READY, world updates (interest, changes only, packing, recovery from 10 % loss), the host ending the game and a joiner noticing a silent host, the 8-player load with limits, the readers under random input, encryption. Runs in CI.
SiegeFX --selftest-eos internet play end to end: sign-in, lobby create/find/leave, and (on Linux) a second copy that joins over Epic, readies up and follows START. Needs the network and credentials.
SiegeFX --selftest-mp-load [players=8] [actors=360] [moving=360] [seconds=300] [link=kbit=1000;loss=2;delay=100] [max-kbit=…] [max-error-cm=…] a host and up to seven joiners in one process on a simulated clock: upload, update age, and how far each joiner's view is from the host's. Five simulated minutes take about a second.
SiegeFX --selftest-mp-peer <host id> <eos_config.txt> <seconds> <n> a headless joiner for load runs against a real host over Epic

Slow-link testing: SIEGEFX_DEV_NET="kbit=1000,loss=2,delay=100" makes a game send through a modeled uplink of that rate, loss and one-way delay (optional overhead=, queue=, seed=), and logs what the link carried every 10 s.

Two copies on one machine need separate Epic identities. On Linux the identity lives in the data folder, so give each copy its own XDG_DATA_HOME.

Building with EOS

Epic's SDK is not in the repository. Download the EOS SDK (with its C# bindings) from Epic's developer portal and unpack it beside the repository (../EOS/SDK), then build the module for your platform:

dotnet build src/SiegeFX.Net.Eos -c Release -p:EosSdkRoot="$PWD/../EOS/SDK" -p:EosPlatform=Linux

(EosPlatform is Windows64, Linux or Mac.) Put SiegeFX.Net.Eos.dll and Epic's native library beside the game. The game also needs EOS client credentials: copy src/SiegeFX.Net.Eos/eos_config.example.txt to the save folder as eos_config.txt and fill in the five values from your own product in the developer portal. Without the module or a valid file, the Internet screen says this copy has no internet play; the LAN and direct IP still work. publish.sh builds the module and bundles both automatically when the SDK and credentials are present.

Current limits

  • Up to 8 players, and the game lives on the host's machine: there is no host migration.
  • A game hosted on the Internet screen takes joiners through Epic only, not by IP address.
  • After a crash, rejoining can take up to about a minute: Epic keeps the old connection open for about 45 s.
  • A multiplayer game starts in the first region; hosting from a saved game is not supported yet.
  • Companions hired in a co-op game are not kept with the character.
  • The original's Team and Start Location choices, Map Settings and Game Settings (teams, player-vs-player, drop on death, new characters only) are not there yet: co-op starts at the farm with everyone on one side.
  • Items are kept on death (the original offered the host an option to drop them).
  • There is no congestion control: on an uplink too slow for everything near every joiner, updates stay about half a second old instead of falling further behind.

Clone this wiki locally