A co-op ALL-player trigger could advance after an unchecked teleport, or while (players) was empty during respawn. Pillar of Autumn then permanently closed its blast door with players still outside.
Preserve player-list provenance in the existing unused header word, block empty or unavailable player-list waits only on the co-op host, and recheck all arrivals after gathering. Use the existing crowd fallback and restore rejected placements instead of leaving a player outside the volume or on a BSP switch. Empty AI lists, non-waiting tests and non-host behavior retain their semantics; no network message or protocol changes.
Validated with 17 production-source gate tests and negative controls, covering 1-128 players, failed or displaced teleports, BSP boundaries, retries, list reuse, allocation failure and empty AI/player lists. Windows and Linux release builds and the full local regression suite pass.
(cherry picked from commit 49b4716c4be187a16800300671221a964094e6fb)
Several of the fixes taken from the forks changed source/ without the
port: comment that marks a change from the original (the second copy's
datagram ports, the downloaded maps' refusals, the main menu's clock,
the one-magazine HUD states, the render lists' sizes and others), and a
comment still named a function of that fork's. Each change now says so.
A map that map sharing downloaded runs only scripts that keep to the
player's own game: one that calls what changes the player's settings or
other players' games (player0_look_invert_pitch, sv_end_game, sv_say) or
sets a global that outlives the map is disabled, as for any map's script
that calls what no map's may, and debug.txt says which. Map sharing names
each map it downloads in custom_maps/downloaded_maps.txt, so they stay
held to this in later runs; game.downloaded_maps names more, or "*" for
every map. The campaign's game_speed and game_save_totally_unsafe stay
allowed.
The setting's entry came into port_config.c with the profile commit before
this one (a conflict's side of it); it is the one entry now.
(cherry picked from commits 7d02d46210ba672512e36b39ffd2c0b0d8e4f3b3 and
d3718e78d4889140a44f8050075b56143e4b7732, wired to OpenCE's map sharing in place of that
fork's map families)
The debug build this port comes from unlocked every campaign level on
every difficulty for each new profile. Retail Xbox Halo does not: a new
profile starts at The Pillar of Autumn and each level opens when the one
before it is finished. New profiles now start that way.
- game.unlock_all_levels (off by default) keeps the old behavior for new
profiles, for those who want it.
- profile_reset_campaign in the console clears the current profile's
finished levels and last played level, after a confirmation
("profile_reset_campaign yes"). Profiles made by earlier versions had
every level open and cannot be told apart from a real full clear, so
nothing is changed automatically.
(cherry picked from commit 224d9a757575509efb0dedab094bd0d34df59e96, without its top README
table, which OpenCE does not have)
A dedicated server's debug.txt grew 2 GB in 12 hours, almost all of it
"maximum light cluster references per map (8192) exceeded", written each
time a light could not join a cluster: thousands of times a second on a
busy map. A full reference list now says so the first time a map, and the
times after are counted and told when the map's lists are let go or made
anew. What fits in the lists is as before.
The dedicated server draws nothing, and a light's clusters are only used
to draw (the lights of an object or the map, and a unit's illumination,
which only debug_unit_illumination reads, from the lights drawn that
frame). The server (HALO_SERVER) no longer joins lights to clusters, so
it does none of that work each tick and never fills the lists.
(cherry picked from commit 50cc6274fb5f7697c6f3c25394e15e22f87106f2)
The campaign's scripts print their progress and music cues ("mission
script is running", "levels\b30\music\b30_01", the dialog lines), which
the Xbox never showed. They are the game's chatter now, on screen when
config.toml's game.console_log is "all", like the rest of it; print and
inspect typed at the console still answer whatever the setting is.
(cherry picked from commit 114453cc1a84be4fc5622053d2afb57c1ae58e7f)
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.
CI builds the profiling flavour too (tools/ci_build.py linux profile: a
debug build with configure.py --profile), so a change that breaks it is
seen; nothing else compiles it. profile_sections.h, which only the port's
builds use, moves from source/cseries to port/linux/include, and the hooks
in the game's sources (the map load, the console, the main loop's exit)
and their includes say they are the port's. The build scripts' import
lines are wrapped as the files' others.
Add a profiling build: CPU recordings for Perfetto and a report tool
Conflicts resolved: CI runs main's tests and test_profile.py; the build
scripts take both main's opus and target flags and the PR's
configuration_defines; port_config.c keeps debug.memory_watch before the
profile settings; main.c and network_distributed.c keep voice chat, and
network_votekick_host_tick stays its own statement after the game state's
profile scope, not under its if.
Split HALO_ANDROID into HALO_ARM64_GUEST, HALO_GLES and HALO_ANDROID
Conflict resolved: main's render-target eviction and mip composites are kept, with the PR's HALO_GLES guard on the ES blit fallback after them.
25 notes ("left unassigned only by this default arm ... approval pending,
2026-09-27 audit") argued that a variable was never read unset. Where the
arm ends in system_exit they were right and simply go. A release build's
assertions return, though, so where the arm only asserts the variable now
starts with a value (a Custom Edition map's HUD data can reach hud_draw's).
A failed map precache reads as failed.
rasterizer_debug_drawing_begin had two declarations, one with an argument
its definition never takes; it now has one, and the port's macro papering
over the mismatch is gone.
(cherry picked from commit 74f8a048e4c3cc7dbc4b050dd41a8bae912bb367)
Uninitialised locals get values (debug vertex, datastore result, index
buffer, map tool direction, AI entry points, escape alignment, grenade aim
from a vehicle, game options descriptions, unreachable switch arms), the
AI's squad and platoon records move whole, a speech cause point is read,
the geosphere's out-of-memory path frees itself, a console cond clause
without a result is refused, and the crash reporter rejects truncated map
files. Gameplay the original game has, kept so it plays the same, is noted
in a line rather than a page.
(cherry picked from commit 75afc4a1e57af2ba697e8be0e33b2afeee7639f8)
objects_can_see_object and objects_can_see_flag on player0 tested one player only, the first with a unit, so the Pillar of Autumn tutorial's panels (look at each light in turn) never lit for anyone else. In network co-op a test of the players now passes when any player looks, as volume_test_objects_all already does.
(cherry picked from commit 1e8e0f9affe212d3e30817b768d3124027b678f7)
Seven of the script engine's conversions and comparisons wrote only the
low byte or word of a long and returned the rest uninitialized, which
the scripts' stacks and globals, and so the saved games, carried along.
Every reader takes only the low byte or word (the script type sizes), so
the scripts' results are unchanged.
(cherry picked from commit bb239e2207668593679eea5de45eda8e5f635b34)
(cherry picked from commit d6811ee3264b2082ba6b943087ce26293f59768a)
HALO_ANDROID meant three things at once. The port's code now names each:
HALO_ARM64_GUEST for the ILP32 AArch64 guest's code paths (the inline
assembly's C, the bss pragmas, the CRT's), HALO_GLES for the OpenGL ES
renderer (its GL header and function list, shaders and capabilities), and
HALO_ANDROID for the app alone (its display, input, files and lifecycle).
The Android build defines all three, so nothing changes for it, nor for
Linux and Windows, which define none. A build for another platform can
then take one part without the others: the ES renderer, or the guest's
ABI, without the Android app.
port/android/README.md also no longer says that the widescreen changes are
in #ifdef HALO_ANDROID: none have been since 50eae19b.
Zero credit: remove 51 guard-only headers, 50 project-list entries, four useless includes and two unused enum definitions. Preserve source assertion locations and both repositories existing READMEs. Clean rebuild, unchanged 8252-row strict snapshot, unchanged code/data credit and admissions, 1663 tests plus 159 subtests pass. Disclose 76 compiler-local label renumberings and two warning source-line shifts; runtime sections and relocations remain identical. No private assets or scratch tooling published.
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 HUD's text takes '|' and the character after it as one escape (draw_string.c, parse_unicode_string), so a map's message ending in '|' had its drawing read past the terminator. sv_say leaves '|' out of what it shows.
Custom Edition maps' scripts call these Halo PC functions (lookout_classic
and the Halo Kart maps show players messages with sv_say), which did
nothing here. sv_say now shows the message in each player's HUD, and
sv_end_game ends the game on the host, as its time running out does.
They go at the end of the function table, so Xbox maps, which call
functions by their place in it, are unaffected; Custom Edition maps call
them by name (custom_edition_scripts.c).
sv_end_game adapted from ChupathingyCE (4e89e01f), by MrMilenko.
coldsnap_20th_xl's scripts start an object_name global as none, which
this compiler refused ("none: this is not a valid object name."), so on
a Custom Edition map the scripts using it were dropped. none is now no
object: the object_name type's own default, which every lookup takes.
Adapted from ChupathingyCE (92342517), by MrMilenko.
The co-op host's console command bringto teleports every other player
beside the host's player, or beside one who already came when there is
no room there, and says how many came. A client is told it is the host's
command.
Found reviewing the Xbox and Custom Edition map loaders before servers
let clients download maps:
- the Custom Edition sound converter sized a resampled buffer from a
stream's own rate, which could wrap the 32-bit size and overflow the
heap; every frame buffer is now bounded
- script constants not parsed again must be of their own type, and the
pass-through forms' arguments of the type they are returned as, as the
compiler emits them; a cache's script source is no longer recompiled
- model index buffer data may not lie in a tag's bytes, parts' own
geometry blocks are emptied, and a lightmap buffer of no vertices is
dropped
- Custom Edition models have their local nodes made before any is
verified, and a resource sound's header must lie in the map's tag data
- bitmap pixel ranges are checked with the size the game reads, and HUD
message spans past their text are cut
Co-op lets a script's wait for every player in a volume pass on one player, so the Maw's lift left with whoever stepped in first and the rest could never follow: it only goes up. When a script waits on volume_test_objects_all of the players (the condition of a sleep_until) and one player is inside, the others are now brought beside that player first. A test of the volume in an if or a not moves no one (hs_runtime_waiting_on_call).
Each (players), (vehicle_riders ...) and the like makes an object list,
and a tick's lists are freed only after all its scripts have run. The
Xbox's 48 lists were too few for coldsnap_20th_xl, whose scripts test
(players) in dozens of places each tick (49 lists in a tick with one
player, 69 once a fighter-bomber's destruction ran): the game halted
("object list header index #65535 is unused or changed") as soon as its
scripts loaded.
The native builds now have 1024 lists and 8192 listed objects (the
Xbox's 48 and 128) in their game state, and a list that cannot be made
is an empty one rather than a halt. Lists are the scripts' own and never
reach the network. A campaign checkpoint saved by an earlier build does
not load in this one (the game state's allocation check refuses it, as
with each change of these pools).
Reported by ugoboom.
(cherry picked from commit 288afc20077e972e531856cc1eb14a8ce02e1bd4)
What the followers of player0 do (teleport, suspend, get out, board) is a
function each in coop_scripts.c, so every engine hook is one call. The
cutscene skip reverts only in a local game or on a co-op host, written as
two plain branches; a non-co-op network host no longer reverts alone.
The scripts check game_safe_to_save before some cutscenes (the Maw's
bridge). The game's test fails while any enemy sees any player, so with
players spread out a fight elsewhere held up the cutscene a player had
reached. For the scripts in co-op, one player alive, on the ground and
unseen is enough; checkpoints keep the game's own test.
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.