109 Commits
Author SHA1 Message Date
eltanschauung 9d7436bd64 Fix co-op gravity lift teleports for extra players
Match the reverse unit order of hs_players when selecting player0 and followers. The forward datum-order assumption returned no followers, leaving players beyond the two named by a50's lift script in the unloaded outdoor BSP.

Tested the actual a50 transition with a host and two clients, including the played and skipped cutscene placement sequences and settled floor positions. Regression coverage exercises 0-128 players, sparse/dead/recycled slots, output bounds, host authority, and every follower action, with negative controls. Windows and Linux release builds pass. This is host-side placement only; the network protocol is unchanged.

(cherry picked from commit 51abcfff9968eee5b91de2bba9d273b70be91850)
2026-10-20 13:25:38 -04:00
MrBruh ab72ff14db Merge pull request #319 from bnunu/halo-ce-universal/coop-own-friendly-fire
Co-op players choose their own friendly fire, and one away is hurt by all

Network version 32, which #257's policy raised and this has not been pushed apart from, also carries the friendly fire choice in a client's input (network_format.txt recorded again).
2026-10-20 02:07:22 +13:00
MrBruh 31ba6b37d8 Raise the network version to 32 for the host's aim assist policy, and declare main_menu_is_active where it is called (#257)
The policy is a new distributed message (90) that a client must not ignore, so machines without it cannot play with those that send it: HALO_PORT_NETWORK_VERSION goes from 31 to 32, with its line in NETCODE.md's versions and port/linux/network_format.txt recorded again. network_aim_assist.c called main_menu_is_active without interface/ui_widget.h, so C89 took it as returning an int. The setting's README row moves to the network.* rows. Checked: network_format.check() is clean, test_linux_port -k network_format and the aim assist harness tests pass, network_aim_assist.c compiles without warnings.
2026-10-20 02:04:53 +13:00
MrBruh f8620d62b0 Merge pull request #257 from eltanschauung/feature/host-aim-assist
Add multiplayer host aim assist options

The aim assist policy's message is 90: testing's text chat took 87 to 89.
2026-10-20 02:03:25 +13:00
MrBruh 22f8beb771 Merge pull request #367 from BirchWoodGod/feat/bots-core
Bots for local Slayer games (bots.count, off by default)
2026-10-20 01:56:14 +13:00
MrBruh ad0c99d753 Merge pull request #371 from BirchWoodGod/feat/latency-meter
Latency meter for network games (display.latency_meter, off by default)
2026-10-20 01:55:37 +13:00
MrBruh 75912da87e Describe net_compare.py's use without other platforms' builds (#360)
The tool's documentation and comments compared an x86-64, x32 or arm64
build against the i686 one, builds the project does not have. They now
compare a build with a simulation, compiler or maths change against one
without, and the wrapper's example is a tracer rather than an emulator.
2026-10-20 00:21:59 +13:00
Paul Hontz 469d03f57a Make net_compare.py stop its copies however it ends, and put a FAIL over an ERROR
Copies outliving the tool: the copies ran in the tool's process group and
were stopped only by play()'s finally, so SIGTERM or SIGHUP to the tool
(which Python does not turn into an exception) left them running, and they
ignored SIGTERM. Each copy now runs in a session (a process group) of its
own, started with the stop signals blocked so that it is always noted as
running; the group is ended with SIGTERM, then SIGKILL two seconds on, on
every way out: returning, an exception, Ctrl-C, SIGTERM and SIGHUP (which
now raise), and atexit. Killed outright, the kernel ends the copies
(PR_SET_PDEATHSIG). Stopped by a signal, the tool says so and exits 128 plus
its number.

Exit status: the worst of the runs was max(), so an ERROR (2) in one run
hid a FAIL (1) in another. A FAIL is the finding and an ERROR only a gap in
it: any FAIL now gives 1, else any ERROR 2 (documented in the tool and
NETCODE.md).

--tolerance persist=3.5 raised a ValueError traceback; each value is now
checked (whole numbers, finite, not negative, drift_share at most 1) and
refused through argparse, as is an unknown name or a missing "=".

--window keeps the caller's DISPLAY or WAYLAND_DISPLAY (documented, and
--display names an X display instead); headless copies now get neither,
so that none can open a window on the caller's desktop. HALO_EXIT_AFTER, the
copies' safety stop, now counts --skip too, as it works headless.

Checked: tools/test_net_compare.py, 28 passed (20 new: the exit status, the
tolerances, the copy's folder, the displays, stop() ending a copy that
ignores SIGTERM and its child, and the tool stopped by SIGINT, SIGTERM,
SIGHUP or SIGKILL leaving no copy). With the maps: i686 (ninja linux)
against x86-64 (ninja linux_x86_64), bloodgulch and beavercreek with --swap:
4 of 4 games in step, exit 0 (drift 0.0%, 0.0%, 3.5%, 14.2%), with no copy
left running.

