Skip to content

System link games of up to 128 players on up to 128 machines - #2

Merged
cybersecurity merged 12 commits into
OpenCommunityEdition:mainfrom
bnunu:halo-ce-universal/players-128
Sep 27, 2026
Merged

cybersecurity merged 12 commits into
OpenCommunityEdition:mainfrom
bnunu:halo-ce-universal/players-128

Conversation

@bnunu

@bnunu bnunu commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The native builds (Windows, Linux, Android) now host system link games of up to 128 players on up to 128 machines. The Xbox game allows 16 players on up to 4. Split screen stays at 4 players per machine.

  • The limits are two constants in port/linux/include/halo_port_limits.h. The memory they need is set in port/linux/include/halo_port_capacity.h.
  • Every change is under #ifdef HALO_LINUX, so the byte-matching MSVC build keeps the Xbox limits. All 621 objects compare identical to main's.
  • The protocol version is now 2, so these builds neither see nor are seen by the Xbox game or older native builds. Every machine in a game needs the same build: the game runs in lockstep, and pool sizes affect the simulation.

This PR is rebased onto the current main (13d1ae2) and no longer depends on #1. That PR clamped colours that the x87 maths pushed a hair past 1. Now that 13d1ae2 has put Windows back on SSE maths, those asserts don't fire in a 128-machine game on Blood Gulch, so #1 is closed.

What changed

  • Networking
    • All limits follow halo_port_limits.h.
    • The 13 KB game settings record is sent in four pieces and reassembled, since a message header holds at most 4095 bytes. The per-tick update of 128 players (3,857 bytes) fits in one message.
    • Player counts are bytes.
    • Each machine's input carries 4 players' actions.
    • Buffers, queues and FD_SETSIZE are larger.
    • Stream writes send a whole message or drop a peer that stopped reading, instead of corrupting the stream.
    • Lobby changes go out at most every 250 ms.
    • In-game joins wait in a queue.
    • The random name and colour pickers terminate.
  • Host behaviour at scale (found with 128 machines)
    • The listen backlog is 128, not 32.
    • Socket buffers are at least 1 MB, enough for the updates a machine hasn't acknowledged yet.
    • Machines that finished loading get a keep-alive every 5 s while slower machines load (the host waits up to 60 s).
    • Logging no longer grows with the square of the session, and debug.txt stays open between lines.
  • Game engine
    • Per-player arrays hold 128 players.
    • Places past 16th get ordinals (17th, 21st, ...).
    • Free-for-all teams are the player's index.
    • Team masks are sized and checked.
  • Interface
    • The lobby's three remote panels show the first three remote machines.
    • The HUD teammate buffer is sized for 128 players.
    • The motion sensor checks range before a unit takes a blip slot.
  • Memory
    • The game state is 16 MB at 0x81A00000 (3.3 MB on the Xbox).
    • Object, effect, particle, contrail, light, sound, cluster-reference and render-state pools are larger.
  • Platform (port/linux/src/xnet.c)
    • HALO_NET_ADDRESS gives each copy of the game its own address, so several copies can play together on one computer.
    • HALO_NET_BROADCAST sends the game search to a list of addresses.
  • tools/system_link_bots.py
    • Stand-in machines that join a host, acknowledge every tick and send input, for testing large sessions without 128 copies of the game.
  • Docs
    • A "System link" section in port/linux/README.md, and new rows in the settings and HALO_LINUX edits tables.

Testing

All on one Windows computer, with the Windows build of this branch on the current main (13d1ae2):

  • 128 machines, 128 players, Blood Gulch Slayer. A host (HALO_NET_ADDRESS=127.0.0.200), a second copy of the game as a real client (127.0.0.201), and 126 stand-ins played for about 90 seconds.
    • The update stream held a steady 30 updates per second, and every machine acknowledged every tick.
    • The real client simulated all 128 players for about 2,600 ticks, 0 ticks behind the host, with no out-of-sync events (the per-tick random seed check passed).
    • No assertion fired on either copy.
    • The host uploaded about 118 Mbit/s.
  • Earlier runs on the previous main (a06ef74).
    • The same session ran for about 3,200 ticks.
    • A host with 127 stand-ins handled all of them leaving at once within a second.
    • With 8 stand-ins taking 25 s to load, the host's own client stayed connected past the 15 s silence timeout.
  • Byte matching. An object-by-object comparison of all 621 MSVC objects against the current main (sections, relocations, symbols) found 0 differences.

