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>
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>
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>
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>
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>
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>
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>
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.
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.
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.
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>
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.
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).
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.
Opus voice chat (port/third_party/opus 1.5.2, port/linux/src/voice_audio.c, port/linux/game/network_voice.c), relayed by the host, in the lobby too. The host decides in Server Setup > Teamplay Options: lobby voice (on by default), the game's mode (off, team near, anyone near, team, team and enemies near: the default), the near distance, and the quality, 8 to 64 kbps (24). Each player picks push to talk (V), open mic or off, the voices' volume, and the output and input devices in Settings > Audio, and mutes others from the scoreboard's menu. A speaker icon marks who talks in the lobby, on the scoreboard and in a list in game. The host takes a machine's frames only with the key it gave it over its stream, within the quality's size, 50 a second and 8 talkers at once, and sends voices only to machines that asked for them, so the network version stays.
Any player of a network game can start a vote to kick another, on any team, by picking their name on the scoreboard (right-click while it is open frees the mouse) or with the console's votekick; the host's own scoreboard also has Kick and Ban (Ban picked twice). The host alone counts (port/linux/game/network_votekick.c): votes only from each voter's stream, one per real address and again per hardware id (which only removes votes), time played on the host before starting (network.votekick_minutes) and voting, more than half of everyone with the target counted and at least two, one vote at a time with cooldowns for a failed vote's starter and target, and a passed vote keeps the player out for network.votekick_ban_minutes. Builds without it drop the new messages, so the network version stays.
A client makes its copy of a unit the host spawns with object_new, and unit_new gives it the unit tag's grenades (a cyborg's four frags), where the host's game engine gives a player's unit the game's own (none on wizard slayer). The host's inventory, which corrects them, comes a tick or two later, so a client's player spawning with the throw button held began a throw with a grenade its unit did not have: the client saw it thrown and go off, and reported its hits, while the host's copy, with no grenade, never threw, and refused every report as damage the player could not deal. A client's copy of a host's unit now starts with no grenades, and takes the host's when its inventory comes.
In the network test (1v1 slayer on wizard, staged hits and the bots' own fire and grenades, with #209's shock wave fix in both builds), with no added latency over 8 games the client threw 41 grenades to the host copy's 39, and the host refused all 5 reports of the 2 explosions it never saw thrown (11 of 2959 reports refused in all); now 55 throws to 55, no explosion refused for want of a throw, and 2 of 2749 reports refused. At 50 ms a receiver (8 games before: 66 throws to 64, 7 reports of 2 explosions refused; 4 games after) 10 throws to 10 and none of 1396 reports refused. Every unseen throw was in the client's first tick or two alive, with four grenades there and none on the host. Kills and deaths stay balanced.
When the host's teleporter sent a player, a client's copy that had not stepped on the pad itself was put at the far end by the host's word, landing on the teleporter there with no note of it (the game's own teleporter notes the one it lands a player on). The client's teleporter sent it straight back, every tick the host's word put it there, until the player stepped half a world unit off: the copy stood on the pad left behind, 24 world units off, for 6 to 9 ticks after most such teleports and for as long as the player stood still (up to 143 ticks in earlier runs), and the client's hits on them there were refused. A player's unit the host's word moves further than a step at once is now on the teleporter it lands on, as one the teleporter sends is.
1v1 slayer on Wizard, two bots shooting, 16 runs of 12 minutes each way at 0 and 50 ms: the client's teleporter sending the host's player back 85 ticks in 18 episodes before, none after; the copy over 5 world units from the host's for 3 ticks or more 12 times (82 ticks, up to 9) before, none after. Hits refused 3.7% before and 4.0% after, the view error (p50 0.03, p95 0.08 world units) the same.
A frag grenade's explosion deals two damage effects, its explosion and its shock wave (a plasma grenade's too), and a client reports each hit of each. The host took the first of them against the throw it had seen and spent the throw; the explosion's other hits came with it only when they were of the same damage, so every hit of the other damage found no throw left and was refused as damage the player could not deal. An explosion of the player's this tick is now one of the grenade, of any damage its tags deal, so its other damage takes no throw of its own (and is still paid for at the grenade's rate).
In the network test (1v1 slayer on wizard, staged hits and the bots' own fire and grenades, 8 games each) the host refused 4.4% of the client's reports with no added latency (134 of 3072, 104 of them a grenade's other damage) and 4.9% at a 100 ms round trip (153 of 3107); now 0.4% (11 of 2959) and 0.8% (25 of 2986). What it still refuses it should: grenades it never saw thrown, targets gone before the report came, and hits on a player the client still had at the teleporter they had left.
EXTRA ENEMIES' PER PLAYER grew each squad by its size for every player past
the first at 100%, held back only by the actor pool: in a game of 65
players each squad of six grunts had about 378 more. The host's ticks
stalled (every client froze, then its players jumped), the clients ran out
of objects (enemies the host had and they had not, which killed players
unseen) and past their renderer's limits, and the enemies with no free
ground left were placed off the map, where the clients' copies fell. A
squad now grows to COOP_ENEMIES_MAXIMUM_GROWTH (8) times its size at most.
Any player crossing a loading zone used to pull the whole team to them, with a two-thirds vote to go back to a BSP already visited: one player running ahead or doubling back teleported everyone, and teams ended up several BSPs back. Now only the host's players switch the BSP (anyone's while none of the host's are alive), the team is brought to the host who crossed (not to a client waiting on the trigger), the stranded and the respawning come back beside the host first, and a client standing on a loading zone is told it waits for the host. Clients still never switch on their own, and the host still waits for every machine to load the new BSP.
The killing blow went once, unreliably, while the units' states, sent every tick, killed a client's copy of a player the moment they said dead, so a late or lost blow left the body to die without its death animation, push or kill message. The host now also sends each killing blow reliably (a client replays one blow of a unit only; the reliable copy is not dropped as overtaken by the damage sent since), and a client waits half a second for it before a player's unit dies without it, as an actor's already did.
The host sent an object coming to rest once, unreliably. One lost left a falling body hanging on a client until its turn in the round of objects at rest, four a tick: in a large game, many seconds. It is now sent three times over half a second. A client also takes the host's word on rest from every state, not only from one far enough off to move its copy, so a copy left at rest while the host's fell no longer hangs; a vehicle the client drives takes it only from a correction, as before, so it is not stopped as it sets off.
Fixups for #99 and #112: the version raised for the object message's
shader permutation, NETCODE.md brought up to date, a networked glass
break's sound placed where it broke, a frame scratch pool that runs out
logged once, and the new comments marked as the port's.
Fixups for #102, #103 and #105-#107: the version raised for the glass,
scenery, cutscene cluster and revert messages, NETCODE.md brought up to
date, co-op's followers' object lists freed as soon as they are used, and
extra enemies that find no free ground placed on the rings they used before
rather than left out (at 32x a quarter of them were), with the places found
taken not tried again for the rest of the squad, and the way to a place
looked along level.