(cherry picked from commit a53d86ccc2216d0949a6c089f57f6e8bd534c93c)
2026-10-20 00:21:59 +13:00
Paul Hontz 4d0bd56968 Add tools/net_compare.py, which checks that two builds play a network game in step
To show that a new build (the x86-64 one, an arm64 one) plays network
games with the i686 Linux build, and to keep checking it. It plays the two
together on the loopback, A hosting and B joining and with --swap the other
way round, each copy with a seeded bot, headless, and compares the per-
second logs of the network test: for every player, the host's view and the
client's at the same tick (positions, aim, vitality, weapons, rounds,
grenades, scores, life), within tolerances taken from how the netcode
predicts and corrects, and the host's drift figure (the previous commit).
It prints PASS, FAIL with the first divergent player, tick and field, or
ERROR, and exits 0, 1 or 2. It needs the retail maps, so it is a local or
self-hosted check: NETCODE.md ("Comparing two builds") describes it, the
log line's format and the calibration; README.md's new Testing section and
AGENTS.md list it. tools/test_net_compare.py tests its judging of logs
without the maps, in CI.

Calibrated with an i686 build against itself, 60-second games both ways
round on bloodgulch, beavercreek, hangemhigh, chillout, prisoner, ratrace,
longest and carousel, up to four games at once: 63 of 64 pass (one copy
crashed, reported as an error), and 12 of 12 with scripted hits and kills
(--events). A build of the same sources whose players run 0.5% or 2%
faster (a change made only for the test, not committed) fails all 16
games, at the first second compared. One whose gravity is 0.5% or 2%
stronger passes: it changes only the ticks in the air, which the drift
cannot tell from what identical builds show; NETCODE.md says so.