Not covered:

  • The Linux and Android builds were not built: this machine has no Linux sysroot or Android NDK. The changes are shared C under HALO_LINUX plus the shared xnet.c. A Linux or Android build check would be welcome.
  • The stand-ins don't play. A game with many real players, for example spawn points on maps made for 16, hasn't been played.

🤖 Generated with Claude Code

Jonas Volman and others added 12 commits September 26, 2026 10:18
halo_port_limits.h (force-included by the Linux and Windows prefix
headers) defines the native builds' multiplayer limits, 128 players on up
to 128 machines, and the struct network_game layout that follows from
them. Game sources use it only under HALO_LINUX, so the matching build
keeps the Xbox limits (16 players, 4 machines).

xnet.c gains two settings: HALO_NET_ADDRESS binds the game's sockets to
one local address and reports it as the machine's system link address,
so several instances can share one computer on different loopback
addresses; HALO_NET_BROADCAST sends the game's broadcasts to a list of
addresses instead, for machines broadcasts do not reach.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With HALO_NET_ADDRESS set, connections and datagrams to 127.0.0.1 go to
that address, and traffic from it is reported as coming from 127.0.0.1.
The game uses 127.0.0.1 for itself (a host joins its own game through
it, and admits only it to a split screen game), so an instance bound to
another address can still host. HALO_NET_BROADCAST now takes a list of
addresses: a host's game advertisement is a broadcast too, and several
instances on one computer each need a copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Xbox game allows 16 players on 4 machines (4 split screen players
each). The native builds raise both limits to 128, keeping 4 players per
machine, and leave the byte-matching build unchanged: every change is
under HALO_LINUX (the MSVC objects compare identical).

- Limits: every local copy of the player and machine limits, and the
  bare 4s and 16s that stood for them, follow halo_port_limits.h. The
  player data array, the machine-to-player table (and its reset) and the
  per-tick update arrays grow with them.
- Records: the copies of struct network_game agree on the larger layout
  and check it against halo_port_limits.h; maximum_player_count becomes
  a byte (128 does not fit a signed char); network_player_is_valid
  checks the machine against the machine limit, not the split screen
  one; a machine's input carries MAXIMUM_LOCAL_PLAYERS actions (16/4
  would have become 128/128).
- Protocol: a message holds at most 4095 bytes (its header has a 12-bit
  length), so the 13 KB game settings record is sent in 3.5 KB pieces
  that clients reassemble; the per-tick update of 128 players (3,857
  bytes) fits. Buffers and the packet codec's limits grow to match, and
  the game search uses protocol version 2 so these builds and the Xbox
  game never see each other's games.
- Connections: the host accepts a client connection per machine (the
  split screen count stood in for it), FD_SETSIZE is 256, stream writes
  send all of a message or drop a peer that stops reading instead of
  corrupting the stream, and the input and update queues are larger.
- Host: lobby changes are sent at most every 250 ms instead of on every
  change (a full lobby would otherwise send ~650 MB), in-game joins wait
  in a queue instead of being dropped, the random name and colour
  pickers stop looping forever once names or colours run out, machines
  get 60 s to load a map, and the debug dump lists only the slots in
  use (128 empty slots took seconds to log).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Native builds (HALO_LINUX) size the multiplayer game engine for
HALO_PORT_MAXIMUM_NETWORK_PLAYERS; the matching build keeps the January
text in #else branches and stays byte-identical.

- game_engine.c: MULTIPLAYER_MAXIMUM_PLAYERS follows the port limit; the
  postgame statistic buffers hold an entry per player; global_goal has a
  slot per player (slayer kill-in-order uses the absolute player index)
  and the goal loops are bounded by the array instead of the address of
  global_variant; game_engine_get_place uses a 128-bit team mask;
  places past the 16th get code-generated English ordinals in the HUD
  score messages, scoreboard and postgame instead of asserting or
  clamping to "16th"; a free-for-all team is the player's absolute index.
