Voice chat's speaker and muted speaker are Lucide's volume-2 and volume-x (1.53.0, ISC: port/assets/icons/lucide, credited in AUTHORS.md, its licence shipped beside the game), drawn white into the menus' bitmap voice/speaker by tools/ce_menus.py and tinted, in place of the speaker made of rectangles. In the lobby, on the scoreboard and in the list of who talks, the icon is centred on the middle of the text's capitals (draw_unicode_string_capital_middle).
Co-op's Server Setup has CO-OP OPTIONS (friendly fire, the extra enemies, player collisions) and VOICE AND VOTING (voice chat and vote kick, as multiplayer's Teamplay Options has them) in screens of their own, as the gametype's options are, each choice with its help; their spinners set config.toml's settings on OK, and the co-op game starts from them.
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.
Our version of #147. With audio.loose_sounds (off by default), each of a map's sounds that has a Halo PC sound tag file of its name in the data root's tags folder plays from it (port/linux/game/loose_sounds.c); loose_sounds_reload and loose_sounds at the console read them again or give the map's back. The files' samples are made the Xbox ADPCM this build plays through the Custom Edition converter, so no channel pools change and stock maps play as before. The override is written into the map's definition in place, keeping the counters the game updates; a change stops every sound and flushes the sound cache first, and old samples are freed once no cache block holds them. A split sound's length is its chain's.
The sound cache is HALO_PORT_SOUND_CACHE_SIZE: 16 MB on desktop, the Xbox's 4 MB on Android, with the debug view's buffer sized to match.
The powerup bounds of #117 move from source/models/models.c to port/linux/game/powerup_render_bounds.c, called from scenario_tags_load beside the other tags-loaded hooks, so the game's models.c no longer takes <xtl.h> and D3D buffer locks. It uses square_root and a relative margin in place of sqrtf and nextafterf, which strict gnu89 does not declare, and the objects.h scale change is marked as the port's. tools/test_powerup_bounds.py is dropped: it redeclares the engine's types by hand and CI does not run it.
On bloodgulch the active camouflage's render radius grows from 0.1 to 0.23 and the overshield's from 0.1 to 0.21; no other equipment changes.
hud_hires_override_point_threshold is bounded as hud_hires_override_coverage is, by hud_hires_asset_count (which caps at MAXIMUM_TEXTURES), and is described in hud_hires.h. tools/test_hud_thresholds.py is dropped: it needs a hidden OpenGL 4.5 window, SDL3, scipy and a 64-bit lld build, so CI cannot run it.
A tree patched by another version of sdl-relative-mouse.patch (an older checkout, or CI's cached third_party, whose key now includes the patches) has the listener put back as SDL has it before the patch is applied, rather than failing the apply and turning the Android build off. The patch is regenerated with git diff, with its context lines.
The engine's real functions, taken from the sources and compiled against a
fake world, so a fix can carry a test CI runs without game assets. A test is
tests/<name>.c (fake world and cases, CHECK) and tests/test_<name>.py (takes
the code, enums and constants from the sources, builds, runs each case under
pytest). Each test has negative controls: the code with a deliberate fault
must fail a named CHECK. CI runs pytest on tools/harness, so new tests need
no workflow change.
- test_object_bounds_cache: collision's loop over a crowded fake world gathers
the same features with the cache, as objects move and after invalidation;
a stale cache is detected.
- test_widget_pool: widgets.c's pool fills to its size and refuses (64 slots
against 128 players' rifles, #145), frees and reuses, and frees a failed
widget's slot.
Custom Edition maps are loaded from the custom_maps folder, as level
custom_maps\<name>, apart from the game's own maps of the same name. The
map lists get CUSTOM SINGLEPLAYER and CUSTOM MULTIPLAYER, by the scenario
type in the map: a custom singleplayer map plays as a campaign level, or
as network co-op from the Map screen; a custom multiplayer map plays as a
multiplayer map. A map with an OpenSauce header that asks for nothing of
OpenSauce's runs as stock Custom Edition runs it.
SteamOS keeps kernel tracing on for its GPU performance captures
(gpu-trace.service), and its Mesa, built with gpuvis, then writes a marker
into the trace buffer for each traced driver function. On the Steam Frame
that is some 480,000 writes a second: the driver's thread spent its time in
them, and the game ran at about 50 frames a second instead of the headset's
72 (#71 has the same for the arm64 build). The Steam Deck runs the same
gpu-trace service and the same Mesa, so it needs this too.
The executable now defines open() and openat() (and their 64-bit names),
exported so that the driver's calls reach them: they refuse trace_marker
and pass everything else on. gpuvis then skips its markers.
HALO_GPU_TRACE_MARKERS=1 lets the driver write them, to capture with gpuvis.
The link takes libdl for dlsym, which glibc before 2.34 keeps there.
Video Setup has Anti-Aliasing (Android's choices its own), Shadow Resolution and Per-Pixel Lighting, its rows closer and the help lower for them to fit; Audio Setup has Reverb.
display.anti_aliasing, off by default: "fxaa" or "smaa" (a pass over the
3D view before the HUD and menus; SMAA vendored from port/third_party/smaa,
MIT), "ssaa2x" (the screen's targets at twice the resolution, at most the
GPU's largest target) and "msaa2x" to "msaa8x". Multisampled framebuffers
have every attachment multisampled, so the mirror's view with the back
buffer's depth buffer is too, and a target's texture and renderbuffer stay
coherent. The setting is read again between frames. Android has FXAA for
SMAA and no supersampling.
Based on #38 and #39 by Tyberious.
The campaign scripts only run on the host, against every player's unit,
but some of their habits assumed split-screen:
- volume_test_objects_all on the players waited for every player in the
volume at once: the Maw's bridge cutscene, a30's Pelican pickups. In
co-op one player arriving is enough now.
- Scripts name players as player0 and player1, so everyone past the
second was left out of what they did: Pelican loads (a30's pickups,
insertions), getting out, cutscene teleports and freezes, starting
profiles. Those players now follow player0 (coop_scripts.c).
- A cutscene filmed far from every player (b30's sword Elite at the
shaft door) wasn't active on clients: the cluster the scripts force
active (object_pvs_activate) now rides in the presentation's spare
bytes, and what the host's camera films is sent to clients as often as
what is next to their players.
- A mission the scripts fail (d40's timer, a50's Keyes) only respawned the
players, leaving them in the failure cutscene for good. It now reverts
like the cutscene skip does; everyone dying still respawns at the last
checkpoint. The scripts' game_revert reverts the same way.
The network version is unchanged: the presentation keeps its size, and a
host without the active cluster sends zeroes, which read as none.
The test compiles main.c's and players.c's death handlers against a fake
world; their network co-op branches call functions it had none of, so it
no longer compiled. Stubbed off, it covers the local game it was written
for, and passes.
Hosting a co-op level from the Map screen (CO-OP CAMPAIGN, a level and its
difficulty, then Server Setup) makes a network game on the campaign map
with no game engine, which is what co-op is. Only the host runs the
scripts and the AI; port/linux/game/network_coop.c sends clients what the
scripts do (cutscenes, camera, fades, HUD, sounds, devices, named objects,
animations), and network_actors.c drives the host's AI units on every
machine. Players spawn beside each other, respawn beside a safe teammate,
and spectate while dead or waiting. Skipping a cutscene is a vote.
Players can join a game in progress at any time.