(cherry picked from commit 0adc286c6497bffe8edfe297181aa39c2b0bbb8e)
2026-10-20 00:21:59 +13:00
MrBruh 4769d75170 Merge pull request #266 from addisonbair/stagekit
StageKit: stage and instrument any scenario on the real engine, with no retail assets
2026-10-20 00:12:39 +13:00
MrBruh e595f89c1b Record the netcode's datagram tick window in version 31
The picked netcode checks add DISTRIBUTED_TIME_WINDOW_TICKS, which
network_format.txt records, and they and the second copy's ports change
what the machines take from each other: version 31, not yet released,
says so in NETCODE.md.
2026-10-19 23:06:45 +13:00
Milenko 8a79394aff Netcode: a datagram's tick near its sender's latest; hits, identity and readiness only over a client's stream
(cherry picked from commit dd23663bb59fb7e7a821d456caae91c0b0a5217a)
2026-10-19 22:31:57 +13:00
MrBruh 167d67ec2f Raise the network version for the keep-alive's message table
The picked keep-alive fix moves the Xbox game's begin game, graceful exit
and pregame keep-alive field lists under their own messages and makes the
keep-alive a long, its size on the wire. No byte on the wire moves but the
keep-alive's last two, which were the sender's stack and are zero now
(tools/test_network_messages.py compares every message's bytes with the
old table's), but the change is one to what the machines send each other,
and network_format.txt records the table: version 31, recorded, and said
in NETCODE.md.
2026-10-19 22:12:00 +13:00
MrBruh 607da50209 Bound the elevator height error a client discards and raise the network version (#390)
A client on the same elevator discarded a vertical error of any size; it now
keeps its floor's height only within 4 world units, the platform's travel
over a round trip, so a teleport or fall in height still corrects. Network
version 30: every machine now carries an elevator's riders by the same
tick's movement, simulation they must agree on. distributed_riding_elevator
(was distributed_riding_moving_elevator) is named for elevators at rest too
and looks the elevator up as a machine, as bipeds.c finds it; the source/
changes are marked port:, the original's one-tick-late carry noted as fixed,
and device_machines.c's new functions moved to its public code.

Checked: ninja linux (debug) and ninja android build; pytest tools/harness
passes (492, with a new case and negative control for the bound);
network_format.txt recorded and its test passes.
2026-10-19 21:40:27 +13:00
MrBruh a4ea7f478e Say that why a host deletes a flag or ball is not known (#349)
NETCODE.md and the comment in distributed_client_delete gave a release
host's unchecked assertions as the reason a host deletes a flag or ball;
that cause has not been found. They now say it is not known and that the
client follows the host's word whatever the reason. The notes on the
skipped assertions also say they cover every deletion of
distributed_client_delete, the client's own objects the host has not told
it of included, not only the host's word. Comments and documentation only.
2026-10-19 21:40:27 +13:00
MrBruh 1081f981fc Merge pull request #390 from eltanschauung/fix/coop-elevator-riders-upstream
Fix co-op elevator riders falling through floors
2026-10-19 21:16:09 +13:00
MrBruh 4614e00eab Merge pull request #349 from OpenH1/client-flag-delete
Netcode: a client deletes the host's flag or ball without halting
2026-10-19 21:16:09 +13:00
MrBruh 951ec3b017 Merge pull request #339 from FractumSeraph/map-share-game-start
A client is not started before the map it downloads is there
2026-10-19 21:15:11 +13:00
MrBruh 8488a161eb Fix the text chat's review findings: no | in lines, case-blind mutes, wrapped lines, per-view and hidden chat, stream-only host messages, no chat in browser-joined public games (#399) 2026-10-18 19:33:08 +13:00
Jonas VolmanandClaude Opus 5.5 7abd376b85 Mute a player's voice and text chat from the scoreboard
A player's scoreboard menu (right-click, then the player) had Mute voice,
this machine's own choice. It is now Mute voice and chat: this machine
plays none of their voice and shows none of their lines, and tells no one.
It is offered whether or not voice chat is on.

The host's menu adds Mute for everyone / Unmute for everyone. It is the
chat's /mute for 20 minutes (every machine told "Milenko muted for 20
minutes"), and it now silences the player's voice too: the host relays
none of a muted player's machine's voice (voice_host_relay), as it relays
none of their lines. /mute from the chat mutes their voice the same way.

debug.network_test_chat now also has the host mute the last player for
everyone (network_chat_host_mute, as its scoreboard does) and unmute them,
and the last player's machine mute the host's for itself. On three Linux
machines over system link, with debug.voice_test's tone as open mic:
- the host's mute: no machine showed the player's line, they alone were
  told the minutes left, and the host and the other client stopped hearing
  their voice until the unmute (one second heard at the edge);
- the client's own mute: only that machine stopped showing the host's lines
  and hearing the host's voice.
CI's Linux test step passes (484, 6 skipped).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:50:59 -07:00
Jonas VolmanandClaude Opus 5.5 e6d1e09bd7 Let the host turn the text chat off and refuse links, and players hide it
The host's TEXT CHAT row (Server Setup > Teamplay Options; in co-op, Voice
and Voting; network.text_chat) is ON (the default: lines with links are
refused), WITH LINKS or OFF: one row, as Teamplay Options has room for one
more. A game listed in the server browser (p2p_hosting_listed: a public
internet game) never has the text chat; a private game, joined by its
invite, and a LAN game have it as chosen.

The host sends its settings to every client in a new message,
_distributed_message_chat_config (89, one byte of flags), as they change
and every 3 seconds. A client without them, as from a host without the
chat, has the chat off. Pressing T where it is off says "Text chat is off
in this game". A line the settings refuse is refused on the sender's
machine and again by the host, whatever the client knew, which tells the
sender why: a changed build gets no further. Older builds drop the new
kind, so the network version stays 29.

Links are found by chat_text_has_link: a scheme, "www.", four numbers and
three dots, or a name and a common top-level domain ("discord.gg/x",
"example.com"), with the dots also written "(dot)", "[dot]", "(.)" or "[.]";
"1.5", "e.g.", "U.S.A" and "wait.comrades" are not links.

TEXT CHAT on Settings > Audio > Voice and Text Chat (display.text_chat)
HIDEs the chat for purists and streamers: no lines, and T and Y open
nothing. /mute's minutes are 20 when none are given.

debug.network_test_chat now also has each machine say a line with a link
(a client's sent past its own check, network_chat_say_unchecked, for the
host to refuse), then the host turns the chat off and on again around one
more line each. On three Linux machines over system link: every machine
showed "Links aren't allowed in this game" and "Text chat is off in this
game" once, the clients' from the host, and no refused line reached anyone.
The settings screens were drawn and the TEXT CHAT row's help follows its
choice. The harness gains four cases and six negative controls (55 pass);
CI's Linux test step passes (484, 6 skipped).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 20:39:29 -07:00
Jonas VolmanandClaude Opus 5.5 da38822634 Re-land the in-game text chat (revert #397)
This reverts 4ac609f0, the revert of #395, so the chat is back as it was
merged: T, Y and Tab, team and vehicle lines, the host's /mute, the
VOICE AND TEXT CHAT settings page and the Android placement.

The one conflict is source/text/draw_string.c, where #395's streamer mode
and testing's (90dd65ca, text_filter.c) both hook the drawn text; testing's
hook is kept. #395's own filter (chat_filter.c, game.streamer_mode) is left
in place, unhooked, for the next commit to take out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:44:03 -07:00
MrBruh 4ac609f0df Revert "Add an in-game text chat for network games" 2026-10-18 15:03:39 +13:00
Jonas VolmanandClaude Opus 5.5 75b2393ccf Merge testing into ingame-chat
testing's SOUND PROCESSING page and the chat's VOICE AND TEXT CHAT page
both split rows off Audio Setup in tools/port_settings.py. Both are kept,
as two category rows, sound processing first. The settings screens and
menus.json are regenerated from it, so they match testing's apart from the
chat's page, its row on Audio Setup and Controls Setup's COMMUNICATION
group.

Checked after the merge: the Linux debug build, the network format check,
the CI Linux test step (467 passed, 6 skipped), and the three-machine
system-link chat test, /mute and /unmute included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:37:12 -07:00
Jonas VolmanandClaude Opus 5.5 8592780212 Let the host mute a player's text chat with /mute
The host types "/mute <player> [minutes]" in the chat (5 minutes when it
gives none, 1440 at most), or "/unmute <player>". The player is found as
the console's kick finds one: the whole name in either case, else the one
player whose name begins with what was typed; none, or more than one, is
said to the host alone. Every machine is told "Milenko muted for 5
minutes" and "Milenko unmuted", as a notice in the chat: a down line with
the new _chat_line_notice_bit, its text alone, drawn in a pale gold
(white in the white style).

The host keeps a mute by the player's name in ASCII, so one who leaves and
joins again under it is still muted, and its minutes by its own clock. It
drops a muted player's lines as they come up and tells that machine alone
"You are muted for N more minutes". The mute ends by itself. A client's
/mute is shown "Only the host can mute players" and never sent, and the
host takes a line that came up as a line, never as a command. The wire
format and the network version (29) are unchanged.

debug.network_test_chat now has the host mute the last player for a
minute, each machine say a line, the host unmute them and each say
another. On three Linux machines over system link the muted player's line
reached no one, they alone got the minutes left, every machine showed the
mute and the unmute, and the clients' own /mute was refused. A drawn
client showed the notices in the chat. The harness gains ten cases and
eleven negative controls for the command's parsing, the name match and
the mute's time (412 pass).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:02:07 -07:00
Jonas VolmanandClaude Opus 5.5 0ca693e681 Let a chat line go to everyone, the team or the vehicle, by Tab
The PC version's chat could talk to everyone, the team or the vehicle.
While a line is typed, Tab now turns it to each in turn ("Say:", "Say to
team:", "Say to vehicle:"), as T and Y still start a line to everyone and
to the team. A vehicle's line goes to the players riding in the vehicle
the sender rides in: the host finds each player's outermost parent object
(a unit in a seat is its vehicle's child), so any map's vehicles count,
Custom Edition maps' too, and a sender on foot sees their line alone. The
host now always sends a line back to its sender's machine. The channel is
a second bit of the line's flags, so the wire format is unchanged.

debug.network_test_chat has the host seat the last two players in one
vehicle and every machine say a line to it.

Checked: tools/harness/tests/test_chat_logic.py (who hears a vehicle's
line, the channel through the flags, with negative controls); three
machines on system link, the two clients seated in one Warthog seeing
each other's vehicle lines and the host's, on foot, seeing only its own;
a host on Xvfb turning a line to the vehicle with Tab, and typing a line of
f's without meleeing; the CI Linux test step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:02:07 -07:00
Jonas VolmanandClaude Opus 5.5 3efe79a38d Add a text chat to network games
Network games had voice chat but no way to type to the other players, as
the PC version's chat did. T (controls.chat) types a line to everyone and
Y (controls.team_chat) one to the team; Enter sends it, Escape cancels it.
The lines are drawn above the motion sensor with the HUD's font: the
player's name in their team's colour (blue without teams), then the text
in a pale blue, and fade after ten seconds (port/linux/game/network_chat.c).

A client sends its line to the host (_distributed_message_chat_up), which
takes it only from the client's own stream and one of that machine's
players, five at once and one each 1.5 seconds after, its text cut to 63
characters of printable ASCII, and sends it on to the machines that hear
it (_distributed_message_chat_down): all, or a team's line those with a
player of the team. Older builds drop both kinds as unknown, so the
network version stays 29, as for the vote kick; network_format.txt
records the two kinds.

While a line is typed the keyboard drives no player, as with the console
(xinput_sdl.c), nor voice chat's push to talk or the debug keys. Controls
Setup gets a COMMUNICATION group with PUSH TO TALK, CHAT and TEAM CHAT
(the controls screen's spinner strings changed by hand to what
port_settings.py writes: ce_menus.py needs the PC version's tags).

Checked: tools/harness/tests/test_chat_logic.py (the text's cleaning, the
host's rate, who hears a team's line, the fade, with negative controls);
the CI Linux test step; three machines on system link with
debug.network_test_chat (new), every machine showing the lines to all in
one order and a team's line only on its team's machines; and a host drawn
on Xvfb with lines typed on its keyboard through xdotool (sent on Enter,
not on Escape, the player still while typing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:02:07 -07:00
eltanschauung 1b03880209 Keep co-op elevator riders aligned through reversals
Carry riders by the current collision transform rather than the previous tick's movement. Preserve supported riders' height through stopped-device predictions and delayed client echoes without suppressing normal jumps, departures or authoritative teleports. Local history only; network messages and protocol version are unchanged.

Validation on upstream main: 379 CPU/menu/assembly tests, including 32 focused elevator cases and negative controls; all three changed C files compile for Windows x86. Earlier native integrated-fork probes passed on b40/c40 at simulated 100/200 ms RTT through reversals and phase snaps.

(cherry picked from commit a38b7db0f4eb4ccc66f5445a6e9c4eef2fb0114a)
2026-10-17 04:17:09 -04:00
Addison Bair 1a20d89b9d Test hooks: a tick a frame, cues at their ticks, a game of one machine logged
debug.fixed_step runs a game of one machine a tick a frame, each frame a tick's time, so it plays the same every time, faster than real time; debug.network_test_cues runs console lines at their ticks from the first tick every player is in; debug.network_test "log" logs a game of one machine, and the scripted player follows the game's clock. All off by default.
2026-10-16 16:16:01 +00:00
justinfilip 7cf24ae589 Stop carrying an unsteered vehicle once the host says it has stopped. 2026-10-15 12:35:57 -05:00
justinfilip e5e97c0526 Carry a vehicle no player drives along the host's positions. 2026-10-15 11:08:46 -05:00
Jonas VolmanandClaude Opus 5.5 9980b9698e Co-op players choose their own friendly fire, and one away is hurt by all.
A network co-op game's pause menu has FRIENDLY FIRE ON or FRIENDLY FIRE OFF
(network.coop_own_friendly_fire, on unless changed). Two players hurt each
other only if both have it on, and then as the host's FRIENDLY FIRE says.
A player with no input for over 70 seconds is hurt by every player,
whatever either chose, until their next input: the choice does not shelter
someone who has left the controls. Enemies and AI allies are as before, and
no other kind of game changes.

Only the host deals damage, so only the host keeps the choices. A client's
input says its player's in a byte that was padding, and the host counts
each player's ticks without a button pressed or let go, or a stick or the
trigger held. The network version is 30 for that byte (29 is the Custom
Edition map names'): an older host would ignore a player's choice.

The button is in the pause menu because a co-op game had no way to its
settings: it opens the campaign's pause menu, which gets SETTINGS only in
single player. pause_campaign_patch lays the menu out as it does single
player's with SETTINGS: five rows, 30 apart, in the taller box. Like
SETTINGS, the button needs the PC version's menus.

Checked on Windows in two games of two hidden copies on a30, one player
hitting the other every 10 seconds, keys pressed by a script, once with the
host's player hit and once with a client's. Hits took shields with FRIENDLY
FIRE ON; none did after switching it OFF; with the player left idle, the
hits from 70 seconds on took shields again; and none did after the next
key. A multiplayer game's pause menu, opened on a client, still has RESUME
GAME, SETTINGS and LEAVE GAME. Not tried with a gamepad or in split screen.
Built and run here on Windows only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 00:47:10 -07:00
BirchWoodGodandClaude Opus 5.5 3f2036eabe Show the scoreboard's pings in the latency meter's colours and measure
With display.latency_meter on, the scoreboard's Ping column agrees with
the meter: where this machine times a player's round trip itself (the
host: every client's, its own players' 0; a client: its own players'), the
column shows that figure, and each known ping is drawn on its own in the
meter's colour for it (green, yellow, red). The other players' pings on a
client are the host's measure in ticks, as the pings message brings them.
With the setting off, the scoreboard is drawn as before.

Without this the two disagreed on one screen: on a LAN the host's measure
in ticks reads 0 to 4 ms while the meter, which counts each machine's wait
for its next tick, reads 25 to 35; with 80 ms of netem the scoreboard said
81 and the meter 107. Nothing more is sent: the pings message still
carries the host's measure in ticks.

Game source change (port: comment): game_engine.c's scoreboard takes a
player's ping from latency_meter_player_ping (distributed_player_ping with
the setting off) and draws a known one alone at the row's fourth tab stop.

Checked: Linux debug and release, web and Android debug and release
build. With netem's 40 ms each way, the host's scoreboard showed 0 and
108 with "Max ping 108 ms", the joiner's 0 (the host's) and 99 with "Ping
99 ms", in the meter's colours; with the setting off the scoreboard looks
as origin/main's (team colours, the host's measure).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ucYZ9cvWn3nrnk7kK3S5X
2026-10-15 01:06:37 -05:00
BirchWoodGodandClaude Opus 5.5 313a1d63c8 Add a latency meter for network games, off by default
display.latency_meter (HALO_LATENCY_METER, default false) shows this
machine's round trip to the host at the screen's top right while a network
game is played: "Ping 45 ms" with four bars drawn as quads, green below
80 ms, yellow below 150, red from 150. The host shows "Max ping", its
slowest client's. A client that has heard nothing from the host for two
seconds shows "Connection problem", pulsing; the host logs a client silent
as long. It is not drawn in the menus, after the game, over a co-op
cutscene (but a connection problem), or when the host plays alone. It is a
setting, off, because the stock HUD has no such meter; only in config.toml.

Nothing more is sent, so the network version stays 29. The meter reuses
the round trips the distributed netcode already takes: the host each
client's from the host tick its input names, a client its own from the
client tick the host's state of its player (or, driving, its vehicle)
names. Those are counted in ticks; the meter times the same echoes in
milliseconds by when this machine sent the tick they name (the last 64
ticks' send times kept), smoothed as TCP smooths its own. A client also
notes when the host's latest message came. All machines, system link,
internet play's tunnel and a browser's WebRTC channel alike, carry these
messages, so the meter reads the same over each. What is shown is
refreshed in the tick twice a second, and only with the setting on;
halo.log then has the round trips every ten seconds ("latency: ...") and
each connection problem as it starts and ends.

Ported from the Halo CE for PS Vita port (BirchWoodGod, contributed under
CC0), where it is on by default.

Checked: Linux debug and release, web and Android debug and release
build; tools/network_format.py: the wire format is the one recorded. A
system link host and joiner in a network namespace with netem's 40 ms each
way: the meter read 89-123 ms on the joiner and 94-128 ms on the host (80
ms of network and each machine's wait for its next tick); all traffic
dropped for 5 s: "Connection problem" on the joiner after
2247 ms, the host "Max ping 2980 ms" in red and its log "machine 1 silent
for 2190 ms", both heard again after the blackout. On loopback with no
delay: 25-35 ms. With the setting off, the game logs the same lines as
origin/main (numbers apart) and config.toml differs only by the new
setting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ucYZ9cvWn3nrnk7kK3S5X
2026-10-15 01:05:36 -05:00
BirchWoodGodandClaude Opus 5.5 1e8ca43cee Play a local split screen game in the network test
debug.network_test "local:<map>[:<variant>...]" sets up the game as the
Xbox menus' Multiplayer > Split Screen does (split_screen_game_initialize:
a server that accepts no remote connections, its client, the split screen
pregame screen) instead of hosting a system link game, then picks the map
and variant, adds controller 1's player and starts it as "host:" does. A
game no other machine can join had no automated way to be played; the bots
of the next commit play in such games.

Checked: "local:bloodgulch:slayer" on a build of origin/main with this
change alone loads and plays the game (one player, debug.test_input
"bot:1"), logs every second as "host:" does and ends with debug.exit_after;
"host:" games are as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ucYZ9cvWn3nrnk7kK3S5X
2026-10-15 00:26:11 -05:00
MrBruh 3e0ba55111 Keep Custom Edition maps' own names: the version goes in the game record
A game's level name is the map's own (custom_maps\hugeass), so no screen or
server browser listing shows a hash, and the game record's map version is
the map's hash's first 32 bits, which a client finds its file by. Maps are
no longer renamed; a download beside another version of its name is named
<name>_<hash>.map and listed as "(2)"; maps the last build renamed get
their names back. Network version 29: the level name and the map version
mean something else.
2026-10-14 23:08:39 +13:00
Paul Hontz 956387d89f Netcode: a client deletes the host's flag or ball on its word without halting
A client on the Steam Frame, in an internet game on carousel, stopped on the
game's exception screen: "objects.c #829: !(weapon_is_flag(object_index))".
The host's object changes had told it to delete a weapon that on the client
is a flag or an oddball (weapon_is_flag reads the weapon tag's "must be
readied" bit, which in the maps only those two have), and
distributed_client_delete deleted it as it deletes any of the host's
objects. The game never deletes a flag or ball while a game type runs, and
asserts so twice: in object_delete_initial_recursive and in weapon_delete.
A release build's assertions only note it in debug.txt, so a release host
can delete one and tell its clients; a client built without --release then
halts, flat or VR alike.

A client follows the host's word: a flag or ball it deletes so is let go of
first (distributed_client_release_item takes it out of its carrier's hands,
as any weapon of the host's), the goal its game type showed at it is cleared
(game_engine_objective_weapon_deleted: CTF's flag and carrier goals,
oddball's ball goal), and the two assertions are not made for an object
distributed_client_delete deletes (network_objects_client_deleting). The
host and the game's own deletions keep them.

On spark, a host told its clients that each flag (one in the client's
player's hands) or the ball was deleted, as a host deleting one does, and
of it again the next tick. Before, a flat client and a VR client halted on
the same assertion as the Frame, in CTF and in oddball; now neither does,
the client's player lets go of the flag, the flag comes back when the host
tells of it again, host and client count the same items, and the player
is killed and respawns after it. A slayer game of deaths and respawns plays
as before.

Files:
  port/linux/game/network_objects.c
  source/objects/objects.c
  source/items/weapons.c
  source/game/game_engine.c, source/game/game_engine.h
  port/linux/NETCODE.md
2026-10-13 23:09:14 -04:00
FractumSeraphandClaude Opus 5.5 62379e5614 A client is not started before the map it downloads is there
A client that joins a lobby on a Custom Edition map it does not have
downloads it by map sharing. When the host started the game meanwhile, the
host held the client's players out, as it should, but the client, whose
download began in the lobby, did not know the game had started and asked
for its player again; the host took the request as a late joiner's and
started it at once. The client then loaded a map it did not have yet:
cache_files_precache_map_begin's damaged disc error, which stops the game
for good. Stock build-201 does the same with its own host.

- The host (network_server_manager.c): a player asked for by a machine
  still downloading the map waits with those held at the start
  (network_game_server_hold_requested_player), and joins with them, the
  machine started as a late joiner, once its download is done
  (network_game_server_release_map_fetchers).
- The client (network_client_manager.c): a start that comes while the
  map is still being downloaded, from a host of an earlier build, is put
  off until the map is there (network_game_client_begin_game,
  network_game_client_idle_pregame), and dropped if the game it was for is
  over by then (another map, or another game).
- NETCODE.md's "Map sharing" says what happens when a host starts during a
  download (it said the host waited, which it has not since version 28).

Nothing sent changes: no network version.

Checked: the Linux CI steps, the web build and its tests, and the Windows
release build. End to end on Linux in Docker, the host starting the lobby
game at once while the client downloads (the case that stopped the game):
this build both ends (the host held the player and started it at game tick
296 once the map was there); build-201 hosting this build's client (the
start put off until the map was there); this build hosting build-201's
client (held, then played). Each client played the game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 08:41:59 -05:00
MrBruh 6d64abdb42 Fix map sharing restart overflow and harden map validation 2026-10-13 07:43:41 +13:00
eltanschauung 4a5505422c Resolve upstream map-sharing conflicts (#257)
Merge upstream/main and preserve its map-sharing includes and updates.

Use message aim-assist policy 87, clear of upstream map-sharing IDs 83-86. Raise network version 28 to 29 because this feature adds a required wire message; record the new format and update its documentation.

Tests compile production message IDs and reject collisions. 293 CPU/menu regressions and three menu/wire-format checks pass.
2026-10-12 14:05:54 -04:00
MrBruh 52e33a391e Harden map sharing and Custom Edition map loading 2026-10-12 22:26:25 +13:00
MrBruh c6a2f9004b Record the map sharing messages and the sizes of the wire format's arrays (#253)
The record read only network_*.c, so the map sharing messages in
map_share.c went unrecorded, and it took each struct sent whole by the text
of its declaration, so a change to a constant that sizes one of its arrays
(MAXIMUM_ENTRIES_PER_MESSAGE, DISTRIBUTED_INPUT_HISTORY and the others)
changed the message without failing the check. It now reads map_share*.c
too, and records each such constant by its definition, following the names
that uses, and the DISTRIBUTED_ sizes in the enum of no name (a unit state's
and a relayed action's least and largest sizes). A size with no definition
found fails the check rather than passing it.

Checked: the items only grow (35 added, none changed), so version 28 is
recorded again; changing MAXIMUM_ENTRIES_PER_MESSAGE in network_actors.c
fails the check as a change.
2026-10-12 21:07:30 +13:00
MrBruh 785e865a87 Merge pull request #302 from hkmodd/fix/limp-body-corrections 2026-10-12 20:46:49 +13:00
MrBruh 5d9081e91e Merge pull request #253 from addisonbair/network-format
# Conflicts:
#	port/linux/NETCODE.md
2026-10-12 20:35:16 +13:00
MrBruh 5a7c655a60 Testing branch review fixes
Network version 28: the host now holds a downloading machine's players out
of the game, and keeps a multiplayer map's first scripted BSP. Dynamic lights
and the flashlight stay on past the Xbox limits, and a second weapon picked
up while firing is readied. Machines of version 27 would disagree on these.

- A large co-op game's respawn fallback no longer picks a teammate who is in
  the air, outside the BSP, or riding an AI vehicle.
- A machine still downloading the map times out when it goes silent.
- Held players join through the late-join queue, so they get the same checks.
  A machine with no held players is started instead of being refused.
- The co-op teleport uses world positions, which are right for seated players.
- Overshield drain no longer cuts off a hit's flash, and hits over 0.02 flash.
- The menu XML is the generator's (Custom Edition's layout) again.
- port: markers on two new declarations.

Checked: ninja linux, android and web build; tools/harness,
test_touch_menu and test_web_build pass.
2026-10-12 20:01:49 +13:00
hkmoddandClaude Opus 5.5 e791cf9ae0 Bodies the host moves are no longer drawn floating or in walls on clients.
In large games, bodies on clients were drawn twisted, hanging in the air,
piled up or inside walls. There were two causes, both in how a client
takes the host's object states.

A body that comes to rest lies limp (bipeds.c): its nodes stay where they
are in the world and are no longer recomputed from the object. The game
stops this when something pushes the body (biped_accelerate). On a client,
that push comes from the damage aftermath, which is sent only to the
machines that see the body's player (network_damage.c). It never comes for
older bodies (a player's dead_unit_index is their latest) or for world
damage. When the host then moves the body, the client's copy moves with
the host's corrections and its own physics, but its pose stays where it
came to rest. A client now stops a body lying limp before it puts the body
somewhere and whenever the host says it is moving. units.c makes it lie
limp again, every tick, once it is at rest on the ground.

A client also takes the host's word that a body is at rest while its own
copy is still in the air, a little behind. Rest stops its physics, and
only its physics clears airborne, so the body kept the pose of its fall
(its root node about 0.36 above the ground) and never lay limp. A client
now clears airborne on a body the host has at rest: the host's is on the
ground, because it comes to rest only when it is not airborne.

Nothing sent changes, so the network version stays the same.

Checked:
- New harness test limp_body_moves takes the code from network_objects.c
  and bipeds.c. Its cases are pushed, landed, moved, blended and living.
  Each of its 5 negative controls (each line of the fix taken out) fails
  its case.
- The whole harness, run natively on Windows: 234 passed, and the
  failures are the environmental ones main also has on this machine.
- The Windows build has no warnings.
- local system link games on one machine (debug.network_test, bots and a
  kill every 2 seconds), with temporary instrumentation: each machine logs
  its bodies every second, and the host sets off a frag grenade's
  explosion under up to 4 resting bodies every 5 seconds. Result: bodies
  at rest drawn more than 0.15 from where the host draws them:
    4 machines, 120 s:  195 of 573 pairs before, 0 of 1221 after;
    32 machines, 150 s: 19479 of 96439 pairs before (20%), 157 of 71237
      after (0.2%), counting only pairs where the host and the client
      agree on the body's position. The rest are bodies still in their
      dying animation, a few ticks apart, at the same rate as on the
      host.
  Copies at rest but left airborne: 195 samples before and 0 after on 4
  machines, 10718 before and 0 after on 32.
- Not checked here: the Android and web builds (they don't build on this
  Windows machine), Linux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 04:46:27 +02:00
Addison Bair fab098a7bd Record the wire format of the network version
AGENTS.md asks for HALO_PORT_NETWORK_VERSION to be raised with any change
to what the machines send each other; nothing checked it, and #95, #99 and
and 18 caught up after them).

tools/network_format.py takes the wire format from the sources: the game's
message numbers, sizes and fields, the distributed netcode's message kinds
and flag bits, its DISTRIBUTED_ constants and the structs it sends whole, as
hashes. port/linux/network_format.txt records them for the version, and
test_linux_port.py fails when the sources differ: an item changed or removed
asks for the version to be raised (--record refuses it until then), an item
added (a new kind or bit, which older builds may ignore) only to be recorded.
It also fails when NETCODE.md says nothing of the version.

Checked by recording each merge's parent and checking the merge: #95, #99
and #106 fail as changed structs; #91, votes to kick, voice chat and the
integrated light bits as additions; #101, which raised the version, passes
once recorded. A comment, or a struct the netcode keeps to itself, changes
nothing.
2026-10-12 01:09:55 +00:00
Addison Bair 9180bd37c8 NETCODE.md: network versions 22 to 25
The versions' list stopped at 21. 22 to 25 as their commits give them:
Custom Edition maps named custom_maps\<name> (4e8ed2f1), their blocks kept
past the Xbox tools' limits (34e2d4fd), the gametype's PC vehicle set
(11117d7c) and a unit's integrated light in its object state (6b55f5cc).
2026-10-12 01:09:44 +00:00
MrBruh a2aad18f65 Custom Edition map sharing
Hosts send players who join without the game's Custom Edition map the map
and the resource maps it needs; the other players help, torrent-style.
Network version 27: new map sharing messages, and players of an internet
game now reach each other.
2026-10-12 08:07:14 +13:00
MrBruh a3277d94dc Fewer draws and GL calls
Decals and light volumes batched, web index and vertex array caches, no FULL_ES3. Prisoner, 15 players: 115 to 172 FPS (A/B).
2026-10-11 22:28:34 +13:00