- slayer, oddball, king, race: per-team and per-player score arrays follow
  the port limit (oddball's ball arrays stay at 16); January size asserts
  are kept for the matching build. Race reads the lap flags of the player
  rather than of the player's team index.
- game_allegiance.c: a team past the 10-team table is not its own enemy,
  like teams 0..9, so a free-for-all suicide no longer scores as a kill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Native builds only (#ifdef HALO_LINUX); the matching MSVC build keeps the
original text and stays byte-identical.

- ui_widget_game_data_input_functions.c: the local machine limit follows
  HALO_PORT_MAXIMUM_NETWORK_MACHINES and the struct network_game copy holds
  HALO_PORT_MAXIMUM_NETWORK_PLAYERS players; its layout is checked against
  halo_port_limits.h. The pregame screen keeps its three remote machine
  panels and shows the first three remote machines instead of asserting
  (sizing its arrays by the machine limit read uninitialized widget
  pointers). netgame_prejoin_players scans every player slot, and the
  player count texts have room for three digits. network_machine_is_valid
  widens the char index before comparing it with 128.
- ui_widget_event_handler_functions.c: the join/unjoin, team swap and
  countdown handlers use the port's players offset and player count
  instead of 0x226/0x242/0x243 and 16.
- hud.c: hud_draw_players sizes its teammate buffer by the port's player
  limit and bounds the fill (more than 16 teammates overflowed the stack).
- motion_sensor.c: multiplayer tests the (horizontal) range before a unit
  takes a blip slot, so distant units no longer crowd nearby ones out of
  the 16 slots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 128-bit team mask is indexed by team; a player whose team is not
set (NONE) would read and write outside it, where the 32-bit mask only
shifted. Such a player now counts on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
halo_ws_sendto rewrote a destination of 127.0.0.1 to the HALO_NET_ADDRESS
address in a structure local to the else block, then sent to it after the
block had ended. At -O2 clang drops the store, so datagrams went to a
garbage address. It hit the host's own machine: its client reaches the
local server as 127.0.0.1, so with HALO_NET_ADDRESS set the server never
got the host's input and dropped the host after the stall timeout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Xbox game state (0x345000 bytes at 0x80061000) ends exactly where the
map-linked tag cache begins and has 948 bytes free, so a 128-player
session cannot even allocate its player datums. The native builds now
place a 16 MB game state (CPU 0xFC0000, GPU 0x40000) at 0x81A00000, above
the tag cache and below everything the Xbox memory window hands out
top-down, and grow the pools that scale with the number of players:

- objects: 2048 -> 8192 headers, 1 MB -> 8 MB pool; the garbage limits
  keep January's proportions (5/10/20 % of the pool, 2.5/5/10 % of the
  headers, 75/50/30 active garbage objects per 16 players)
- cluster partition reference lists 2048 -> 8192 (collideable and
  noncollideable objects, lights); a full list drops objects out of
  rendering, collision and sphere queries
- effects 256 -> 2048, effect locations 512 -> 4096
- particles 1024 -> 8192 (render_particles' visible list, filled without
  a bound check, follows), particle systems 64 -> 256, their particles
  512 -> 4096
- contrails 256 -> 1024, contrail points 1024 -> 8192
  (MAXIMUM_CONTRAIL_POINTS stays the pool size for the same reason)
- lights 896 -> 4096, object looping sounds 1024 -> 4096
- cached object render states and rendered objects 256 -> 1024
- script object-list references 128 -> 1024, objects one explosion can
  damage 64 -> 256

The values live in port/linux/include/halo_port_capacity.h, included by
halo_port_limits.h. With 128 player datums the CPU part holds 14,256,908
bytes (2,258,164 free). Saved games and cores grow to 16 MB. Every change
is under HALO_LINUX; the MSVC objects are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by running a 128-machine session with tools/system_link_bots.py:

- The listen backlog was 32 connections, so machines joining at once were
  refused; it is now the machine limit.
- Socket buffers are at least 1 MB. A host's connection to a machine must
  hold every update the machine has not acknowledged, up to 128 ticks of
  3.9 KB with 128 players; a full buffer blocked the host for the write
  timeout. The host's connection to its own client must also hold all it
  sends in one frame.
- Each machine that finished loading logged a line for every machine
  still loading, 8,000 lines as 128 machines load; it now logs a count.
- Removing a machine dumped the whole server state to the log; when many
  machines left at once the host wrote dumps for minutes.
- Machines that had loaded heard nothing while the others loaded, and a
  client drops a connection that is silent for 15 seconds, less than the
  60 seconds the host waits for slow machines. The host now sends its
  pregame keep-alive every 5 seconds while machines load; a client in game
  ignores it.

All under HALO_LINUX; the MSVC build is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
write_to_error_file opened and closed debug.txt for every line, which takes
milliseconds on Windows. A host logs thousands of lines when a hundred
machines join, load or leave at once, and while it writes them its own
client and every other machine wait. The native builds now keep the file
open and flush each line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/system_link_bots.py joins a native-build host with many lightweight
machines, each with one player, speaking the system link protocol directly:
join, settings (reassembling the fragmented record), player, map precached,
loaded, then a UDP input message each tick that acknowledges the host's
updates, as a real client keeps a lockstep game running. Each machine binds
its own loopback address (127.0.0.2 and up), since the host tells machines
apart by address.

    python tools/system_link_bots.py --machines 127 --start

fills a 128-machine session on the host at 127.0.0.1 and starts it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
port/linux/README.md (which the Windows and Android READMEs refer to for
settings) describes the native builds' session limits, what a session of
128 machines costs the host, the lobby and scoreboard behaviour, running
several copies on one computer with HALO_NET_ADDRESS and
HALO_NET_BROADCAST, and tools/system_link_bots.py, and lists the game
sources the change touches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bnunu
bnunu force-pushed the halo-ce-universal/players-128 branch from f2fa457 to 7fc08be Compare September 26, 2026 17:24
@cybersecurity
cybersecurity merged commit 193ab12 into OpenCommunityEdition:main Sep 27, 2026
damiantw referenced this pull request in damiantw/halo-ce-web-multiplayer Sep 30, 2026
Dedicated server: control channel for a supervising process (JSON events on stdout, commands on stdin)
isifire pushed a commit to isifire/halo-ce-vita that referenced this pull request Oct 2, 2026
Issue OpenCommunityEdition#2 (v1.0, a10): the game thread crashed in game_sound_restore after
a game state load. The registers show the looping sound it was on,
0x00010012, carries identifier 0x0001, which no datum of the looping
sound array can have (identifiers keep bit 15 set), and that tag_get on
its definition index returned NULL. The state that was loaded was
already bad in that array; every revert runs at the top of the main loop
with the tick joined, so it is not the tick racing the restore. The
partial dump does not show where the bad data came from.

Defensive only: such a datum is deleted and named in the log instead of
being read through NULL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ucYZ9cvWn3nrnk7kK3S5X
xshxdex98 referenced this pull request in xshxdex98/DamnationCE Oct 3, 2026
Upstream has merged the 128-player system link work (PR #2), rebased onto
its main. This takes upstream's tree as it is, so the fork matches
upstream; that drops the colour clamps this branch still carried, which
upstream's return to SSE maths (13d1ae2) made unnecessary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xshxdex98 referenced this pull request in xshxdex98/DamnationCE Oct 3, 2026
…e fix

Network version 10 (OpenCE build-73), joins 9 and 10; probe/Online Games invite fix
xshxdex98 referenced this pull request in xshxdex98/DamnationCE Oct 5, 2026
The co-op player's unit was given its owner only after it was made, so a
player's Elite chose its first animation slots from the Elite's own graph
and then used them against the Spartan's, which it switches to once owned:
a slot past the Spartan's graph halted a debug build as the player
respawned (tag_groups.c, "#2 is not a valid index in [#0,#1)"). The unit
is now made already owned.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants