Compare commits

...
Author SHA1 Message Date
Stefan MaierandClaude Opus 5.5 0e64092553 VR online list: the games players host before the dedicated servers
The server browser lists the master servers' dedicated servers first,
13 of them now, and the VR online panel shows 10 rows: a game a friend
hosts (or an OpenCE player's) was below them, out of sight. The panel
now lists the games players host first (the server browser's, then the
VR list's own), then the dedicated servers, and takes up to 64 listings
from the server browser rather than 32.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:14:20 +01:00
Stefan MaierandClaude Opus 5.5 1d506bb952 Merge upstream/main (2026-10-27): OpenCE network version 39, dedicated servers
Dedicated servers (halo --server, web admin, RCON, master server
listings, the server browser's DEDICATED tab; network version 39), each
client telling the host its build (distributed message 92), the
dedicated servers' chat filter, Gun Game's plasma weapons.

Conflicts: our join state enum for the VR menus is renamed
P2P_JOIN_STATE_* beside upstream's join progress (P2P_JOIN_*), and both
are kept in p2p.c; the entry sizes of message 92 and our 240 both;
config_write_file keeps our rename over the file and takes upstream's
dedicated server branch; hud_hires.c takes upstream's null renderer
check; build.yml and README.md stay ours. The new server-image workflow's
jobs are off (Mjolnir VR ships no server image).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 20:14:20 +01:00
bnunu 3c7ed45e6f Merge pull request #434 from s3anyboy/controller_layouts
Recon and fishsticks
2026-10-26 08:35:50 -04:00
S3anyBoy 8bc9c179cb Merge branch 'OpenCommunityEdition:main' into controller_layouts 2026-10-26 06:57:42 -05:00
MrBruhandClaude Opus 5.5 5388f2f418 Write the join screen's menu file again with the browser word for a refused name (tools/port_settings.py)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-27 00:11:19 +13:00
MrBruhandClaude Opus 5.5 09123e997c Let a dedicated server keep words out of its chat and its players' names (server.chat_filter, off by default): streamer mode's list by group and severity (slurs from mild, swearing and insults off by default), "censor" starring a line's words, "block" refusing the line and refusing at the door a machine whose name has them (rejection code 7, "name", which an updated game shows as "The host refused: your player name").
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-27 00:00:42 +13:00
MrBruh d9e9c86e56 Merge pull request #494 from OpenCommunityEdition/client-build-info
Have a client tell the host its build, the GitHub repository it is ma…
2026-10-26 23:24:34 +13:00
MrBruhandClaude Opus 5.5 d0acb3bd61 Have a client tell the host its build, the GitHub repository it is made from and the mods it plays, which a dedicated server shows in /api/players (client) and logs once: a new distributed message older builds drop as a kind they do not know, so the network version stays 39.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 23:20:52 +13:00
MrBruh be29b20fe2 Merge pull request #492 from OpenCommunityEdition/browser-dedicated-masters
Show a game under the server browser's DEDICATED on a master server's…
2026-10-26 21:33:53 +13:00
S3anyBoy 75743bb5c7 Merge branch 'OpenCommunityEdition:main' into controller_layouts 2026-10-26 03:30:33 -05:00
MrBruhandClaude Opus 5.5 41473a55d2 Show a game under the server browser's DEDICATED on a master server's word alone, not its listing's own dedicated flag, which any host can set: a game the brokers alone list is shown as the players'.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 21:28:01 +13:00
MrBruh 51150d617c Merge pull request #491 from OpenCommunityEdition/dedicated-server-chat
Give a public dedicated server its text chat, network.text_chat's (on…
2026-10-26 20:33:45 +13:00
MrBruhandClaude Opus 5.5 1042811584 Give a public dedicated server its text chat, network.text_chat's (on by default, live with sv_set), while a public game a player hosts keeps none: a client follows the host's chat in a game joined from the server browser only when a master server lists it as a dedicated server's, an approved operator's, not on the listing's own word.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 20:33:04 +13:00
MrBruh 0a46688c3d Merge pull request #490 from OpenCommunityEdition/dedicated-server
Dedicated server
2026-10-26 19:49:37 +13:00
MrBruhandClaude Opus 5.5 686e7c8774 Build the server image only after a Build of main, weekly and by hand, not on pull requests, where it built the image of main's last release rather than the request's code.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 19:38:15 +13:00
MrBruhandClaude Opus 5.5 1fc3a35079 Log a failed SDL_Init once, and again only when its reason changes, rather than on every frame that asks for SDL (tens of thousands of lines a minute without a display).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 19:36:55 +13:00
MrBruhandClaude Opus 5.5 27f79d3eb9 Describe the master's /apply as taking the region, from a menu, and the number of servers as its options, its form keeping the display name, the public key, the proof and a note.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 19:09:27 +13:00
MrBruhandClaude Opus 5.5 5653960e78 Open the server browser on ALL GAMES, the dedicated servers first and then the games players host, rather than on DEDICATED: network.browser_source defaults to "all".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 18:11:47 +13:00
MrBruhandClaude Opus 5.5 a8b85a09c0 Let a fleet run the dedicated server: the owner's role over the web from loopback, POST /api/bans and sv_ban_address, sv_stop and sv_restart drains, the players' addresses in events, the server's identity in its status and capabilities in the RCON's hello, and each release's SHA256SUMS signed with the release key (deploy/release-key.pub, tools/release_sign.sh).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-26 17:21:00 +13:00
S3anyBoy bd362e1e2f Merge branch 'OpenCommunityEdition:main' into controller_layouts 2026-10-25 21:48:51 -05:00
bnunu b09a3ea739 Merge pull request #477 from PeterRichardsonGolf/fix/gun-game-plasma
Gun Game: the plasma rifle and plasma pistol doesn't fire
2026-10-25 16:40:58 -04:00
Stefan MaierandClaude Opus 5.5 04cfd1a12c Host line of sight: a VR player's weapon hand is a muzzle too
Upstream's host now refuses a reported hit when the shooter had no line
of sight to the target, from the unit's camera (or a vehicle gun's
muzzle). A VR player fires from the weapon in their hand, and their view
follows their head up to vr.lean_limit from the body: a shot past a
corner or over an edge that the unit's camera is behind was refused.

The host takes the hand their machine sends (network_vr.c) as a muzzle
in the trail points, as a vehicle's gun: the line from the eye to it
must be clear (no hand through a wall), and it must be within 0.4 world
units (1.2 m) of the camera.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:15:43 +01:00
Stefan MaierandClaude Opus 5.5 2b40b42f75 Merge upstream/main (2026-10-25): host line of sight check, early inventories
Network version stays 38. The conflict: upstream's unbinding of the read
framebuffer before the swap (for OBS) goes into the window's branch,
where the swap is (VR presents through vr.c).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 21:11:13 +01:00
MrBruh 43c8f6eebe Read config_answer_only in the config_write test's file writer, so that its probe-writes negative control compiles under clang 23's -Wunused-but-set-global. 2026-10-26 02:44:19 +13:00
MrBruh 5177040890 Ask the official master server, master.opencommunityedition.com, built into the game and the web page, when masters.txt names no master. 2026-10-26 02:26:03 +13:00
MrBruh 815d3a0ca5 Add a dedicated server (halo --server) with its web admin page, RCON, master server listings and the server browser's DEDICATED tab, marking dedicated games in the LAN advertisement and internet listing in network version 39. 2026-10-25 21:37:29 +13:00
S3anyBoy 1095223b2b Merge branch 'OpenCommunityEdition:main' into controller_layouts 2026-10-24 20:48:00 -05:00
bnunu 7fb941b0b2 Merge pull request #474 from johncranston303/los-fix
Stop a client from instantly wiping the whole lobby through walls
2026-10-24 18:51:56 -04:00
John Cranston 933cad12b3 Host: refuse a reported hit with no line of sight to the target. 2026-10-24 13:22:27 -07:00
PeterRichardsonGolfandClaude Opus 5.5 0a8584b260 Test Gun Game's spent weapon check against battery weapons' magazines (#477)
The real gun_game_weapon_empty against a fake plasma rifle (one magazine
holding nothing), pistol and weapon without magazines: full and spent
batteries, rounds loaded, carried and none. Negative controls: the old
return, the empty magazine not skipped, and every magazine skipped, each
caught by a named case. Checked: the test's 7 pass and the whole harness
passes (867).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 13:33:02 -06:00
PeterRichardsonGolfandClaude Opus 5.5 8e8fa4d760 Gun Game: the plasma rifle and plasma pistol can fire
gun_game_weapon_empty used the battery (age) only for a weapon with no
magazines. Halo CE's plasma rifle and plasma pistol have one magazine
block with every count 0, so the loop found no rounds and called them
empty: gun_game_equip deleted the weapon and gave a new one on every
update, and those levels could never fire (bots holding them slid around).

A magazine whose definition holds no rounds is skipped; a weapon with no
magazine that holds rounds is judged by its battery, as one without
magazines is.

Found in Halo CE: Arena Evolved.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-24 13:25:20 -06:00
bnunu 3a2c4649e8 Merge pull request #466 from medic-code/fix/spawn-inventory-wait
fix(net): apply an inventory once its objects come
2026-10-24 14:49:33 -04:00
medic-code 32604c8e7e Describe in NETCODE.md what a client does with an early inventory
The inventory bullet gains what a client does with an inventory whose
unit or weapons have not come yet, and when it drops one, so the
protocol notes match the retry list.

Checked: documentation only; ninja linux links and
test_retried_inventories.py passes (17 tests).
2026-10-24 20:48:18 +02:00
medic-code 07c8430b5a Keep a client's inventory until its weapons have come too
A weapon is made on the stream too, so an inventory naming one not here
yet left its slot as it was until the host's next inventory. What is
here is still applied at once, and the inventory is kept until every
weapon it names has come, then applied in full with its ammunition.

Checked: ninja linux links with no warnings; test_retried_inventories.py
passes (17 tests): six cases, now including a weapon arriving late, ten
negative controls each failing its own case, and the wiring check.
2026-10-24 20:48:18 +02:00
medic-code 667c2f29a0 Keep a client's inventory until the unit it names has come
An inventory goes as a datagram and a unit is made on the stream, which
can trail it, so one naming a unit not here yet was dropped: a player
just spawned held nothing until the host's next. The client keeps the
newest of each unit's, one per player at most, and applies it once the
unit comes; it is dropped after three host refreshes or at a new game.

Checked: ninja linux links with no warnings; test_retried_inventories.py
passes (15 tests): five cases (unit late, newest kept, list full,
forgotten, expiry), nine negative controls each failing its own case,
and a wiring check.
2026-10-24 20:48:18 +02:00
bnunu 6036adfa2d Merge pull request #445 from johncranston303/obs-capture
OBS's game capture no longer records the game upside down
2026-10-24 14:39:31 -04:00
Stefan MaierandClaude Opus 5.5 9ef32d4304 Merge upstream/main (2026-10-24): OpenCE network version 38
OpenCE gametypes named in the server browser (network version 37), and
the host may leave the shield effects to each player (SHIELD EFFECTS'
PLAYERS' OWN, network version 38). The conflict: our Android listing
declarations stay beside upstream's p2p_set_game_listing with its new
kind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:00:39 +02:00
MrBruh 5e27f3a27b Merge pull request #468 from OpenCommunityEdition/testing
Testing
2026-10-24 14:08:46 +13:00
MrBruh 68d8048333 Let the host leave the shield effects to each player with SHIELD EFFECTS' PLAYERS' OWN, still synced by default, in network version 38. 2026-10-24 14:00:35 +13:00
MrBruh dc7feb1c41 Name OpenCE gametypes in the server browser (SWAT, HC CTF), sending each game's kind with its LAN advertisement and internet listing in network version 37. 2026-10-24 13:44:16 +13:00
Stefan MaierandClaude Opus 5.5 24ddaecd78 Merge upstream/main (2026-10-23): OpenCE network version 36
New gametypes (SWAT, Infection, Gun Game, Hardcore), shield effects the
host's choice in network games, co-op ally friendly fire, network fixes
(scenery shields, tripmines, co-op teleports), Custom Edition HUD and
texture fixes, ASTC for Android GPUs without S3TC.

Left out on purpose: upstream's two Android workarounds for Mali phones,
the GL shadow state refreshed before every draw (prepare_draw) and the
geometry cache bypassed (mirror_range). Both would cost every draw of
both eyes on the Quest, which never showed the corruption they work
around; their tests are skipped. Conflicts: the GL capabilities log
keeps multiview and adds ASTC; xbox_textures.c keeps vr.c's texture work
counters; README.md stays the player's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 21:58:11 +02:00
bnunu dedb3755d4 Merge pull request #462 from OpenCommunityEdition/testing
Merge Testing
2026-10-23 14:03:01 -04:00
bnunu d86e8b01ee Merge branch 'main' into testing 2026-10-23 13:34:20 -04:00
bnunu 3a917551ca Merge pull request #460 from bnunu/halo-ce-universal/coop-client-floor-plane
Co-op clients take a teleport that leaves their feet on a floor's plane
2026-10-23 09:35:48 -04:00
Jonas VolmanandClaude Opus 5.5 d17ecdd6ad Co-op clients take a teleport that leaves their feet on a floor's plane
After 343 Guilty Spark's flood cutscene (x50) with three or more players, a
client could stay on its own screen where the cutscene had parked its
player (at player0_int_base or player1_int_base) while the host had it in
the room. That client saw everyone but itself in the first Flood fight.

A client keeps its own player where it is while the host's position for it
is outside the client's loaded BSP (a BSP switch still on its way). That
was tested at the unit's feet. A teleport can leave the feet a few
millionths below the floor's plane (z -3.1e-6 on the control room's floor
at 0), which bsp3d_test_point counts as solid, so the client never took the
host's position. In a two-player game the only client is player0, whose
spot happens to test inside.

The test now uses the unit's middle: the host's position plus the client's
own feet-to-bounding-sphere offset, against its own bounding sphere center.

Checked on Windows: a hidden c10 host, two joining copies and a system link
bot, the cutscene played and skipped. Before, both clients stayed at the
parking spot; after, every machine has all four players in the room, and
each client still waited at the cutscene's two BSP switches until it had
loaded the BSP. test_elevator_correction passes (its fake object gains
bounding_sphere_center). No network version change: nothing the machines
send or agree on changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 06:33:13 -07:00
bnunu 3c7fe4594c Merge pull request #463 from bnunu/host-shield-effects
Make the shield effects the host's choice in network games
2026-10-23 09:12:00 -04:00
bnunuandClaude Opus 5.5 63fd36775e Make the shield effects the host's choice in network games
The shield glow (display.shield_glow and its Light Show intensity) and the
overshield effect (display.overshield_effect) were each player's own, in
network games too. The glow lights what is around a shield as it flares,
so a player who had it saw enemies a player without it did not, and the
overshield effect keeps an overshielded player lit: anyone who did not
turn them on was at a disadvantage.

Now a network game's are the host's, for every machine: SHIELD EFFECTS in
Server Setup > Player Options (network.shield_effects): STOCK (the
default), GLOW, LIGHT SHOW, OVERSHIELD, GLOW+OVERSHIELD or
SHOW+OVERSHIELD. Each player's own settings apply offline only (the
campaign, split screen), and Video Setup's helps say so.

- network_shield_effects.c: the host sends its choice to each client
  reliably when it is ready (distributed message 91) and to them all when
  it changes, as the aim assist policy is sent; a client takes it only
  from the host's stream, whole and valid, and draws stock until it has it.
  shield_glow.c and overshield_effect.c draw what it says in a network
  game.
- Network version 36: a build that would draw its player's own cannot
  join. network_format.txt recorded.
- Player Options' rows 27 apart in place of 30 (ce_menus.py's
  CHILD_OFFSETS), so that the tenth fits above the help, which keeps its
  three lines, and the screen's divider and buttons stay where they were;
  test_aim_assist.py's layout check follows.

Checked: ninja android builds, the changed files compile for the web, and
CI's tests pass (936). On the Android emulator, Player Options shows the
row and steps through its values with each value's help, and Extra
Effects shows its new helps. Not played as a network game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 05:57:35 -07:00
bnunu 83ae96e2d7 Merge pull request #456 from bnunu/android-etc2-transcode
Give Android GPUs without S3TC DXT1 textures as ASTC 4x4, by moving bits (no encoder)
2026-10-23 08:08:53 -04:00
Jonas VolmanandClaude Opus 5.5 99c2030784 Give Android GPUs without S3TC DXT1 textures as ASTC 4x4, by moving bits
On Android GPUs without S3TC (Mali), DXT textures are decoded on the CPU
and uploaded as 32-bit texels, 8 times DXT1's 4 bits. A DXT1 block is two
565 colors and a 2-bit index per texel, which an ASTC 4x4 block holds
nearly as it is, so where the driver has KHR_texture_compression_astc_ldr
each DXT1 block is now rewritten as an ASTC block (dxt1_astc_block):
8 bits a texel on the GPU, with no encoder, no search and no threads.
- Four colors (c0 > c1): RGB direct endpoints of 8 bits, weights of 0,
  21, 43 and 64 sixty-fourths for DXT's thirds.
- Three colors, none transparent: weights of 0, 32 and 64 (exact).
- Three colors with transparent texels: two planes (alpha's weights of
  its own) and RGBA endpoints of 7 bits, as many as fit; a transparent
  texel takes the darker color where DXT's is black.
- The endpoints are ordered so that the decoder does not swap them and
  blend blue into the others.

DXT3 and DXT5, whose alpha does not fit beside full colors, are decoded as
before, as are volume textures, the web, and GPUs with S3TC or without
ASTC. display.dxt1_astc (Android only, true) turns it off.

On the four maps measured for the earlier encoder (928 of 1,469 DXT
textures DXT1), DXT1's GPU memory goes from 287 MiB to 72 MiB, all
textures' from 439 MiB to about 224 MiB.

Checked: a one-off check, not
kept, decoded 65,536 blocks with astcenc 5.7.0's decoder: no invalid
blocks, colors within 2 of the
game's DXT1 decode, alpha exact. ninja android builds; the CI test suite
passes. Not run on a device.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 04:43:36 -07:00
bnunu fbc71b42de Merge pull request #461 from bnunu/fix-vehicle-set-strings
Show the PC vehicle set's name and help instead of <missing string>
2026-10-23 07:16:08 -04:00
MrBruh cfb212fed9 Add SWAT, Infection, Gun Game and Hardcore as OpenCE gametypes in their own OPENCE bank of Select Gametype, with an OPENCE OPTIONS screen in the gametype editor. 2026-10-23 23:55:39 +13:00
bnunuandClaude Opus 5.5 782f4ce1bd Show the PC vehicle set's name and help instead of <missing string>
Vehicle Options' VEHICLE SET spinner indexes its strings as the sets 0
to 7, then PC (8) and CUSTOM (9) (menu_functions.c's VEHICLE_PRESET_PC
and VEHICLE_PRESET_CUSTOM), but its list had no PC: a side whose set is
CUSTOM showed "<missing string>", and one whose set is PC showed CUSTOM.

6b1d81c7 added the PC string to strings.xml by hand, and f97b3339 made
strings.xml again with tools/ce_menus.py, which wrote only what
port_settings.py says, so it went. It is now one of port_settings.py's
STRING_INSERTS, where making strings.xml again keeps it, and so is its
help: the helps are the rows' values' in turn (gametype_option_help),
so without one, PC showed CUSTOM's help, CUSTOM the first vehicle
count's, and every count the one before its own.

tools/harness/tests/test_vehicle_set_strings.py checks that the list has
the code's sets, that each value has its help, and that strings.xml has
port_settings.py's inserts. On testing, all three fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 03:10:44 -07:00
bnunu f1066974ca Merge pull request #453 from chestahh/opence-customedition-hudfixes
Fix Custom Edition custom HUD placements, textures and bitmap rendering
2026-10-22 18:55:32 -04:00
chestahh 384d9ba5d0 Fix Custom Edition HUD replacements, meters and gameplay prompts
Match stock CE resource sheets by pixel identity and translate stock meter
redraws to CE layouts. Preserve custom artwork, correct mixed-use meter
channels and warning placement, and draw aligned controller glyphs with
High-Res HUD enabled or disabled. Scope Bigass Open v1.1 ammo changes to
its known map name and checksum.

Based on OpenCE testing 4cb621cd. Keep upstream Android texture ownership,
eviction and validation unchanged; use its residency helper in the new
stock CE upscale lookup. No unrelated main-branch commits, networking
changes, regression test files or investigation documents are included.

Validation: Windows portable release build passed; 175 local CE HUD and
Android regression checks passed, 4 asset-dependent checks skipped.
Additional residency checks cover Android-managed pixels, desktop cache
entries, identity rejection and map reset. git diff --check passed.
Android/Web targets are unavailable in this Windows build graph; no new
runtime gameplay validation was performed for this rebase.

Player-Note: Fix Custom Edition HUDs, ammo counters and prompts while preserving custom artwork.
2026-10-22 23:47:28 +01:00
bnunu 4cb621cd26 Merge pull request #450 from OpenCommunityEdition/fix-scenery-shields-network
Scenery shields break, and tripmines go off, for clients as for the host
2026-10-22 18:25:20 -04:00
Stefan MaierandClaude Opus 5.5 4e9de9a11c Merge upstream/main (2026-10-22): co-op script fixes, cached high_res_hud, menu cursor
Network version stays 32. The conflict in D3DDevice_Present: the swap
stays in the window's branch (VR presents through vr.c); upstream's
visibility_read_batch, after the swap, is desktop GL only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:23:25 +02:00
GuvenandClaude Opus 5.5 4ab042e144 The stale player damage test's world has static_target_damage [skip ci]
test_stale_player_damage compiles network_damage_deals from the sources,
which now asks static_target_damage whether damage is a static target's
own; its world had static_target but not that, so the test no longer
compiled (the Linux job's failure since the tripmine commit). Its world has
no static targets, so no damage is one's own.

Checked: test_stale_player_damage and test_static_target pass (33); the
whole harness on Windows fails the same 245 environment-bound tests as
testing, no others.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:05:58 +01:00
GuvenandClaude Opus 5.5 962c3eca45 A client's hit on its own copy of a tripmine reaches the host's
A tripmine (Altis94's Bigass) is scenery a grenade's effect makes, so each
machine makes its own copy, at an index of its own: the map's scenery is at
the same index everywhere, but such an object need not be. A client's hit
on its copy named an index at which the host had no such object, and was
refused: clients could not shoot their own tripmines to set them off. And
the host's damage to its copy named an index the client had none at, so the
client's copy did not break or go off on its screen.

A machine given another's index of a static target it has no object at
now takes its own nearest static target where the other had it
(network_damage.c's static_target_near): the host, for a client's hit, its
nearest to where the client had the target, within the target's size and
STATIC_TARGET_MATCH_TOLERANCE (a world unit), and then checks the hit as
any other; a client, for the host's damage, its nearest to the damage,
within REPORT_IMPACT_TOLERANCE and an explosion's radius. The map's
scenery, at the same index everywhere, is found by its index as before.
Part of network version 34 (no message changes); NETCODE.md says so.

Checked: the Windows build; network_format.py; test_static_target with
cases of the nearest static target where another machine had one, none
out of reach, no unit or unbreakable scenery, and the nearer of two, and
two negative controls. Not yet checked in a game: a client shooting its
own tripmine, and seeing the host's go off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 13:52:25 +01:00
bnunu ce95f7deed Merge pull request #424 from eltanschauung/fix/coop-a10-softlock
Fix co-op Pillar of Autumn softlock glitch
2026-10-22 08:36:58 -04:00
bnunu a20ced034c Merge branch 'main' into fix/coop-a10-softlock 2026-10-22 08:36:44 -04:00
GuvenandClaude Opus 5.5 ef0e3be45b The host deals a client's player's tripmine going off under its vehicle
A tripmine a client's player throws (Altis94's Bigass) is scenery the
grenade's effect creates, owned by the thrower, so the explosion of its
shield breaking is theirs too. The host deals no damage of a connected
client's player's but what that client reports, and this explosion went
off where the host had it, its vehicle running over the tripmine, so no
client had a hit of it to report: the host's vehicle drove over a client's
tripmine unharmed, though a host's tripmine hurt clients.

What a static target deals itself (network_damage.c's
static_target_damage: the damage's owner object scenery or a device that
damage can break) the host now deals whoever owns it, and a client does
not report it, so that it is dealt once. Part of network version 34 with
the shields' sharing before it; NETCODE.md says so.

Checked: the Windows build; network_format.py; test_static_target with a
case of a tripmine's own damage, a destructible scenery's, a unit's or
projectile's, and none, and a negative control. Not yet checked in a game:
a client's tripmine under the host's vehicle, and whether the client sees
its own copy of that tripmine go off (a tripmine is made in the game, so
it need not be at the host's index on the client).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 13:35:58 +01:00
GuvenandClaude Opus 5.5 5744d2d174 Clients break and see broken the map's scenery shields, as the host does
A scenery or device whose shield is all damage can break (Rat Race's
energy shields, the tripmines of Altis94's Bigass) was no static target
of the netcode, which took only those with a body to destroy
(network_damage.c's static_target). So a client's hits on it were never
reported to the host and the host never sent its damage: clients could
not break the shields, and when the host broke one, clients still saw it
and stood on it while the host saw them floating.

static_target now takes those with a shield too. The host sends its
damage to them, as to destructible scenery, and takes clients' hits on
them. Clients, which otherwise run no object's shields but take the
host's state of them, run these objects' shield stun and recharge
themselves (damage.c's objects_update_shields), since the host sends no
state of the map's scenery: each machine deals the same damage and
recharges the same way, so the shield's regions go and come back on
every machine.

Network version 34: a host and a client must agree on which objects'
damage is shared. NETCODE.md says so and network_format.txt is recorded
for 34.

Checked: the Windows build; the new harness test test_static_target (a
body, a shield only on scenery and on a device, neither, a unit, no
object, and three negative controls, one the old rule); the whole harness
on Windows fails the same 245 environment-bound tests as testing does,
no others; network_format.py. Not yet checked in a game: a client
breaking Rat Race's shields and Bigass's tripmines, and seeing the host's
broken and recharged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 12:33:06 +01:00
bnunu fc22e8467d Merge pull request #442 from bnunu/cache-high-res-hud-setting
Read display.high_res_hud once per settings change, not at every bind
2026-10-22 06:56:16 -04:00
bnunu 9a1160573f Merge pull request #449 from OpenCommunityEdition/coop-ally-friendly-fire
Co-op hosts can make the players' AI allies immune to their fire
2026-10-22 06:49:23 -04:00
GuvenandClaude Opus 5.5 1d38b3f936 Co-op hosts can make the players' AI allies immune to their fire
Server Setup's Co-op Options has ALLY FRIENDLY FIRE
(network.coop_ally_friendly_fire), ON by default as in the campaign. OFF,
the players' weapons, grenades, melee and vehicles do their AI allies no
damage, so that no player can kill Captain Keyes on Truth and
Reconciliation, the Marines, or the other allies a level needs. The AI's
own hits of its allies, the enemies' damage, and FRIENDLY FIRE between
players are unchanged. A hit is the players' when its owner is a player
and the victim is a friendly unit that is no player's, so friendly-team
vehicles are immune to the players too, splatter included. An ally that
cannot die cannot raise an allegiance incident, so the players cannot turn
the Marines hostile either.

The rule is in game_engine_friendly_damage (game_engine.c), which only the
host's damage reaches: a client's hits are the host's to deal. The host
sends the choice with the game's settings, as bit 1 of network_game's
cooperative_flags beside PLAYER COLLISIONS', so network version 33: a
client of version 32 would play the game's settings without it. NETCODE.md
says so, and network_format.txt is recorded for 33.

The menu row is tools/port_settings.py's (COOP_SETUP_SCREENS); only the
server settings screen was written again. Its 8 rows fit above the help
area.

Checked: the Windows build; the harness tests, the new
test_coop_ally_friendly_fire (a player's hit of an ally ON and OFF, the
AI's own hit, players' friendly fire in every mode, no unit, the campaign
without co-op, and four negative controls) and test_coop_scaling_menu with
the eighth row; network_format.py; the Co-op Options screen drawn with the
row; and in play as the host on a10 and a50 (Keyes and the Marines
unharmed with OFF) and b30 (no splatter). Not yet: a client's hits online,
d20's ending, c20's Sentinels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 10:45:03 +01:00
John CranstonandClaude Fable 5.1 a689a8a3fa Unbind the read framebuffer before the swap, for OBS's game capture.
The renderer left the back buffer's render target bound as the read
framebuffer after the display blit. OBS's game capture copies the window
from the read framebuffer bound in the context at SwapBuffers, so it copied
that render target, which is drawn top row first, and the recording was
upside down. Recorded with OBS 32.2.2 before and after: upside down, then
upright.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-22 01:58:46 -07:00
bnunuandClaude Opus 5.5 038252b1ae Read display.high_res_hud once per settings change, not at every bind
texture_entry_result, reached from every texture bind (even the recent
table's fast path in xgpu_texture_get), called
config_boolean("display.high_res_hud"): the settings' lock and a strcmp
over every setting's name, about 2000 times a frame. It is now read again
only when config_changes() moves, as the renderer's other settings are
(per_pixel_lighting in d3d8_gl.c). config_write, the only writer once the
settings are loaded, counts each change, so a change in the menus takes
effect at the next bind as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 19:48:16 -07:00
bnunu 61621e6513 Merge pull request #435 from bnunu/halo-ce-universal/menu-cursor
The menus show a blue arrow cursor in place of the system's
2026-10-21 19:41:11 -04:00
bnunuandClaude Opus 5.5 1d79584ea4 Hide the menus' cursor while a controller is used
The menus turn the pointer on whenever they are up, so a player on a
controller had the arrow left in the middle of the screen until the menu
closed (as with the system's cursor before). Now a controller's button,
or a stick or trigger past its rest (8000, as gamepad_idle has it), hides
the cursor while a menu is up, and the mouse moved or clicked shows it
again. A menu opened while the controller was used last opens without
it: platform_ui_pointer_set_active hides it again after the release's
SDL_ShowCursor.

The warps are not taken for the mouse being used: the one to the middle
as a menu opens (SDL reports it as motion), and the ones back to where
the mouse is held without relative mode. Hidden, the cursor cannot move
the focus, so the d-pad keeps it. Desktop builds only, as the cursor.

Checked: the new functions built with clang -Wall -Wextra -Werror
against SDL 3.4.16 and run on Windows (a controller hides the cursor in
the menus, not in play; the mouse shows it again). The whole game not
built here, nor played with a controller.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 16:40:04 -07:00
bnunuandClaude Opus 5.5 577f718a63 Put the menus' cursor's click point on the arrow's tip
The picture's arrow has a soft edge, so its tip is at (2, 2), not the
picture's corner, where the click point was. MENU_CURSOR_TIP is scaled to
the cursor's size with the picture (rounded): (1, 1) up to 1080p, (2, 2)
at 1440p and 2160p. From GuvenHuso's review.

Checked: built with clang -Wall -Wextra -Werror against SDL 3.4.16 and
run on Windows from 640x480 to 3840x2160, the click point SDL kept read
back at each size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 16:32:10 -07:00
bnunuandClaude Opus 5.5 b68d259c81 Size the menus' cursor to the window, as Halo PC's was
Halo PC drew its cursor in the menus' 640 x 480, so it took the same part
of the screen at any resolution. The cursor was a fixed 20 x 29 (or SDL's
scaling of it on Windows), small at 1440p and 4K. It is now 32 of the
menus' 480 lines tall: 48 pixels at 720p, 72 at 1080p, 96 at 1440p, 144 at
2160p. The lines are the window's height, or for a window narrower than
4:3 its width's three quarters, and the cursor is never under 16 pixels.

menu_cursor_fit makes it from the embedded picture (halved while that is
twice the height or more, then scaled once) and is called at window
creation and at every event pump, where it does nothing unless the
window's size in pixels has changed: resizes and fullscreen toggles
follow. With a pixel density above 1 (macOS, Wayland at a scale) the
cursor is made at its size in points with the pixels' image as its
alternate; SDL_HINT_MOUSE_DPI_SCALE_CURSORS is no longer set, since the
size already comes from the window's pixels.

Checked: the new functions built with clang -Wall -Wextra -Werror
against SDL 3.4.16 and run on Windows at 640x480 to 3840x2160 (cursors
22x32 to 99x144, one made per size change and none when the size is the
same). The cursor SDL makes on Windows matches the picture's rows (the
upside-down arrow seen in an OBS recording was OBS's). The whole game
not built here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 16:03:37 -07:00
bnunu 600edf7bb9 Merge pull request #433 from hkmodd/fix/visibility-readback-nosync
Ask the driver nothing before the swap in the visibility readback
2026-10-21 18:55:09 -04:00
bnunu 0b3b6f0749 Merge pull request #432 from chestahh/opence-android-customedition-fix
Fix Android rendering bugs on Custom Edition maps
2026-10-21 18:31:16 -04:00
bnunu 343716689d Merge branch 'testing' into opence-android-customedition-fix
# Conflicts:
#	.github/workflows/build.yml
2026-10-21 15:23:21 -07:00
Stefan MaierandClaude Opus 5.5 35554cd243 Rename the mod to Mjolnir VR
"Ringfall" is another game's name. Every name players see is now
"Mjolnir VR": the app, the Steam Frame installer and shortcut, the logo
(tools/mjolnir_art.py, was ringfall_art.py), the release's title and
assets (MjolnirVR.apk, mjolnir-vr-android-*.zip,
mjolnir-vr-steam-frame.zip, Install-Mjolnir-VR.desktop), the docs. The
updaters look for Cesarus85/mjolnir-vr, the repository's new name
(GitHub forwards the old one).

Builds 302 to 313 keep working: every release also carries its assets
under their old names, which those builds' updaters and the links
already shared ask for. Kept as they were: the app id, the signing key's
alias, the Steam Frame's ~/Documents/RingfallMjolnirVR and the file
names inside its bundle (an installed game's Steam shortcut starts the
launcher there). The Frame installer now also refreshes the library
pictures of a shortcut that is there already, and says how to rename a
shortcut still called "Ringfall: Mjolnir VR".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:01:25 +02:00
hkmoddandClaude Opus 5.5 a0f22cbba7 Ask the driver nothing before the swap in the visibility readback
The desktop readback through the query buffer (711aa46d, aecdd895) asked
the driver for answers at every present before the swap: a GL error three
times, a fence's state once a batch, and a flush. A threaded driver
(NVIDIA's) answers only once its thread has carried out every command
queued before the question, here the whole frame just drawn, so the
render thread stopped working ahead of the driver at every frame. Build
220's readback asked nothing.

Now a batch is sent before the swap and read after it:

- visibility_send_batch, before the swap, writes VISIBILITY_UNCOPIED over
  the batch's places in the coherent mapping, sends the copies and a
  fence, and asks nothing. GL errors are asked for with debug.gl_debug
  only, as the renderer's other per-frame checks are, and the glFlush is
  gone: the swap sends the commands.
- visibility_read_batch, after the swap, reads a batch only once its
  fence has passed (glClientWaitSync with GL_SYNC_FLUSH_COMMANDS_BIT and
  no wait), so completion is still known from the fence; the batch sent
  that present is looked for from the next one. A batch the GPU is late
  with is waited for however long; none is given up on. A place that
  still holds VISIBILITY_UNCOPIED after the fence is a copy that failed,
  seen without asking for GL errors.
- A batch whose copies or fence failed is dropped, as before, and from
  then on the counts are read from each query, as with AMD's driver: a
  dropped batch is not used again, and with the buffer the readback would
  stop once all three had been.

AMD's CPU readback, the counts, the batches and the query ownership from
711aa46d are unchanged.

Checked: tools/harness/tests/test_visibility_readback.py, 5 cases and 7
negative controls. On the code before (its readback taken as the send,
before the swap), 2 cases fail: the driver asked 46 times before the swap
in 12 presents, and a count that was never copied taken for one.
A 4-machine a50 co-op on an RTX 4080 (Windows, 1440p, 4x MSAA,
debug.gl_debug), 120 seconds: 13000 batches sent and 13000 read, every
fence passed by the next present (none unavailable), no GL error, none
dropped, the buffer used throughout, 8649 nonzero counts. The harness
suite fails the same tests with and without the change (those that need
POSIX headers, on Windows). The Windows release build compiles.

Not checked: Linux, Mesa, Intel, AMD (whose path changes only in being
read after the swap), and a 32-player co-op game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 18:23:30 +02:00
bnunu 9f04a51259 Merge pull request #429 from eltanschauung/fix/coop-gravity-lift
Fix co-op gravity lift soft lock glitch
2026-10-21 05:35:42 -04:00
bnunu 1ea29f7675 Merge pull request #419 from chestahh/opence-android-render-fix
Fix Android geometry & graphics corruption by bypassing the persistent geometry cache
2026-10-21 03:42:24 -04:00
Jonas VolmanandClaude Opus 5.5 47981601da Credit pugzysixtyfour for the menus' cursor (#435)
The blue arrow cursor this pull request adds (port/assets/cursor) is
pugzysixtyfour's art. AUTHORS.md says so beside the other art credits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 23:49:00 -07:00
Jonas VolmanandClaude Opus 5.5 a4e4bd8317 The menus show a blue arrow cursor in place of the system's.
With a mouse, the menus' pointer (platform_ui_pointer_set_active) was the
system's cursor. The game window now has a cursor of its own:
port/assets/cursor/menu_cursor.png, an 80 x 116 blue arrow with a
transparent background, embedded by tools/embed_assets.py as the window
icon is and made into an SDL colour cursor when the window is created.

At 100% display scale the cursor is the picture at a quarter of its size
(20 x 29, by two halvings); the picture itself is the alternate image SDL
scales from at larger display scales (SDL_HINT_MOUSE_DPI_SCALE_CURSORS,
which SDL implements on Windows). The hot spot is the arrow's tip, the
picture's top-left corner.

Desktop builds only: the code sits beside the window icon's, so Android
and the web keep theirs. The cursor shows wherever SDL shows one over the
window, which is the menus' pointer, the scoreboard's and a mouse freed
with F12; in play the mouse stays captured and hidden as before.

Checked on Windows: the build, and one start to the main menu with a
hidden window. A check line (not in this commit) logged the cursor made
and active, 20 x 29 with the 80 x 116 alternate, and saved SDL's 20 x 29
image, which looks as intended. Not built here for Linux, where SDL uses
the 20 x 29 image at every display scale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 23:40:55 -07:00
s3anyboy d97c98c726 Recon and fishsticks
add controller layouts
2026-10-20 21:13:43 -05:00
chestahh 3975480a3b Fix Android Custom Edition texture residency and draw state
Keep synchronous cached CE pixels and hardware headers at stable addresses
outside the fixed Xbox arena, bounded to 128 MiB and 8192 entries. Only
older-frame textures can be evicted; unregister bindings, GPU copies and
channel annotations before freeing, including map/bitmap teardown and flush.
Preserve the original readers, conversion, normal GPU caching and HUD
replacement behavior. Refresh Android GL shadow state before each draw.

Keep the player-tested Android streaming geometry bypass and its regression
tests, since current upstream main does not contain that fix yet. Desktop
and web preserve their cache, upload and draw-state behavior. No fullscreen,
simulation, networking or map-format changes are included.

The engine-owned bitmap getter and teardown require small source hooks,
each marked port:. The renderer admits heap pixels only through a registered
header and validates the resource extent against its allocation. Diagnostics
are limited to unload summaries and the first allocation warning per lifetime.

Device evidence: the tester reports correct rendering and stable FPS, with
zero allocation failures, fallback substitutions or evictions across four CE
visits on Mali-G615 and 87.5 MiB peak residency. Local tests cover stable
uploads, bounds, OOM, frame locks, eviction, cleanup, HUD residency, reduced
diagnostics, normal write tracking, Android-only state refresh and geometry
streaming. All 30 upstream-focused checks passed before final CI validation.
Full Android/web builds require CI because local targets are unavailable.
No fork packaging, branding, map adapters or unrelated changes are included.

Player-Note: Fix Android Custom Edition texture corruption and repeated cache upload hitches.
Player-Note: Prevent stretched or missing geometry on Android by streaming geometry directly.

(cherry picked from commit 4ac4685b96c39b20491095579ce469f8afcd2d3c)
2026-10-21 01:27:08 +01:00
Stefan MaierandClaude Opus 5.5 d4687b24e2 Merge feature/bots-ladders: OpenCE network version 32 (HDR off in VR, Ringfall's host bots kept), bots climb ladders and take partial paths
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 22:05:49 +02:00
Stefan MaierandClaude Opus 5.5 9d65e7e63f Roadmap: network version 32, bots' ladders and paths
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 22:05:48 +02:00
Stefan MaierandClaude Opus 5.5 f3ce8c3bf0 Bots: paths to as near the goal as the search reaches; the goal's floor looked for deeper; ladders for goals above
On Damnation two of three of the bots' path searches failed (1078 against
527 in 80 s), and the goal's floor was not found 357 times: a search had
to reach the goal's surface itself, and the floor under the goal was
looked for 3 world units down. As OpenCE's bots do (bots.c), a search now
goes to as near the goal as it reaches (the bot searches again from
there), and the floor is looked for 6 units down. A path that ends short
of a goal above the bot takes a ladder near it (now within 8 units).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 21:44:14 +02:00
Stefan MaierandClaude Opus 5.5 bce5c2b560 Bots climb ladders (Damnation's), which the game's pathfinding does not take
A bot whose way is not found twice running, or that is stuck, with the
foot of a ladder going up within 5 world units, walks to the foot, turns
to the ladder and walks into it (as a biped climbs a climbable surface,
bipeds.c), and on past its top a moment; then it finds its way anew. Ten
seconds without getting up, it gives that up for fifteen. The ladders are
the level's climbable upright surfaces, near ones facing alike merged,
found the first time one is wanted in a game. Reported: bots stuck in a
gap of Damnation's that only a ladder leads out of.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 21:29:10 +02:00
eltanschauung 9044950d78 Remove unused co-op script test counter (#429)
The CI compiler treats -Wunused-but-set-global as an error. list_count was incremented by the fixture but never read, so remove it instead of weakening compiler warnings. All 12 targeted regressions, including their negative controls, pass. This changes test code only.
2026-10-20 14:23:46 -04:00
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
Stefan MaierandClaude Opus 5.5 8097ac23b0 Merge OpenCE upstream: network version 32, HDR, model detail, texture filtering, co-op options, aim assist policy, its local bots, 5.1 sound
Ringfall's side of it:
- The bots: OpenCE's own (bots.c, a local split screen game's) are not
  used in VR builds (their lobby hook would take Ringfall's host bots
  off, and their bots.count and bots.skill differ); Ringfall's are now
  host_bots.c, so that OpenCE's bots.c merges as it changes. Each
  platform's bots.count and bots.skill are its own (config_setting_index
  takes this platform's of two of a name).
- HDR is never on in VR (d3d8_gl.c); the flat window's present is
  OpenCE's, inside the VR port's branch for it.
- The VR frame begins where the events are pumped (sdl_platform.c), as
  before, now that exit_after_check is apart.
- The message numbers: OpenCE's aim assist policy (90) before the VR
  port's poses (240), which stay last.
- The visibility counters keep the VR port's snapshot ring (both eyes'
  counts halved); OpenCE's tone mapping goes before the antialiasing, off
  the VR eyes' pass.
- Join codes, the online list and OpenCE's probes all in the signalling
  topics; the HUD's PNG helpers both ways; the disc import keeps the
  language file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:10:30 +02:00
bnunu 9662f0f6cb Merge pull request #426 from OpenCommunityEdition/readme-rewrite 2026-10-20 08:44:33 -04:00
bnunu 951b8387dd Merge pull request #425 from OpenCommunityEdition/readme-rewrite 2026-10-20 08:34:08 -04:00
Guven a84f8c87f9 Rewrite the README to name OpenCE and include latest features
Previously the README did not name OpenCE, list what the port adds, or say what players need before they start. It now leads with that and moves the developer material to the end. 

- An introduction naming OpenCE (Open Community Edition), with the icon, Download / Play in browser / Discord buttons and an updated screenshot of the latest version.
- The disc image comes first, then the downloads, with a Guide column linking each platform's README in place of the Platforms section.
- Multiplayer, Custom Edition maps (with Halo PC's own maps), mods, and a table of the graphics options and their defaults.
- Testing, optimisation profiles and profiling builds under "For developers".
- Credits naming the upstream projects and main contributors.
- A disclaimer that OpenCE is an unofficial fan project, as on the web site.
2026-10-20 12:57:20 +01:00
eltanschauung cdce33647a Fix co-op Pillar of Autumn softlock glitch
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)
2026-10-20 07:25:20 -04:00
chestahh 34c17f11fc Use streamed geometry on Android to prevent cache corruption
Affected Android players report that the cache-bypass APK resolves stretched
and missing geometry, with some also reporting better performance. Decline
persistent vertex/index mirror requests on Android so draws use the existing
streaming fallback and current CPU index extents. No settings are needed.
The exact cache failure mechanism remains unconfirmed.

Keep streaming uploads, frame fences, shaders, texture caching and CPU write
tracking unchanged. Desktop and web retain their existing cache paths.
Xbox and Custom Edition loaders are unchanged. No simulation or network
protocol change is involved.

Carry the focused fix onto OpenCE main without fork history. Resolve the CI
entry against upstream's existing test list and omit fork-only documentation.
Add real-source Android, GLES, desktop and web regression controls, including
large/reused ranges, counter rollover and negative controls. All 33 focused
checks passed locally; the resolved files match the tested adaptation.
Local configure cannot fetch SDL3 and has no Android/web targets; CI must
perform full builds. Device evidence is player feedback on the bypass APK,
not a new on-device or cross-platform multiplayer run of this revision.

Player-Note: Prevent stretched or missing geometry on Android by streaming geometry directly.

(cherry picked from commit ea85578bf80c8d937b8c66704aff6cf6aa96e68b)
2026-10-19 20:21:06 +01:00
Stefan MaierandClaude Opus 5.5 c8dfd3773f Merge merge/upstream-agpl: OpenCE upstream under the AGPL 3.0 or later, Ringfall with it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 18:29:39 +02:00
MrBruh 6ec42856d7 Merge pull request #417 from OpenCommunityEdition/testing
Merge Testing
2026-10-20 03:29:18 +13:00
MrBruh 399ead3f46 Keep SETTINGS in network co-op's pause menu and add FRIENDLY FIRE after it (#319)
#319 put FRIENDLY FIRE in place of SETTINGS, written when co-op's pause
menu had no SETTINGS; testing now gives network co-op SETTINGS through the
campaign's pause menu, so as merged co-op lost it. pause_list_patch now
adds SETTINGS always, then END GAME for a multiplayer host or FRIENDLY FIRE
in network co-op (still two buttons, the four tags reserved), and
pause_settings_patch runs in co-op again. Six rows at the PC pitch need a
box 66 taller than the stock 159, which had no frame (only 195, for five),
so tools/port_settings.py draws the pause and objectives boxes at 225 too
(the same redraw), and pause_campaign_grow_box picks the frame by height.
Single-player and split-screen pause menus are unchanged.

Checked: menu_tags.c and menu_functions.c with -fsyntax-only for Linux,
the Android guest and the web; tools/harness and test_touch_menu.py pass.
On Xvfb, d40 with co-op forced in a local build showed the six rows and
the 225 box with the prompts under them, FRIENDLY FIRE switching
network.coop_own_friendly_fire; a10 still logs five rows (36 more, 195)
and bloodgulch SETTINGS. No network game was run.
2026-10-20 03:12:34 +13:00
MrBruh d49973e426 Draw each permutation's most detailed level, and a model's inside only from within, at native and highest model detail (#344)
"highest" chose level 4 for every model, and render_model_parts draws
nothing for a permutation without geometry there, so those parts
disappeared; and a10's cryotube's top level (a cutoff of 3000) is its
inside only, which "highest", and "native" at 4K, showed from outside.
With display.model_detail other than "stock", each permutation now draws
its most detailed level at or below the model's that has geometry, and a
level with a cutoff over 1000 only while the camera is inside the model's
bounding sphere (port/linux/game/model_detail.c), from eltanschauung's
#282. "stock" draws the model's level as before; so does
rasterizer_debug_model_lod.

Checked: python -m pytest -q tools/harness (#282's lod-* cases in the
transparent enclosure test and its interior, bounds and permutation cases
in test_model_detail.py, with negative controls); models.c, d3d8_gl.c and
model_detail.c compile (-fsyntax-only) with the Linux, Android guest and
web commands.
2026-10-20 02:30:11 +13:00
MrBruh 575f69d2a3 Leave a hit on an overshield its own effect when that effect must be deterministic (#320)
Such an effect draws from the game's random seed, which every machine must use alike; played in place of the projectile's own on this machine only, it would change how the seed is used there (on a host, for everyone). overshield_effect_hit_effect_allowed refuses it, with effects.c's flag bit named where its enum cannot be reached. Checked: overshield_effect.c compiles without warnings.
2026-10-20 02:17:01 +13:00
MrBruh 79ff0cb1ee Keep the passengers' third-person view to the campaign and co-op (#368)
Seeing around the vehicle from behind it is an advantage the other players of a multiplayer game do not have (raised in the PR's review), so reference_seat refuses while a game engine runs. The README row and the menu help say so. Checked: passenger_camera.c compiles without warnings; the FOV and Viewmodels screen written again by port_settings.settings_files() is the one committed.
2026-10-20 02:16:16 +13:00
MrBruh 2f625ae360 Filter textures as the Xbox does unless anisotropic filtering is chosen (#343)
display.anisotropic_filtering defaulted to 4, which changes the stock look on every platform, phones included; the rules want a departure from stock to be chosen, and the author offered 0. The setting's comment, its README row and the TEXTURE FILTERING help (no longer "4X costs very little") say so. Checked: the Graphics screen written again by port_settings.settings_files() is the one committed.
2026-10-20 02:15:30 +13:00
MrBruh c97a2d124c Play stereo unless 5.1 is asked for, and copy the 5.1 mix's channel count into the sound tests (#341)
With audio.channels "auto" by default, a desktop whose device reports six or more channels (an HDMI sink, a virtual 7.1 headset) changed its mix unasked; the rules want a departure from stock to be chosen, so the default is "stereo", and an unknown value plays stereo too. audio_start set audio_device_name twice. tools/harness/audio.py copies MAXIMUM_OUTPUT_CHANNELS, which the 5.1 mix's structures now use, so the sound stream tests compile again. Checked: dsound_sdl.c compiles without warnings; the harness's sound tests pass.
2026-10-20 02:14:47 +13:00
MrBruh 218f629535 Give the elevator correction test the latency meter's call its function now makes (#371)
distributed_correct_own_unit now times the host's echo of a prediction for the latency meter (distributed_note_own_round_trip), which tools/harness/tests/elevator_correction.c, taking the function from the source, did not declare, so it did not compile. Checked: test_elevator_correction.py passes (14).
2026-10-20 02:12:53 +13: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 c593ee4478 Give the co-op options menu's test the text chat row its function now holds at OFF (#322)
On testing, gametype_option_help also holds TEXT CHAT at OFF while the game is listed (TEXT_CHAT_SPINNER, TEXT_CHAT_OFF, p2p_hosting_listed), which the PR's harness test, written before it, did not declare, so coop_scaling_menu.c did not compile. Checked: python -m pytest -q tools/harness -k 'pickup or vehicle or coop or scaling or menu' passes (58).
2026-10-20 01:58:33 +13:00
MrBruh 9f89f9253d Merge pull request #322 from eltanschauung/feature/coop-extra-pickups-and-vehicles
Add extra pickups and vehicles options for co-op hosts
2026-10-20 01:57:38 +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 733cd78a1e Merge pull request #341 from frankyfife/audio-aniso
Add 5.1 surround sound
2026-10-20 01:54:44 +13:00
MrBruh 11abcf65f3 Merge pull request #280 from hkmodd/feature/hdr
Add opt-in HDR rendering with tone mapping (display.hdr)
2026-10-20 01:53:52 +13:00
MrBruh d265c2ad41 Merge pull request #344 from OpenH1/model-detail
Models' level of detail can be chosen by the display's resolution
2026-10-20 01:53:12 +13:00
MrBruh 59b1ea7548 Merge pull request #343 from OpenH1/anisotropic-filtering
Anisotropic filtering, 4x by default
2026-10-20 01:52:40 +13:00
MrBruh b83e8b5a5d Merge pull request #320 from jcompto95-tech/overshield-effect
Animated overshield
2026-10-20 01:52:23 +13:00
MrBruh 9f024543b7 Merge pull request #336 from jcompto95-tech/extra-effects-menu
Video Setup: an Extra Effects Sub menu
2026-10-20 01:51:36 +13:00
MrBruh 9f4c675c32 Put the match clock's row on Graphics through the generator, and document display.match_timer (#364)
The PR's Graphics screen was edited by hand: tools/port_settings.py did not name display.match_timer among the Graphics rows, so the generator put MATCH TIMER on Video Setup's own screen, and test_web_build's check that the menus match their generator failed. The setting gets its row in the settings table of port/linux/README.md. Checked: the screens written again by port_settings.settings_files() are the ones committed.
2026-10-20 01:51:15 +13:00
MrBruh 801d51c9ae Merge pull request #364 from medic-code/feat/match-clock-opt-in
Opt-in match clock on the multiplayer HUD
2026-10-20 01:49:42 +13:00
MrBruh ede2a251fb Merge pull request #368 from biu788/feat/passenger-third-person
Add optional third-person view for Warthog and Scorpion passengers
2026-10-20 01:49:41 +13:00
MrBruh 54c52c84e3 Merge pull request #327 from hkmodd/feature/menu-language-text
Show the PC menus' text in the language of the Custom Edition's loc.map
2026-10-20 01:49:41 +13:00
MrBruh 1dedc8b33f Merge pull request #402 from neopetal/WebUI-sticky-menu
WebUI: Make nav menu sticky at desktop width
2026-10-20 01:49:41 +13:00
MrBruh 837443532c Merge pull request #324 from steverice/upstream-pr-dot-zw
Give a z-sprite's fragments the depth its DOT_ZW stage computes
2026-10-20 01:49:41 +13:00
MrBruh 0a5a553881 Merge pull request #409 from johncranston303/fix/f11-settings-mode
Preserve the F11 window mode when saving settings
2026-10-20 01:49:41 +13:00
MrBruh 789d0b2915 Merge pull request #416 from OpenCommunityEdition/testing
Merge in testing
2026-10-20 01:04:38 +13:00
MrBruh 40d9017e68 Give StageKit's runs the header of an Xbox ui.map, which the game's check of the maps folder asks for (#266)
Since the check of the maps folder (e075fffa2), a game whose maps folder has no ui.map says so and quits, so every StageKit run stopped before its map loaded and CI's Linux test step failed (16 tests). The runner now writes the eight bytes the check reads ('head' and the Xbox's version 5); nothing in a run reads more of ui.map, as it had none before. Checked: python -m pytest -q tools/stagekit/tests with the --portable --release build passes (20), and fails the same 16 without this change.
2026-10-20 00:47:59 +13:00
MrBruh bcecef568c Warn of a software OpenGL renderer only on desktop Linux, where the Android guest's SDL is not called (#275)
SDL_GetCurrentVideoDriver is not among the SDL functions the Android guest bridges, so the llvmpipe warning stopped the guest image from linking. The warning points at the Linux README's NVIDIA on Wayland, so it now has the same guard as the rest of #275. Checked: ninja android, linux and web build.
2026-10-20 00:36:15 +13:00
MrBruh bfd949a34a The loading screen draws over black without the disc's picture, as the game does, and has no download mode (#334)
progress_bar_download_display, progress_bar_download_end and
progress_bar_downloading were for a download screen that is not merged; they
are removed and progress_bar_eachframe is as it was. Without loading.tga the
texture is no longer filled with clouds the port made: it is left as
created, black, as the game leaves it, so that no invented picture ships.
The disc image importers' comments say so.

Checked: node --test "port/web/tests/*.test.js", pytest tools/harness and
tools/test_web_build.py; the changed C files compile (-fsyntax-only) with
the Linux, Android guest and web commands; XisoExtractor.java with javac.
2026-10-20 00:29:34 +13:00
FractumSeraph 84e7b5add2 Halo's loading screen draws, with the Xbox's own picture or clouds
The loading screen (interface/progress_bar.c: a light sweeping from left to
right over a picture, with a glowing trail) drew nothing: it uses the
fixed-function pipeline (SetVertexShader with 0), which d3d8_gl.c did not
have, and its picture, loading.tga, is on no retail disc.

- d3d8_gl.c draws fixed-function immediate-mode draws with a vertex shader
  of its own (nv2a_fixed_function_glsl), positions transformed by the
  world, view and projection matrices; and keeps a front buffer (a 640x480
  render target taking the middle of the frame at BlockUntilVerticalBlank)
  for GetBackBuffer with -1, the frame before, whose blur makes the light's
  trail. The screen is drawn in the middle 640 columns, as the menus are on
  a wide screen.
- After it, the rasterizer's caches of the device's states and of its
  vertex shader are made the device's again (rasterizer_forget_hardware_state,
  rasterizer_vertex_shader_runtime_forget): it sets them itself; and
  SelectVertexShader (a packed program) always leaves the fixed-function
  pipeline.
- On OpenGL ES (Android, the web) the fixed-function shader turns
  Direct3D's clip space as the others do (HALO_GLES); without OpenGL
  (debug.null_renderer) it is not compiled.
- The picture is the Xbox's own: the retail default.xbe holds it (320 by
  240, runs of 4-bit greys after its width and height), and importing a disc
  image now writes it into the maps folder as loading.tga: port/linux/src/
  xiso.c, the Android app's XisoExtractor.java and the web page's
  xiso-worker.js. Without one (a maps folder copied by hand) the light
  sweeps over clouds the game makes. No game data is in the repository.
- progress_bar.c also has a mode a download screen over the menus can use
  (a share given, from 0, drawn over the menus, and let go of).
- A picture that cannot be written whole is not left cut short (the
  loading screen would read past it).

Checked: port/web/tests/xiso.test.js takes the picture out of made-up
executables (and an image's default.xbe); the Java decoder gives the same
bytes as the JavaScript one; xiso.c's picture from a Rev 2 disc image
matched a separate decoder's byte for byte; the Linux CI steps and the
web build and its tests in Docker; the Windows release build.

[The disc image parts (xiso.c, xiso-worker.js, XisoExtractor.java and
xiso.test.js) redone on the rewritten disc-image readers.]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 010110026e719c3e8c38d36b93d57cf67b616230)
2026-10-20 00:29:34 +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 cdc3bab723 Reset the drift figures with each game, and keep a client's tick from overflowing them
The host's drift figures for net_compare.py (distributed_divergence in
network_distributed.c) were zeroed only as each second's line read them:
the last taken prediction's ticks and each player's latest input tick
stayed from the game before, so a game's first ticks could be compared
against the last game's, and before any game they were 0 rather than none.
network_distributed_new_game now clears them, the ticks to NONE.

The input's tick and the prediction's time are the client's word: a hostile
one near LONG_MIN made "newest - TICKS_PER_SECOND" and "time - 1" overflow
(undefined behaviour). Both are now guarded.

The harness's elevator prediction test (tools/harness) did not compile
since the drift figures came in (distributed_take_prediction notes them):
it now takes distributed_divergence's definition and
DIVERGENCE_APART_DISTANCE from network_distributed.c, as it takes the
functions, so it runs the real code.

Nothing the machines send or must agree on changed: the figures are the
host's log's only, so HALO_PORT_NETWORK_VERSION stays.

Checked: python -m pytest -q tools/harness, 293 passed (5 failed before);
ninja linux and ninja linux_x86_64 build; net_compare.py with both builds
(the next commit's message).

(cherry picked from commit dcfed92a3a7687755b50bcc075669addb9704c3e)
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
Paul Hontz 8e9ee175a9 Log how far apart the host and a client move the client's player in a tick
The network test's per-second line (debug.network_test) now ends with
"| drift <ticks> <apart> <mean> <largest>": on the host, the ticks in which
it moved a client's player one tick on from where it took the client's
last prediction, with the client's input of that tick, as the client did,
and how far apart the two moves were. Builds that simulate alike move the
player alike to the last bit, so this is what shows a build that simulates
otherwise; the positions the machines log agree either way, as the netcode
corrects every copy toward the host's. tools/net_compare.py (next commit)
judges two builds by it.

Only counters and the test's log line change: nothing sent between
machines, nothing a game does with them, so the network version stays.
Checked with i686 builds against themselves (0 to 13% of such ticks apart,
up to 42% on a loaded computer) and against one whose players run 0.5% or
2% faster (77 to 93%).

(cherry picked from commit 95b2ddf86d4abc279c4ed462789e0ff7a75a7881)
2026-10-20 00:21:59 +13:00
Paul Hontz 03e28a245e Stop a game without a window after debug.exit_after, and on SIGTERM or SIGINT
platform_pump_events returned at once when there was no window, and it is
where debug.exit_after (HALO_EXIT_AFTER) was checked and where SDL's quit
events, which SDL makes of SIGINT and SIGTERM, were read. So a game with
debug.null_renderer (the network tests, net_compare.py), or without a
display, never stopped by itself after debug.exit_after, ignored SIGTERM
and Ctrl-C, and never answered the updater's question with
debug.update_answer.

Without a window (debug.null_renderer, or after the window was asked for
and could not be made) it now checks debug.exit_after, reads the events for
a quit request and runs updater_poll when debug.update_answer answers it,
on the thread that first pumps (the game's, as each frame ends); the rest
of the events, of no window, are dropped. The time for debug.exit_after
starts there at the first frame. With a window nothing changes.

Checked with the null renderer (SDL_VIDEO_DRIVER=dummy, no DISPLAY) on
bloodgulch, ninja linux and ninja linux_x86_64: with HALO_EXIT_AFTER=8 both
builds exit with status 0 after 8.2 s ("exiting after debug.exit_after"),
where the builds before ran until killed at 25 s; SIGTERM at 10 s ends both
with status 0 in 0.1 s ("asked to quit (no window)"), where the build
before ran on.

(cherry picked from commit a5531f3e5774b29ce27602bad7c7c2468c2fc074)
2026-10-20 00:21:59 +13:00
MrBruh 6875132aec Add the display.structure_culling setting for the structure culling tests (#404)
The culling tests could be changed only with the console's
structure_culling, which did not last past the game's end. The setting
(HALO_STRUCTURE_CULLING) gives the starting bits, 31 (every test) by
default as before, read in render_initialize; the console command still
changes them while the game runs. Documented in the settings table.

Checked: tools/harness (640 passed, the structure culling tests among
them), test_linux_port's config section and menu settings tests, and the
changed files compiled with -fsyntax-only for Linux, the Android guest
and the web.
2026-10-20 00:21:19 +13:00
MrBruh aecdd895d5 Read visibility tests on the CPU only with AMD's own driver, and keep running without the queries (#403)
The change sent Mesa (radeonsi, Intel, the Steam Deck) and Intel's Windows
driver to read every query with glGetQueryObjectuiv, which Mesa's GL thread
makes wait for everything before it: that is why the counts were copied
into a buffer. Only AMD's proprietary driver (GL_VENDOR "ATI Technologies
Inc.") now reads them on the CPU; every other driver keeps the fenced
buffer. When the queries cannot be made, the game no longer exits: it logs
it and every visibility test reports fully visible. The new comments and
logs are written in the file's style, with the Mesa note restored.

Checked: d3d8_gl.c compiles with -fsyntax-only with the Linux, Android
guest and web commands from build.ninja (with -w removed), with no
diagnostics beyond those of the file before the change.
2026-10-20 00:19:18 +13:00
MrBruh 310eed25f1 Fix the Windows cross build's typos, docs and shim link (#363)
configure.py goes back to mode 100644, which the pull request changed by
accident. The help, messages and README lose their typos, AGENTS.md and
README.md mention --windows-cross beside `ninja windows`, and the Windows
README says that the SDK needs versioned Include and Lib folders, which a
plain xwin splat lacks. The StdDef.h shim is linked again when a dangling
link is left behind, where os.symlink raised before.

Checked with python configure.py --help and a configure run, and
py_compile of the changed scripts.
2026-10-20 00:17:51 +13:00
Stefan MaierandClaude Opus 5.5 856ca06a5d Merge OpenCE upstream under its new licence, the AGPL 3.0 or later
OpenCE moved from CC0 1.0 to AGPL-3.0-or-later (4b09168e7), and Ringfall:
Mjolnir VR moves with it: README's Legal section and THIRD_PARTY_NOTICES
say so (releases before this one stay CC0 1.0, LICENSES/CC0-1.0.txt).

Its disc image readers were written again from a description of the
format, extract-xiso's code and notice gone. The app's (XisoExtractor.java)
is OpenCE's new one with Ringfall's language folders (maps_es, maps_it)
added again; the Steam Frame's (steam_frame_xiso.py) is written anew the
same way, the language folders kept; the bundle and the release job no
longer carry extract-xiso's licence. Also in: bnunu/halo-1's latest
matches, the web build's WebRTC and loading fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:17:41 +02: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 16bcdc4b0e Merge pull request #362 from OpenH1/upstream-early-crash-report
Report a crash on Linux from the start, also with the null renderer
2026-10-20 00:11:49 +13:00
MrBruh 5b258a7c20 Merge pull request #275 from OpenH1/nvidia-wayland-x11
NVIDIA on Wayland: X11 first when there is no 32-bit egl-wayland
2026-10-20 00:11:10 +13:00
MrBruh ba45f95bf8 Merge pull request #361 from OpenH1/upstream-looping-sound-orphan
Stop a looping sound's track when its looping sound is gone
2026-10-20 00:07:48 +13:00
MrBruh cd7262c516 Merge pull request #392 from pastudan/pr/scope-zoom-passes
Scope zoom effect: a few passes at any resolution, not one per screen scale
2026-10-20 00:07:31 +13:00
MrBruh 56b06c1b94 Merge pull request #403 from chestahh/opence-renderer-fix
Fix broken rendering behaviour on AMD - missing and flickering lens flares/coronas/lights
2026-10-20 00:07:31 +13:00
MrBruh 19b1df8f59 Merge pull request #404 from HiIAmMoot/bsp-culling
Cull the structure triangles the camera can't see
2026-10-20 00:07:31 +13:00
MrBruh 15495116c5 Merge pull request #363 from mcilloni/add-cross-msvc
Add support for cross building for Windows with Clang+MSVC on Linux
2026-10-20 00:07:31 +13:00
MrBruh 5fd319d00b Merge pull request #376 from hkmodd/feature/texture-idle-time
Drop idle textures after half a minute of time, not 1800 frames
2026-10-20 00:07:31 +13:00
MrBruh ff76b33f0b Merge pull request #337 from innerhat-dev/nav-marker-before-draw
Classify a nav marker before drawing it
2026-10-20 00:07:31 +13:00
MrBruh 69ec31a49d Merge pull request #328 from GiovanH/fix/split-screen-start-now
Port: Allow split-screen play in PC menus
2026-10-20 00:07:31 +13:00
MrBruh b97c4ad155 Merge pull request #375 from hkmodd/feature/biped-discard-client
Leave the host's bipeds fallen out of the map to the host on a client
2026-10-20 00:07:31 +13:00
MrBruh 9057384308 Merge pull request #394 from eltanschauung/fix/coop-monitor-scaling
Fix co-op enemy scaling behavior for The Monitor/Guilty Spark
2026-10-20 00:07:30 +13:00
MrBruh 86a1173f14 Merge pull request #366 from BirchWoodGod/fix/unicode-pipe-terminator
Keep a unicode string that ends in '|' from being read past its end
2026-10-20 00:07:30 +13:00
MrBruh 54a9c8f0e6 Merge pull request #385 from arimwilson/bumper-jumper
Add the Master Chief Collection's Bumper Jumper button layout
2026-10-20 00:07:30 +13:00
MrBruh e00bbc610b Bring two harness tests up to the picked changes
map_share_restart.c stands in for what download_finish calls, which now
names the downloaded map in custom_maps' list (downloaded_maps_mark);
test_server_teardown.py's negative control finds the grace period's guard
as it now reads, with HALO_WEB.
2026-10-19 23:15:24 +13:00
MrBruh f1c2861da5 Make log_address's salt without pthread_once
The Android guest's C library cannot link pthread_once (its cleanup
handlers are not there), so the picked log_address.c stopped the Android
build from linking. The salt is made once by the first thread to log an
address, the others waiting for it.
2026-10-19 23:11:46 +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
MrBruh 5aafe7af9e Mark the second batch's changes to the game's sources as the port's
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.
2026-10-19 23:06:19 +13:00
xshxdex98 04a63d9094 The main menu's scene keeps running behind every menu
A tick waits for an input update with an action for each player, and the
menus change who the local players are (signing profiles in and out, a
lobby's network game owning the queues). In a lobby, or once a client's
clock waited for a game, the main menu's scene stopped ticking; and a
screen flagged to pause game time (Mods, the map and gametype screens of
setting up a game) paused the clock that runs the scene, which then
started again from its beginning as the screen closed.

The main menu's scene now always ticks on this machine's clock
(game_time_menu_scene), its players idle where the queues have no input
for them, and at the main menu those screens no longer pause it. The
distributed netcode's per-tick work is not done for the scene: a lobby's
machines do not share it.

(cherry picked from commits 8444785804f8cfb413020da5a03fc5dd65f918e9, 6267d199117f87e56a3f41fc8932966a39a3feb2
and 3a4097bb48f34245b750028a5c0ae28e03bae037, made again on OpenCE's game_time_update,
which has no separate clock connection for the scene)
2026-10-19 23:02:57 +13:00
MrBruh 34002780ce Draw a HUD message's high-res button before looking for the map's picture
The high-res HUD draws a message's controller button by its place among
the HUD globals' button icons, but only once the map's message icons
bitmap had a picture at the button's sequence. Halo PC's message icons
have 13 sequences to the Xbox's 24, so on a Custom Edition map a prompt's
button past them was not drawn at all, and the rest only because the map
had some other picture there ("Hold [rocket launcher] to swap" on
coldsnap, with the high-res HUD off). The high-res button is now tried
first. With display.high_res_hud off, a Custom Edition map still draws
its own pictures.

Found in commit 4c3e74cf29341ff344aca09febc0fab7e1d19919, which instead
reads the Xbox's button pictures out of ui.map for every Custom Edition
map; that is not taken.
2026-10-19 23:00:31 +13:00
xshxdex98 44da7f8265 Web: no second's wait leaving a hosted game
network_game_server_dispose keeps the Xbox's one-second grace period
(SleepEx) only for builds other than Windows, Android and Linux, whose
transports need none; the web build is no __linux__ to Emscripten, so it
still waited a second each time a hosted game ended. It is left out there
too.

(cherry picked from the server part of commit bea41d710988e9aeac6cec44f4fd69b5203906e2,
whose dispose change OpenCE already had for the native builds)
2026-10-19 22:59:22 +13:00
xshxdex98 81faf309bb Give a version-ineligible packet field no size
_data_packet_verify read an uninitialised field_size for a first field
outside the packet's version, and gave later ones the size of the field
before them. No field of the game's packets has a version limit, so no
packet changes; the size starts at 0.

(from commit c01c68537bcbda79f0e4def4211cad611dfdee74's data_packets.c
change; the rest of that commit, comments rewritten, is not taken)
2026-10-19 22:58:39 +13:00
Milenko e075fffa28 Maps folder: Halo PC maps there are explained, not a blue screen
A maps folder holding Halo PC's maps (one pointed at a Halo PC install's,
say) had its ui.map, a Custom Edition or retail Halo PC cache file, loaded
as the Xbox's: game_load() failed, and the player got a blue screen.
Before the window opens the desktop builds now read ui.map's header: when
it is Halo PC's, or there is no ui.map, the game says where the Xbox maps
go (the data root's maps folder, named in full) and that Custom Edition
maps go in custom_maps beside it, and quits. Halo PC maps in maps/ itself,
which play only from custom_maps, are named in a warning. Runs with no
window log it instead. The README's game data section says so.

(cherry picked from commit 223c4aaf16b2819c57cd31d470d5dd04c9daa4f7, made again for
OpenCE's custom_maps folder in place of that fork's maps/ce and md_maps,
and not on the web build)
2026-10-19 22:57:53 +13:00
Milenko 76984235b7 CE maps: a limp body whose graph has more nodes than its model relaxes
A Halo PC map's animation graph may have more nodes than its model
(beavercreek_rev_beta's cyborg has a graph of 20 on a model of 19), which
Halo PC's engine posed past the biped's node matrices. Such a dead body
was left unrelaxed here. It is now posed in a copy of the biped's node
matrices with the graph's every node, the extra ones where their parents
are, and the model's taken back.

Reported by Sabriel.

(cherry picked from the limp body part of commit 5fdbccf6cdf4f5be4bcaf1b90f4fe712877ddb4b;
OpenCE's checker already let such a graph be, and its own check of the
nodes is kept for the counts and links)
2026-10-19 22:56:04 +13:00
MrBruh 62058ba925 Drop a setting of the fork's that came in with the profile commit
The new profiles pick took its side of a conflict in port_config.c whole,
and with it game.move_old_map_folders, which moves that fork's Halo PC map
folders: OpenCE has no such folders, and nothing read the setting.
2026-10-19 22:53:31 +13:00
Milenko 87643c392d Scripts: downloaded maps' allow-list
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)
2026-10-19 22:53:18 +13:00
MrBruh 397e199690 Build Mbed TLS with its own locks on Linux
Mbed TLS is built without locks of its own, and on Linux two threads use
it: the self-updater's (posix_update.c, its update check at start-up) and
internet play's (posix_dtls.c, its certificate and its handshakes with
browsers). Two threads setting up its PSA crypto or drawing on its random
generator at once could free a hash context under the other and crash.
The Linux build now defines MBEDTLS_THREADING_C and
MBEDTLS_THREADING_PTHREAD for Mbed TLS and its users, which makes its PSA
core and random generator take their own locks (Mbed TLS 3.6); its config
is left unchanged. The other builds use it on one thread.

Ported from commit 04fa0517b32cde1c52ead7a2f21d249d0c5459e2, which made
the threads take turns with one lock held over each whole request; here
the updater's download cannot hold internet play's thread up.
2026-10-19 22:49:56 +13:00
xshxdex98 74649ec993 Server browser: pings coloured by how good they are
The Join Game server browser shows each ping coloured: green under 80 ms,
yellow under 160, red above. A text colour hook of the menus'
(pc_menu_text_color) gives the ping its colour; the rest keep their own.

(cherry picked from commit 335c6522e9fb7aa33762e3f2cf27c518ac0e48ce, without that fork's
Online Games screen, the thresholds and colours kept in menu_functions.c)
2026-10-19 22:48:05 +13:00
xshxdex98 2759fc6db7 Server browser: each host's ping, timed over the tunnel to it
The PING column showed nothing, as a host was reached only on joining. The
browser now probes each listed host as a join would reach it: the tunnel
made with the listing's invite (a signalling request of its own token,
answered on its own topic), the shortest of three pings kept, and the
tunnel closed again. Hosts on any version answer, as it is the ordinary
join. A few are probed at once, each again every two minutes and on
REFRESH. The game sees nothing of a probe's peer, and a join of a host
being probed takes its tunnel up. A game with a password is not probed:
its listing hides its token. Direct Link's games show the open tunnel's
ping.

(cherry picked from commit 98889fc72bc8cfd765b765d6e86c973c2040f841, merged into OpenCE's
internet play: its retired sessions keep their identifier, and its
server browser's NAT advice is kept in the README)
2026-10-19 22:47:21 +13:00
xshxdex98 5ead9910a8 Linux: PlayStation controllers through the kernel's driver, as a setting
SDL drives a DualSense or DualShock 4 itself through its hidraw device,
which only udev rules (Steam's) open to the player; without them the
controller was not found at all. With input.playstation_kernel_driver on,
where the kernel has its driver of them (hid-playstation, hid-sony), the
game leaves the controllers to it: their buttons, sticks, triggers and
rumble, through the input devices a player can read, but not the
lightbar or the motion sensors SDL's own has. Off by default.

(cherry picked from commit fa1ff85ebd46ce183522baa5a91c9a29c9006c9f, made a setting, off
by default, as it gives up what SDL's driver has)
2026-10-19 22:46:09 +13:00
xshxdex98 336dc0d262 Server browsers: a game selected in yellow, as Custom Edition's
In Join Game's server browser a click selects a game, its row in the
yellow-outlined picture Custom Edition draws for it, and a click on the
selected game, a double click, A, Enter or JOIN joins it; one click
joined it before. The keys move the selection.

(cherry picked from commit 472d232246568ed2e3ee56bc93d97a3530feea54, without that fork's
Online Games screen; OpenCE's rows already take the focus only from a
click, not from the pointer passing over them)
2026-10-19 22:45:37 +13:00
xshxdex98 fb8a0e63ca Server browser: an empty list says how many games are on a newer version
An old build sees none of the public games (each list leaves out other
network versions), and said only that there were none. The hosts heard
listing a game of a newer version are now counted (by their keys' hashes,
forgotten as a game is), and the server browser's empty list says so:
"2 games are on a newer version: update the game to join".

(cherry picked from commit 4a4a4134519b76a5bcfd11e0e47cf397d7a228a2, made again on OpenCE's
server browser, whose listings come through p2p_lobby.c; that fork's
counted its own game list's)
2026-10-19 22:44:26 +13:00
Milenko 329d311c16 Campaign: new profiles start at the beginning, as on the Xbox
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)
2026-10-19 22:42:30 +13:00
Milenko 87d0c46bf6 Draw more particles and lights a frame on desktop
A busy multiplayer fight fills the Xbox's per-frame lists of what a view
draws: 512 particle runs and 128 dynamic lights. Past them plasma bolts,
charge glows and flashlights stop drawing. The desktop builds now draw
2048 and 512. These lists are rebuilt every frame, outside the game state,
and nothing of them reaches the network. Android's and the web build keep
the Xbox's.

(cherry picked from commit f1abebdd6e89de292569ea466496fa0484476e0e; OpenCE already drew
2048 transparent geometry groups on every build, so only the particle runs
and lights are taken, with OpenCE's guards for the builds that keep the
Xbox's sizes)
2026-10-19 22:41:14 +13:00
Milenko 60e568235d High-res HUD: a map's PNGs decoded as it loads, on a thread of their own
The first frame that drew each HUD picture decoded its PNG, which held that
frame up. On the desktop builds a map's HUD PNGs (and its sprite sheets')
are now decoded as the map loads, on a thread of their own, so that the
first frame only uploads them; what is decoded and not drawn is let go when
the map is unloaded. Android and the web build decode each when it is
first drawn, as before.

(cherry picked from commits afa4feaee4935b118dbc6d94140587842205cbfe and
27a22a2ded1bc78323f7f4bf35033dd843f7b956, made again on OpenCE's high-res HUD, its sprite
sheets queued too, and not on the web build)
2026-10-19 22:40:09 +13:00
MrBruh b5099759a5 Drop the picked guards of defines OpenCE does not have
The full reference list pick left a light's clusters out under
HALO_SERVER, its dedicated server's define, and the debug.txt roll's
header check under HALO_XBOX_CONSOLE; OpenCE defines neither, so both
were dead or always true. The comment no longer cites a dedicated
server.
2026-10-19 22:37:15 +13:00
Milenko 7b91023137 Full reference lists warn once a map; the server's lights join no clusters
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)
2026-10-19 22:36:35 +13:00
Milenko 3a49815b2f debug.txt: the header check after a roll is the desktop builds' alone, as the roll is
(cherry picked from commit 6e57e4a04a6e906002c42ad2fedfaaeb3a02a15e)
2026-10-19 22:36:35 +13:00
Milenko fc67022b2e debug.txt: repeated lines counted, rolled to debug.old.txt at 64 MB
A line the same as the one before is counted rather than written again
("last line repeated N times", when a different line comes). When
debug.txt passes 64 MB it is renamed debug.old.txt (replacing the one
before) and a new one starts, with the header again for crash reports'
addresses. A game left running for days had no limit to its log.

(cherry picked from commit d4b46c829dcdaa53ee72d7d2901bc166115b0e69, without its dedicated
server README; the web build's copy of each line to the page's log is
kept)
2026-10-19 22:36:35 +13:00
Milenko 9c571c1bce Settings: name a config.toml that is not read on every desktop build
The desktop builds now name a config.toml.txt beside the real file
(Windows hides the .txt an editor may add, so the file looks right and
does nothing), and Linux names a config.toml in the folder the game was
started from when that is not the executable's:

  settings: .../config.toml.txt is not read: Windows saved it as
  config.toml.txt; rename it to config.toml

(cherry picked from commit b4f73a749f62cc5ed28092b7155d04638f5bb84d, without its macOS
check, and not on the web build)
2026-10-19 22:36:05 +13:00
Milenko dca73bbe65 Settings: words in any case, and an unknown one said in the log
display.mode = "Windowed" was not "windowed", so the window followed
display.fullscreen instead, without a word. The settings that take one
of a few words (display.mode, resolution_scaling, anti_aliasing, menus,
player_names, the chat, voice, co-op and streamer words,
crash_reports.upload and others) now take them in any case, and one that
is none of them is logged once:

  settings: display.mode = "Fullscreenn" is not "fullscreen",
  "borderless", "windowed" or empty; using display.fullscreen's

(cherry picked from commits b1d3eacadbc4047f6294c324b7d8e23009d3f2c1 and
80196e309a8af878309151cbec6794b8386759a8, made again with OpenCE's settings' words and
the port's own case compare, which the Android guest has)
2026-10-19 22:35:27 +13:00
Milenko e6998a4273 Settings: skip a line that cannot be read instead of the whole file
One TOML mistake, such as an unquoted word or a capitalized False, put
every setting back at its default. The line the parser stops at is now
skipped (in memory; the file is left as the player wrote it) and the
file read again, up to 16 times, so every other setting still counts.
Each skipped line is logged with its number and the likely fix:

  config.toml line 87 cannot be read (invalid value), so it is skipped:
  true and false are lowercase, high_res_text = false

  config.toml line 100 cannot be read (invalid value), so it is skipped:
  text needs quotes, menus = "pc"

(cherry picked from commit b3ee742ce7aef353e3f06af72b7cf83fff23544d, made again on
OpenCE's config_load: the file is not completed or rejoined while a line
of it is skipped, and the case compare is the port's own, which the
Android guest has)
2026-10-19 22:33:41 +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
xshxdex98 7b447b1065 Weapon HUD: no secondary warnings for one-magazine weapons
A one-magazine weapon's second magazine, read as empty, still set off the
secondary states: the secondary trigger ready, flash, reload and "no ammo
for reload" crosshair states, and the draw states of its total and loaded
ammunition (flashing, disabled), which coldsnap's and hugeass's master HUD
draws as NO FUEL beside the weapon's own NO AMMO. These now apply only to a
weapon with a second magazine.

(cherry picked from commit 488bd83eafaabf26a7e392eaf8b240b1319e07c3; OpenCE already cleared
the state first, kept the overlays off and checked two of the crosshair
states, so only the rest is taken, and no CHANGELOG.md)
2026-10-19 22:31:57 +13:00
Milenko 7351c3998e Discord: "In Menus" on every build
Only the 64-bit builds showed "In Menus" with the game's picture while not
hosting; the 32-bit ones cleared the activity, so Discord showed nothing.

(cherry picked from commit e69b5c9f48fb71282ef18efba8ae5c949ff55a7a)
2026-10-19 22:31:25 +13:00
xshxdex98 64084d77af LAN: a game search from off the client port answered directly
A second game on one computer listens on another port and hears none of the
host's broadcast advertisements, and the host answered searches no more
than once each advertisement interval. Such a searcher is now answered
directly every time; the broadcast keeps its interval.

(cherry picked from commit b49c83d1c871a23f76825bdcdc52a228167f1c37)
2026-10-19 22:30:58 +13:00
xshxdex98 1eb3db02d9 Netcode: a datagram's source port kept with it
The unreliable queue kept only a datagram's source address, so every
reader saw port 0. The host's answers to where a client's datagrams come
from (a second game on its computer, a direct answer to a game search) went
to port 0. The queue now keeps the port after the address.

(cherry picked from commit 781b98e9333c2cb85a85360267b24cc6656100c1)
2026-10-19 22:30:58 +13:00
xshxdex98 b6dba5a621 Netcode: a second game on the host's computer gets the host's per-tick state
Both games bound the client port (SO_REUSEADDR lets Windows share it and
hands each datagram to one of them), the host matched a distributed
datagram to the first machine at its address (its own), and sent every
client's per-tick datagrams to the fixed client port. The second game got
only the reliable messages: no units, no spawn, no co-op cutscene camera,
its camera left at the origin (the purple screen), and in PvP it waited
for space to clear.

A client now listens on any free port when another game has the client
port, the host takes a distributed datagram from the machine at its address
that isn't its own before its own, and answers each machine where its
datagrams come from. A game search from another port is answered there too.

(cherry picked from commit 0f4e71eba0787b8d3cd28c436cdb44ec6d79abda)
2026-10-19 22:30:58 +13:00
Milenko 4e1d97ee22 Network test: skip the intro movie before setting up the game
Hosting or joining with the intro movie still playing halted in
texture_cache_steal_memory (xbox_texture_cache.c #320): the movie holds
the texture cache's spare memory, which starting the map takes too. The
test now skips the movie, as a button press does, and waits for the menu
to settle again.

(cherry picked from commit 2d4a7deff6fda9bf3e8b9926a9353dd625be70b1)
(cherry picked from commit 47671d6b6ce24b693eb640c8398e78ef3e63a877)
2026-10-19 22:30:16 +13:00
Milenko 94209aaaf6 Hosts that run their own games clear the joins a finished game left queued
A machine that asked to join as a game ended, before the server switched
to the pregame, was left waiting behind the one the server holds: the
server refused every later join with "network_game_add_player() failed"
and sat in the lobby for good. A host that starts its next game by itself
(network_test.c's host, which the dedicated servers run) never recovered.
Those joins are now dropped when the next game starts. Only the host's own
bookkeeping changes; nothing on the wire does.

From JoshRob297's halo-ce-touch (18a6fc58), reworked for our tree, which
has no network_game_server_port_set_minimum_players.

Co-authored-by: JoshRob297 <99092299+JoshRob297@users.noreply.github.com>
(cherry picked from commit 4582817b2bb4241b53bfa4c523f02aa267595feb)
(cherry picked from commit be7362f2d0b9983e3e12a25f5c76871be5e9132f)
2026-10-19 22:30:16 +13:00
Milenko 89719c7422 Internet play: brokers.txt's list built in, for a game without the file
A game with no brokers.txt beside its config.toml (one run from somewhere
the builds did not put it) had no brokers, so no invites and no listings.
Without the file, the game now uses the same list (a test keeps the two
alike); an empty file still means no brokers.

(cherry picked from commit e728f63b590affb701dfc0ad3bde803bedd3b1c1, without its dedicated
server, macOS and 64-bit build parts, which OpenCE does not have)
2026-10-19 22:30:16 +13:00
MrBruh 66d787cf13 Declare log_address for the game's sources and tag map sharing's addresses
The picked log_address change declared the function inside
transport_address.c; the port's functions the game's sources call are
declared in halo_linux_source_fixups.h. Map sharing, which OpenCE added
since, logged a closed connection's address whole: it now goes through
log_address too. debug.log_addresses is in the settings table.
2026-10-19 22:29:50 +13:00
Milenko e708a64461 Logs show other machines' public addresses as tags, not whole
Players post debug.txt in public, and a host's log names everyone who
joined it. Every address the game logs now goes through one helper,
log_address: a public one is written as a tag (addr#3f2a9c), the same for
the same address all run long, so lines still match up, with its port as
it is. Local network, loopback and internet play's stand-in addresses stay
whole. debug.log_addresses in config.toml writes them all whole, for
debugging one's own network.

Display and logging only: nothing sent or compared changes.

(cherry picked from commit c76df1d00a88072d50af64cde39fb957fa5b842c, without its
64-bit-only xnet.c line)
2026-10-19 22:29:11 +13:00
Milenko c2e06963a6 CI: the ninja and ccache downloads checked against pinned hashes
(cherry picked from commit 8b64b14a13ddaa573b09bcf2006a8450af8d9a04, without that fork's 64-bit Windows
job; the four hashes checked against today's downloads)
2026-10-19 22:27:59 +13:00
Milenko 67f1719d0e Build tools: musl and SDL3 checked against pinned hashes and commit
The Android build's musl download and the Windows build's SDL3 package
are refused unless their SHA-256 matches the pinned one, and the Android
build's SDL3 clone unless its tag is the pinned commit.

(cherry picked from commit 11d25e2b1b7570ff1ae5da1537f886be66d5924e, without
its macOS build script, which OpenCE does not have; the hashes and the
commit checked against musl 1.2.5, SDL3-devel-3.4.16-VC.zip and
release-3.4.16 as downloaded today)
2026-10-19 22:27:24 +13:00
Milenko 4c736be46a Audio buffer as a setting
audio.buffer_frames sets the audio device's buffer in sample frames on the
desktop builds: 512 by default, as before (11 ms at 48 kHz), and up to
8192 for a machine whose sound cuts out when the mixer is late (2048 is
43 ms). Android keeps its 1024 and the web build SDL's own.
HALO_AUDIO_BUFFER_FRAMES overrides it.

(cherry picked from the audio part of commit
580fd78feeffba4e1ada8510f617a0103a49ce5c; OpenCE's windows already draw at
their size in pixels, and its macOS default and build changes do not
apply)
2026-10-19 22:25:54 +13:00
xshxdex98 6ec6e565b4 Custom Edition maps draw the port's high-res HUD
48 of the high-res HUD's textures have bitmaps Halo Custom Edition lays out
alike (the same sprites, the same size or twice it), compared against its
bitmaps.map (port/assets/hud/custom_edition.json). In a Custom Edition map,
such a bitmap that is Halo PC's own, read from bitmaps.map, is drawn with
the texture without the pixels' check, as the port's HUD looks; a map's own
HUD art, and the 21 sheets laid out otherwise (the meters, the reticles),
are drawn as they are.

(cherry picked from commit 503e78331a6885571af662037a79e7a59f5c4937, merged
into OpenCE's high-res HUD, which already had the loader's stock tag check
and a larger texture table)
2026-10-19 22:24:46 +13:00
MrBruh 61c328f0dc Take a Custom Edition enum with its bytes swapped when that is valid
Some Custom Edition maps' tools left shorts big-endian, as tag files store
them: Hornet's Nest's assault rifle muzzle flash light holds function
types of 256 where the stock light holds 1 ("zero"). The tag checker
reset such a value to 0 ("one"), so the light's functions read as one
where the map meant zero. A two-byte enum of a Custom Edition map whose
value is past its field's values now takes the value with its bytes
swapped when that is one of them, else 0 or none as before.

Ported from commit cc70ed67f86bc20a8e172ddf000eda0b1ca2e3e1, which made the
same repair at run time in periodic_functions.c; OpenCE's checker corrects
the value as the map loads, before the game reads it.
2026-10-19 22:23:11 +13:00
xshxdex98 ff6cbeb7f5 debug.gpu_stats: the frames that stall, and the textures uploaded
The per-second line averaged every count over its 60 frames, which hides
the frame that stalls. It also tells the textures uploaded in those
frames (how many, and their size: a texture cache filling up uploads
them again as the view turns), and the most draws and the longest time
of one frame.

(cherry picked from commit 68e58d36cb5321e95f87d9b30581458b1df0aaad)
2026-10-19 22:22:08 +13:00
Milenko e931bc7b44 debug.test_input "menu:<buttons>": menu buttons pressed one a second, for testing the menus
(cherry picked from commit 4b80d7dfa50f8a99db883ce61c9bf5a5199f20f1)
2026-10-19 22:21:59 +13:00
Jonas Volman a6e7a3a7a0 Port: keep blended colours within [0, 1]
Starting a system link game on Blood Gulch in the Windows build stopped
on two colour range asserts: rgb_colors_interpolate's result (a
structure decal's random tint) and a transparent shader stage's
animated constant colour. Both blend two valid colours by a factor in
[0, 1], and the native builds' x87 code can carry such values (and
periodic_function_evaluate's 255*(1.0f/255.0f)) at more than single
precision, a hair past 1. The blends are pinned to [0, 1] before the
asserts, and periodic functions return values in [0, 1].

(cherry picked from commit 5c1e49f189e09212d6bc9568649b1cdb7b1e005e, with
the changes marked port: in place of that fork's HALO_LINUX, which OpenCE
never defines, and without its README table)
2026-10-19 22:21:30 +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 7153e0b717 Mark the picked fixes to the game's sources as the port's
Several of the fixes taken from the forks changed source/ without the
port: comment that marks a change from the original, and three removed
the notes on the original's bugs without saying they were fixed. Each
change now says so.
2026-10-19 22:05:16 +13:00
xshxdex98 81a79fdf74 Keep the map lists still under a resting pointer
The campaign's level list and the multiplayer map list moved on whenever
their first or last row had the focus, so a mouse resting on either end
scrolled the list every frame. They now move on only when the d-pad put
the focus there (list_scrolls_at_end), as every other list in the menus
does. The host screen's listing spinner also checks that its option
exists before reading whether it shows.

(cherry picked from part of commit c75850e945093518881d1ffa184dd145967422e1,
a larger tidy of that fork's menus; only these two fixes apply to OpenCE)
2026-10-19 22:03:40 +13:00
Milenko 0a32d97bae Disc setup: multi-language European discs
Some European discs (the Spanish/Italian one, for example) have no maps
folder at the root, only one per language (maps_es, maps_it, ...), so
setting up from such a disc image stopped with "the disc image has no maps
folder".

When there is no maps folder, the extractor now takes the language folder
for the system's language (the desktop's LC_ALL, LC_MESSAGES or LANG; the
Android device's locale; the browser's language), else maps_en, else the
first one, and copies it to maps as before. debug.txt says which one it
took.

(cherry picked from commit 6cb6dfa7332934f6d86c7b4feefb645e76f977bb, made
again on OpenCE's disc readers, and in the web build's xiso-worker.js with
a test)
2026-10-19 22:02:55 +13:00
Milenko 3c67ccc6c1 Linux: the mouse wheel works under X11 again
The portable build's own SDL was configured without XInput 2.1's scroll
classes: SDL's check links a test program against the system root's
libX11, whose libxcb the system root has not got, so the check failed.
Built that way, SDL still drops the wheel's button events as emulated by
XInput 2 but reads no scroll valuators, so under X11 the wheel sent
nothing. CMake's checks now link with --allow-shlib-undefined, and
tools/linux_sysroot.py requires SDL_VIDEO_DRIVER_X11_XINPUT2_SUPPORTS_SCROLLINFO.

(cherry picked from commit 311f02bb50d4a0d2ffaf372a474ca0e8e98168c6, without
its 64-bit system root, which OpenCE does not have)
2026-10-19 22:00:22 +13:00
Milenko c90c5a2078 Controls Setup: Android keeps the keys' US names (its guest has no keyboard layouts)
(cherry picked from commit 9fa5fec148869ba145518d6e3cb66ad805403e57)
2026-10-19 21:59:38 +13:00
Milenko b68c0a650d Controls Setup: keys shown by the keyboard's own labels
A binding names a key by where it sits (its scancode, named as on a US
keyboard), so WASD stays under the fingers on any layout, as Halo PC's
DirectInput keys did. The menus showed those US names, so on a German
keyboard the zoom key "Z" (the key right of the left Shift, labeled Y)
showed as Z, and pressing the key labeled Z to rebind it showed Y. The
menus now show each key by the character the keyboard's layout gives it
(SDL_GetKeyFromScancode); keys with no character of their own keep their
names. config.toml keeps the US names, so a config means the same keys on
any keyboard.

(cherry picked from commit 86bf862d0ebf47f252fb41a0a77fdd52f96615f1, without
its dedicated server stub, and with the settings table in
port/linux/README.md)
2026-10-19 21:59:38 +13:00
Milenko 494523978e Settings: say why a config.toml change did nothing
- An unknown setting whose name belongs to one setting says which section
  it goes under (menus at the top of the file: under [display]).
- A file that cannot be parsed says every setting is at its default until
  it is fixed, and that a word value needs quotes.
- port/linux/README.md says so.

(cherry picked from commit d474fac1feb9b7faa55229d9e48c91aadadfc5d2, without
its macOS check, and with its README note in port/linux/README.md's
settings)
2026-10-19 21:58:46 +13:00
xshxdex98 592b01a7c6 test_linux_port.py: skip the Linux-target builds on other hosts
test_xdk_headers_compile_for_the_game and test_p2p_signatures_and_listings
compile for i686 Linux, and the p2p one runs what it builds. On Windows they
failed whenever clang was on the PATH: no Linux system headers, and shlex
stripped the backslashes from ninja's command. CI runs them on Linux.

(cherry picked from commit 6aa077faf95dd270fb2cea1b4caa65ef67e96dca)
2026-10-19 21:58:02 +13:00
Milenko f7f5c5b003 Linux: the portable build says when its path has a space
In a checkout whose path had a space, the portable Linux build failed in
SDL's configuration with errors that did not say why: CMake was given the
toolchain file's path unquoted, and SDL's build splits the system root's
include folders from pkg-config at their spaces. The paths are now quoted,
and the SDL step stops at once with a message saying to move the checkout;
port/linux/README.md says so too.

(cherry picked from commit 21e0aa6531e92547f0b8dc53120674da32207a1c)
2026-10-19 21:58:02 +13:00
xshxdex98 7fcb46d1df Gamepads: the log says each pad's kind and the device it is read through
debug.txt's gamepad line gives the pad's kind (ps5, xboxone, ...) and the device SDL reads it through: on Linux, a hidraw node when SDL's own driver has the pad (a DualSense's lights and rumble), an event node when the kernel's does. A player whose pad misbehaves can say which.

(cherry picked from commit ff238bfaae21b52a819de093e5141a028373e1f3)
2026-10-19 21:57:31 +13:00
xshxdex98 fc51992a7e Menus: a failing event function names its widget
"event handler function failed" said nothing of which: the gametype
editor logged it fifteen times. It names the function's number and the
widget's tag, taken before the function runs (it may delete the widget).

(cherry picked from commit 5f4494f42b1919dc1e38c6f8e168b646768e74f1)
2026-10-19 21:57:31 +13:00
Milenko f066c257c9 A sound refused for its format is named in the log
"attempt to play a sound that was not a mono 22k compressed sound..."
now says which sound tag, and its compression, encoding and sample
rate, so a report of it can be traced to the map that has it.

(cherry picked from commit e859daf04476ad2b0608f130e9b63ac0cf7140a8)
(cherry picked from commit 9e9104b26907b748ace50e004fd5e9f2c0167b11)
2026-10-19 21:57:31 +13:00
Milenko f3912112b2 Scripts' print and inspect on screen only with console_log = "all"
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)
2026-10-19 21:57:25 +13:00
Milenko 4126d5e3d2 Write watch: a write racing another thread's to the same page is no crash
mark_written cleared a page's protected mark before making the page
writable. A second thread whose write to that page faulted in between
found the mark clear and took the fault for a crash: Windows passed it on
to the crash reporter, and Linux and Android reported a crash and put back
the previous handler, so the next watched write ended the game. The I/O
thread's platform_contiguous_write and the main thread's writes meet this
way on a shared page.

A page made writable by the watch is now marked so (2), and a fault on it
is retried; a page the watch does not know (0: freed, or not watched) is
still a crash.

(cherry picked from commit 57baaec22eef2332ee69b1b4a79686e3bb8f0e3d, made
again on OpenCE's watches without the fork's 64-bit and macOS code)
2026-10-19 21:56:59 +13:00
MrBruh 7f625aa3a0 Name the build numbers in the gamescope update line
The picked line named the fork's release versions, which OpenCE's updater
does not have: it reads build numbers.
2026-10-19 21:56:02 +13:00
xshxdex98 465ab85e66 Updates: the log says why a build doesn't look
A build without a number, or one with update.auto off, now logs that it
does not look for a new version and why, rather than nothing.

(cherry picked from commit 05ee6d14c82009d0dd368adff01b96d63356f98b, with
OpenCE's conditions: a build number and update.auto)
2026-10-19 21:55:20 +13:00
Milenko feac00205d Updates: no question under gamescope
The update question is SDL's message box, a system dialog, which crashes
the game under gamescope (the Steam Deck's Game Mode). There the game does
not ask: it writes the new version in the log, and the player updates from
the desktop or with debug.update_answer. gamescope is known by
GAMESCOPE_WAYLAND_DISPLAY, XDG_CURRENT_DESKTOP, or Steam's SteamDeck and
SteamGamepadUI together, which Wine passes on to the Windows build.

(cherry picked from commit 1339ca420f710882dacdf1151a63f96db6211c29)
2026-10-19 21:54:57 +13:00
Milenko a3c65cd041 Updates: the replaced files put back when one cannot be replaced
(cherry picked from commit 26faf2ef088d088a9d0df0f4e195b0edc2fd4ab2)
2026-10-19 21:54:57 +13:00
Milenko 2e0bf83819 Updater: downloads and unpacked files bounded in size, release names checked
The self-updater now refuses a download larger than it expects (the
release check at 1 MB, the zip at 256 MB, in place of one 512 MB limit for
both), stops unpacking a stored entry once it passes its stated size (an
inflated one already stopped), accepts only plain release file names, and
unpacks the zip from the one copy it read.

(cherry picked from commit 2045acc0b6d2fe2306d10c9cc66b4342ae8ef90e, without
its check of a release's version name: OpenCE reads a build number)
2026-10-19 21:54:57 +13:00
Milenko 15f2417f6a Invites: only their first digits in the log
Players post their logs, and an invite lets anyone join while its host
runs. The hosting line and a new invite for a listed game now log the
link's first digits only; the whole link stays on the clipboard and in the
menus.

(cherry picked from commit e8c35a43bccc6047f1bf7494614876a0a6eb5d84, without
its macOS, startup-stash and dedicated server parts, which OpenCE does not
have)
2026-10-19 21:53:10 +13:00
Milenko 6e03bc9199 Discord: the application id taken only as digits
It goes into a file's path on macOS and into the handshake; an id that is
not a number is ignored, as an empty one is.

(cherry picked from commit 56b38e49b7af6be0054f5c13eb446a4f36baaacc)
2026-10-19 21:52:14 +13:00
Milenko 2de41ae5fd Internet play: a STUN server not found is looked up again
A STUN server whose name did not resolve was given all its attempts with
no address, and the only way back (attempts reset after a refresh's time)
needed an address. So a lookup that failed once, as when the game hosts or
joins before the network is up after a wake, left that server unused
until a restart: no public address was offered, and joins across NATs
failed unless UPnP forwarded a port. It is now looked up again every 25
seconds until found.

(cherry picked from commit 775aaf9adb92af58336b41010dc82cf397e67ade)
2026-10-19 21:52:14 +13:00
Milenko 4973c5f1d5 Internet play: a broker closed while its topics change asks for them again
broker_sync_topics went on after a subscribe or unsubscribe whose send
closed the broker (a send error, or its output full), and recorded every
wanted topic as had. broker_close had just forgotten them, and nothing
on reconnecting does, so the next sync found nothing to do: the broker
stayed without the host, join, slot or query topics until they changed
again. It now stops when the broker is no longer ready.

(cherry picked from commit e4bc27dcf68ea0de841bb1c097465cd67009cfbc)
2026-10-19 21:52:14 +13:00
Milenko b24b89f920 Internet play: a listing a closed broker never got is published again
broker_publish_listing answered 1 even when the publish's send closed
the broker, so its caller recorded the listing's version as published.
Closing reset that version and the broker's will cleared the slot, but
the caller then set it back, and the listing was missing from that broker
until the next republish (up to 30 seconds) or a tombstone was skipped.
It now answers whether the broker is still ready.

(cherry picked from commit dfe3514d75a5ca79e0403796da45ad6dbf55dea9)
2026-10-19 21:52:14 +13:00
Milenko c9f3d07eb5 Take a live game listing only within an hour of now
A browser took a retained listing only if its time was near its own, but a
live one at any age: anyone watching the brokers could publish an old, signed
listing of a host long gone again and fill browsers with dead games. A live
listing now has to be within an hour of this machine's clock, wide enough for
a host whose clock is off.

(cherry picked from commit fbdbd07300230e42ca92474b141eb8b78fae8683)
2026-10-19 21:52:14 +13:00
Milenko c827e29b63 Leave out tunnel addresses no machine can have
A joiner's offered addresses were all tried. Ones in 0.0.0.0/8, multicast,
reserved or broadcast, or with port 0, are now dropped before the host sends
anything to them. Loopback and LAN addresses stay, for two copies on one
machine or network.

(cherry picked from commit 69bd0d6a5669534a8b7fb3fa5d639af933c42381)
2026-10-19 21:52:14 +13:00
MrBruh d692e067cb Use the validator's own file check in the bitmap group fix
The picked change named the fork's file_contains and file_length, which
OpenCE's validator does not have: tag_validate_file_contains checks every
file of a Custom Edition map's offsets. The pixel data is moved to 0 only
once, so a map of no bytes cannot check its bitmaps forever.
2026-10-19 21:52:08 +13:00
Milenko ab2533a1a3 Validator: a bitmap emptied by its group stays clean
A bitmap whose pixels its group's data offset put outside the map was given
the start of the file as a negative offset, which the bitmap's own check
then corrected again: the tags as corrected were not clean when checked
again. It is now given the group's data, moved to the start of the file
when the data starts at its end. Found by fuzzing.

(cherry picked from commit 6b38c3df8daf16ab134f81287404f94ba5b07f06)
2026-10-19 21:51:49 +13:00
MrBruh 4f0313af55 Let Custom Edition models have more than 32 shaders
The map check cut a gbxmodel's shaders to 32, the tools' maximum, and set
the parts that used the rest to shader 0, so they drew with the wrong
shader. The game reads a model's shaders only through their block, by its
parts' shader indices, which the check holds to the block; Halo PC's
engine took more (h3_foundry's spartan has 56). The block's maximum is
now the tools' (TAG_SCHEMA_TOOL_BLOCK), as for the other blocks Custom
Edition's tools went past.

Ported from commit accc904940b00dcdbea3418c3f34bea5748a9a1e, which made the
same change in that fork's own loader. Reported by Sabriel.
2026-10-19 21:50:54 +13:00
Milenko 904fbbfc1e Network: the pregame keep-alive is a long, as on the wire
The packet table had begin game, graceful exit and keep-alive each on its
neighbor's field list (the Xbox game's table order is not their type
order), so a keep-alive's short was encoded and decoded as a long: the host
sent 2 bytes of stack past it, and a client wrote 2 bytes from the network
past it on its stack. The field lists now sit under the messages whose wire
they describe, and the keep-alive is a long. Not a byte on the wire moves:
begin game still carries a short (16 bits of its long), exit and keep-alive
a long, so OpenCE and earlier builds play as before; the keep-alive's 2
bytes that came off the stack are zero now. tools/test_network_messages.py
encodes every message under AddressSanitizer and compares the bytes with
upstream's table.

(cherry picked from commit ee32bdad42615402053291344e3c89f8bcc04161)
2026-10-19 21:49:02 +13:00
Milenko ce59b93165 Checkpoints: wait in game ticks, not frames
A checkpoint (game_save) waits for a safe moment and gives up after
240 ticks; it checks every 10. Those counts were advanced once a frame,
which on the Xbox was once a tick. At 60 frames a second or more a
checkpoint gave up in a few seconds or less, often before the fight it
waited for was over, so few checkpoints came: on Halo (a30) the player
could get only the first. The counts now advance once a game tick, as
the Xbox's did: 8 seconds at any frame rate.

tools/test_checkpoint_timing.py checks it at 30 to 990 frames a second.

(cherry picked from commit abc42998232b6261220a764e6de44641067ff2f5)
2026-10-19 21:49:02 +13:00
Milenko a321e98fd0 PC menus: the mouse wheel leaves a screen's button bar
A screen's button bar (SELECT and BACK under a list) steps sideways, so
once the wheel scrolled past a list's last row onto the bar it went back
and forth between the two buttons until a row was chosen again with the
mouse. A touch drag already left the bar with up and down, as the d-pad
does; the wheel now does the same.

(cherry picked from commit 2ddad079fa5435a70a383d757de16b960c421505)
2026-10-19 21:49:02 +13:00
xshxdex98 a9bfd7d4fa Menus: a tapped level, map or gametype is the one started
A tap on the touchscreen focuses a row and activates it in the same frame,
before the list's update has read which row has the focus, so New Game
started the level shown before (Pillar of Autumn on a new profile, as
players on Android saw), and the map and gametype lists took the entry
shown before too. Each list's choice is read again from its focus when it
is activated. A mouse hovers first, so the desktop builds were not seen to
do this.

(cherry picked from commit fba908137d3a3dfe8b61a5a84678f7c2bb4435b8)
2026-10-19 21:49:02 +13:00
Milenko 5e1cb4ced7 Network co-op: clients see the AI's grenades
Only the host runs the AI. An actor picks its unit's grenade type as it
gives it grenades, so on a client an AI unit kept no grenade type at all.
When the host's control told it to throw, unit_throw_grenade_begin found
no grenades of type NONE and the throw never began: a client saw no
grenade, then took the blast that the host's damage brought.

A client now gives an AI unit the type of the grenades it carries, as
the host's inventory says, when it has none or none of its type left,
and never in the middle of a throw.

(cherry picked from commit 92aaf6388fdfe37b640cacd162fd101dbae0ebe2)
2026-10-19 21:49:02 +13:00
Milenko 518a628553 Unit mouth debugging draws nothing for a unit without a head marker
debug_objects_unit_mouth_apeture placed its label at an uninitialized
marker when the unit had no head.

(cherry picked from commit a56597a9666a354a0a8f9e7f2e017cc418d4922a)
2026-10-19 21:48:48 +13:00
xshxdex98 c8cbc3e3cb Co-op: clients show the letterbox outside cinematics too
A cutscene that leaves the players their controls shows the black bars
without starting a cinematic; clients took the host's letterbox only while
a cinematic ran, so they had none.

(cherry picked from commit 43f646b586626fa8a154d87118abefe6d31d2ced)
2026-10-19 21:48:48 +13:00
xshxdex98 05d7fd38fc Server: players held for a map download are the pregame's again after the game
A machine still downloading the map when the game started has its players held out of it until the download finishes (OpenCE's). One whose download did not finish before the game ended came back to the pregame without its players, as the reset dropped the held ones; they are put back in the game's list instead, as the other players are kept.

(cherry picked from commit 6aa90bfb59557b8c98e2c8543aa4c42d9e3918c1)
2026-10-19 21:48:24 +13:00
Milenko 3f06bf815f No shield damage sound while an overshield drains
On a network client the overshield's drain arrives with the host's updates
after the HUD has taken its last shield value for the tick, so every frame
the shield looked a step lower than the HUD remembered and the shield
damage sound played for the whole overshield. The Xbox plays nothing.

The meter's flash already told the drain from a hit (a step of 0.05 or
less from past full); the sound now uses the same test, so a drain is
silent on host and client while a real hit on an overshield still flashes
and sounds.

(cherry picked from commit fd9c918313076ecc686209a0f4c75a6ef23340c4)
2026-10-19 21:48:24 +13:00
Milenko 2223f94579 Multiplayer: no Running Riot for a killer with no kills in a row
The kill announcement picked Running Riot whenever the killer's kills in a
row were a multiple of five, and zero is one. On the Xbox the killer had
just been credited with the kill, so it was never zero; a network client's
copy of the statistics can have the kill credited to no one or to someone
else until the host's statistics arrive, and announced Running Riot after
one or two kills.

A killer with no kills in a row now gets the plain kill message.

(cherry picked from commit 1b6cf950532f6d9590690c86c253def5395a132f)
2026-10-19 21:47:44 +13:00
Milenko 8453be8dc3 Team Oddball's built-in variant starts cleared
build_game_variant_team_oddball left its variant uninitialized while
every other builder clears it, so the fields it does not set (the name
and the flags beyond those it sets among them) were whatever was on the
stack.

(cherry picked from commit 4c3e7664aab234e1b56a84a3b3dd3c8d1bebf58a)
2026-10-19 21:47:44 +13:00
Milenko e0cf4f8809 Static vertex and index buffers: a failed creation leaves no buffer
A failed CreateVertexBuffer or CreateIndexBuffer may leave its output
unwritten, which was then tested uninitialized.

(cherry picked from commit 28f0a433093e4b7d6082123b45e19fd37beee262)
2026-10-19 21:47:44 +13:00
Milenko f1f33b05d1 Contrails of an unsupported render type release their buffers
A ground contrail (or another type the renderer lacks) left its
triangle and vertex buffers locked and allocated, and the rasterizer's
lock operation set, for every frame it was drawn.

(cherry picked from commit b335386569e840c601eaf238d7585c73fa60516c)
(cherry picked from commit 56f5d99b1d6e81c30c4021c2437b2bd71475c83d)
2026-10-19 21:47:44 +13:00
Milenko 88eb3e3c73 Sound: a map's close stops channels no sound owns any longer
A weapon's charging loop still on a channel as a network game ended
("still counted as playing ... let go", sound_cache_close) was finished
by the next map's sound_render against the next map's sound cache, which
halted the game as the next network game loaded (data.c: xbox sound index
is unused or changed). sound_dispose_from_old_map stops every channel
still holding a permutation, so its count is given back while the map's
sounds are there.

(cherry picked from commit 8e5b3f9f23beb9f015a0caf487caa82ada16c73e)
2026-10-19 21:47:44 +13:00
xshxdex98 88b0290186 Progress bar: a depth buffer released only if there is one
OpenCE's leak fix releases the surfaces the capture took; the device's
depth buffer may be none (D3DDevice_GetDepthStencilSurface's NOTFOUND),
and D3DResource_Release reads the resource it is given.

(cherry picked from commit ff654a774061c74d30f31ce986d7ebdefcafad74)
2026-10-19 21:47:44 +13:00
Milenko 572c674e43 Change Name in a game no longer crashes
The virtual keyboard kept its keyboard tag from the main menu's map after
that map was unloaded, so Change Name in Settings from a game's pause
menu opened it on a stale tag and the game stopped. The keyboard is now
let go with the old map's interface, as its tags are; at the main menu it
is set up again, and in a game Change Name does nothing.

(cherry picked from commit 4c05c5b3237330669f55c2bdb123b7cde4e4e056)
2026-10-19 21:47:44 +13:00
Milenko d161df18ed Network co-op: respawns keep working after everyone has died
When every player dies in network co-op, the host brings them back at
the last checkpoint without reverting. A revert restores the players'
all-dead flag from the saved game; this path left it set. With it set,
players_update_after_game neither respawns a player who dies nor ends
the game, so the next death, of one player or of all, left everyone
waiting for a respawn that never came. The flag is now cleared once a
player is back.

(cherry picked from commit 2e7c88b538b8aada54d0ce877740fb1be091e17d)
2026-10-19 21:47:44 +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 d490fd8bd4 Undo the map list's own return to the main menu after a match (#329)
The Xbox's postgame map select wrappers already carry
main_menu_if_no_history, with which the stock event code calls
main_goto_main_menu() when Back closes the last screen. The PC menus'
connected_map_select_wrapper, which menu_tags.c opens in their place,
was given the same flag by tools/port_settings.py in 4301ca88d, so the
hard-coded names and the network_game_abort() call in
widget_instance_go_back_to_previous were not needed, and they changed
stock split-screen behaviour (a split-screen game is a local server).
widget_instance_go_back_to_previous is as it was before 7d128e1bd.

Checked: the harness and touch menu tests (491 passed), test_web_build
(11 passed), ui_widget.c compiled for Linux and the Android guest.
2026-10-19 21:40:27 +13:00
MrBruh 9f6ea739b8 Take the mouse on focus and leave clicks alone in the XInput2 fallback (#381)
Without the keyboard focus, SDL_SetWindowRelativeMouseMode only sets
SDL_WINDOW_MOUSE_RELATIVE_MODE, so platform_mouse_capture(TRUE) at window
creation or when a menu closed while alt-tabbed went into the fallback
(hidden cursor, grab, warp) where relative mode works. It now releases the
mouse and clears the flag until SDL_EVENT_WINDOW_FOCUS_GAINED, whose call
then tests relative mode for real (SDL_SetWindowRelativeMouseMode returns
at once only when the flag is already set). The fallback needs no clearing
of its own: SDL clears the flag when relative mode fails.

The click that took the mouse back is removed: it undid F12's release on
any click, and the click also reached the game. The fallback does not need
it. The Linux README lists lib32-libxi.

Checked against SDL 3.4.16's SDL_video.c, SDL_mouse.c and SDL_keyboard.c;
sdl_platform.c compiles (-fsyntax-only) for Linux (debug and release),
Windows (clang, i686-pc-windows-msvc) and the web (emcc).
2026-10-19 21:40:27 +13:00
MrBruh 168a4718a8 Describe the disc image next to the executable at the first start (#377)
The first start now uses a disc image found next to the executable
without a file picker, and where no file picker can be shown it says to
put the image there and quits, instead of asking again without end. The
READMEs still described only the file picker. Checked against
platform_offer_game_data and data_find_image in sdl_platform.c: the
folder of the executable is searched, .iso and .xiso match in any case,
the question is still asked, and Windows runs the same code.
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 d55174f3a0 Merge pull request #381 from actes2/opence-wayland-mouse-fix
Fix | Linux: Lock and Hide the mouse for Wayland/misc compositors
2026-10-19 21:16:09 +13:00
MrBruh 0393f54f26 Merge pull request #377 from crowtrx/first-start-without-file-dialog
Fix an endless loop asking for the disc image on Linux
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 7270ddc069 Merge pull request #329 from innerhat-dev/postgame-menu-begin-random
Allow a full begin_random list and return to the main menu after a match
2026-10-19 21:16:09 +13:00
MrBruh 63786f98e5 Merge pull request #373 from pastudan/web-index-cache-metal-waits
Fix web stutter on Macs: per-frame index cache buffers
2026-10-19 21:16:09 +13:00
MrBruh d8e77289db Merge pull request #370 from addisonbair/swap-stall
Log a frame the graphics driver holds for seconds
2026-10-19 21:15:57 +13:00
MrBruh a4a5b4633f Merge pull request #369 from addisonbair/nat-hint
The server browser says when this network's NAT stopped a join
2026-10-19 21:15:11 +13:00
MrBruh 79e5c979e0 Merge pull request #347 from johncranston303/fix/windows-borderless-alt-tab
Prevent Windows borderless mode from taking exclusive fullscreen
2026-10-19 21:15:11 +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 1330f0bdaf Merge pull request #310 from eltanschauung/fix/coop-cutscene-skip-crash
Fix co-op cutscene skip crashes
2026-10-19 21:15:11 +13:00
MrBruh 8295f446c7 Merge pull request #396 from OpenCommunityEdition/android-emulator-fcvt
Fix SIGILL on the x86 Android emulator: rewrite scalar float-to-integer conversions
2026-10-19 20:58:20 +13:00
MrBruh 7a1b683328 Merge pull request #415 from OpenCommunityEdition/testing
Switch OpenCE to the AGPL 3.0 or later, with the disc image readers w…
2026-10-19 20:58:05 +13:00
MrBruh 4b09168e7f Switch OpenCE to the AGPL 3.0 or later, with the disc image readers written again from a description of the format 2026-10-19 20:12:12 +13:00
Mootjuh c53c31c00b Cull the structure triangles the camera can't see 2026-10-19 07:36:40 +02:00
John Cranston 4cbec6c826 Preserve the F11 window mode when saving settings
Only apply the configured window mode when it changes so saving sensitivity or other settings preserves the F11 choice.

Checked with eight local regression assertions against the latest testing baseline. The original behavior and a negative control failed as expected. Live gameplay and a full build of this baseline were not checked.
2026-10-18 17:37:23 -07:00
MrBruh c208663539 Merge pull request #406 from OpenCommunityEdition/testing
Testing
2026-10-19 07:47:40 +13:00
chestahh 711aa46d0c Fix desktop visibility query ownership and asynchronous readback
Create and validate typed query objects, retain each batch until its results
are consumed, and publish completed counts in submission order with the
scale captured for each test. Read available queries directly on the CPU
on AMD and other vendors, avoiding the mapped delivery path that lost AMD
results. On NVIDIA, preserve throughput with disjoint batch storage and
zero-timeout polling of a fence placed after all query-buffer copies.
Keep previous counts while work is pending and quarantine failed batches.

Consolidate the renderer changes developed in GulchCE through
b0c5d017 and 95748a74ce13a77444a7bb3bfe236bf97cfbb21d, as validated at
e5347765275bf324513eca4d044b15b3f6fc12b8, onto OpenCE testing 0110c160.
Only d3d8_gl.c and gl.h change. No MCC or fork features, GLES behavior,
network protocol, build files, tests, or documentation are included.

Validation: all 18 external delayed-GPU and negative-control regression
cases pass against this upstream tree; Windows portable release builds.
The implementation matches the AMD-confirmed and NVIDIA-benchmarked
renderer after excluding the three MCC additions in the fork's file.
Android/web targets are unavailable on this Windows host; branch CI
provides those builds. Android GPU runtime confirmation remains pending.
2026-10-18 18:35:55 +01:00
Stefan MaierandClaude Opus 5.5 307d2a953c Merge fix/lan-join-bots: a lobby the bots fill is open to a person on the LAN too
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:09:46 +02:00
Stefan MaierandClaude Opus 5.5 ce8eba8088 Roadmap: bots make room on the LAN too
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 19:09:45 +02:00
Stefan MaierandClaude Opus 5.5 62dee62eb6 A lobby the bots fill is open to a person on the LAN too
The bots make room for a person who joins, and the server browser's
listing counted their slots as free, but the LAN advertisement and the
join's check still asked for a free slot: with 15 bots and the host a
lobby of 16 was advertised closed, and a Quest on the LAN was refused
("attempted to join a closed game"). Both now count a bot's slot in a
lobby as a person's, as the listing does (network_game_server_room_for_person).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 17:59:49 +02:00
Stefan MaierandClaude Opus 5.5 cf11cc86ce Merge merge/upstream-chat: OpenCE's text chat and streamer mode, the scores on the menu button held
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:15:13 +02:00
Stefan MaierandClaude Opus 5.5 40e4ff2cf8 Roadmap: the scores on the Quest, the text chat in VR
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 15:15:12 +02:00
Stefan MaierandClaude Opus 5.5 e901c92b41 VR: the menu button held shows the scores (the Quest's controllers had no way to them)
A press pauses as before, now as it is let go; held 0.4 s, the scores
show as long as it is, as the Xbox's Back. In the menus it is Start at
once. The Quest's controls page says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:42:41 +02:00
Stefan MaierandClaude Opus 5.5 dc52b7a592 Merge fixes: a brace the p2p.c merge lost; in VR the chat lines where the desktop has them
The chat's lines go at the top left on Android, where the touch controls
keep the bottom left free; VR has none, so it lays them out as the desktop
does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:34:36 +02:00
Stefan MaierandClaude Opus 5.5 93b67eb7d7 Merge OpenCE upstream: text chat in network games, streamer mode, faster map sharing, the web launcher
The touch controls stay off in VR alongside the chat's on-screen
keyboard (xinput_sdl.c); the join codes and the online list beside the
server browser's listed hosts (p2p.c).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:33:00 +02:00
MrBruh 81a768bc81 Export the game functions the web page calls, so its loading notice hides once the menus draw and rooms reach the game 2026-10-18 23:49:26 +13:00
MrBruh 1b61b52929 Keep a browser's WebRTC channel from stalling under loss by reporting every gap and giving up old ones, and let the test's lossless run tolerate a dropped burst 2026-10-18 23:36:08 +13:00
neopetal ed54b29c7b Make nav menu sticky at desktop width 2026-10-18 11:19:15 +01:00
MrBruh 0110c16001 Merge bnunu/halo-1 main into testing, keeping OpenCE's code where both changed it and leaving out its matching tooling and logs 2026-10-18 22:31:23 +13:00
MrBruh 38ccb5f9a4 Merge pull request #401 from OpenCommunityEdition/testing
Merge Testing
2026-10-18 22:07:36 +13:00
MrBruh 54de4909ef Count the website's visits with Umami, its script loaded with CORS as the isolated page needs 2026-10-18 21:56:56 +13:00
MrBruh 1797ecd9ef Serve the web build's wasm from jsDelivr instead of GitHub Pages, checked against its SHA-256, with the site's copy as the fallback 2026-10-18 21:52:06 +13:00
MrBruh eb16cf4bdc Let a host share maps at up to 20 MB/s and a player helping it at up to 10 MB/s 2026-10-18 21:46:23 +13:00
MrBruh 52c1934dab Add sexual, hate and harassment words that Surge AI's list lacks to streamer mode 2026-10-18 21:35:30 +13:00
MrBruh bc37efd5da Redraw the web launcher in the menus' look, with a Ready to Play screen and Discord links on the page and in game 2026-10-18 21:11:01 +13:00
frankyfifeandClaude Opus 5.5 f396577e8f Add 5.1 surround sound
The mixer outputs stereo or 5.1 (audio.channels: "auto", "stereo" or
"5.1"; HALO_AUDIO_CHANNELS; Linux and Windows). On 5.1 the speaker config
reports AC-3, as an Xbox set to Dolby Digital, so the game sends its 2D
voices to the center and back mix bins itself; the speaker mix bins 0-5
are SDL's 5.1 channel order. 3D voices pan with equal power between
adjacent speakers at the ITU-R BS.775 angles, spread out when close or
above, at the stereo pan's power; stereo voices positioned in the world
pan their middle the same way and keep their width on the front pair.
The voices' resampling, low passes and reverb send are the stereo mix's;
the reverb plays from the front and back pairs, the voice chat from the
front pair, and the limiter takes all six channels. Stereo output mixes
as before. Android and the web stay stereo.

"auto" is the Xbox's own behaviour: the game asked the console's speaker
setting and mixed for 5.1 when Dolby Digital was on.

Checked on Windows with a 6-channel device: "sound: 5.1 surround" is
logged and the device opens; with "stereo", and with no 6-channel
device, the output is the stereo mix as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:07:57 +02:00
MrBruh f9bfca84b0 Merge pull request #400 from OpenCommunityEdition/testing
Merge Testing
2026-10-18 20:21:11 +13:00
MrBruh 453c24e6e0 Show the host's TEXT CHAT as OFF, and keep it from changing, while the game is listed in the server browser (#399) 2026-10-18 20:10:54 +13:00
MrBruh b1d71f070f Draw the text chat in team colors by default, with white as Custom Edition's an option (#399) 2026-10-18 20:01:04 +13:00
MrBruh 90ab2ff094 Move the text chat settings to Network Setup's Multiplayer HUD, put voice chat back on Audio Setup, and show Network Setup's in-game pages in the pause menu, co-op too (#399) 2026-10-18 19:57:16 +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
MrBruh 4c624fcbed Merge pull request #399 from OpenCommunityEdition/ingame-chat-v2
Re-land the in-game text chat with the review's changes (#395, #397)
2026-10-18 18:26:36 +13:00
Jonas VolmanandClaude Opus 5.5 8652d565da Hide the touch controls while the chat's on-screen keyboard is up
On Android the touch controls' overlay keeps every finger while it shows
(the look, outside its controls), so a tap on the keyboard never reached
the game. While the keyboard is up the overlay now hides, as it does for a
menu or a cinematic, and lets the fingers through: a tap on a key types it,
and a tap elsewhere closes the keyboard, as B and the other touch controls
are hidden then.

A tap's place is a fraction of the screen, which the viewport fills, so the
keys' place is now kept as a fraction of the viewport, not of the HUD's
window inside it, which is smaller on Android.

Checked on the x86 Android emulator (with #396's fix for its ARM
translation, not part of this change): the touch D-pad's Up, held as a
finger holds it, opened the keyboard at the bottom of the screen and hid
the touch controls; taps on h and i typed "hi", a tap on SEND said it
("chat: Player: hi"), the keyboard closed and the controls came back; a
tap away from the keys closed it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:38:15 -07:00
Jonas VolmanandClaude Opus 5.5 bc2344fb44 Let a controller or a touchscreen type a chat line on an on-screen keyboard
A controller's D-pad Up opens an on-screen keyboard for a line to everyone
(the D-pad only repeats the left stick's movement in the game). The stick,
or D-pad Left and Right, moves between its keys, A types one, X deletes, Y
switches capitals, D-pad Up and Down turn the line to everyone, the team or
the vehicle, Start sends it and B closes the keyboard; its last row has
Aa, #+= (the symbols' page), space, DEL and SEND. It is drawn at the bottom
right, clear of the lines above the motion sensor; on Android at the bottom
in the middle, between the touch controls, as the lines are at the top
left there.

The touch controls' D-pad Up opens it too, and a tap on a key types it
(touch_input.c hands the taps outside the menus to it). While it is up the
game gets none of the first controller (xinput_sdl.c), nor the touch
controls' stick and look.

The automated tests press the controller's buttons through
test_input_press: debug.network_test_chat now has each client type
"gg ez" on the keyboard and send it to the team. A drawn Linux client typed
it and the host received "chat (team): Player 2: gg ez"; the frames show
the keyboard over the game, the key picked lit, the typed line growing.
The harness gains three cases and six negative controls for the keys'
characters, moving between them and a tap's key (64 pass); CI's Linux test
step passes (493, 6 skipped).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:17:38 -07: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 e94255072e Remove the duplication in network_chat.c
No change in behaviour:
- chat_message_new builds a line's message (header and line), which
  chat_host_send, chat_host_notice and chat_send each did by hand.
- chat_send_to_clients sends a message to every client machine.
- chat_line_text turns the typed line into wide characters too, in place
  of the drawing's own loop.
- chat_player_name_length is the one count of a name's characters, for the
  line's name and its ASCII copy.
- CHAT_DRAWN_SIZE and the comments about the chat's old filter are gone;
  testing's streamer mode keeps a word's length, so a line's buffers are
  CHAT_TEXT_SIZE.

The name match keeps its own lower-casing (chat_lower): the console's
network_game_server_name_begins_with is static in network_server_manager.c,
and the harness takes chat_name_match without the game.

Checked: the Linux debug build, the chat harness (45), and the three-machine
system-link chat test (lines, channels, /mute, /unmute) as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:55:22 -07:00
Jonas VolmanandClaude Opus 5.5 c527105b31 Use testing's streamer mode for the text chat
Testing's streamer mode (90dd65ca, text_filter.c) hides offensive words in
every string drawn, through draw_string.c, so it already covers the chat's
lines and players' names. The chat's own filter goes: chat_filter.c and
its harness tests, game.streamer_mode (which also shared
HALO_STREAMER_MODE with streamer.enabled) and the STREAMER MODE row on the
VOICE AND TEXT CHAT page. Settings > Network Setup > Streamer Mode is the
one toggle. The README's Streamer mode section now names the chat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 19:45:37 -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 90dd65cab4 Add a streamer mode that hides slurs, swearing and insults in text from other players, maps and mods 2026-10-18 15:16:12 +13:00
MrBruh efabb0d056 Merge pull request #397 from OpenCommunityEdition/revert-395-ingame-chat
Revert "Add an in-game text chat for network games"
2026-10-18 15:03:57 +13:00
MrBruh 4ac609f0df Revert "Add an in-game text chat for network games" 2026-10-18 15:03:39 +13:00
bnunu be521e2621 Merge pull request #395 from OpenCommunityEdition/ingame-chat
Add an in-game text chat for network games
2026-10-17 22:03:27 -04:00
Jonas VolmanandClaude Opus 5.5 e2e62e706a Write the guest's scalar float-to-integer conversions in a form the emulator runs
On the x86 Android emulator every automated network test (debug.network_test
"join", "browse" or "host:<map>") stopped the app with SIGILL a few seconds in,
in hud_calculate_point. That was not an assertion: logcat's ndk_translation
line names the instruction, fcvtzs s0, s0, which clang makes of
(short)fast_ftol(point.x) (convert in the SIMD register, store its low half).
The emulator's ARM translation has none of the scalar conversions from
floating point to an integer in a SIMD register (fcvt{a,m,n,p,z}{s,u} s/d,
with or without fraction bits); the general-register and vector forms work.
The guest image had 141 of them, the HUD's the first to run once a game is
drawn. Devices run them, so only the emulator was affected.

tools/android_asm_convert.py now writes each as fmov to the destination, then
the vector form on it: the copy clears the other lanes, which convert 0.0 to
0, so the register and FPSR end as the scalar form leaves them (the game
reads FPSR through _statusfp).

Checked: on the emulator, each replacement against the general-register
form, 315 cases (every rounding mode, signed and unsigned, single and
double, saturating, NaN, infinity, denormal): the same register and FPSR,
except that the translation's own vector fixed-point form leaves out the
invalid flag on overflow (the guest has no fixed-point conversions). The
rebuilt image has none of the scalar forms left. debug.network_test =
"host:bloodgulch" now hosts and plays on the emulator (game started,
player states logged for 45 s, no signal); "join" searches for 150 s
without stopping. tools/test_android_asm_convert.py (new cases, and clang's
output through the pipeline), ci_build android debug and
tools/test_touch_layout.py pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:53:06 -07: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 861b8faca7 Draw the text chat at the top left on Android, the newest line highest
On Android the touch controls' Down button and Move stick are at the bottom
left, where the chat lines went (above the motion sensor). There the lines
now start under the weapon's panel at the top left, in the same column, and
go down: the line being typed first, then the newest, the older ones below
it. At most four show at once, the typed one among them: a fifth would
reach the tops of the Up and Fire buttons. The other builds draw them as
before.

Checked on the x86 emulator (host:bloodgulch: the test's three lines, two
typed with the keyboard, a third being typed): the typed one and the three
newest show, clear of the touch controls. Linux debug build and the
harness (391) pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:02:07 -07:00
Jonas VolmanandClaude Opus 5.5 f9676b2962 Replace bad words in players' names wherever streamer mode draws them
Streamer mode replaced the words of its list only in the chat's lines and
names, but a player named with a slur showed it on the scoreboard, above
their head, in the kills and in the lobby. Every text the game draws or
measures now goes through the filter at draw_string.c's start, both
draw_string and draw_unicode_string, so names are replaced wherever they
show and the text's width matches what is drawn. A text's format codes (a
| and the character after it) are kept, each run between them filtered on
its own, so a code never joins the words beside it ("|t" before "its").
The same strings are drawn every frame, so a cache of 48 keeps it cheap; a
text too long for its entries is drawn as it is rather than cut short.
The chat no longer filters its own lines (the draw does), so the line you
type shows replaced too. "hoe" now shows as "rake": "garden hoe" held the
word it replaced.

debug.network_test_name names a machine's player in an automated test
game, to test this.

Checked: tools/harness/tests/test_chat_filter.py (the format codes, the
cache, every replacement passed through the filter again unchanged, with
negative controls); a host on Xvfb, streamer mode on, named "fuckhead",
with a client named "n1gger": the chat showed "fudgehead" and the
scoreboard "nugget" and "fudgehead", while debug.txt kept the names as
they were; 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 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 9fc9f4e7a4 Let the text chat be drawn all white, as Custom Edition
display.chat_style (CHAT TEXT on Audio Setup's VOICE AND TEXT CHAT page)
picks the chat's colours: "default", the player's name in their team's
colour and the text a pale blue, as the PC version drew them, or "white",
all of it white, as Custom Edition. The line typed follows it too.

Checked on Xvfb: a host's lines drawn white, and the page with its new
row; 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 8a899e7650 Give Audio Setup a VOICE AND TEXT CHAT page with STREAMER MODE
Streamer mode needs a row a player reaches in a game, from the pause
menu's SETTINGS. Network Setup is not offered there and has no room on
desktop, and Audio Setup has none either, so its VOICE CHAT and VOICE
VOLUME rows move to a page of their own, VOICE AND TEXT CHAT, opened from
Audio Setup as Video Setup opens GRAPHICS, with STREAMER MODE under them.

The screens are port_settings.py's settings_files and menus.json lists the
new one, both written as tools/ce_menus.py writes them (it needs the PC
version's tags, so only these were written: the other screens come out
unchanged). Checked on Xvfb: both screens drawn, STREAMER MODE changed with
the keyboard and OK writing streamer_mode = true to config.toml; 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 8413225a55 Add streamer mode to the text chat
A player who streams needs the chat on screen to stay clean whatever the
others type. game.streamer_mode (off by default) shows the slurs, swearing
and sexual words in the chat's names and lines as harmless, funny ones
(fuck: fudge, cunt: chop stick, faggot: flurry), keeping their case and
fitting their endings (fucking: fudging, faggots: flurries). Only what
this machine shows changes: the lines go to every machine as typed, so the
network is unchanged, and debug.txt keeps them so
(port/linux/game/chat_filter.c).

It finds the words in the spellings that dodge a plain list: digits and
signs for letters (n1gg3r, c00n, $hit), l for i, q for g, k for c, v for u,
ph for f and * for a vowel; letters and endings typed more than once; a
word spelled out with separators (n i g g e r); and a few words inside
others (sonofabitch, thundercunt). Each word may stand alone, with endings,
at a word's start or anywhere, and harmless words that hold one are kept
as typed (Scunthorpe, snigger, Niger, assassin, cocktail, rapper, FAQ).

Checked: tools/harness/tests/test_chat_filter.py (8 cases with 11
negative controls); every word of the repository's documents and of an
English dictionary of 370,000 words (dwyl/english-words) run through the
filter, the harmless ones it changed kept, and a community-submitted list
of 2,800 entries used to find gaps (neither list is in the repository: the
community one has no licence); a host on Xvfb with streamer mode on,
lines typed on its keyboard shown replaced while the client logged them
as typed; and the CI Linux test step.

No Settings row yet: Network Setup has no room on desktop and is hidden
from the pause menu's SETTINGS, so where the toggle goes is still to be
chosen; config.toml and HALO_STREAMER_MODE set it meanwhile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:02:07 -07:00
Jonas VolmanandClaude Opus 5.5 0792a3aecb Draw the chat lines with a soft shadow
The text drawing gives every string a hard shadow, a black copy a unit
down and right; the PC version's chat had a softer one, further out. The
chat lines now draw theirs as translucent black copies one to three units
down and right, which add up to a dark middle with a soft edge, and the
text's own shadow clear. Checked on a host drawn on Xvfb against the
reference screenshot.

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
MrBruh e393fbe716 Draw the main menu's MODS item in the redraws' own letters, spaced and glowing as the other items are 2026-10-18 12:24:15 +13:00
eltanschauung 7a1a9d017c Exclude Monitors from co-op enemy scaling
Use the existing Monitor actor type to keep authored squad counts even when its encounter is hostile. Check the effective actor variant too, including starting-location overrides and major upgrades, before placing an extra actor. Ordinary placement and enemy squads retain their existing paths.

Ten focused regression checks pass, including negative controls for both guards. Only the host creates these actors; no message or protocol change is required.
2026-10-17 18:21:55 -04:00
Stefan MaierandClaude Opus 5.5 5530d19769 Merge fix/vr-weapon-arm: each weapon held so that its hand lands on the real one, the scope's disc with it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:00:14 +02:00
Stefan MaierandClaude Opus 5.5 603ad6b543 Roadmap: each weapon held so that its hand lands on the real one
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 18:00:13 +02:00
Stefan MaierandClaude Opus 5.5 322146f3cb Scope: the disc moves with the weapon from its old fixed place in the hand
The disc was placed for the weapons at the old fixed place (7 cm above
the hand, 22 cm ahead of it): with the pistol now held 20 cm further
back it sat at the muzzle. It moves as the weapon did (forward and up;
sideways the barrel places it, as before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 17:54:43 +02:00
Stefan MaierandClaude Opus 5.5 a31c482d5a Hold each weapon so that its hand lands on the real one
The first-person model hung from a camera at one fixed place from the
weapon hand's controller (18 cm behind, 10 cm left, 14 cm above), but
each weapon's animations hold the hand elsewhere from that camera: the
pistol's wrist came out 15 cm before the controller (22 cm before where
the free hand's wrist is, 7 cm behind its grip), the plasma pistol's 11,
the assault rifle's 8 cm aside and 10 cm low; the weapon arm looked
longer, reaching forward. Measured on the Quest 3 (2026-10-17).

Now the wrist as the weapon is held ready is kept for each weapon
(eased, the last eight weapons), and the camera put so that it lands
where the free hand's does: 7 cm behind the grip, along the aim. The
model, its barrel, the laser and the shots move with the camera, as
before. A weapon not yet seen is held at the old place for its first
frame. vr.weapon_hand_forward/_right/_down (0 by default) move the hand
on the weapon; vr.weapon_forward/_right/_down, the old fixed place, are
gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 17:41:48 +02:00
Stefan MaierandClaude Opus 5.5 e543fe53e0 Merge merge/upstream-oct18: OpenCE's high-resolution Xbox HUD, pause menu artwork, client-side mods, resampler, unsteered vehicles
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 16:31:03 +02:00
Stefan MaierandClaude Opus 5.5 76fbf67334 Merge OpenCE upstream: the high-resolution Xbox HUD completed, pause menu artwork, client-side mods, the resampler setting, unsteered vehicles' netcode
README.md keeps Ringfall's; OpenCE's README is copied to
docs/OPENCE_README.md as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:54:00 +02:00
MrBruh 7524ba6168 Merge pull request #391 from OpenCommunityEdition/testing
Testing
2026-10-18 00:53:59 +13:00
MrBruh 5a1b8a4094 Set the resampler from a Sound Processing screen of Audio Setup (#245)
Audio Setup had no room for another row (nine on the desktop, as Video and
Network Setup), so Reverb moves with the new Resampler row (SINC, the
default, or LINEAR) to a SOUND PROCESSING screen opened from Audio Setup,
as Video Setup's categories are; the devices and volumes stay on Audio
Setup. The setting is read again when it changes, so the TODO saying it
could only be set in config.toml goes, and the help, the setting's comment
and the README say what linear does: it lets more of the images above a
sound's rate through, which is what sounds brighter.

The mixer's harness test takes the resampler from dsound_sdl.c, which now
calls config_string: it stubs it, and a new case checks linear mode against
linear interpolation worked out in double precision at every rate (with a
negative control). Checked: the harness and touch menu tests, test_web_build,
ninja linux, android and web, and both screens drawn on Xvfb with
HALO_AUDIO_RESAMPLER=linear; the menus regenerated with tools/ce_menus.py
change only the two audio screens.
2026-10-18 00:13:26 +13:00
MrBruh b718ad8468 Merge pull request #245 from WhiteI3ulL/resampler-linear-interpolation
Add linear resampler for brighter global audio
2026-10-18 00:03:17 +13:00
MrBruh af07c05d21 Say which shaders a mod's models can be drawn with
A mod's model can name a shader of any group the map has, but
shader_transparent_chicago_extended is Gearbox's addition for Halo PC and
the Xbox game has none: the note says to use a transparent chicago shader
instead, and that a Custom Edition map's chicago extended shaders are
converted to chicago shaders as the map loads.
2026-10-17 23:53:28 +13:00
MrBruh 9a8382f1ae Let mods replace models whose nodes, markers and regions match the map's 2026-10-17 23:49:30 +13:00
medic-codeandClaude Opus 5.5 c6c02192ff Draw the match clock on the multiplayer HUD
The gametype's time limit ends a game, and its time left is drawn on the
scoreboard, but a game without one shows no time at all, and the
scoreboard is a button hold away. The clock players time the weapons by
was a stopwatch beside the television.

It counts up from the game's first tick, which is the tick the items'
respawn periods count from as well (game_engine_update_item_spawn's
game_time_get() % respawn_period), so an item is back on the map at every
multiple of its period on the clock: on Chill Out the overshield is up at a
minute, the rocket, camo and overshield at two. Elapsed rather than
remaining for the same reason: a game with a time limit is the one a
player most wants to time the rockets in, and its time left is on the
scoreboard already.

It is drawn at the bottom right, where the versions players know it from
put it, clear of the first person's weapon. The HUD's own text colour is
lost against a map as bright as it, so the clock is lifted toward white and
drawn whole. ticks_to_unicode_time_string leaves the minutes out below a
minute, which a countdown reaches only at its end but a clock counting up
starts at, so the digit is put back at the call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01194FWhCaedcdGjEksr1H8w
2026-10-17 11:12:58 +02:00
medic-codeandClaude Opus 5.5 142ab20ba2 display.match_timer, off, and its row in the graphics settings
The match clock is opt-in: Custom Edition's HUD has no match clock, so the
HUD looks as it did unless a player asks for one. MATCH TIMER goes on the
graphics settings screen after GLOW INTENSITY. The screen is as
tools/ce_menus.py writes it with the row. Nothing draws the clock yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01194FWhCaedcdGjEksr1H8w
2026-10-17 11:12:58 +02:00
Stefan MaierandClaude Opus 5.5 7da699b035 Merge feature/disc-languages: the European disc in Spanish and Italian imported, in the headset's language
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 10:39:31 +02:00
Stefan MaierandClaude Opus 5.5 c5c604bb95 Import the European disc in Spanish and Italian, in the headset's language
That disc has no maps folder but one for each language, maps_es and
maps_it, and the disc image setup stopped: "The disc image has no maps
folder: it is not a Halo disc." The Quest's (XisoExtractor.java) and
the Steam Frame's (steam_frame_xiso.py, RINGFALL_LANGUAGE or the
system's) now take the folder of the device's language, else English's,
else the first, and write its language into maps/language.txt. The game
takes that as the Xbox's language (XGetLanguage) while game.language is
unset, so the menus speak it too. (Reported for ChupathingyCE, another
OpenCE fork, in its issue 145; written anew here.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 10:39:30 +02: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
Dan PastusekandBridge e6fdb07c39 Scope zoom effect: a few passes at any resolution, not one per screen scale
A scope's zoom (the pistol's, the sniper rifle's 2x and 8x, any weapon whose
HUD has a convolution screen effect) draws a warp: copies of the screen a
radius apart, blended outside the scope's mask, in two full-screen passes on
the Xbox. To keep the copies from standing apart as ghosts at high
resolutions, f815e1cc split the radius into as many times the passes as the
screen's pixels to the Xbox's one, up to six: ten full-screen passes (and
their target copies) at 2160 lines. On an M4 Mac mini in Chrome (ANGLE on
Metal) at 3840x2160 that took a frame from 17-22 ms to 34 ms with the
pistol zoomed on b30 and from 16.7 ms to 22-56 ms with the sniper rifle on
a50, all of it GPU time: with the screen effect off, a zoomed frame took
what an unzoomed one did, though zoom raises the draws (the level's LOD and
culling: 230-350 draws to 470-640 on b30) nowhere near enough to matter.
It also narrowed the blur: N passes at a radius over N have 1/N of the
variance, under half the Xbox's at 2160 lines.

The Xbox's passes are now drawn as they were, at the effect's radius, and
two more, at a third and a ninth of it, put eight copies between each two:
up to nine screen pixels to the Xbox's one (2160 lines are 4.5) they stand
no farther apart than the Xbox's did in its own pixels, two more again up
to 81. The blur's variance is 6% more than the Xbox's. At 640x480 nothing
changes.

Checked at 3840x2160 (headless Chrome, device pixel ratio 2): b30 pistol
zoomed 17-22 ms, as unzoomed (was 34 ms); a50 sniper rifle 2x and 8x zoomed
16.7 ms, as unzoomed (was 22 ms, 56 ms in one view). New harness test
(tools/harness/tests/test_screen_effect_passes.py): the pass count and
radii, the gaps between the copies and the blur's width at screen scales
1 to 1000, with negative controls.

Co-authored-by: Bridge <agent@bridgedesktop.com>
2026-10-17 00:51:02 -07:00
MrBruh 01797c0434 Merge pull request #388 from OpenCommunityEdition/testing
Merge testing
2026-10-17 17:54:12 +13:00
MrBruh 7059096e1b Merge main into testing, bringing in the pause menu and HUD fixes of #378 2026-10-17 17:43:40 +13:00
MrBruh 54c2d31c97 Show the Mods list as a resource pack list, the top mod winning 2026-10-17 16:19:49 +13:00
MrBruh dd16971b6c Add client-side mods with a Mods menu, load order and incompatibility checks 2026-10-17 16:19:49 +13:00
Guven 7c942e9a57 Merge pull request #384 from lukerb52/main
Added reference to AUR version to the README
2026-10-17 00:40:26 +01:00
hkmoddandClaude Opus 5.5 c91badc15b Fit the texture idle test to the HUD's refresh of cached bitmaps (#376)
xgpu_texture_cache_begin_frame now also reads the high-res HUD's
settings (#378), drops an entry's upscaled sheet (#380) and forgets the
recent lookups. The test's fake world gains those members and functions,
with settings that never change. Its 3 cases and 4 negative controls
pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:58:54 +02:00
hkmoddandClaude Opus 5.5 90bf6fcb3a Merge main (build-213) into the texture idle time branch
The one conflict was in xgpu_texture_cache_begin_frame's declarations.
Main gained the high-res HUD's refresh of cached bitmaps (#378) and here
the sweep counts time (idle_since, now); both are kept, declarations
before statements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:58:52 +02:00
hkmoddandClaude Opus 5.5 558f85f803 Tone map display.hdr from the resolved texture without copying it (#280)
With multisampling, the tone map reads the resolved back buffer and draws
into its multisampled renderbuffer, so it need not copy the texture first:
the copy is kept only for the case that draws into the texture it reads.
The shader takes each pixel with texelFetch, and the copy was a nearest
blit of the same format, so what is drawn is unchanged.

Checked in a 48-machine co-op at 2560x1440 with msaa4x, uncapped, in
alternating windows of one game: the tone map's GPU time falls from 236
to 224 us a frame, and with the presentation's resolve from 270 to 228 us.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:50:23 +02:00
hkmoddandClaude Opus 5.5 6337fb45d2 Merge main (build-211) into the HDR branch
The one conflict was the generated Graphics screen, which gained HIGH-RES
HUD on main (#380) and HDR and HDR DISPLAY here. It was resolved by
writing the screens again with tools/port_settings.py: the generator
gives the merged file byte for byte. tools/test_web_build.py's menu test
passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:50:02 +02:00
bnunu 25697bbaa1 Merge pull request #378 from chestahh/opence-ui-fixes
Fix regression in pause menu & The Maw timer, restoring legacy Xbox behaviour. Fix incorrect Edit Gametypes positioning
2026-10-16 17:49:30 -04:00
bnunu a5e6102413 Merge pull request #372 from innerhat-dev/unsteered-vehicle-path
Carry a vehicle no player drives along the host's positions
2026-10-16 17:49:16 -04:00
Ari WilsonandClaude Opus 5.5 a22dd332f7 Add the Master Chief Collection's Bumper Jumper button layout
Settings > Gamepads > Button Layout gets a sixth choice, Bumper Jumper as
the Master Chief Collection has it for Halo: Combat Evolved: jump on the
left bumper (the Duke's white), melee on the right (its black), switching
grenades on A, action and reload on B and the flashlight on X; the
triggers, stick clicks, Back and Start are the default layout's. The Edit
Profile picture, which shows the Xbox's pictures of its five layouts,
keeps the row's own picture for this one, of which the Xbox has none.

A layout the build does not know (a later build's) now gets the standard
buttons. With no case for it, every control was on A, start and back
with it, and input_abstraction_update_local_player_preferences' assertion
stopped the game, release builds too. Builds before this one still stop
when they load a profile saved with Bumper Jumper.

The buttons are mapped on each machine before its players' actions are
sent, and profiles are not sent (only names and colours), so
HALO_PORT_NETWORK_VERSION is unchanged.

Checked: tools/harness/tests/test_button_presets.py (every layout keeps
start and back and gives each control a button of its own, Bumper
Jumper's mapping, an unknown layout is the standard one, and three
negative controls), built 64-bit on macOS; the settings screens written
again with port_settings.settings_files(), which changed only Gamepads,
and test_web_build.py's and test_linux_port.py's menu checks; the units
that include player_profile.h and menu_functions.c compiled for i686
Linux against the portable system root. Not yet played with a controller,
and not built for Windows, Android or the web.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:23:35 -07:00
chestahh b9e393c00c Apply high-resolution HUD and text settings without reloading maps
Recheck committed font settings before cached font selection, including when
high-resolution text was disabled at first use. At each frame boundary,
invalidate only cached HUD/title bitmaps affected by a changed setting so
both enabling and disabling rebuild the appropriate texture on next use.
Keep decoded replacements, font atlases, world textures and menu Apply/Cancel
semantics intact. This corrects shared cache behavior; it adds no MCC data,
map-family routing or networking/simulation changes.

Validation: Windows portable release build and 370 focused production-code,
HUD, menu and MCC checks passed. Tests cover repeated toggles, disabled text
at startup, font scale/cache reuse, selective bitmap refresh and Apply/Cancel.
Android and Web targets were attempted but are unavailable in this Windows
build graph. Live Custom Edition validation is limited by unavailable maps.

(cherry picked from commit ddeb650d82a73b570e182ff2bfd87e3cc3daa98d)
2026-10-16 21:42:21 +01:00
Constantine Luke Matthew Bubar c54185c088 punctuation 2026-10-16 16:30:26 -04:00
Constantine Luke Matthew Bubar 6b214c63a7 added reference to AUR version to the README 2026-10-16 16:27:22 -04:00
bnunu 19d84ce775 Merge pull request #383 from chestahh/opence-hud-fixes-2
Fixes to latest high-res HUD commit
2026-10-16 15:56:39 -04:00
chestahh 5f4beed534 Match high-resolution controller glyphs to the Xbox HUD
Match visible A/B/X/Y and supplied trigger/thumbstick artwork to the Xbox
bitmap bounds and center prompts around the visible glyph. Keep Black and
White unchanged. Preserve all three stick press frames and rename the
Menu Gems display label to Dark Gems without changing saved values.

Use the original HUD ONE/INVSRCALPHA blend for sticks and triggers, with
premultiplied asset edges, to avoid dimming them by prompt alpha twice.
Increase the existing high-res texture table so every new frame loads.
All changes remain in the existing high-res feature and menu wiring;
low-res fallbacks and unrelated icon indices retain their existing paths.

Checked: 358 focused HUD/menu/MCC tests on testing, Windows portable release
builds on testing and a clean upstream checkout, and hidden Xbox a10
captures for size, spacing and brightness. Android/Web targets are absent
from this Windows build graph. No protocol or simulation changes.

(cherry picked from commit c21ec87e6fbc6eb3370597dbdbc92cbe5464bb61)
2026-10-16 20:45:36 +01:00
bnunu 17a4c84a0a Merge pull request #326 from innerhat-dev/screen-fog-fov-testing
Screen fog stays on the canopy when the field of view is widened
2026-10-16 15:29:45 -04:00
bnunu 2c849ca30c Merge pull request #380 from chestahh/opence-hud-fixes
Fix low-res bitmaps overlap issue with High-Res HUD, as well as general high-res bitmap issues
2026-10-16 15:25:40 -04:00
chestahh c794406c20 Restore authored opacity for high-resolution HUD buttons (#380)
Xbox button definitions override prompt text with full-opacity colors.
Honor that existing override for Classic, Gems and Menu Gems while
retaining text alpha for icons without it. Embedded artwork supplies RGB.

Validated original Xbox a10/Blood Gulch definitions, opaque asset
interiors, Windows portable release compilation, and production-function
checks for all three styles, full/partial alpha, fallback, and cleanup.
Android/Web targets were unavailable in the Windows configuration.
No MCC/fork code, tests, documentation, assets or workflow changes included.

(cherry picked from commit ce6cf74623912bf442efcfb2e0d6849a84f86011)
2026-10-16 20:08:22 +01:00
Stefan MaierandClaude Opus 5.5 8662777f11 Merge feature/frame-updater: the Steam Frame's launcher updates the game
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:53:11 +02:00
chestahh f80bd15210 Add supplied Xbox Gems and retain Menu Gems as a third style (#380)
Use the supplied transparent A/B/X/Y artwork for Gems, fitted within the existing button bounds. Preserve the original menu redraws as Menu Gems and share their Black/White glyphs between both gem styles. Classic remains the default. Keep the source PNGs and deterministic asset conversion, and reserve enough placeholder entries for all styles across map changes.

Validated the Windows release build and 269 focused checks, including generated texture bounds, centering, repeatable output, all three native/MCC style transitions, disposal, and menu row spacing. Existing menu glyph PNGs remain unchanged.

(cherry picked from commit 5f66c51927f09c31095e224b0cbfbeb8aeef0bb8)
2026-10-16 19:44:57 +01:00
chestahh 4773c351d2 Add Classic and Gems high-resolution HUD button styles (#380)
Default high-resolution gameplay A/B/X/Y/Black/White buttons to plain Xbox faces made from the existing redraws. Retain the shaded faces as Gems and keep glyph geometry unchanged. Show Button Style below High-Res HUD only while enabled, following pending edits and closing the row gap when hidden. Recreate owned glyph placeholders on style changes and dispose them on map unload.

Validated the Windows release build, hidden menu captures with both styles and HUD off, isolated Xbox gameplay, and 268 focused production-code and menu checks including repeated visibility/focus and glyph lifecycle changes. Android and Web builds were attempted but their targets are unavailable in this Windows configuration.

(cherry picked from commit e8f36b8aa2b906a738856ac198efbd591cc9bcc0)
2026-10-16 19:44:33 +01:00
Stefan MaierandClaude Opus 5.5 e24df8ead9 The Steam Frame's launcher updates the game
The game's own updater (the Quest's) cannot on the Steam Frame: Lepton
installs the APK in the game's folder there at every start. So the
launcher does it: while the game runs it asks GitHub for the latest
release, fetches a newer one's Steam Frame bundle into update/, checks
it (its build.txt is the release's number), and puts the APK and the
launcher in place as the next start begins, before Lepton runs the APK.
update.auto = false in config.toml stops it, as on the Quest; a build
with no number (build.txt, which the bundle now carries and the
installer copies; 0 for local builds) never looks; without the internet,
or when anything fails, nothing changes. The in-game updater no longer
starts there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:40:26 +02:00
actes2 10b461b739 Linux: hold and center the mouse when XInput2 is missing
32-bit libSDL3 loads libXi.so.6 and libxkbcommon.so.0 with dlopen at run time. Where a 64-bit distribution's 32-bit SDL3 package leaves those out (such as Arch Linux's lib32-sdl3), SDL falls back to X11/XWayland without XInput2 and SDL_SetWindowRelativeMouseMode fails with SDL_Unsupported(), or leaves SDL_WINDOW_MOUSE_RELATIVE_MODE set before the window has focus so non-relative motion is dropped.

When SDL_SetWindowRelativeMouseMode does not take, platform_mouse_capture now clears SDL_WINDOW_MOUSE_RELATIVE_MODE, hides the cursor, grabs the mouse in the window (which lets XWayland's xwl_seat_maybe_lock_on_hidden_cursor lock the pointer and emulate warps), and warps the pointer back to the middle of the window after motion.

Checked with ninja linux and ninja android.
2026-10-16 12:26:12 -05:00
Stefan MaierandClaude Opus 5.5 7a10a413c8 Merge feature/vr-refresh-auto: the eyes drawn one by one (multiview's doubled pictures online), the refresh rate stepped down while frames come late
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:57:18 +02:00
Stefan MaierandClaude Opus 5.5 5ce39c27a7 Roadmap: multiview to look into later; refresh rate steps in main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:57:17 +02:00
Stefan MaierandClaude Opus 5.5 67d738451c Draw the eyes one by one by default; multiview only with debug.multiview
On the Quest 3, in big online games (26-35 players), the picture was
doubled and shifted from the start as the head turned, nauseating; with
48 bots it was not, and with the eyes drawn one by one it was not online
either. Not the frame rate: one by one, online at 72 Hz, the compositor
repeated as many frames and nothing was seen. The Steam Frame had the
same doubling with multiview (2026-10-05) and draws them one by one
since. So both now do by default; debug.multiview (was vr.multiview, and
debug.multiview_steamvr) draws them at once, for finding what the shared
pass draws wrongly. The refresh rate's steps take up the extra cost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:48:27 +02:00
Addison Bair 623bc8ced5 StageKit: stage and instrument any scenario on the real engine, with no retail assets
tools/stagekit writes a scenario's map in code (a floor in a box, props, players' starts) and plays it headless, a tick a frame, the same every time. Every player is logged each second and tests check what happened: walking, an obstacle, an elevator ridden up and down. 20 tests, about 2 s, CI-safe.
2026-10-16 16:26:08 +00: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
Stefan MaierandClaude Opus 5.5 91bdb7e67c Refresh rate: step down at one late frame in 32, show the rate it runs at
The first try stepped 90 to 80 Hz a few seconds into a 30-player game,
and no doubled picture was seen, but 2-5 stale frames a second were left
at 80: one late frame in 32 (was 16) takes it on to 72. The VR options
show the rate chosen and the one it runs at while lowered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:44:18 +02:00
Stefan MaierandClaude Opus 5.5 bf778b6d60 Step the refresh rate down while frames come late (vr.refresh_rate_auto)
In big online games a client's ticks cost 3 to 5 ms (twice what bots or
smaller games do), and the frames with a tick missed 90 Hz's 11.1 ms:
the headset showed them twice, a doubled picture when turning, with the
GPU at 60-80 %. While more than one frame in sixteen comes late, two
two-second windows running, the rate now steps down to the next the
headset offers (90 to 72 Hz); after ten windows in which hardly any
would be late at the rate above, it steps back up, never above
vr.refresh_rate. A load's long frames count for neither. On by default,
"Lower it when frames are late" on the VR options' MORE page; nothing
where the headset offers one rate (SteamVR).

Also the BUTTONS page's notes below its ten rows (the push to talk row
pushed the last one onto them).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:38:59 +02:00
Stefan MaierandClaude Opus 5.5 d8a786f5a1 Merge merge/upstream-oct16: OpenCE's string conversion fix, voice packets checked, the postgame host screen's menu
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:34:03 +02:00
chestahh e7d334c66a Match the legacy size and spacing of gameplay button glyphs
Increase the existing Xbox faces from 20 to 26 HUD units, centre them on the text capital midpoint and compensate for the text renderer's following-run inset. Preserve their original artwork and split-screen scale. Checked the portable Windows OpenCE build and hidden 1280x720 gameplay captures; the Press X to prompt has balanced visible gaps. Retain the repository's rocket reticle unchanged at the user's request.
2026-10-16 16:17:40 +01:00
chestahh f8b3eecb37 Complete the high-resolution Xbox HUD
Upscale the original vehicle/passenger labels, warnings, reload dots and
messaging sheets without changing their glyph layout or sprite coordinates.
Use the existing Xbox controller redraws for A/B/X/Y/Black/White prompts;
other controller symbols retain the original Xbox art through upscaling.
Match the existing high-resolution vehicle meter contour to remove the
double wrench. Restore the rounded diamond sniper reticle, contain the
flashlight fill and assign discrete thresholds to whole ammunition bullets.

The HUD module owns interpolation and tag selection. A narrow texture-cache
hook is needed to obtain decoded original pixels and invalidate generated
textures with their source; the messaging hook selects the existing button
art. Both respect the High-Res HUD setting. MCC's separate draw/texture
paths remain intact. No MCC bitmap data is embedded; label interpolation
was retained at the user's request, with its limited sharpness acknowledged.

Validated Windows portable release build; 246 focused HUD tests; 15,360
real GPU ammunition checks; hidden Xbox HUD captures enabled/disabled;
Xbox/MCC/Xbox transitions at 30 and 120 FPS with no exceptions. Player health
and sniper zoom-label pixels outside the targeted regions are unchanged.
Android/web targets are unavailable in this Windows build configuration;
Custom Edition gameplay data is unavailable, so that path was inspected.
No simulation or network format changes; network version stays unchanged.

(cherry picked from commit 5398553e4a95d7d68376acf023e6ca671066a614)
2026-10-16 16:07:49 +01:00
chestahh adccd47fe0 Restore online pause gradients and campaign Settings
Apply the existing campaign gradient to the three- and four-button online
pause artwork, retaining the translucent footer. Admit full-screen Xbox
network campaigns to the existing Settings insertion while keeping local
split-screen unchanged and preventing network Settings from pausing time.

Checked Windows release build, pause-art and campaign-context harnesses,
and hidden two-peer Xbox multiplayer and co-op sessions. The co-op client
can select and open Settings. Android/web builds are unavailable in this
Windows configuration; no wire-format or simulation contract changed.

(cherry picked from commit 04cd1e25e910c5234fee323033b1efb8c71e8e0a)
2026-10-16 14:35:52 +01:00
Stefan MaierandClaude Opus 5.5 7c294c9c39 Merge OpenCE upstream: its fix of the string conversion (taken over ours), voice packets checked, the postgame host screen's menu
Upstream fixed the Android asm conversion's string bug independently
(d9669f6ee, also from a Steam Frame crash); its converter and tests
replace ours (c85cf8ece), which they cover and extend (quoted names
honour escapes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:05:24 +02:00
bnunu f90d955527 Merge pull request #374 from chestahh/opence-multiplayer-menu-fix
Restore the main menu when leaving the postgame host screen
2026-10-16 00:13:42 -04:00
chestahh 5e228ea9db Restore pause menu artwork, timer digits and multiplayer menu spacing
Restore the pause header and footer bands while retaining Settings, the
campaign body gradient, and the Xbox footer alpha of 191/255. Initialize
the scripted timer scaling flags so stack data cannot shrink its digits.
Move Edit Gametypes and its divider inside the connected panel.

This implementation-only upstream contribution excludes MCC, regression
tests, documentation, optional regeneration commands and workflows. The
complete fixes and tests remain on GulchCE testing and the local validation
branch. Original cherry-picked changes below were combined and scoped to
OpenCE; authorship is retained.

Validated: portable release Windows builds on both bases; 25 upstream
art/timer tests and 52 GulchCE tests before upstream test exclusion; hidden
Xbox and MCC pause captures, both timer HUD settings, and multiplayer menu
captures. Final generator matches all committed pause SVGs. Android/web
targets are unavailable in this Windows configuration; online co-op was
not replayed.

(cherry picked from commit a0c20572f8bfa6af0d2f94c57abef32357750bf0)
(cherry picked from commit c76a3a596f7dbe8ced581b2e6c759aae0baababb)
(cherry picked from commit f0804e1e5c857db6dc84ff16c72d3f8b8abf95a2)
(cherry picked from commit bff920f6a5ec614caf98ff5e8555a0320b0a29f1)
(cherry picked from commit b1b59eb7237d901dba10aa0b5187b184f3f44a7b)
2026-10-16 04:43:57 +01:00
bnunu 085676d827 Merge pull request #346 from startupfoundry/arm64-guest-strings
arm64 and Android guests: string literals kept byte for byte (fixes a voice chat crash)
2026-10-15 23:37:14 -04:00
crowtrx ddfe8b0427 Fix an endless loop asking for the disc image on Linux
On Linux, SDL's file picker needs the desktop portal, which the 32-bit
game reaches through a 32-bit libdbus-1.so.3, or zenity. Where neither is
present, SDL reports an error, but the game took it for a cancelled
choice and asked the same question again, without end.

The game now looks first for a disc image (.iso or .xiso) next to the
executable, names it in the question and extracts it without a file
picker. When no picker can be shown, it says why (put the image next to
the game, or install zenity) and quits. Cancelling the picker still asks
again, and if extracting the image found fails, the next try opens the
picker.
2026-10-15 21:02:35 -06:00
hkmoddandClaude Opus 5.5 7c19ec27ea Drop idle textures after half a minute of time, not 1800 frames
The texture cache dropped a texture once it had gone 1800 frames without
being drawn, and checked every 600 frames: half a minute and ten seconds
at 60 frames a second. At 360 frames a second it is five seconds, so
looking away from part of a level for five seconds dropped its textures,
and turning back converted and uploaded them again; the faster the
display, the more often. On b30 at 357 frames a second, 105 textures were
dropped in 38 seconds of play.

The cache now sweeps every 10 seconds of time (SDL_GetTicks, which the
Android guest and the web have too) and drops the textures not drawn since
the sweep three sweeps before: 30 to 40 seconds after they were last
drawn, which is what it did at 60 frames a second. The frame counter still
orders the palette variants.

Checked with a harness test (tools/harness/tests/test_texture_idle.py)
that runs xgpu_texture_cache_begin_frame from the sources against a fake
clock: a texture last drawn 1 or 25 seconds in is dropped 30 to 40 seconds
later at 360 frames a second, and at 60 as before; on main the 360 cases
fail (dropped after 5.7 and 6.7 seconds) and the 60 one passes. Four
negative controls. Not measured: the time saved, which is small on an
RTX 4080 (each upload took 1.6 ms or less) and larger where conversion and
uploads cost more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 04:37:43 +02:00
hkmoddandClaude Opus 5.5 40c1db8abf Leave the host's bipeds fallen out of the map to the host on a client
biped_check_discard erases a biped that has fallen 2000 units below a
map it has left. On a client the biped is often the host's, whose
delete network_objects_may_delete refuses, and the host stops sending
the state of objects outside the map, so the biped stays there, falling,
and the check logged "is in a bad place ... erasing" every tick: a
co-op client's debug.txt had 6122 such lines for a few flood carriers
in three minutes (up to 3942 a minute), each a localtime, a vsnprintf,
a write and a flush on the game thread. The check now leaves the host's
bipeds to the host, as the falling damage erase already does (49350528).

Checked with a harness test (tools/harness/tests/test_biped_discard.py)
that runs biped_check_discard and network_objects_may_delete from the
sources: the host's biped on a client is neither logged nor deleted over
30 ticks, a client's own biped and the host's are erased once, and one
above the limit is left; without the new condition the client case fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 04:18:34 +02:00
chestahh 4301ca88d6 Restore the main menu when leaving the postgame host screen
The postgame host map picker opens without menu history. Give it the
existing main_menu_if_no_history flag used by the lobby so leaving the
last screen restores the main menu instead of leaving only the background.
Update the generator and generated XML together. Preserve OpenCE's
existing handlers; no MCC code or regression-test files are included.
This local menu change does not require a network version bump.

Resolved the fork cherry-pick against OpenCE main 8102e711 by adding only
the flag override. Windows portable release build passed. Existing menu
teardown, touch and web checks returned 28 passes and 6 failures: missing
m.lib/pthread.h and Windows path separators in POSIX-oriented tests.
Android and web build targets were unavailable locally. Checked generator
round-trip stability and git diff --check. The two simple wrappers were
regenerated through ce_menus.widget_xml from their exported XML fields;
the original PC tag inputs were unavailable. No upstream runtime game
session or Linux build was run. Earlier scripted runtime verification
was performed on the GulchCE implementation, not this upstream revision.

(cherry picked from commit c811c7f527d70d5476578ed88ea0edcef2269967)
2026-10-16 02:51:54 +01:00
Dan PastusekandBridge 2482087ea8 Web index cache written into each frame's own buffer, so Metal does not wait
The index cache (a3277d94) keeps an index array drawn twice the same in
one 8 MB buffer, and draws from it every frame. An array was written into
that buffer the moment it was kept, while queued draws read the buffer:
WebGL on Metal (ANGLE, which Chrome and Safari use on a Mac) waits for the
GPU to finish everything queued before such a write, as #262 found for the
stream and index uploads. While a level is explored arrays are kept every
few frames, so play hitched again and again, worse the longer one played:
one WebGL call of 20 to 40 ms every few frames, and Chrome's GPU process in
-[MTLCommandBuffer waitUntilCompleted] (macOS sample). Resolution and
window size made no difference.

Now there is a buffer for each frame of the ring, drawn from only in that
frame, as #262's chunks are. An array kept is listed, and written into a
frame's buffer as that frame begins (index_cache_begin_frame), when the GPU
is done with it; until its frame's buffer has it, a draw of it uploads its
indices as before. Android and the native builds are unchanged.

Checked on an M4 Mac mini, Chrome 155, b30 with the scripted walking bot
(HALO_TEST_INPUT=bot:1), six 10-second windows from a fresh load each:

  before: 60, 56.2, 57.6, 51.4, 52.6, 51.6 fps; frames over 25 ms
          3, 43, 24, 61, 57, 89; longest 152 ms
  after:  60, 59.9, 59.6, 60, 60, 60 fps; frames over 25 ms
          2, 6, 3, 2, 3, 0 (the same as with the cache bypassed)

The cache still serves 80 to 99% of indexed draws, as before. ninja web
builds; d3d8_gl.c's changed lines compile without warnings under -Wall;
port/web/tests/*.test.js pass, and tools/test_web_build.py passes but for
test_the_sockets_inside_the_page, which fails the same without this change
on macOS (getentropy undeclared in web_net.c, compiled with the host's cc).

Co-authored-by: Bridge <agent@bridgedesktop.com>

Bridge-Chat: #60
2026-10-15 11:55:41 -07: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
Addison Bair 689913a09e Log a frame the graphics driver holds for seconds
A player's game froze for about 48 s at a time, in the menus too, and online each freeze ended in "connection failed"; a dump showed the frame's swap waiting inside the graphics driver. The log now says so whenever showing a frame takes 2 s or more, so such a report points at the driver without a dump.
2026-10-15 06:46:38 +00:00
Addison Bair 9a19c441f7 The server browser says when this network's NAT stopped a join
A game the browser cannot reach shows "Could not reach <name>" for a few seconds. When this machine's NAT gives each destination its own port and no router forwards the tunnel's (what the log already reports), it now shows "Strict NAT, turn on UPnP: could not reach <name>", the advice first so that a long name is what gets cut.
2026-10-15 06:40:39 +00:00
BirchWoodGodandClaude Opus 5.5 d1cbce9e7d Test the latency meter's arithmetic in the harness
tools/harness/tests/test_latency_meter.py takes latency_meter_color from
latency_meter.c and, from network_distributed.c, the round trip timed by
when its tick was sent, its smoothing, the host's silence on a client and
a client's on the host, with the constants they use, and checks them in a
fake world: the colours and bars at 79/80, 149/150 and 249/250 ms; a tick
no longer kept (or never sent) gives no round trip; at most 9999 ms, also
across the millisecond clock's wrap; the first sample as it is, then an
eighth of each difference; a connection problem after more than 2000 ms
of silence, never before the host's first message, after the game or on
the host. Eight negative controls (a bound moved by one, the stale-tick
check or the ceiling taken out, a quarter's smoothing, no game-over test)
each fail their case. Two source checks: the setting is off by default
and the netcode refreshes the meter only with it on, and the scoreboard
draws a known ping alone in its colour.

Checked: python -m pytest -q tools/harness tools/test_touch_menu.py (358
passed, 16 of them these).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ucYZ9cvWn3nrnk7kK3S5X
2026-10-15 01:07:06 -05: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 317b33e406 Test the bots' lobby and local games with bots
tools/harness's bots_lobby takes network_game_server_port_bots from
network_server_manager.c, the players' functions from
network_game_manager.c and the bots' machines and names from bots.c, and
checks who joins a lobby, on which team, who leaves, and that no bot joins
a game other machines may join, another gametype, a game once started or a
lobby with no player. Each of its 8 negative controls (a guard or a team
rule taken out) fails its case.

tools/test_bots.py plays a local Slayer game with bots on Blood Gulch and a
Team Slayer game on Chill Out for 75 s each (debug.network_test "local:",
SDL's offscreen video driver) and checks that every bot walks, the bots
kill and die, the game ends cleanly with no assert, and that a system link
game gets no bot whatever bots.count asks. It needs the retail maps in
assets/maps and skips without them.

Checked: python -m pytest -q tools/harness tools/test_touch_menu.py, 361
passed; tools/test_bots.py, 3 passed in 4 minutes; test_linux_port.py and
the format tests, 214 passed, 8 skipped (Custom Edition maps, perfetto);
test_web_build.py, 10 passed. tools/test_death_timing.py does not compile
with clang 22 here, on origin/main too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ucYZ9cvWn3nrnk7kK3S5X
2026-10-15 00:27:32 -05:00
BirchWoodGodandClaude Opus 5.5 4a275b608d Add bots to local Slayer games (bots.count, off by default)
Computer players for a game played on this computer alone: bots.count (0,
the default, to 15) of them join the lobby of a local split screen Slayer
game, as far as it has room, on the teams bots.teams asks for ("even", or
"against" the players), and follow the players' team changes there. A bot
is the one player of a machine of its own at the top of the machine indices
(113 up), which no real machine takes, so every machine of the game - only
this one - spawns, respawns and scores it as any player.
network_server_manager.c adds and removes them in the pregame;
players_update_before_game takes each bot's action of the tick from
port/linux/game/bots.c in place of its empty queue.

A bot looks for enemies (the AI's line of sight, its skill's reach and
field of view), reacts, turns at a limited speed with an aim error that
settles and leads by its weapon's projectile speed, fires in bursts or a
press a shot, throws grenades, strikes close up and strafes; otherwise it
goes where an enemy was last seen, to a better weapon lying near (the
action button once the game offers the swap), or about the map's starting
locations and item spawns. It walks the paths the campaign's AI finds:
path.c's searches over the walkable surfaces every stock multiplayer map
carries, on a copy of them less those with no room above (Longest's walls)
and those under scenery or machines its path's obstacle avoidance found no
way past (Sidewinder's blast door); stuck, it jumps, sidesteps and looks
again. The searches run in the tick, three a tick at most, so two runs of
the same input play the same. bots.skill sets reaction, sight, aim, turning
and bursts; debug.bot_trace logs each bot's state.

Nothing changes in system link or internet games: no bot joins a game that
accepts remote connections, and bots_update_actions returns unless a local
game runs, so nothing the machines send each other changes and the network
version stays the same. Other gametypes get no bots yet (their goals come
in a later change). The code is BirchWoodGod's, from the Halo CE for PS
Vita port's offline bots, contributed under CC0; this is their core.

Checked (Linux, debug and release builds, SDL's offscreen video driver,
debug.network_test "local:" with debug.test_input "bot:1" as the player):
- 150 s games on Blood Gulch (Slayer, 6 bots; Team Slayer, 7, legendary),
  Chill Out (Team Slayer, 5, heroic), Longest (Slayer, 8), Sidewinder (Team
  Slayer, 9), Hang 'em High (Team Slayer, 5, "against"), Damnation (Slayer,
  6, easy) and Prisoner (Slayer, 15): every bot walked 125-280 units, the
  bots killed 7-118 times and died, teams evened or all against the
  player, and every game ended cleanly with no assert. With
  debug.bot_trace, bots took up rocket launchers, shotguns and sniper
  rifles, and debug.txt had none of the distributed netcode's idle action
  lines.
- bots.count 0: a 60 s local game logs the same lines as origin/main with
  only the network test's local mode (numbers apart), and config.toml
  differs only by the new settings' lines (and the network test's
  comment). bots.count 6 in a system link
  game (host:) and in a local CTF game: no bot joins, nothing logged.
- ninja web and ninja android (Emscripten 6.0.10, NDK r27c) build, debug
  and release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ucYZ9cvWn3nrnk7kK3S5X
2026-10-15 00:27:32 -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
biu788 f648b6cb24 Add third-person camera for Warthog and Scorpion passengers 2026-10-15 12:59:22 +08:00
BirchWoodGodandClaude Opus 5.5 2dea8f6c70 Keep a unicode string that ends in '|' from being read past its end.
parse_unicode_string took the character after every '|' and only then
looked for the 'n' of "|n", so a string ending in '|' skipped its
terminator and was drawn on through whatever followed it, until a zero.
Players' names, server messages and a map's string lists are drawn this
way, so any player can send one. Any other '|' also ate the character
after it, and kept the result of the character before (on a string's
first character, the parser's uninitialized one). The next character is
now taken only when it is 'n'; any other '|' is a character, as
parse_string has it. "|n" is unchanged.

Checked: new harness test unicode_line_breaks takes parse_unicode_string
from draw_string.c; its cases are bar-n-ends-a-line, bar-is-a-character and
bar-at-the-end. On the old code the last two fail, and each of its 2
negative controls (the next character taken whatever it is, the '|' left
with the last result) fails its case. tools/harness and test_touch_menu:
347 passed. ninja linux (debug and release) and ninja web build with no
new warnings; Android not built here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ucYZ9cvWn3nrnk7kK3S5X
2026-10-14 23:57:18 -05:00
John Cranston 27977a87dc Keep Windows borderless mode from taking exclusive fullscreen
NVIDIA's OpenGL driver can treat SDL's popup fullscreen window as exclusive even when no display mode is selected, blanking the monitor on Alt+Tab. Clear popup and topmost styles for borderless windows before context creation and presentation, including after SDL restores them. Explicit fullscreen keeps its existing behavior.

Checked: Windows release build; hidden native window probe covering styles, bounds, focus, repeated application and mode isolation; git diff --check. Live Alt+Tab was not tested. Android and web targets were unavailable; pytest was not installed.
2026-10-14 17:32:16 -07:00
Marco Cilloni a422135709 configure: add support for cross building with Clang+MSVC on Linux 2026-10-14 18:21:49 +02:00
Paul Hontz da22f9b453 Put the crash handler back over a library's at the renderer's device, and report SIGBUS, SIGILL, SIGFPE and SIGABRT too
memory_watch.c's SIGSEGV handler is installed before main (for the crash
report with the null renderer), and the device's memory_watch_initialize
returned early because it was already active. A library that installed its
own SIGSEGV handler in between (a GL driver as the window opened, a sound
library) then took the faults of the pages memory_watch_protect made
read-only: a crash or a hang at the first write to a texture.

memory_watch_initialize now asks which handler is installed and, where it
is not this file's, installs it again, chained to the one found; the
renderer's frame (memory_watch_begin_frame) checks the same, as a library
can install one at any time. A handler that chains back to this one ends
in the default action instead of a loop.

The same report (signal, registers, stack, calls, to the terminal and
debug.txt) now covers SIGBUS, SIGILL, SIGFPE and SIGABRT. Each is handed on
to the handler before it, or to the default action: a fault runs its
instruction again under it, and a signal sent (kill, abort) is raised again;
a sent signal that was ignored stays ignored. The registers are read by
name on x86-64 too, so that the file compiles there for its test.

Checked: tools/test_memory_watch.py (new; memory_watch.c compiled as it is
with a window of the test's own memory) installs a library's handler over
this one, then initialize: the watched write is noted and let through, the
library's handler gets only an unrelated fault (after the report), and the
next frame puts this one back after a handler that jumped out. Each signal
is reported once and ends the process with that signal. The old file, with
the same main (-m32), leaves the library's handler in place and dies at the
watched write. ninja linux builds.
2026-10-14 11:02:46 -04:00
Paul Hontz 313b6c2191 Report a crash on Linux from the start, also with the null renderer
The SIGSEGV handler that writes the crash report (the faulting address
and the calls) to the terminal and debug.txt is memory_watch.c's, and
only the renderer's device installed it (d3d8_gl.c): with
debug.null_renderer, as the network tests run, no device is made, and a
crash said nothing, as a looping sound's crash in null-renderer network
games did. A crash before the device was made went unreported too.
The handler is now installed when the program starts, as the Windows
build's crash handler is.

Checked: a null-renderer game sent SIGSEGV wrote the report to its log and
debug.txt; the crashes of a forced looping sound crash in null-renderer
games were reported with the faulting address.
2026-10-14 11:02:46 -04:00
Paul Hontz c94e7053e1 Stop a looping sound's track when its looping sound is gone, even in the millisecond it started
A carousel network game between two builds, null renderer on both
(the cross-build sync check's calibration), ended with the joining copy's
SIGSEGV. Its core dump (kept by systemd-coredump) shows
update_channel_for_looping_sound reading a NULL looping sound
(sound_manager.c:2912): the track's looping sound had been deleted by
process_looping_sounds (not refreshed since the last frame, around the
respawn), and the track had started in the same millisecond as the
frame (start_time 55674, render_time 55674, ticks_elapsed 0).
refresh_sound skips the tracking of a sound that has not started yet
(start_time >= render_time), and the tracking is what stops a track
whose looping sound is gone; so the track went on to update_channels.
That needs three frames in one millisecond: never on the Xbox, but the
null renderer runs up to 21 (measured in these games). The original's
bug; refresh_sound now stops such a track first.

Checked: a new asset-free test (tools/harness/tests/test_sound_orphan.py)
runs the real refresh_sound in frames in the same millisecond and later,
with a negative control (the test of the start time alone) failing it.
A build that skips every seventh frame's looping sound refreshes crashed
at the same line in 8 of 8 carousel games without the change, and
played 16 of 16 with it; 40+ unforced carousel games played through.
Sounds are each machine's own: nothing the machines send or agree on
changes.
2026-10-14 11:00:50 -04:00
Stefan MaierandClaude Opus 5.5 b5cdd9268c Merge merge/upstream-net29: OpenCE network version 29
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:26:16 +02:00
Stefan MaierandClaude Opus 5.5 34b61fba31 Roadmap: OpenCE network version 29
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:06:41 +02:00
Stefan MaierandClaude Opus 5.5 87fe9fc1ad Merge OpenCE upstream: network version 29, Custom Edition maps keep their names, map sharing hardened
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:04:54 +02:00
MrBruh 8102e711c3 Merge pull request #353 from OpenCommunityEdition/testing
Merge testing into main
2026-10-14 23:21:30 +13: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
MrBruh 37ded3287c Fix harness tests for clang 23 2026-10-14 21:17:03 +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
Paul Hontz 9c70f5f5e3 Voice chat: a packet that is not one 20 ms frame is dropped before decoding
Every build sends one 20 ms frame per packet (voice_audio_encode). A packet
whose table of contents says otherwise, or that Opus cannot parse, is
damaged: voice_audio_play now drops it before the decoder sees it
(opus_packet_get_nb_samples), and the next frame makes up for it as lost.
Cheap defence in depth: Opus's decoder already refuses such packets, and the
crash in decode_pulses came from a valid packet and the guest's garbled
pulse cache, fixed in the assembly conversion.
2026-10-13 22:13:44 -04:00
Paul Hontz d9669f6eef The guest's assembly conversion keeps string literals byte for byte
The arm64 and Android guests are compiled for arm64_32-apple-watchos and
their Darwin assembly converted to ELF (tools/android_asm_convert.py). The
converter took a quoted .ascii/.asciz operand for a quoted symbol name and
dropped a leading "_" from it, as it does from symbols; and its quoted-name
pattern ended at an escaped quote (\"), so the "_" of a word after one was
dropped too. Clang writes byte tables as .ascii strings, so a table holding
95 ("_") lost that byte and everything after it moved down by one.

In Opus that table was CELT's pulse cache (modes.c, cache_bits50): the bits
a band may have are read off it, and shifted by a byte it lets a band have
more pulses than the PVQ tables hold. Decoding a frame from another player
then read past CELT_PVQ_U_ROW in decode_pulses (n 32, k 240) and crashed: a
client on the Steam Frame, in a CTF game on the internet. The guest's own
encoder could crash likewise in encode_pulses. The same build of Opus,
driven with the game's settings and with clean, corrupt, truncated and
random packets and voice_audio_play's loss handling, crashed within a few
thousand frames (and identically at -O0); with the fix it ran 4.8 million
decodes clean, and its encoded bytes and decoded samples are identical to a
native 64-bit build's.

The same loss hit other strings and tables of the guest: musl's iswalpha,
iswpunct and towctrans tables and iconv's charmaps; tomlc17's "_0123..."
digit sets (TOML numbers with "_"); the attract mode's "_de"/"_fr" names,
"_lan_", "_lit" and "_%s"; the progress bar's compressed monk tone and the
DirectSound effects image; and assertion and error texts. All are now
assembled as compiled.

String directives now go through unchanged, and quoted names honour
escapes. tools/test_android_asm_convert.py checks both, and a table of
every byte value through clang, the conversion and the assembler.
2026-10-13 22:13:44 -04:00
Jonas Volman 2e376b992e Reconcile approved single-pixel S3TC decoder (+1 function, zero losses)
Apply only the Q-UP25 delta from de53e9bf, preserving both original-defect BUG comments and inferred length spelling. Fresh canonical checkpoint: +493 meaningful / +496 padded code and +76 data, zero losses; 1663 tests pass, 5 skipped, 159 subtests. Whole-board W3 unchanged at 4091; only s3tc.obj changes. Encoder and object admission remain held.
2026-10-13 17:02:57 -07:00
Jonas Volman ff2e07fac1 Replace parked stristr assembly with exact ordinary C (zero credit)
Use the natural C implementation from punpckhdq/halo 745c66b8. All 621 object fingerprints and inherited exact verdicts are unchanged; the single asm-implemented park is retired. Fresh full checkpoint: 1663 tests pass, 5 skipped, 159 subtests. Current scoring already counted the assembly: no additional function or byte credit.
2026-10-13 16:59:49 -07:00
Paul Hontz dfc738fcc6 Model Detail in the Graphics settings
Video Setup's Graphics screen gets a MODEL DETAIL row, which sets
display.model_detail. Its values are stock, native and highest. Its XML
is written by tools/port_settings.py's settings_files(), as
tools/ce_menus.py writes it; no other screen changes.

Checked: tools/test_web_build.py's test that the menus match their
generator; the screen opened with debug.menu_open shows the row with the
setting's value, above the help.
2026-10-13 18:38:27 -04:00
Paul Hontz 5db4184dc6 Texture Filtering in the Graphics settings
Video Setup's Graphics screen gets a TEXTURE FILTERING row, which sets
display.anisotropic_filtering. Its values are 0 (off, as on the Xbox),
2, 4, 8 and 16. Its XML is written by tools/port_settings.py's
settings_files(), as tools/ce_menus.py writes it; no other screen
changes.

Checked: tools/test_web_build.py's test that the menus match their
generator; the screen opened with debug.menu_open shows the row with the
setting's value, above the help.
2026-10-13 18:38:27 -04:00
Paul Hontz b83a259b53 Models' level of detail can be chosen by the display's resolution
The game chooses a model's level of detail by its size in pixels on the
Xbox's 480 lines, whatever the resolution, so at high resolutions distant
models drop to their simpler levels long before it would show.
display.model_detail (HALO_MODEL_DETAIL): "stock" (the default) chooses as
the Xbox did; "native" by the size on the display's lines (without
supersampling's factor); "highest" draws the models native does, each at its
most detailed level. The first-person weapon and the sky are unchanged.
Android draws 480 lines, so there native is stock.

Checked: the same b30 view at 854x480 and 2400x1315 gave the same sizes in
stock; at 1080p native measures 2.25 times as much and moves the corpses and
weapons up a level, highest puts everything drawn at its top level. The
level chosen is used only to draw (render_model's callers are the objects',
the first-person weapon's and the sky's drawing), so nothing a network game
agrees on changes; stock multiplies by exactly 1. Not checked with ssaa2x or
on a 4K display, nor built for Android or the web.
2026-10-13 18:33:50 -04:00
Paul Hontz c31d985492 Anisotropic filtering, 4x by default
The Xbox game never asks for anisotropic filtering: every sampler is linear
or point with an anisotropy of 1, so floors, walls and terrain seen at a
grazing angle blur into their small mip levels. display.anisotropic_filtering
(HALO_ANISOTROPIC_FILTERING) samples the map's mipmapped, linearly filtered
textures (base, detail, bump and environment maps) along the angle, at most
2, 4, 8 or 16 times and at most the GPU's limit; 0 filters as the Xbox did.
Textures the game renders itself are left alone: at 16x the water's ripple
map turned the far ocean into noise. Lightmaps have no mip levels and are
unchanged.

Checked: a10's cryo bay floor and b30's beach, off, 4x and 16x on the same
frozen frame (grates and sand stay sharp, the water is unchanged); about
+0.08 ms a frame on a10 at 1080p; tools/harness passes. Android and the web
apply it only where GL_EXT_texture_filter_anisotropic exists; not built here.
2026-10-13 18:04:31 -04:00
Stefan MaierandClaude Opus 5.5 238f3e2391 Merge fix/frame-voice-crash: the Android asm conversion keeps string data (the Steam Frame's voice chat crash), push to talk on any button
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:45:46 +02:00
Stefan MaierandClaude Opus 5.5 e18c2b37d6 Push to talk on any button, never sent by a grip reaching for the weapon
Push to talk becomes an action of the BUTTONS page (vr.button_talk),
on an eighth button there, the free hand's grip (its default): it can
trade places with any other action (the move stick's click, say, so
that a player holding a weapon in both hands can talk).

The free hand's grip as a button is pressed once held 0.15 s, at the
threshold halo_vr_two_hand takes the foregrip at, and not at all while
it holds the weapon's foregrip or the Warthog's wheel: taken in that
time, the press is theirs until the grip opens. Before, the runtime's
grip button (which closes before the foregrip's threshold) could open
the microphone for a frame or two as the hand took the weapon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:45:40 +02:00
Stefan MaierandClaude Opus 5.5 c85cf8ece1 Keep string data as it is in the Android asm conversion
android_asm_convert.py renamed identifiers in every directive's operands,
string data included: a string starting with "_" was taken for a quoted
symbol and lost the "_", and one with \" in it was split wrongly, so a
"_" starting a word in it was dropped too. 26 of the guest's objects had
strings or tables cut short. Among them Opus's pulse cache (cache_bits50,
one byte short from entry 338): its encoder picked more pulses than its
tables hold for some bands and crashed (the Steam Frame's crashes in
online games, with push to talk). Also musl's wide character tables,
map sharing's hashed names ("_%s"), the "_lan_" menu check, libtiff's
and the sound code's tables.

.ascii, .asciz and .string are now passed on unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:06:59 +02: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
justinfilip 36ea3fb80c Classify a nav marker before drawing it. 2026-10-13 08:06:03 -05:00
justinfilip 7d128e1bde Return to the main menu from the map list after a match. 2026-10-12 20:50:57 -05:00
justinfilip 521c44f862 Keep screen fog on the canopy when the field of view is widened. 2026-10-12 20:50:57 -05:00
justinfilip ecfee9b0b8 Allow a begin_random list that fills one word. 2026-10-12 20:50:57 -05:00
Gio aa51dfccc6 Port: Allow split-screen play in PC menus
On the "pc" menu style, a LAN game whose players were all on the host's
computer could not start a local splitscreen game. This patch fixes that
check.

START NOW only starts a countdown for a game that takes no remote connections, has two machines, or
is "alone" (server_alone). server_alone only passed if there was literally one player, making splitscreen play impossible.
Note the "xbox" menu style did not have this bug since its splitscreen
didn't accept remote connections at all.

server_alone becomes server_players_on_one_machine: TRUE if there is at
least one player and every player is on the same machine.
One player still qualifies. Machines that have joined without a player are not
counted, so a lone host's countdown is not stopped when one connects (the
per-tick server_ok_to_countdown check).

The countdown is fully controlled by the host machine, so there is no
change to the network protocol.

There's an existing test failure on test_elevator_prediction which also
exists on main, ignore.
2026-10-12 20:13:42 -05:00
jcompto95-techandClaude Opus 5.5 ad8b012089 Video Setup: an Extra Effects screen for the visual modifiers
Video Setup gets a third category, EXTRA EFFECTS, beside Graphics and FOV
and Viewmodels: a screen for the visual modifiers the stock game does not
have, so Graphics keeps to how the game is drawn. Shield Glow and Glow
Intensity move to it from Graphics; the settings themselves, their
defaults and their names are unchanged.

The screens were written again with tools/port_settings.py; only Video
Setup, Graphics and the new screen changed, and menus.json lists the new
screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtk9vPUKbWd48oBMzXuzd
2026-10-12 18:28:03 -04:00
Stefan MaierandClaude Opus 5.5 01076509dd Merge fix/vr-frame-hitches: OpenCE network version 28, a shader worker and cache, graphics "medium" by default
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:04:19 +02:00
Stefan MaierandClaude Opus 5.5 0f6aa62eda VR: graphics "medium" by default; the PTC Quest's doubled pictures noted
On the developer's Quest 3 the picture doubles when turning with every
build back to 2026-10-06's, at 90 and 72 Hz, even when the compositor
reports no stale frames; that Quest runs Horizon OS from the Public Test
Channel. "medium" is smooth there, so it is the default until a Quest
outside PTC has tried "high" (roadmap). An unknown vr.graphics is
"medium" too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:04:03 +02:00
Stefan MaierandClaude Opus 5.5 5696244b1c VR roadmap: graphics "auto", what the effects cost, and its headset test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:18:04 +02:00
Stefan MaierandClaude Opus 5.5 1c7adac6e5 VR: graphics "auto", effects left out while the GPU falls behind and brought back after
On the Quest 3 the shader worker took away the long frames, but a 15-bot
game on Blood Gulch still had seconds at a time of 9-13 ms of GPU a frame
(GPU% 0.91-0.94, 4-37 stale frames a second, the picture doubled), between
spells of 5-7 ms; "medium" held 90 Hz.

- vr.graphics "auto", the new default (the VR options' Graphics row has
  Auto): "high", less what the GPU's time asks to leave out. host_xr.c
  reads XR_META_performance_metrics' app/gpu_frametime each frame (the
  metrics are now on wherever the runtime has them; logged only with
  vr.log_performance), handed to the guest by host_xr_gpu_time.
- vr_graphics.c: past four fifths of the frame (8.9 ms at 90 Hz, scaled
  with the rate) for three quarters of a quarter second's frames, the next
  steps are left out, as many as their known costs say the excess takes:
  the shield's glow, object shadows, reflections, specular light, lens
  flares, bump maps, the rest of "low". Steps that leave out nothing
  drawn (the glow without display.shield_glow) are passed over. A step's
  cost is learnt from the GPU time before it and the second after it
  settled. An effect comes back once the GPU time plus its cost stays
  under seven tenths of the frame for 3 s; an effect that had to go again
  within 10 s of coming back makes the wait twice as long, up to a minute,
  back to 3 s after a minute without leaving anything out. Each change is
  logged with the GPU time.
- Where the headset does not tell the GPU's time (SteamVR: the Steam
  Frame) "auto" draws everything, as "high" did.
- shield_glow.c draws no glow while "auto" leaves it out.
- debug.vr_graphics_measure leaves each effect out in turn (1.5 s on, 1.5 s
  off) and logs its cost by the GPU time, with means every round.

Checked: the APK builds, android_abi_check finds nothing, the desktop and
web paths compile; a host simulation of vr_graphics.c with a lagging
synthetic GPU time (heavy spells of 12 ms, effects of 0.3-2 ms) left out
two or three steps within 0.6 s of a spell and brought them back 3 s
after it, longer after a second spell; the measure mode recovers the
simulated costs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:17:41 +02:00
Stefan MaierandClaude Opus 5.5 2df0c9bd12 VR roadmap: the Quest's measurements, the shader worker and its headset test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:02:52 +02:00
Stefan MaierandClaude Opus 5.5 482a5ecf3a Android menus: pictures decoded on a thread of their own
A menu screen's first frame decoded its PNG pictures on the game's thread,
about 15 ms each ("7 menu pictures in 108 ms"): in VR a frame shown late.
Now a picture not yet decoded is asked of a decoding thread and drawn
transparent for the frames that takes; the game's thread only uploads the
decoded texels. Forgotten art's results are dropped by generation.
Decoding them all ahead was not done: 165 pictures, about 350 MB decoded
with their mipmaps.

Checked: the APK builds; desktop and web keep decoding as before (the
thread is the app's only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:58:33 +02:00
Stefan MaierandClaude Opus 5.5 d396eedb61 Android: shader programs made on a worker thread; every map's programs asked for as a map loads
The shader cache took most programs from their binaries the second time a
map was played, but the user still saw doubled pictures in VR: 29 new
programs in one frame 22 s into a match (794 ms), single ones at 18-64 ms,
and draws of 254 ms and 74 ms with nothing compiled at all (most likely
the driver making a program's variants at its first draw).

- port/android/host/host_gl_worker.c: a thread with an EGL context that
  shares the game's (surfaceless, or on a 1x1 pbuffer) makes the programs
  asked for: from the file's binary, or compiled and linked from the
  sources (its binary handed back for the file). It sets the samplers'
  units, draws once with each program into a small target of the eyes'
  kinds (RGBA8 with depth and stencil, plain and two-layer multiview) and
  puts a fence after it. The game uses a program only once the fence has
  passed (glClientWaitSync, never waiting). Urgent requests (a draw needs
  the program) go before the rest and can hurry a queued one along.
- d3d8_gl.c: a draw whose program is being made is left out for the frames
  it takes (draws_skipped_waiting), instead of the frame waiting; a lit
  program's wait no longer marks the lighting failed. A program's uniform
  setup is its own function, run when the program arrives.
- xgpu_program_cache.c: jobs by key in place of the ready table. As a map
  loads, its own list is asked of the worker, then every map's programs
  (shader_cache/all.txt, a union kept as any map draws), eight a frame, so
  a map first played or a weapon first fired mostly finds its programs
  made. The worker's compiled programs' binaries are written two a frame.
  Without the worker (display.shader_worker false, or no shared context)
  the map's programs are made on the game's thread as before.
- debug.shader_warm_up: "worker" (default), "load" (the map's programs
  waited for, up to 3 s, and drawn with on the game's thread as the map
  loads, for comparing) or "off".
- vr.log_performance: draws are timed (the CPU's counter); a long frame's
  line gives the draw calls' time, the slowest draw with its vertex
  program and program key, and the draws left out waiting; the 5 s report
  counts the draws left out.

Checked: the APK builds, android_abi_check finds nothing, the desktop and
web paths compile; a host harness with a mock worker (first run, second,
a first-played map, driver change, no worker, "load"); on the emulator
(SwiftShader, which refuses kept binaries, so all compiled there) the
worker runs on a pbuffer, the menus' load asked the other maps' 63
programs, Blood Gulch found 69 of 73 made, and 4-bot games ran with the
worker, with "load" and without the worker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:58:33 +02:00
Stefan MaierandClaude Opus 5.5 d46e3fcb38 VR: foveation off by default, its "high" level taken out
On the Quest 3 (graphics "high", 90 Hz, multiview, a 15-bot game on Blood
Gulch) the app's GPU time was 5.5-6.4 ms with foveation off, low, medium
and high alike: the GPU is not what makes the frames late. "high" also
drew a grey rectangle below the player. vr.foveation is now "off" by
default with "low" and "medium" left to try; an unknown value is off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:34:57 +02:00
hkmoddandClaude Opus 5.5 0c3e9f9353 Show the PC menus' text in the language of the Custom Edition's loc.map
Each Custom Edition (English, French, German, Italian, Spanish, Japanese,
Korean, Chinese) has the PC version's menu string lists in its language
in loc.map, by the tags' names. The menus showed the English of
strings.xml whatever edition the maps came from. Now each <strings> names
its PC tag (map=) and each <string> that is the PC version's its index
there; menu_tags.c takes those strings from the loc.map in custom_maps
(or the Custom Edition install's maps folder), read through a
bounds-checked reader in custom_edition_cache.c. The strings the port
adds or rewrites have no index and stay English. A setting,
display.menu_language ("maps", "english"), keeps English for whoever
wants it.

strings.xml is written by ce_menus.py as before; port_settings.py's
OVERRIDE_ORIGINALS maps the lists the port rewrites to the PC strings
they keep. The text without the new attributes is unchanged.

The text is drawn by text_hires.c, as the English is. The pictures with
words (titles, the main menu's items) are still English. CJK editions'
characters are not in Overpass and fall back to the map's font.

Checked: the Windows build has no warnings. With an Italian Custom
Edition's loc.map, all 1164 strings with an index come from it, and the
Multiplayer, Settings and Item Options screens show Italian, accents
included (window hidden, debug.menu_open). With "english" the same
screens are as before (without a loc.map the code takes the same path,
not run). test_linux_port.py's menu tests pass. Android and the web were
not built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:34:03 +02:00
Stefan MaierandClaude Opus 5.5 8b7857c070 Android shader cache: no format check of its own, and a refused binary logged
The emulator's GL (ANGLE under gfxstream) lists ANGLE's binary format but
hands out binaries of format 1, so every kept binary was passed over before
the driver saw it. The driver checks a binary itself (it made it, the same
driver by the hash); the first three it refuses are logged with the format,
size, GL error and info log, and the cache's start names the driver's
format.

Checked on the emulator with SwiftShader: programs kept (4 MB for the menus
and Blood Gulch), the binaries refused there (gfxstream's do not outlive
the process), so the map's programs were compiled from the kept sources as
it loaded, up to the two seconds, the rest as drawn; a 4-bot game ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:52:48 +02:00
MrBruh 6d64abdb42 Fix map sharing restart overflow and harden map validation 2026-10-13 07:43:41 +13:00
Stefan MaierandClaude Opus 5.5 0989fe8b91 VR roadmap: frame hitches and GPU time (foveation, metrics, shader cache), with a headset test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:35:32 +02:00
Stefan MaierandClaude Opus 5.5 a79dab53d1 Android: a program binary cache, and a map's programs made ready as it loads
The Quest's driver compiles and links a shader program the first time it
is drawn with, 10 to 30 ms each, which the frame waits for: a long frame
at a map's start (14 programs, 256 ms) and near its end (61 in 780 ms),
seen in VR as stale frames.

- port/linux/src/xgpu_program_cache.c: a linked program is written to
  shader_cache in the data folder, named by its sources' hash, with its
  binary (glGetProgramBinary), the driver that made it (GL_RENDERER and
  GL_VERSION) and both sources. Asked for again it is loaded with
  glProgramBinary when the driver, the sizes, the checksum and the sources
  agree; otherwise compiled as before and written again. A refused binary
  only falls back to compiling.
- d3d8_gl.c makes its shaders with their sources and compiles them only
  when a program of them must be linked (shader_make, program_make), so a
  program from the cache costs no compile. Each program entry keeps its
  cache key and lists itself once per map.
- Each map keeps the programs it drew with (shader_cache/maps/<map>.txt,
  written every 20 s while it grows and as the map goes). As a map loads
  (game_initialize_for_new_map, a port: hook) its programs are loaded from
  their binaries; after a driver update compiled again from the kept
  sources, in the background with GL_KHR_parallel_shader_compile where the
  driver has it (polled each frame, binaries written two a frame), else
  for at most two seconds of the load.
- display.shader_cache (true) turns it off; the folder is emptied past
  three quarters of 192 MB or 12000 files at the start, and may be deleted.
- VR's long frame line now also says how many programs came from the cache
  and what textures were uploaded (and decoded from DXT: the Quest has no
  S3TC), menu pictures decoded and render targets made, and in how long,
  for the menus' long frames that compile nothing.

Checked: the APK builds; the desktop and web paths compile; a host harness
with a mock driver went through the first run, a second run (all loaded),
a driver change (compiled from the kept sources, with and without
background compiling), a corrupt and a truncated file (compiled again) and
a missing list. On the emulator (no binary formats) the cache logs itself
off and a 4-bot game on Blood Gulch runs as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:34:37 +02:00
Stefan MaierandClaude Opus 5.5 52ca15dcf0 VR: fixed foveated rendering of the eyes on the Quest (vr.foveation)
The Quest 3 at 90 Hz with graphics "high" spends 5-9 ms of GPU a frame
on the eyes and shows stale frames (ASW, doubled pictures as the head
turns); "medium" holds, so the frame is short of GPU time. The eyes are
now drawn at full detail about each lens's centre and coarser towards
their edges with GL_QCOM_texture_foveated (the Quest's Adreno 740), where
the lenses blur them anyway.

- vr.foveation "off", "low", "medium" (the default) or "high", a row
  on the VR options' MORE page (dim where the GPU has no foveation, as the
  Steam Frame's zink); read as it changes.
- The focal point of each eye is its lens centre, tangent 0, in the
  picture's coordinates: off the middle, the Quest's views being
  asymmetric. Full detail within sqrt(1 + area) / gain of it (0.69, 0.58,
  0.43 of the half width), down to a least density of 1/2, 1/4, 1/8.
- Only the eyes' targets: the multiview layers, which nothing else draws
  into; one eye at a time, the back buffer only from the eye's camera to
  its copy into the headset's image, so the HUD, menus, scope, crosshair
  and names drawn into it after stay at full detail. A texture is first
  foveated when the setting is not off (it cannot be undone); "off"
  afterwards sets the gain to 0, full detail everywhere.
- The GL ES log line names foveation, GL_KHR_parallel_shader_compile and
  the program binary formats the driver has; the first foveated target
  logs the extension's features and focal points, vr.c the level and the
  lens centres.

Checked: the APK builds; the desktop and web paths compile (no foveation
code outside HALO_ANDROID). Not tried on a headset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:34:22 +02:00
Stefan MaierandClaude Opus 5.5 aa7a490d1f VR: the compositor's performance counters logged with vr.log_performance
XR_META_performance_metrics was asked for its counters with room for 16,
and the Quest 3 has more, so the call failed and the metrics stayed off
("xr: performance metrics off (0 counters)"). The counters are now
enumerated with the two-call idiom (up to 48 kept), sampled every third
frame and logged every five seconds as mean/most, named by their last two
path parts (app/gpu_frametime, compositor/dropped_frame_count), so the
app's GPU time and the stale frames show beside the game's own report.
They are enabled only with vr.log_performance (host_main.c passes it on as
HALO_XR_LOG_PERFORMANCE), as is the copies' timing that came with them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:26:59 +02:00
hkmoddandClaude Opus 5.5 2fb25f8101 Merge main (build-201) into the HDR branch
Main's base vertex extension (#273) changed the same lines of gl.h and
gl_initialize as this branch's half float render targets. Both are kept:
the extension's optional entry points and base vertex detection, then the
half float check, and the log line names both.

Checked: the tree is the same as this branch rebased on main; the Windows
release build has no warnings; a 20-second bloodgulch run with HDR and GL
debugging had no GL errors; the menu generator test passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:25:22 +02: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
Stefan MaierandClaude Opus 5.5 57beb4c2a7 VR: log long frames with their phases and shader compile time
A frame two of the headset's frames long logs its input, ticks, camera,
draw and present times and the shaders compiled or linked in it, so the
hitches seen as stale frames can be told apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:03:09 +02:00
Steve RiceandClaude Opus 5.5 beb5fe7d35 Give a z-sprite's fragments the depth its DOT_ZW stage computes
nv2a_psh.c translated the DOT_ZW texture stage to a dot product and dropped the depth the stage replaces the fragment's depth with. Every z-sprite particle (a bullet impact's puff, for one) was depth tested as its flat card, so the surface it stands on cut it in half along a straight line through the hit point.

The game's effect shader (rasterizer_xbox_transparent_geometry.c, the z-sprite branch) feeds stages 2 and 3 so that their quotient is the depth of a point up to the sprite's radius nearer than its card, shaped by the second map, in the depth buffer's units (16777215 for the 24-bit buffer). The translated shader now writes that quotient over 16777215 to gl_FragDepth. Only shaders with a DOT_ZW stage change their text.

Checked on a macOS fork of this translator (OpenGL and Metal, a30, assault rifle fired at the lifeboat's floor): puffs are whole, and decals and hit positions are unchanged. Not built or run on this tree's Linux, Windows, Android or web builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011KLToxkJE32PPuVNUQEe9a
2026-10-12 10:35:38 -07:00
eltanschauung 1f47875802 Add extra weapon and pickup options for co-op hosts
Offer Per Player, Static Multiplier and None for authored campaign weapons, grenades, health kits, overshields and camouflage. Share amount rows with extra enemies, cap pickups at 8x, retain ammunition and checkpoint counts, and only place copies on clear nearby ground. Use ordinary host-created replicated objects without changing the wire protocol.

(cherry picked from commit 6f3a75507690a03382759f0f1b70c13e38abc851)
2026-10-12 12:43:46 -04:00
eltanschauung ddc75bc62b Use an exact two-thirds modifier for extra vehicles (#306)
Scale extra growth by the integer ratio 2/3 in both modes before rounding down. Default 50% Per Player now yields the first extra vehicle with four players. Update setting descriptions and count expectations. All 25 focused vehicle/menu tests and the Windows build pass.

(cherry picked from commit c1881dd5e73fa49b45c3b29b7eb74b9ba9e08b7c)
2026-10-12 12:37:39 -04:00
eltanschauung 9e435d02cf Halve extra vehicle growth before rounding down (#306)
Apply a flat 0.5 modifier to additional vehicles in per-player and static modes. Update generated setting descriptions and documentation. Verified all 25 focused vehicle/menu tests, including counts from 1 to 128 players, lifecycle checks and a negative control for the modifier. The combined Windows build and isolated startup test pass.

(cherry picked from commit c3a54123390e56603b4001fbba2ae94f450981c6)
2026-10-12 12:37:39 -04:00
eltanschauung 68f0d2d303 Add extra vehicle options for co-op hosts
Offer Per Player, Static Multiplier and None. Register authored campaign vehicles, anchor delivered cargo after ground contact, and replace player-driven vehicles after both bounding spheres clear the home with a 15 percent buffer. Keep host authority, pad collision checks, allocation limits and checkpoint budgets. Replicate ordinary vehicles without changing the wire protocol.

(cherry picked from commit 75abf60ed676b0b5b8a20de4f82068c9a3ba281f)
2026-10-12 12:37:39 -04:00
jcompto95-techandClaude Opus 5.5 30ddad86f6 The overshield rises up the body and stays lit, in its own color (display.overshield_effect, off)
With display.overshield_effect (Video Setup > Graphics > Overshield, off
by default: the game's own), this machine draws the overshield's flare
rising up the body from the feet as it charges (2 seconds; 3 from a
shield that was down), then lit, whole, for as long as it lasts, its
energy at 30% of its speed, in its own color (the powerup's, 253, 78,
116) whatever shield color its player chose; its glow rises and stays
with it, and with display.viewmodel_shield it comes up the first-person
arms. A hit on it plays what the projectile plays on an Elite's shield,
where that only shows and sounds. The rise is a height fade in the vertex
shader (d3d8_gl_set_height_fade).

Reworked after review: the setting the earlier version removed is back,
off by default, so the stock overshield is unchanged unless a player
turns it on. Only drawing and sounds change; no network change (the wire
format check passes). Checked: the changed units compile for Linux; menus
regenerated (only Graphics changed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtk9vPUKbWd48oBMzXuzd
2026-10-12 06:59:37 -04:00
MrBruh 52e33a391e Harden map sharing and Custom Edition map loading 2026-10-12 22:26:25 +13:00
Stefan MaierandClaude Opus 5.5 9e1eef683a VR roadmap: up to date with OpenCE, network version 28
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:23:08 +02:00
Stefan MaierandClaude Opus 5.5 7c6fa07cb5 Merge upstream 2026-10-12: network version 28, map sharing, pregame lobby, body and BSP fixes
Conflicts:
- rasterizer.h: upstream raised the transparent geometry groups to 2048
  as we had (halo_port_capacity.h); theirs.
- d3d8_gl.c: the ES capabilities' log with upstream's base vertex and our
  multiview.
- p2p.c, p2p_internal.h, p2p_signal.c: upstream's several joins and
  reconnect, with our join codes' state and the codes' and public list's
  updates each pass.
- network_server_manager.c: a machine downloading the map is not refused,
  and the bots' room logic stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:23:08 +02:00
MrBruh b1ef7f7e7a Merge pull request #311 from OpenCommunityEdition/testing
Merge testing into main
2026-10-12 21:20:11 +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 6578c3b84a Merge pull request #274 from startupfoundry/config-sections 2026-10-12 20:46:50 +13:00
MrBruh 5587c856f8 Merge pull request #273 from bmanturner/opence/base-vertex-extension 2026-10-12 20:46:50 +13:00
MrBruh dda756582e Merge pull request #272 from bmanturner/opence/p2p-resume-after-stall 2026-10-12 20:46:49 +13:00
MrBruh 785e865a87 Merge pull request #302 from hkmodd/fix/limp-body-corrections 2026-10-12 20:46:49 +13:00
MrBruh 5930a46f23 Merge pull request #294 from hkmodd/fix/titles-under-menus 2026-10-12 20:46:49 +13:00
MrBruh bbe00a4049 Merge pull request #286 from hkmodd/fix/hires-text-fit 2026-10-12 20:46:49 +13:00
MrBruh 3cd1a6622e Merge pull request #305 from hkmodd/perf/debug-model-vertices-static 2026-10-12 20:46:49 +13:00
MrBruh e71e6514f3 Merge pull request #304 from hkmodd/fix/transparent-geometry-groups 2026-10-12 20:46:49 +13:00
MrBruh 9fbbf6a628 Merge pull request #303 from hkmodd/perf/shader-key-compare 2026-10-12 20:46:49 +13:00
eltanschauung 3804028ab5 Reset every live input queue after checkpoint loads
Cutscene skipping in co-op restores a checkpoint. The legacy reset assumed player queue slot zero existed and dereferenced NULL when it was empty. Iterate the rebuilt live queues and restore each update counter, including remote players. Empty arrays are safe. The old reset fails six regression cases; all 278 standalone CPU tests pass with the fix. Message formats and network version are unchanged.
2026-10-12 03:41:05 -04:00
MrBruh 3d67397cdd Record the wire format at network version 28 (#253)
The testing branch is at version 28, whose changes (holding back the
players of a machine still downloading the map, the multiplayer scripted
BSP, lights past the Xbox limits, readying a second weapon) change no
message, kind, bit or struct the record holds; only its version changes.
2026-10-12 20:35:23 +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 ce5e382b37 Merge pull request #262 from machinsk/web-metal-buffer-waits 2026-10-12 20:34:15 +13:00
MrBruh 065d7f2b1d Merge pull request #263 from addisonbair/pregame-lobby 2026-10-12 20:34:15 +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
hkmoddandClaude Opus 5.5 eea1c3641a Keep the debug model vertices off the stack
rasterizer_debug_model_vertices declares 2048 debug vertices of 64 bytes
on the stack. render_model calls it for each part it draws, and the
release build inlines it there, so render_model's frame was 136,304
bytes, and the Windows build probed it a page at a time (_chkstk, 33
pages) for every model drawn, although the debug display is off. Its
vertices are static now: only the vertices below debug_vertex_count are
read, each written when it is added, and the function runs on the
render thread only, so nothing it draws changes.

Measured on Windows (7800X3D, RTX 4080, release build, vsync off), b30
co-op with 32 times the enemies and a bot, 75 s a run, five runs of each
build alternated: render_model's frame is 5,360 bytes (the PDB's
S_FRAMEPROC); _chkstk fell from 2.0% to 0.5% of the game thread's
samples; fps over windows with 2000 draws a frame or more, origin/main
128, 141, 139, 134, 107 (median 134), this change 140, 139, 144, 141,
132 (median 140). Linux, Android and the web build do not probe the
stack, so the change only shortens their frame. They were not built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 03:18:12 +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
hkmoddandClaude Opus 5.5 2c6c85cf38 Raise the transparent geometry groups a frame from 384 to 2048
Every transparent thing a frame draws takes a group: each transparent
part of a model, each widget (light volumes, antennas, flags, glows),
each batch of dynamic geometry (particles, contrails), each transparent
surface of the structure. The Xbox's 384 were a limit the native builds'
larger pools of objects, widgets, effects and particles could pass: a
frame past it was drawn without the rest, and debug.txt said "too many
transparent geometry groups". A part refused a group has no sort record
(models.c checks the group's index), so nothing else changes.

Checked on Windows in b30 co-op with eight times the enemies and a bot,
75 s a run: origin/main overflowed in 3 runs of 4; a build counting the
groups asked for showed up to 464 in a frame, while the dynamic triangle
and vertex buffers peaked at 123 and 328 of their 1024 and the dynamic
triangles at 10272 of 49152, so they are left as they are. With this
change, the same count showed 391 in a frame and no overflow; two more
runs had none. The pool is 2048 * 160 bytes, malloc'd; the model order
pass keeps 14 bytes a group on the stack (28 KB). Harness: the
transparent_plane_order tests pass at the new size (natively on Windows;
the other 44 failures there are the same on origin/main, POSIX headers).
Android and web builds not built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:55:40 +02:00
hkmoddandClaude Opus 5.5 1068da0c3a Compare shader keys and draw uniform inputs a word at a time
Every draw compares its pixel shader key (256 bytes) with the recent and
cached keys, and its uniforms' inputs (77 words) with the last draw's.
The Windows build's memcmp (vcruntime) compares them a byte or a word at
a time and was the game thread's largest single function: 7.9% of its
samples in a b30 co-op game with 32 times the enemies (about 3900 draws
a frame), 5.3% from fragment_shader_get and 2.2% from the uniforms.
words_equal has no early exit, so that clang compares several words at
once (four 512-bit compares a key with -march=native).

Measured on Windows (7800X3D, RTX 4080, release build, vsync off), five
runs of each build alternated, 75 s each, b30 co-op x32 with a bot:
fps over windows with 2000 draws a frame or more, origin/main 123, 139,
135, 123, 122 (median 123); this change 142, 147, 138, 149, 131 (median
142). A sampled run of this change shows memcmp gone and words_equal at
1.2%. The keys are memset before they are filled and have no padding,
so word equality is byte equality. Android and web builds not built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 02:27:50 +02:00
bnunu 173248fd8e Merge pull request #299 from innerhat-dev/overshield-drain-testing
An overshield's drain is not a shield hit, and a second weapon is readied
2026-10-11 20:02:20 -04:00
bnunu 5181d33b49 Merge pull request #298 from innerhat-dev/menu-row-spacing-testing
Menu rows keep one vertical step
2026-10-11 20:02:09 -04:00
bnunu a33bf2d537 Merge pull request #297 from innerhat-dev/coop-respawn-fallback-testing
Respawn a large co-op fight beside the group after ten seconds
2026-10-11 20:01:58 -04:00
bnunu caad6e772a Merge pull request #296 from innerhat-dev/multiplayer-sky-bsp-testing
A multiplayer sky script no longer reloads the level
2026-10-11 20:01:43 -04:00
bnunu 4f59a20a01 Merge pull request #295 from innerhat-dev/multiplayer-flashlight-testing
A multiplayer match keeps the flashlight on
2026-10-11 20:01:21 -04:00
bnunu 9633a7c34a Merge pull request #293 from innerhat-dev/coop-grav-lift-testing
Bring a co-op team inside with a scripted teleport
2026-10-11 20:00:40 -04:00
justinfilip 2598dbe8b6 An overshield's drain is not a shield hit, and a second weapon is readied. 2026-10-11 18:34:17 -05:00
justinfilip 679bade466 Menu rows keep one vertical step. 2026-10-11 18:34:16 -05:00
justinfilip 0b4a03561b A large co-op fight respawns beside the group after ten seconds.
The wait only counted teammates the map called grounded, so jumping, riding, or standing on a beach left it with nobody to use. A late joiner after a checkpoint uses the same wait when nobody is safe to spawn beside.
2026-10-11 18:34:15 -05:00
justinfilip b71bd6a0fa Keep a multiplayer map's first scripted BSP switch so the host's game type chooses the sky. 2026-10-11 18:34:14 -05:00
justinfilip c3365b1c69 A multiplayer script that switches BSP to cycle the sky no longer reloads the level. 2026-10-11 18:34:14 -05:00
justinfilip 417de7e2ad A multiplayer match keeps the flashlight past the Xbox player and vehicle limits. 2026-10-11 18:34:13 -05:00
hkmoddandClaude Opus 5.5 5e5cb08820 Test that a scenario's titles stay under the menus
An asset-free test (tools/harness) of the real cinematic_render, taken
from cinematics.c with its structures, over a fake scenario with one
title for the whole game, as Extinction's: it is drawn in the game, not
while a menu is up and again once the menu is gone, and its time goes on
under a menu, so it ends when it would have. Two negative controls (the
title drawn over a menu, its time stopped under one) each fail their
case.

Checked: in archlinux:latest with CI's packages (clang 23.1.1,
lib32-glibc), python -m pytest -q tools/harness passes (271); the new
cases fail on origin/main's cinematics.c ("the title was drawn over a
menu in frame 0").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 01:30:09 +02:00
justinfilip d6df944886 Bring a co-op team inside with a scripted teleport.
Campaign scripts only name two players, so a large group was left on the beach when Truth and Reconciliation's grav lift switched into the ship.
2026-10-11 18:27:24 -05:00
bnunu 0b2c6759bb Merge pull request #289 from innerhat-dev/late-join-same-checks
Adding a player after a map download uses the same checks as any other late join
2026-10-11 18:27:47 -04:00
justinfilip baab0780f6 Adding a player after a map download uses the same checks as any other late join. 2026-10-11 17:25:11 -05:00
bnunu 5fccdda2b1 Merge pull request #288 from innerhat-dev/lobby-map-for-testing
A downloading player's body waits until they join, and custom_maps is created when missing
2026-10-11 18:08:02 -04:00
justinfilip 05dd9ccdea Create the custom_maps folder on startup when it is missing. 2026-10-11 17:03:37 -05:00
justinfilip 67f2214219 A player still downloading the map no longer spawns until that player joins. 2026-10-11 17:03:37 -05:00
hkmoddandClaude Opus 5.5 13ddcf802a Keep a scenario's titles under the menus
A scenario's titles (cutscene titles, their text from the scenario's
ingame_help_text) are drawn with the screen's overlays, which come after
the players' windows and so after their menus. Some Custom Edition maps
show their name with one for the whole game: Extinction's "Extinction",
at the bottom right, was drawn over the pause menu and over the profile
screen's CANCEL button. cinematic_render already leaves out the letterbox
while a menu is up; it now leaves out the titles too. Their time goes on
as before, so a title still fades out when it would have.

Checked: the Windows build (clang 23.1.3, full LTO) without warnings;
Extinction from the Italian Custom Edition in a hidden window, START
pressed by a local test input after 14 seconds: the title is drawn in
the game and not over the pause menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:37:59 +02:00
hkmoddandClaude Opus 5.5 7a6fc70eb9 Fit the high-res text's fonts to the widths of a Custom Edition loc.map's letters
The high-res text sizes Overpass by the font tag's capitals and centres
each glyph on the tag's character width, and Overpass was chosen for the
Xbox maps' widths. A Custom Edition loc.map's fonts are narrower: the
Italian one's large_ui and small_ui are the same font, its capitals 10
units tall and its letters 87% as wide as Overpass's at that height
(against 95 to 98% for the Xbox maps' interstate, large_ui and small_ui),
so the menus' and HUD's letters ran into each other. Each font is now
narrowed until its letters A to Z and a to z are, summed, as wide as the
tag's; the Xbox maps' fonts are narrowed by 2 to 5 hundredths.

The fonts were also remembered by the tag's name, and the Xbox maps and a
loc.map have tags of the same names with other measures, and by the font
tag's address, which the next map's tags may reuse: they are remembered
by their measures too, and forgotten with the map's tags.

Checked: the fonts' widths measured from the Italian loc.map and the Xbox
ui.map against Overpass's advances; the Windows build (clang 23.1.3, full
LTO) without warnings; Extinction from the Italian Custom Edition, whose
large_ui is now drawn 87% wide, its letters apart, and the main menu's
small_ui 98% wide.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:16:04 +02:00
bnunu 86be005acc Merge pull request #283 from innerhat-dev/lobby-countdown
A player joining or leaving the lobby no longer restarts the countdown
2026-10-11 16:36:27 -04: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
justinfilip 71ceb363f1 Players who already have the map no longer wait for a machine that is still downloading it. 2026-10-11 13:37:40 -05:00
justinfilip 2059b6cef5 A player joining or leaving the lobby no longer restarts the countdown. 2026-10-11 13:37:40 -05:00
hkmoddandClaude Opus 5.5 1b496ca7de Keep a browser's window 8-bit with display.hdr_output, and name NVIDIA's setting
The web build reads the desktop settings, so display.hdr_output would
ask a browser's canvas for floating-point pixels it does not have. The
web build now keeps the window 8-bit, as Android does; the renderer
already keeps its scRGB path off under HALO_GLES.

With NVIDIA's driver on Windows, an OpenGL window with floating-point
pixels in fullscreen or borderless turns black, then oversaturated,
unless the NVIDIA Control Panel's "Vulkan/OpenGL present method" is
"Prefer layered on DXGI Swapchain". The README says so.

Checked: the Windows build; display.hdr_output on an HDR OLED display
with that setting. The web build was not built here (no Emscripten).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:31:43 +02:00
hkmoddandClaude Opus 5.5 8b2efdb4f2 Add HDR and HDR DISPLAY to Video Setup's Graphics screen
display.hdr and display.hdr_output could be set only in config.toml or
the environment. Two rows on the Graphics screen, after Glow Intensity,
set them as the screen's other rows set theirs; HDR DISPLAY is shown on
the desktop platforms only, as the setting applies there only. The
category's help names HDR.

Checked: the two generated screens are the only files that change, and
tools/test_web_build.py's check that the menus match their generator
passes; the Windows build opens the screen (debug.menu_open) with both
rows above the help.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:31:43 +02:00
hkmoddandClaude Opus 5.5 c65fd673d8 Document the HDR settings in the Linux README
The settings table gains display.hdr, display.hdr_exposure,
display.hdr_tone_mapping and display.hdr_output; "What operates" gains
"HDR", which says what the back buffer holds, where the tone mapping
runs and what the HUD and the menus keep; the game source changes name
the tone mapping in render/render.c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:31:43 +02:00
hkmoddandClaude Opus 5.5 e7a85652f3 Add opt-in HDR rendering with tone mapping (display.hdr)
The back buffer and its multisampled renderbuffer become GL_RGBA16F
(GL_HALF_FLOAT on Android) when display.hdr is on, so blended light past
white is kept instead of clipped. A tone mapping pass over each window's
3D view, before the anti-aliasing and the HUD, brings it back into range:
neutral (identity below 0.8, a hue-preserving shoulder above), ACES
(Narkowicz) or extended Reinhard, after display.hdr_exposure. It writes
colour only, leaving the game's destination alpha alone.

The display blit becomes a draw that clamps to 1, or, with
display.hdr_output on desktop, writes scRGB to a floating-point window
up to the display's HDR headroom (SDL's window HDR properties); a driver
without such a window falls back to 8 bits. Android turns HDR on only
with ES 3.2 or GL_EXT_color_buffer_(half_)float and caps MSAA at the
samples RGBA16F allows. Settings are read between frames; toggling
display.hdr recreates the back buffer's storage.

Checked: the Windows build (clang, no warnings); bloodgulch and a30 with
MSAA, FXAA and SMAA, toggling each of the settings while playing with
HALO_GL_DEBUG, without GL errors; the frame rate unchanged, the tone
mapping pass 0.04 ms at 1080p; neutral leaves the picture as without
HDR below 0.8; display.hdr_output on an HDR OLED display. The Android
guest's sources pass clang -fsyntax-only for arm64_32; neither Android
nor the web build was run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:31:43 +02:00
Paul Hontz 6c8842c376 NVIDIA on Wayland: X11 first when there is no 32-bit egl-wayland
NVIDIA's EGL draws into a Wayland window through egl-wayland (or
egl-wayland2), which distributions ship 64-bit only: Arch's
lib32-nvidia-utils has libEGL_nvidia and libGLX_nvidia, and no package has
a 32-bit libnvidia-egl-wayland. SDL 3.4 prefers Wayland where the
compositor has fifo-v1 (KDE Plasma 6, GNOME), and its OpenGL there is EGL,
so the 32-bit game fell back to Mesa's llvmpipe: drawn on the processor,
a flickering window that never finished loading. On X11 (XWayland) SDL's
OpenGL is GLX, which the 32-bit driver has.

With NVIDIA's module loaded (/sys/module/nvidia), a Wayland session, and
neither libnvidia-egl-wayland.so.1 nor libnvidia-egl-wayland2.so.1
loadable by this process (the names NVIDIA's EGL loads them by,
/usr/share/egl/egl_external_platform.d), SDL now tries x11 before
wayland, and the log says so. SDL_VIDEO_DRIVER, when set, still decides.
OpenGL on llvmpipe or softpipe is also logged as drawn on the processor,
and port/linux/README.md has "NVIDIA on Wayland".

Only the 32-bit Linux build: the arm64 build's host is 64-bit.
2026-10-11 14:05:48 -04:00
Paul Hontz 1e7946659c config.toml with each section once, and a file with [display] twice read again
The settings table has display.viewmodel_shield, first_person_legs and the
shield's glow and color after the game settings, so the defaults the game
wrote had a second [display] after [game]. TOML refuses a table defined
twice: every start since logged "table defined more than once; using the
defaults", and no change to the file took effect. Only a file written
fresh since those settings came in has it; an older one gets them added
to its own [display] (config_add_missing).

The defaults now put each section once, with all of its settings, in
whatever order the table has them. A file that has a section twice gets
the later parts joined to the first when it is read, and is written back
that way once it parses.

tools/test_linux_port.py builds port_config.c on its own and checks both.
2026-10-11 13:57:23 -04:00
Brendan Turner dc9b842ce2 Draw with a base vertex on OpenGL ES 3.0 and 3.1 with the extension
The vertex and index mirror draws with glDrawElementsBaseVertex, which the
renderer enabled only on OpenGL ES 3.2. ANGLE on Metal and many Android
drivers report ES 3.0 or 3.1 and offer the function as
GL_EXT_draw_elements_base_vertex or GL_OES_draw_elements_base_vertex, so
they rebased and uploaded each index array at every draw.

base_vertex is now on for ES 3.2, or when the context lists either extension
and its entry point loads; glDrawElementsBaseVertex then points at
glDrawElementsBaseVertexEXT or glDrawElementsBaseVertexOES. A function that
cannot be loaded leaves the existing path. The capabilities log line says
which source is used. The two suffixed names are in a new optional list in
gl.h (never a load failure); android_gl_stubs.py generates guest stubs for
them, and the host resolves them like the other hostgl_ imports.

Affects Android and ANGLE-based Apple ports. The web build is unchanged: it
does not load the optional functions and WebGL 2 lists neither extension.

Checked: ninja android (guest and host) and ninja web build. On an iPad Pro
(M2, ANGLE on Metal, ES 3.0) through an iOS port that runs this Android
guest, the log reads "base vertex 1 (GL_EXT_draw_elements_base_vertex)",
Blood Gulch plays, the draw counts are unchanged and the vertex data
streamed each frame falls by about a third (e.g. 148 KB to 95 KB, 52 KB to
35 KB at the same spawn points). Not run on Android hardware.
2026-10-11 12:26:14 -05:00
Brendan Turner 0a5a235f5e Ask STUN and the brokers again after the p2p thread was stopped
When the process is suspended (a phone app in the background) or the
machine sleeps (a laptop), the p2p thread's wait lasts far longer than it
asked for. The NAT's mapping of the tunnel may have lapsed and the brokers'
TCP connections may be gone, but nothing notices until each one's silence
timeout, so joins and listings fail for a while after the game resumes.

When a wait lasts longer than its length plus PEER_TIMEOUT, the thread now
discards what the sockets reported, asks the STUN servers for the public
address again, and has every broker close and connect again at once
(p2p_signal_reconnect). A wait of normal length changes nothing.

Nothing sent over the network changes, so HALO_PORT_NETWORK_VERSION stays.

Checked: the Android guest and the web build compile with no new warnings.
The same change, together with an iOS host fix for the sockets iOS takes
from a suspended app, made server browser joins work on an iPad Pro after
about five minutes in the background (they failed until a relaunch
without both). Not run on Linux, Windows or Android.
2026-10-11 12:09:43 -05:00
Jonathon MachinskiandClaude Opus 5.5 568f8c1fe7 Web draws' uploads put in buffers no queued draw reads, so Metal does not wait
WebGL on Metal (ANGLE, which Chrome and Safari use on a Mac) waits for the
GPU to finish everything queued before it writes a buffer that a queued
draw reads, unless its shadow buffers are on, and Chrome leaves them off.
The renderer wrote its stream and index buffers after each draw, some 2000
times a frame, so each frame waited for the GPU as many times. On an M1
Pro in Chrome the Silent Cartographer ran at 7 to 9 frames each second and
Blood Gulch at about 22, at any resolution (640x480 too): Chrome's GPU
process spent half its time in -[MTLCommandBuffer waitUntilCompleted]
(macOS sample), and --enable-angle-features=useShadowBuffersWhenAppropriate
alone tripled the rate.

Now, on the web, each draw's vertices and indices go into a buffer that no
queued draw reads: web_chunk_get takes one from pools of power-of-two sizes
(4 KB to 32 MB), one set for each frame of the ring, and a buffer is written
again only when its frame comes round again. A draw counter (draw_serial)
tells stream_reserve and index_upload when the current buffer may be read
by a draw. Android is unchanged.

Checked on an M1 Pro, Chrome 155, 3024x1626, uncapped, A/B against
e0b581b1: b30 6.1 to 170 frames each second, Blood Gulch 57 to 595; with
vsync, 60 throughout. Screenshots of Blood Gulch before and after are the
same. ninja web builds without warnings; tools/test_web_build.py and
port/web/tests/*.test.js pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:12:42 -06:00
Addison Bair 59a74b0c4b The pregame screen opened through the port's menus everywhere
Fast setup and the network test's join opened ui.map's pregame screen by name, where ui_widget.c opens the port's lobby; a ui.map of only the menus' tags hosts and joins with both.
2026-10-11 14:08:51 +00:00
Stefan MaierandClaude Opus 5.5 3c0d860c10 VR roadmap: arm swinging (online too) and its sprint (offline), the 2048 groups in main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:30:52 +02:00
Stefan MaierandClaude Opus 5.5 9f084155d0 CI: the web pages job off without a second condition
The merge left the pages job two if: keys, which made the workflow
invalid; it is off (if: false) as the web job is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:13:47 +02:00
Stefan MaierandClaude Opus 5.5 6fdee4fabb Merge merge/upstream-2026-10-11: OpenCE network version 26, its first-person legs off in VR
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:12:19 +02:00
Stefan MaierandClaude Opus 5.5 74bdffb957 Merge upstream 2026-10-11: network version 26, first-person legs and shield flare, web port, fixes
Conflicts:
- render_objects.c: upstream's first-person legs and our VR body share one
  node_matrices (ours no longer shadowed it); upstream's legs never in VR
  (first_person_legs_wanted: set back behind a flat view's eye, they would
  stand behind the player; the VR body is vr.body "full").
- AndroidManifest.xml: upstream's ':game' process left out (the Steam
  Frame's Lepton waits for the app's process to end; the headsets have no
  Java heap over the Xbox window); HaloApplication kept, idle without it.
- xinput_sdl.c: the touch controls and the web port's, never in VR; the
  controllers' rumble in VR before the phone's.
- d3d8_gl.c: multiview's uniforms and upstream's blend constant, both.
- build.yml: Android only; upstream's web and pages jobs off.
- README.md: ours; upstream's in docs/OPENCE_README.md.
- menu_tags.c: our VR pause-list fit dropped: upstream now spaces the rows
  30 apart and moves the button hints below them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 14:04:08 +02:00
Stefan MaierandClaude Opus 5.5 7566f49f5e CI: builds for main only
Every pushed branch built (and mailed its failures); the branches are tried
on the headsets from local builds, and only main makes releases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 13:50:56 +02:00
MrBruh e0b581b157 Web: disclaimer footer 2026-10-11 23:03:29 +13:00
MrBruh 17a57832d8 Windows: tag block accessor builds again
msvc_comdat.c defines __inline as weak on Windows, which a static function cannot be; the accessor is __inline__. Checked by compiling msvc_comdat.c for i686-pc-windows-msvc.
2026-10-11 22:45:33 +13:00
MrBruh 5cd65a0cb0 Web: meter blend in the shader
WebGL refuses blendFunc(CONSTANT_ALPHA, CONSTANT_COLOR), so the meters (ammo counters) drew with the previous blend and a GL error each frame.
2026-10-11 22:38:11 +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
eltanschauung 2689fcbe25 Add multiplayer host aim assist options
Let hosts block optional mouse magnetism without overwriting local preferences or altering controller, touch or bullet autoaim. Replicate a validated reliable policy on ready and change; raise the network version to 27. Validation: 9 focused CPU tests, Windows compilation, native two-peer policy changes and Player Options persistence/Cancel.
2026-10-11 02:43:00 -04:00
MrBruh 6399ed9d4a Text drawn a string at a time, web immediate draws packed
Scoreboard text on the web: 1.41 to 0.41 ms a frame. The network test's browse joins the fullest game that is not co-op.
2026-10-11 16:16:35 +13:00
MrBruh 01afcb654a First-person shield flare kept close only with display.viewmodel_shield (#254) 2026-10-11 15:13:49 +13:00
MrBruh 3fa1f348a9 Merge pull request #254 from chestahh/codex/opence-viewmodel-shield-fit 2026-10-11 15:05:51 +13:00
MrBruh f1c31076ff Merge pull request #252 from addisonbair/bots-hardware-id 2026-10-11 15:05:51 +13:00
MrBruh 0bfe211289 Web: stable pages hashed less often, fewer clock reads
18-player Blood Gulch in Chrome at 1080p: 135 to 225 FPS.
2026-10-11 14:59:19 +13:00
chester 2e45259bea Delete tools/test_viewmodel_shield.py
not required for upstream commit
2026-10-11 01:59:40 +01:00
chestahh 248210cedb Keep first-person shield flares close to the arms 2026-10-11 01:52:48 +01:00
MrBruh 19f1bd9f5d Web: pointer lock taken back on a click, wheel notches 2026-10-11 13:47:22 +13:00
MrBruh c89eb80d3a Web: no return addresses, SIMD page hash
Blood Gulch in Chrome: 43 to 242 FPS.
2026-10-11 13:21:14 +13:00
MrBruh 6dc423fdbf Internet play with browsers
Network version 26: signalling says each machine's WebRTC.
2026-10-11 12:46:28 +13:00
MrBruh 887f40bd2c Web opaque canvas 2026-10-11 12:46:28 +13:00
MrBruh 47b7eac9a8 Web rooms over WebRTC 2026-10-11 12:46:28 +13:00
MrBruh 4a9e515d54 Web touch controls 2026-10-11 12:46:28 +13:00
MrBruh 532407588e Web loading indicator 2026-10-11 12:46:28 +13:00
MrBruh a55a182086 Web deploy, tests, docs 2026-10-11 12:46:28 +13:00
MrBruh acfa9a2a4d Web build 2026-10-11 12:46:28 +13:00
MrBruh 095973133e Shield color on the first-person arms too (#244) 2026-10-11 12:25:51 +13:00
MrBruh 6ac7e3c3d7 Merge pull request #244 from jcompto95-tech/shield-color 2026-10-11 12:19:34 +13:00
MrBruh 1f48f0555d Shield glow: one light per unit in split screen, light show not clamped (#247) 2026-10-11 12:18:47 +13:00
MrBruh 79bcc86066 Merge pull request #247 from jcompto95-tech/shield-glow 2026-10-11 12:14:25 +13:00
MrBruh f735ca152d First-person legs: one mesh kept, its waist forgotten on a new map (#243) 2026-10-11 12:11:51 +13:00
MrBruh 03d054aa6e Merge pull request #243 from jcompto95-tech/first-person-legs 2026-10-11 12:06:18 +13:00
MrBruh 21db5f9065 Vehicle homes: no other home's vehicle taken, no ridden one deleted (#249) 2026-10-11 12:05:13 +13:00
MrBruh 098ca66a72 Merge pull request #249 from innerhat-dev/vehicle-teleporter-name 2026-10-11 11:59:05 +13:00
MrBruh ebb1f093ae Campaign pause rows 30 apart, as the PC version's (#246) 2026-10-11 11:58:34 +13:00
MrBruh e04079b4bd Merge pull request #246 from innerhat-dev/campaign-pause-spacing 2026-10-11 11:55:27 +13:00
MrBruh fb97e52663 Stale player handles forgotten in the port's loop (#250) 2026-10-11 11:54:50 +13:00
MrBruh dcb06d83e3 Merge pull request #250 from eltanschauung/fix/coop-stale-player-handle 2026-10-11 11:51:38 +13:00
MrBruh d03f6e2777 Merge pull request #248 from jcompto95-tech/viewmodel-shield
Energy shield effects visible on view models (on a toggle)
2026-10-11 10:45:31 +13:00
MrBruh 2c4b216a8b Merge pull request #251 from innerhat-dev/hud-digit-bitmaps
Draw a custom HUD digit from the bitmap it names
2026-10-11 10:34:24 +13:00
MrBruh a5b9ddda6a Merge pull request #238 from bertoaldo/coop-elevators
Fix co-op clients falling through moving elevators
2026-10-11 10:33:48 +13:00
MrBruh 0fe3f13e43 Merge pull request #240 from bnunu/halo-ce-universal/settings-new-profile
The main menu's Settings with no player profile asks for a new one's name
2026-10-11 10:33:20 +13:00
MrBruh e6c31e3404 Merge pull request #239 from bnunu/halo-ce-universal/custom-maps-listing
List the Custom Edition map folders through a handle of their own
2026-10-11 10:32:57 +13:00
Addison Bair 0f9a8904cd Test the stand-in machines' messages against network_messages.c
test_linux_port.py takes a stand-in machine through a join (join request,
settings request, player, precache, loaded, game update) and checks each
message's number against enum network_game_message_type and its size
against the fields network_messages.c decodes, so a change to a message
fails CI rather than every machine's join. It fails on the script before
the previous commit (a join request of 80 bytes where 112 are decoded).
2026-10-10 20:35:30 +00:00
Addison Bair 77794aa1b7 Stand-in machines send the join request's hardware id
Since the join request carries the joining machine's hardware id (32 hex
digits after the join token, for bans and kick votes), the host refuses
one without it ("server failed to decode a message_client_join_game_request
packet") and closes the connection, so tools/system_link_bots.py could not
join any machine. Each machine now sends its own number as its id, so the
host tells them apart.

Checked on build 177: 14 machines join a Blood Gulch game (16 players with
the host's and a client's), spawn and play it out; with the script before
this, every machine is refused.
2026-10-10 20:35:30 +00:00
justinfilip a3c139ec53 A custom HUD digit that is its own bitmap no longer trips the check that every digit shares one bitmap. 2026-10-10 15:33:11 -05:00
eltanschauung 6ea142ceee Fix co-op stale player handle crash 2026-10-10 15:30:31 -04:00
justinfilip af7ad15d3c Keep a respawned vehicle on the name its teleporter uses.
The timer was leaving the named vehicle on the pad and creating another on top of it. The home now follows that named vehicle, and a pad that already has one does not get a second.
2026-10-10 13:25:53 -05:00
jcompto95-techandClaude Opus 5.5 1463c87090 Energy shields glow as they flare, in their own color (display.shield_glow)
Video Setup > Graphics > Shield Glow, off by default; Glow Intensity:
DEFAULT or LIGHT SHOW (three times as far and as bright).

As a unit's shield flares (hit, charging, falling), it lights what is around
it, and itself, as a plasma bolt does: a Spartan's gold, an Elite's blue,
any unit's its shield shader's own edge color. The glow's strength is the
flare's, the intensity the shield plasma is drawn with (its intensity
source, an object function, to its exponent), so it rises and falls with
the flare seen on the body, and fades with active camouflage.

port/linux/game/shield_glow.c chooses up to 8 units within 25 world units of
the camera each frame. object_lights.c gains port glows: ordinary dynamic
lights attached to an object's node, kept alive, colored and sized each
frame by their maker (light_port_glow_set) and gone a moment after it stops,
drawn with a light definition the map already has (one without a lens flare
or a flashlight's flags, a plasma one where there is one): no new assets.

Only the drawing changes: nothing the game simulates, nothing sent between
machines. Checked by compiling the changed units.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtk9vPUKbWd48oBMzXuzd
2026-10-10 13:56:38 -04:00
justinfilip 2af955e1d6 Space the campaign pause rows like the profile settings list.
Adding Settings centred the last row onto the B and A prompts. The rows now use that list's pitch, and the box grows so the prompts stay under the last row.
2026-10-10 11:46:10 -05:00
WhiteBulL 0f3ceb9eab refactor: worked on comments a little bit more 2026-10-10 19:39:39 +03:00
jcompto95-techandClaude Opus 5.5 8664d2f24b Your energy shield in a multiplayer armor color (display.shield_color)
Video Setup > FOV and Viewmodels > Shield Color: DEFAULT (the shield's own)
or one of the 18 multiplayer armor colors (player_profile.c's table: white,
black, red, blue, gray, yellow, green, pink, purple, cyan, cobalt, orange,
teal, sage, brown, tan, maroon, salmon), whatever the armor's color.

No new assets: the shield's own plasma shader is drawn, its perpendicular
and parallel colors replaced by the chosen color at their own brightness
(their brightest channel), so its noise, animation and intensity are the
shader's. A local player's unit is drawn with a copy of its change colors as
the modifier's colors (render_object_list); rasterizer_plasma_energy_draw
knows that copy again and replaces the shader's colors only for it. Plasma is
added to what is behind it, so black is drawn as a faint grey.

Only this machine's drawing of its own players' shields changes: others see
your shield as their own settings draw it, and nothing sent between machines
changes. Checked by compiling the changed units.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtk9vPUKbWd48oBMzXuzd
2026-10-10 10:30:19 -04:00
jcompto95-techandClaude Opus 5.5 8ee3806792 The energy shield's flare on the first-person arms (display.viewmodel_shield)
Video Setup > FOV and Viewmodels > Arm Shields, off by default. The stock
game draws a unit's shield flare (its modifier shader, the cyborg's shield
plasma, as its shield is hit, charges and falls) on the body only; the
first-person arms never showed it. With Arm Shields on, the hands model is
drawn with the unit's own modifier shader and the unit's own function values
and change colors, as render_object_list draws the body: the same flare, on
the arms. The first-person weapon is drawn without it.

The flare is a first-person transparent group, so it is drawn in the
viewmodel's projection (display.viewmodel_fov) and lines up with the arms.
The setting is read in view_fov.c with the other viewmodel settings.

Only the drawing changes. Checked by compiling the changed units.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtk9vPUKbWd48oBMzXuzd
2026-10-10 10:30:18 -04:00
jcompto95-techandClaude Opus 5.5 d36c15c313 Your own legs in first person, seen looking down (display.first_person_legs)
Video Setup > FOV and Viewmodels > Legs, off by default. In first person the
player's own body, which the game animates as everyone else sees it but does
not draw for its own player, is drawn from the waist down: looking down,
your legs walk, run, strafe and crouch as the body's animation moves them.

How it is drawn (port/linux/game/first_person_legs.c):
- A copy of the body's pose as drawn: everything but the hips and legs (by
  node name: pelvis, thigh, calf, foot, toe) folded into a lid over the
  waist, the root kept.
- The body set back behind the eye, each frame, as far as keeps the waist's
  open rim beyond the bottom plane of the view (the way Halo 2's fp_body
  keeps its open top out of sight): looking ahead, 4 cm; straight down at
  105 degrees, about 67 cm. The legs are seen as they swing forward.
- Drawn with the world's camera, so it keeps its place and size at any FOV;
  not in mirrors, not in the shadow pass, without what it carries.

The waist's rim is found from the model's own triangles: object_mesh.c reads
an object's drawn mesh (its most detailed geometry, in the permutations it
shows) from the vertex and index buffers the game draws it from, as
powerup_render_bounds.c reads them; its cache is forgotten on a new map
(objects_initialize_for_new_map).

Only the drawing changes: the body, its hit boxes, markers, the camera and
everything sent between machines are the game's. Checked by compiling the
changed units; the fold and set-back were checked against a capture of the
cyborg's mesh and pose and played on Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtk9vPUKbWd48oBMzXuzd
2026-10-10 10:30:16 -04:00
WhiteBulL 39fcfdafaf feat: resampler linear mode
Fixed read past a buffer issue and
untied previous solution from reverb.
Also added config variable audio.resampler
(but no UI implementation since currently
there is no more space for that option).
2026-10-10 17:21:08 +03:00
Stefan MaierandClaude Opus 5.5 e09af359f3 CI: Android only, the Linux and Windows jobs off
Ringfall: Mjolnir VR ships the Android app (Quest, Steam Frame) only. The
desktop jobs stay in the workflow, off (if: false), so that upstream's
changes to them still merge; the release and its notice wait on Android
alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:03:06 +02:00
Stefan MaierandClaude Opus 5.5 7d3533e7b9 Merge feature/vr-voice-and-body: the whole body in VR, push to talk on the free hand's grip
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:41:48 +02:00
Stefan MaierandClaude Opus 5.5 bb2f2b19a1 VR roadmap: step 3b, the whole body, done on the Quest
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:41:45 +02:00
WhiteBulL 4afb59660a Disable global audio muffling when reverb is false
previous incorrect implementation
2026-10-10 14:20:09 +03:00
Stefan MaierandClaude Opus 5.5 486f182fec VR body: the legs walk the way the player moves, the feet on the ground
- Moving, the hips and legs are no longer turned to the torso's heading:
  they face as the game faces the body, so the game's own choice of
  animation (forward, back, aside, as for any player, by the way it moves
  against the way it faces) runs along the way the player moves; the spine
  turns the torso. Standing, the hips turn to the torso's heading again,
  eased (0.12 s). Walking off, the feet ease from where they stood into the
  walk (0.2 s); stopping, they stay and step into place.
- The feet rest on the ground: as they are put down a ray down under the
  body and one under each foot (one more as a foot steps; none walking)
  find the ground, read only; each foot keeps the animation's height above
  it, tilts with the slope, and a foot's ground below lowers the hips, up
  to 45 cm, eased in and out. Nothing found: level.

Checked flat (debug.vr_body): strafing, backwards and diagonally with the
touch stick, the game's sideways and backwards walk; on a Blood Gulch base's
ramp the feet 15 cm up and 10 cm down, both on it; a two-minute bot game
without errors. 0.02 to 0.1 ms a pose in the emulator, a few rays a second
at most. Roadmap: the new behaviour and its headset tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:07:47 +02:00
Stefan MaierandClaude Opus 5.5 313668e0da VR roadmap: step 3b, the whole body, built on the branch
What was built, its settings (vr.body "full", debug.vr_body), what the
emulator showed, and the headset test list for the Quest 3 and the Steam
Frame, with the branch's push to talk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:34:55 +02:00
Stefan MaierandClaude Opus 5.5 22f9d03742 VR body: the neck at its height, the stance reset after a jump, mirrors
- The body is moved down for its neck to be where the head's pivot puts
  it (its model has the neck 4 cm higher standing, 9 cm crouched by the
  button: its shoulders came up by the eyes); the physical crouch still
  sends the hips back.
- Feet more than 80 cm from their planted place (a teleporter) are put
  down at once rather than stepping across.
- A mirror's view no longer decides the frame's pose.
- debug.vr_body moves the flat eyes round the neck as the head pitches,
  as a real head's go.

Checked flat: standing and crouched by the button (touch Crouch), looking
down: shoulders below the eyes, torso upright standing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:34:07 +02:00
Stefan MaierandClaude Opus 5.5 de509fb1a2 VR body: the neck shrunk with the head, the collar closed below the eyes
The neck's own skin stayed around its root, 20 cm below and 14 cm behind
the eyes: its open collar could be seen into looking down. Shrunk onto its
root with the head, the collar closes (checked flat, debug.vr_body back and
down: no stretched skin).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:25:34 +02:00
Stefan MaierandClaude Opus 5.5 378f8bf897 VR: the whole body, vr.body "full": torso, hips and legs under the arms
The player's own biped is drawn in first person among the world's objects
(it sorts against the world by depth; the first-person weapon and arms,
drawn first into the stencil, stay over it), posed from a copy of its node
matrices once a frame (port/linux/game/vr_body.c): turned to the torso's
heading, the neck below and behind the eyes as the head turns about it, the
hips down with the physical crouch and part of the way with a lean, the
spine upright to the neck, the feet planted while standing and stepping
after the body as it turns, the knees bent by the arms' two-bone IK. The
head and the third-person arms are shrunk to nothing, the weapon and what
the biped carries left out; the first-person arms hang from the body's
shoulders (mirrored with the weapon left-handed). A passenger in an open
vehicle sits as the seat's animation has it; at a wheel or a gun the body
is drawn as before, in a closed seat none. Nothing of the game's own pose,
hit boxes or network messages changes.

The neck pivot, the planted feet and the leg's clamped reach follow
HaloCE-Quest-VR (CC0), rebuilt for this port's skeleton and caches.

vr.body gets "full" (VR options, Body: "Full body"); "arms" stays the
default. vr.log_performance reports the body's pose. debug.vr_body shows
the body flat (the emulator): from the eyes, looking down, or from the
front, side or back, the head turning, crouching or leaning on its own.

Checked in the emulator: the cyborg's 19 nodes found, the body posed
standing, crouched, leaning and turning (feet stepping), 0.006 ms a pose;
two-minute bot games on Blood Gulch with it on and off, no errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:22:55 +02:00
MrBruh 9da2a45ffa The weapons' reticles drawn from the PC HUD sheet's exact matches
The reticles of the PC HUD sheet's drawing that match the Xbox's exactly
(14 of them, in HUD2_left_reticles/xbox_reticle_matches.md) take the
place of the reticles' redraws: each copied unchanged, at the size and
place that match its Xbox sprite best (tools/hud_assets.py reticles; a
likeness of 0.97 to 1.00). They also give high-res textures to two
bitmaps that had none: the rocket launcher's and wraith's reticle, and the
sniper rifle's, whose bitmap holds its zoom labels too; no redraw has the
Xbox's 2x and 10x, so they are set in Overpass 900 (of the project's
fonts the one that fits them best), sized, placed and spaced to match.
The warthog's and the scorpion's keep their redraws: only variants of
them are on the sheet. The build places a redraw at any scale, and the
layout step runs again (a name hid its match function).

AUTHORS.md credits the high-res HUD's redraws to MrBruh, and the reference
photos they are based on to Aerocatia.

Checked: each texture beside the Xbox bitmap; a build reproduces the
PNGs; ninja linux and android; in a game on Blood Gulch the plasma
pistol's reticle draws from its new texture, and all 71 HUD textures are
found there.
2026-10-10 22:46:32 +13:00
Stefan MaierandClaude Opus 5.5 53e13fc69d VR: voice chat's push to talk on the free hand's grip, a Voice chat row
Upstream's voice chat talks while controls.push_to_talk is held, a key. In
VR the free hand's grip is push to talk while it holds neither the weapon's
foregrip nor the Warthog's wheel (it is no button of the game's there).
The VR options' PLAY page gets Voice chat: push to talk (grip), open mic or
off (audio.voice_chat); the controller page names the grip's talking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 11:29:32 +02:00
Stefan MaierandClaude Opus 5.5 697a85f1cb Merge merge/upstream-2026-10-10: OpenCE network version 25, in-game settings, touch controls (off in VR), FOV options (off in VR)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 11:11:50 +02:00
Jonas VolmanandClaude Opus 5.5 3dcacc9530 The main menu's Settings with no player profile asks for a new one's name
With no player profile, Settings opened nothing and showed "event handler
'new campaign decision' failed", where Campaign, Multiplayer and Profiles
open the new profile's name screen. Reported on Android; the menus' code is
every build's, and the Windows build did the same.

Settings runs the PC version's "profile set edit begin", which only failed
with no profile. The screens its <conditional> then opens run the game's
"new campaign decision", and that makes a profile only once "new campaign
chosen" has asked for its name: the other items' "new game if no plyr
profiles" asks, and "profile set edit begin" did not.

- "profile set edit begin" with no profile, in the main menu, asks for the
  name (the game's "new campaign chosen") and fails as before, so that the
  same screens make the profile. A game's pause menu runs the function too
  and has no such screens: there it only fails, as before.
- "new campaign decision" is the port's for the menus' widgets: it runs
  the game's, and for Settings' screen then begins editing the profile
  made. The PC version's screens open Settings with nothing more run, and
  Settings closed at once with no profile being edited ("close if not
  editing profile"). For the other items' screens it is the game's alone.

The menus' files are as tools/ce_menus.py writes them: nothing in them
changes, nor in the game's sources.

Checked on a Windows build, by keys posted to the game's hidden window, on
save folders with no profile: Settings opens the name screen, then
"Creating player profile", then Settings with the new profile being
edited; the name screen left without a name goes back to the main menu
with no profile made (and the game's "new campaign decision" failure shown,
as for the other items); Profiles makes its profile and opens the profile
list as before. With a profile, Settings opens at once. The pause menu's
Settings with no profile does nothing, as before. The menus' checks of
tools/test_linux_port.py pass. Not run on Android or Linux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 01:40:27 -07:00
Jonas VolmanandClaude Opus 5.5 a845bbf55d List the Custom Edition map folders through a handle of their own
custom_edition_maps_look_for listed custom_maps and the install's maps
folder with find_files_start and find_files_next, which keep their place
in one global (find_files_globals). As the game starts, the menus' thread
for the file system's checks (filesystem_initialization_thread_proc in
ui_widget.c) makes the default gametypes' folders with
directory_create_or_delete_contents, which deletes a folder's files with
the same two functions. A map list looked for meanwhile takes that
listing's place, and the thread then deletes the files of the folder being
listed: the player's Custom Edition maps.

The menus take no input until that thread is done, so it takes a start
that asks for the maps at once. Seen with debug.menu_open set to the
Select Map screen
(main_menu/multiplayer_type_select/mp_map_select/mp_map_select_screen) on
a Windows build of c9e4f13f, with six files in custom_maps: one was left.

Each folder is now listed with FindFirstFileA and FindNextFileA on a
handle of the function's own. The same start, twice, left the six files
and listed the three maps among them. Not run on Linux or Android.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 01:37:01 -07:00
MrBruh c9e4f13fdb Never change a don't-dump ART mapping, and test the mincore fallback anywhere
Splitting the window's slice off ART's large object space restores it with
MADV_DODUMP; on a space already marked don't-dump the split does nothing,
the read fails, and the restore then cleared the flag on the slice and
split ART's mapping for good. The space's VmFlags are now read first, and
such a space is left as it is. A slice with swap of its own is reported as
in use (objects are there), and each failure says what failed.

debug.halo.art_overlap takes -nopagemap (pagemap treated as refused) and
-swapped (the stand-in's objects paged out with MADV_PAGEOUT), so the
mincore fallback and the slice's swap can be tested on any device.

Checked on a Pixel 9 Pro XL: a normal start, and idle, busy, idle-swapped,
idle-nopagemap, busy-nopagemap, idle-nopagemap-swapped (the reclaim goes
ahead: swap in the space, none in the window) and busy-nopagemap-swapped.
On Linux, a space marked don't-dump stays one mapping with its flag.
2026-10-10 21:17:44 +13:00
Milenko e90a097b84 Android: ART's large object space taken back when only other parts of it are swapped out (the range's own swap read by splitting it off for a moment)
Needed on a device whose ART large object space (765bd000-965bd000, 512 MB)
covers the window, that refuses /proc/self/pagemap and has some of that
space swapped out: build-175 could not tell whether the swap was in the
window, and did not start.

(cherry picked from commit c8fa616c0c28250546b1c2ef4204f3a393020d81)
2026-10-10 21:17:44 +13:00
Stefan MaierandClaude Opus 5.5 6f43e4f915 VR: the campaign's pause menu keeps its list's height with SETTINGS
Upstream puts SETTINGS into the single-player pause menu's list; a list
with room grows longer and is centred, which put its last row (SAVE AND
QUIT) on the line of the button hints below it (B Cancel, A Select). In VR
its rows now go closer within the span they had, as a list without room's
do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 10:17:28 +02:00
MrBruh bb5471b6a2 Retry the ART reclaim on kernels that take MAP_FIXED_NOREPLACE as a hint
Before 4.17 the kernel puts the mapping elsewhere when something is in
the way; reserve() then set errno to EEXIST but returned before the
reclaim, so it was never tried there. memory_map.txt now also names the
attempt each reason came from (at start-up, or when the image loaded),
where it listed them twice.

Checked on a Pixel 9 Pro XL: a normal start, debug.halo.art_overlap=idle
and =busy behave as before, and the busy report shows both attempts.
2026-10-10 20:28:08 +13:00
Milenko 1c4a64c284 Android: memory_map.txt says why the fixed addresses could not be had, and on what device
(cherry picked from commit aaa6130565fb44fc1dd041d2e245fbb55899a685)
2026-10-10 20:27:58 +13:00
Milenko 0c8f4183f1 Android: start on devices whose Java runtime covers 0x80000000
On some handhelds (the AYN Thor, Retroid devices), ART's large object
space covers the fixed Xbox window, and the reclaim from upstream #19 found
pages of it in use, or could not read /proc/self/pagemap, so the game did
not start.

- The game runs in a process of its own (":game"), with a fresh Java heap:
  nothing the launcher, a disc image import or an earlier game allocated is
  left in the large object space.
- HaloApplication loads libmain.so first thing in that process, and its
  JNI_OnLoad reserves the window and the image range before the Java side
  allocates anything large.
- Whether the slice over the window is idle is read from pagemap, or, where
  the device refuses pagemap, from mincore and the space's swap total. The
  log now tells "in use" (how many pages, where) from "cannot tell".
- When the window cannot be had, the game says so plainly and writes the
  mappings below 4 GB to memory_map.txt in its data folder and to logcat.
- The debug.halo.art_overlap system property (idle or busy) puts a stand-in
  for ART's space over the window, to test the reclaim on any device.

Ported by hand: our host_memory.c had moved on (the page-hash write
tracking), and README.md and host.h took both sides. The commit's change
to the "game data was not found" message is left out.

Checked on a Pixel 9 Pro XL (Android 17): the game runs as
com.halo.decomp:game and reserves the fixed ranges from JNI_OnLoad;
debug.halo.art_overlap=idle reclaims the stand-in and the game starts,
=busy leaves it alone and shows the message.

(cherry picked from commit c71d7f8b8ea475564f8f70ab08ea585af8f8c681)
2026-10-10 20:27:58 +13:00
Stefan MaierandClaude Opus 5.5 2548dd4a54 Desktop updater: the repository's new name
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 09:10:48 +02:00
MrBruh 775015e879 The buttons' letters placed where the map's are, and their settings read again (#56)
The map's letters do not sit alike on every icon: on the message icons'
sheet, X's and Y's are about 1.7 texels lower on their ovals than the
redraws put them. tools/button_assets.py layout now moves each icon's
letter (and Y's shadow, given the id "shadow") to where it best matches
the map's, and buttons.json keeps the move; the menus' large buttons move
by under 0.03 texels, the message icons' by up to 1.7. The PNGs are drawn
again, the sheet with all 256 levels of alpha rather than 32.

hud_hires_sprites_drawable read display.high_res_hud and high_res_text
only once, and hud_hires_sprite_bitmap kept its answer for the bitmap's
address, so turning either setting off or on left the message icons as
they were: both check again when the settings change, as
hud_hires_override_find does. ui_widget.c declares hud_hires_sprite_bitmap
with the port's other functions instead of in the block that calls it.
AUTHORS.md credits saulob for the redraws.

Checked with button_assets.py check against ui.map and side by side with
the map's icons; ninja linux and ninja android build, and the harness and
test_linux_port pass.
2026-10-10 20:03:40 +13:00
Luke 8dad0c759a Co-op elevators: the host takes a riding client's player only across, so they no longer fall through going up 2026-10-10 17:57:19 +11:00
Stefan MaierandClaude Opus 5.5 e005b4ef3f VR roadmap: up to date with OpenCE, network version 25
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 08:51:53 +02:00
Stefan MaierandClaude Opus 5.5 b6a943f1a6 Merge upstream 2026-10-10: network version 25, touch controls, in-game settings, FOV options, voice chat, votekick
248 OpenCE commits (network version 25): touch controls and touch menus for
phones, single-player in-game Settings, opt-in FOV/viewmodel settings, voice
chat and votes to kick, lens flare culling, memory leak fixes (render
targets, gamepads, the Android audio stream), profiling builds, the
HALO_ANDROID split into HALO_ARM64_GUEST / HALO_GLES / HALO_ANDROID.

Conflicts and how they were resolved:
- README.md: ours kept whole. Upstream's README changes (portable build,
  profiling builds) went into docs/OPENCE_README.md: upstream's new text
  with our header note, links rewritten to ../ and our "made from your own
  copy" wording.
- port/android/README.md: both merged; upstream's touch, WSL and save
  notes taken, with our application id in every adb path; our VR section,
  signing.properties and wording kept; the stale "How it works" list
  dropped as upstream dropped it.
- Launcher icons (mipmap-*/ic_launcher_foreground.png, art/android-icon.png):
  ours (the Ringfall art). Upstream's helmet monochrome layers,
  drawable/ic_launcher_background.xml and its ic_launcher.xml were left out;
  our ic_launcher.xml, values/ic_launcher_background.xml and
  tools/android_icon.py stay. (port/assets/icon and tools/app_icon.py come
  in for the desktop builds' window icon only; do not run app_icon.py, it
  would write the helmet into the Android resources.)
- port/android/host/host_main.c: both config helpers kept, and upstream's
  emulator write tracking (native_bridge_active); upstream's display width
  block dropped in favour of ours (640 in VR) and its display density for
  the touch controls moved into our flat (non-VR) branch.
- port/linux/game/menu_functions.c: platform_text_field(typing, password)
  from upstream; the VR keyboard launch for text fields kept after it.
- port/linux/game/network_distributed.c: upstream's profile_scope wrappers,
  with network_vr_tick and bots_tick kept in their places.
- port/linux/src/dsound_sdl.c: both (our debug.audio_log, upstream's
  audio.output_device).
- port/linux/src/port_config.c: our temp-and-rename config write kept,
  under upstream's HALO_ARM64_GUEST (it is guest file I/O).
- port/linux/src/sdl_platform.c/.h: upstream's (Android menu pointer from
  the touchscreen, Steam keyboard, scoreboard pointer); our quit marker for
  the Steam Frame launcher, clipboard_seen and VR frame hooks kept. Our
  flat-mouse menu pointer (an emulator aid) is replaced by upstream's touch
  pointer, which also drives the menus on the emulator.
- port/linux/src/d3d8_gl.c: halo_ui_pointer_update is upstream's, with the
  VR controller ray taking over while VR is active (the touchscreen then
  is not the pointer); our persistent-buffer writes kept for stream and
  index uploads, now writing upstream's exact length and under HALO_GLES
  like the code around them (stream_reserve, index_upload, mirror_refresh
  too); multiview uniforms stay under HALO_ANDROID, the counter binding
  under HALO_GLES. Visibility counters: ours kept (the snapshot ring with
  multiview views, host_gl_counters_snapshot) over upstream's per-frame
  glCopyBufferSubData into counter_snapshots: ours reads finished frames
  without stalling and counts both eyes' samples once; upstream's
  desktop query batches come in unchanged.
- port/linux/src/xinput_sdl.c: both (VR controllers and touch controls).
  In VR the touch controls neither show nor act (touch_input_gamepad,
  touch_input_controls, touch move/look skipped); rumble goes to the
  controllers in VR, to the phone otherwise.
- source/interface/first_person_weapons.c: both (VR scope pass check and
  display.viewmodel_visible).
- source/interface/ui_widget.c: upstream's reworked menu targets taken
  (ui_mouse_fit_button_targets, merged rows, widened values), which do
  what our icon_x0 key picking and the 12-unit value arrows did; our VR
  label distance (25) kept, and a VR ray counts as a coarse pointer, so it
  gets the taller legend areas and the widened values (not the merged
  rows: a row's label still presses A). Our join code and VR options
  panels kept; their early return now forgets the frame's targets too.
  The on-screen keyboard: upstream's virtual_keyboard_click for taps and
  the mouse, ours (hover highlights the key) for the VR ray only.
- source/main/main.c: both (VR phases around upstream's profile scopes).

VR guards for new upstream features:
- view_fov.c: display.fov, display.viewmodel_fov and
  display.viewmodel_visible do nothing while VR is active (the headset
  gives each eye's frustum; the weapon is in the hand).
- Touch controls: off in VR (xinput_sdl.c), the menus' touch pointer too
  (d3d8_gl.c); the overlay stays hidden as the game never reports a scene.
- Lens flare culling (744040c8, b1a71709) uses global_window_parameters'
  frustum, which in VR is each eye's (per-eye passes) or the union frustum
  of the multiview centre camera: no change needed.
- In-game Settings: VR options still open with Y on the main and pause
  menus (checked flat with debug.vr_options).
- The back button change (enableOnBackInvokedCallback=false) keeps the
  legacy KEYCODE_BACK path, which targetSdk 35 already had: no change on
  Quest or Frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 08:51:46 +02:00
MrBruh 1a0045edda Merge pull request #56 from saulob/feature/high-res-ui-buttons
Add high-res UI controller buttons
2026-10-10 19:17:43 +13:00
MrBruh bd98c8fb20 AGENTS.md, and how the menus are written again
AGENTS.md is a guide for coding agents working in the repository: where things are, how to build and test each port, the defines (and that HALO_LINUX and HALO_CUSTOM_EDITION are never set), the rules for the game's sources, the SDK declarations, the netcode's version, the menus, art and credit, Custom Edition maps, settings, vendored code and licences, and how commits and pull requests are made, with an index of the other documents. port/assets/menus/README.md says how tools/ce_menus.py writes the menu files again, which it points to.
2026-10-10 18:03:32 +13:00
MrBruh 5e8ab023c1 Merge pull request #237 from innerhat-dev/coop-majority-respawn
Respawn beside the largest group in a large campaign game
2026-10-10 17:41:58 +13:00
MrBruh 231708d2e0 CI's profiling build only when a run asks for it (#223)
Every push built the profiling flavour as a build of its own, which costs runner minutes for a check few changes need. It now builds only in a run started by hand (workflow_dispatch) with its new profile box ticked; tools/ci_build.py linux profile still builds it locally.
2026-10-10 17:33:53 +13:00
MrBruh c44930abbe Single-player SETTINGS: placed by REVERT TO SAVED's tag, main's centring kept (#67)
SETTINGS goes before REVERT TO SAVED found by its tag
(restart_at_save_point_button: it opens its confirmation, so no function
names it), not at the second row, so that a map whose list has it elsewhere
gets it in its place, and one that has it not is left alone. The rows are
spaced over their own span only when the list grew for it, and the list
then keeps its height; a list with room keeps the centring pause_list_patch
now gives it.

Gamepads' OK no longer copies the changed bytes into the player's profile
itself: saving the profile makes it the player's (#227). The edited
profile's index has its comment, and the README says what the list does.
2026-10-10 17:13:14 +13:00
MrBruh 9a264e9230 Merge pull request #67 from saulob/feature/in-game-settings-menu
Add in-game Settings menu

Conflicts resolved: the campaign's pause menu goes before main's
menus_pc_chosen, which the early return keeps using (display.menus in any
case), and pause_campaign_patch passes pause_list_patch the growth it now
returns.
2026-10-10 16:46:51 +13:00
MrBruh 085512fe41 Profiling build: built in CI, its header with the port's, port marks (#223)
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.
2026-10-10 16:40:43 +13:00
MrBruh 66dfd1367c Merge pull request #223 from HiIAmMoot/profiling-cpu
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.
2026-10-10 16:28:45 +13:00
MrBruh b1a71709ef FOV and viewmodel settings: one unit, port marks, the lens flare cull (#234)
render_fov.c, viewmodel_fov.c and viewmodel_visibility.c are one unit,
port/linux/game/view_fov.c, with a header comment as the others have, real
for float, _pi for its own pi, the rasterizer's target by name, and no
isfinite shim in math.h (the angles' range test already refuses NaN). The
game code's hooks are marked "port:", their include at the end of each
block, and rasterizer_lights.c's loops declare before they act (C89).

The first-person weapon's lens flares are not culled by the world's
frustum (main's new off-screen cull): they are
drawn at the weapon's own projection, which display.viewmodel_fov can make
wider than the world's.

HALO_VIEWMODEL_VIS is HALO_VIEWMODEL_VISIBLE; the settings' descriptions and
help lines say "stock" view and fit their boxes; Video Setup's XML is
written again by ce_menus.py; the README's field of view text is a section
of its own.
2026-10-10 16:24:04 +13:00
MrBruh a8178af24a Merge pull request #234 from innerhat-dev/fov-viewmodel-controls
Add opt-in FOV and viewmodel controls to Video Setup
2026-10-10 15:52:26 +13:00
MrBruh 867425241c HALO_GLES for the embedded SMAA data, the desktop check without all three (#123)
embed_assets.py still wrapped the SMAA shader and textures in #ifndef HALO_ANDROID while xgpu_post.c now uses them under #ifndef HALO_GLES, and wsl_desktop_check.sh took out only -DHALO_ANDROID, so its desktop branches would have been checked with the guest's and the ES renderer's defines still set. The Android README's doubled blank line goes too.
2026-10-10 15:51:47 +13:00
MrBruh 400d62bb76 Merge pull request #123 from startupfoundry/split-halo-android
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.
2026-10-10 15:41:32 +13:00
justinfilip 8618c2919a Respawn a large co-op game beside the largest group of teammates.
Five or more players come back with that group, beside someone there who is safe to stand next to. A fight in the whole group waits ten seconds. Fewer players still follow the host.
2026-10-09 21:41:04 -05:00
MrBruh b5a4870d05 Saved profile's controls: the comment wrapped as the file's (#227) 2026-10-10 15:23:16 +13:00
MrBruh 30771b1b2c Structure triangle limit: port comments (#232)
The raised limits' and the walk's comments are marked as the port's, with the unverified HAC2 and MCC comparison left out; the walk's says why the index is compared whole, and the once-only log of a frame reaching the limit gets a comment of its own.
2026-10-10 15:20:36 +13:00
MrBruh a58fd479e5 Item resting surface: a port comment, its helpers with the private code (#231)
The helpers' comment is marked as the port's and says plainly what the short holds; their definitions move to the file's private code, and the plane the item is moved onto is indented inside its new surface check.
2026-10-10 15:14:51 +13:00
MrBruh 691532327e Frame throttle sleep: a port comment, no unused precache test (#229)
The sleep's comment is marked as the port's and says what the spin cost (a core held busy, not system calls: the clock is read through the vDSO on Linux and Android), and the precache test the sleep no longer depends on is gone rather than cast to void.
2026-10-10 15:09:24 +13:00
MrBruh 124cdd0b17 Any controller's menus: helpers placed and declared as the file's (#228)
The new helpers sat between widget_takes_events_of_controller's comment and the function, so the comment now heads it again; pc_menu_split_players is declared with the file's other port prototypes rather than as an extern among the functions, the players' count is read with players_port_local_player_count (safe before the players' globals are made), the comment names process_ui_widgets, and the test widget_takes_any_controller already makes is not made twice.
2026-10-10 15:06:52 +13:00
MrBruh f2cb0916dc Merge pull request #232 from innerhat-dev/structure-triangle-limit
Let a frame draw 32768 structure triangles
2026-10-10 15:00:00 +13:00
MrBruh da24e4c726 Merge pull request #231 from innerhat-dev/item-rested-surface
Keep an item's resting surface past 32767
2026-10-10 15:00:00 +13:00
MrBruh 96ee110671 Merge pull request #229 from thelinkin3000/fix/throttle-sleep
Sleep in the frame throttle instead of spinning
2026-10-10 15:00:00 +13:00
MrBruh ef87ea5ae1 Merge pull request #228 from thelinkin3000/fix/menus-any-controller
Let any controller drive the menus while one person plays
2026-10-10 15:00:00 +13:00
MrBruh 010d7520e4 Merge pull request #227 from thelinkin3000/fix/profile-controls-on-save
Apply a saved profile's controls to the player using it
2026-10-10 15:00:00 +13:00
MrBruh 7124534a4b Memory leaks: render targets, gamepads, Android audio stream 2026-10-10 14:42:05 +13:00
xshxdex98 5ed36d17f2 CE maps: an index past a tag block's end no longer stops a debug build
Halo PC reads a Custom Edition map's tags unchecked, so maps made for it
can hold an index past a block's end that never mattered there. OpenCE
already gives such an index the empty data and logs it, as it does for a
tag data's offset, but a debug build still stopped at the block's
assertions first (foundation@ce, 13 seconds in). With a Custom Edition map
loaded they are skipped, as tag_data_get_pointer's are; Xbox maps keep
their checks.

Only the assertions: OpenCE had the rest of the commit's change, made
another way (tag_empty_data).

(cherry picked from commit c953c65409ff9f3f230849bc6fc287727e3e3213)
2026-10-10 13:59:04 +13:00
Milenko 90f3fdf25f Director debug controls: each variable's own hyper scale
The debug camera controls applied the height variable's hyper-scale
setting to every variable. Camera debugging only.

(cherry picked from commit 3db16e4499c7aa8ee9c82f28c3454009f877bccc)

(cherry picked from commit a5074861b2f9682e791adfd587c3a2b43e2f6daf)
2026-10-10 13:59:04 +13:00
Milenko 42cfa397f7 AI debugging checks: the vehicle exit time
The unit activation check compared the last vehicle's index where it
meant the time the unit left it, so it could report a false "unit
activation logic error" just after a vehicle exit. Messages only.

The actors.c line only; the commit's other fix, the movement point's y
printed as z, came with "Log and error messages print what their formats
ask for".

(cherry picked from commit e3089220a6bfa8479ba2e334a0070f97c97dd851)
2026-10-10 13:59:04 +13:00
xshxdex98 234d4c2295 AI profiler: meters averaged right
The AI profiler's meters took the oldest sample out of their running sum
but never put the new one in, so their averages were wrong; the actor
line's units also lacked their tab and a separator.

The ai_profile.c fixes only, from a commit that is otherwise a rewording
of the AI code's comments.

(cherry picked from commit 95b838d13e276e41aa37f4646ce4acf5d18b8639)
2026-10-10 13:59:04 +13:00
Milenko 03a5891a2b Menus: display.menus takes "PC" in any case, and says what it needs
"PC" or "Pc" fell back to the Xbox menus without a word. Any case now
works, and a value that is neither "xbox" nor "pc" is named in the log.

With csstrcasecmp, as the port's other files compare names, in OpenCE's
check (which also skips a multiplayer map with no pause menu).

(cherry picked from commit f12bb1ba0c3eb6b83d58850821b525fedbcf5e7a)
2026-10-10 13:59:04 +13:00
Milenko 20e678f757 Keep the back button and gesture for the game on Android
android:enableOnBackInvokedCallback="false" keeps them KEYCODE_BACK, which
SDL passes to the game as B. With predictive back, the default once the
app targets Android 16, the system would take them and the game would not
see them.

(cherry picked from commit f83c1711c3581963d6167321e1157bda60f7a132)
(cherry picked from commit 40063dcb519e6a46515c7c93cc9142fe51a53bc7)
2026-10-10 13:59:04 +13:00
Milenko 985cb7c14d Android updater: download bounded in size
A release's download and the app extracted from it stop at 256 MB (a
release is about 35 MB), so a wrong or hostile answer cannot fill the
phone's storage.

The size cap only: OpenCE's updater reads its build number from a
"build-N" tag with Integer.parseInt, so the version check the commit adds
for its own "v" tags is not needed here.

(cherry picked from commit fc92683b63ab6ca2725d0bfce0abeb8264c15a7c)
2026-10-10 13:59:04 +13:00
Milenko f2a0f91e48 Antennas and cloth flags follow a jump of more than one unit
Their attachment's movement was truncated to whole units before the
test, so it took two units of movement in one update, not one, to move
the vertices along with it. Visual only.

(cherry picked from commit 99c2db2d4da3d6d66b6ec28d459c756aeab55a64)
(cherry picked from commit eb0667bcc81ed1d970422c7856162e8aadff6fd7)
2026-10-10 13:59:04 +13:00
xshxdex98 5d5b5dd35e Custom Edition maps: HUD sounds, dialogue variants and camouflage on odd tag data
Three engine checks no longer stop the game on data Halo PC tolerated:
HUD sounds with no tag, dialogue variants with no dialogue, and non-model
shaders under camouflage (an environment shader on a first-person weapon:
not drawn while camouflaged rather than misread as a model shader).

These three only: OpenCE already had the commit's other repairs (protected
maps, node trees, scripts that fail to load, HUD meters and digits, object
functions, collision edges, weapon lists, more than 16 dialogue variants),
some made another way, and has no 64-bit build for its script stacks.

(cherry picked from commit d15b6ca913266b984966897e1be4e24c69a75b30)
2026-10-10 13:59:04 +13:00
xshxdex98 02eae329d0 Pause menu centred on Custom Edition maps
Halo PC's pause list (five rows, which Custom Edition maps keep two of)
has room for SETTINGS and END GAME: the buttons are centred in it rather
than added below, which pushed the column down the box. The Xbox's two-row
list grows as before.

The menu_tags.c part only, written for OpenCE's tags (no XBOX_POINTER);
the weapon HUD's magazine overlays the commit also fixed came with #180.

(cherry picked from commit 91566224a8bd493a2a5e4c211a033e0aba66cff2)
2026-10-10 13:59:04 +13:00
xshxdex98 14ebfae230 Menus: the pointer no longer runs a list through all its items
A list whose focus reached its first or last row scrolled on and moved the
focus a row in; the pointer hovering that row took the focus back the next
frame it moved, so the list ran through everything and the row under the
pointer never stayed lit (the game types, the lobby, the internet lobby).
A list now scrolls on at its end only when the d-pad (keys, controller,
the wheel) put the focus there (ui_widget_port_pointer_focused).

(cherry picked from commit e74f5bbb541a480a72871adeb719472908607d12)
2026-10-10 13:59:03 +13:00
Milenko fee00e9c79 Particles: a Halo PC map's particle of more bitmaps than a draw takes
Particles of one kind are drawn together, at most 8 bitmaps' worth. A Halo
PC map's particle may have a bitmap a frame: beavercreek_rev_beta's fluid
bursts have up to 56. A debug build halted ("a build_sprites_begin call can
accomodate at most 8 bitmaps") when such particles were in view; a release
build drew the first 8 bitmaps' sprites and not the others. The builds that
play Halo PC's maps now do as a release build does in a debug build too, and
log it once. The Xbox's maps' particles stay within 8, and the Xbox build is
unchanged.

Without its #ifdef HALO_CUSTOM_EDITION, which OpenCE never defines (it
compiled to nothing): the log is for Custom Edition maps
(custom_edition_cache_tags_loaded), the assertion stays for the Xbox's.

(cherry picked from commit afa6413f1e922621cc04e7953e730d5571b3aae6)
2026-10-10 13:59:03 +13:00
xshxdex98 c875cbda5d Sound: play a Custom Edition sound louder than a channel at full gain
Some maps' sounds have gain modifiers above 1 (foundation's Reach sniper
rifle fire, 1.5; ten on Hornet's Nest). The game asserted on them (a halt
in a debug build; in a release build the volume went on to assert again in
dsound_volume_from_gain); it now clamps the gain to 1, as Halo PC plays
them.

Written for OpenCE's dsound_channel_set_properties: the original change
only joined a 32-bit build to its 64-bit build's clamp, which OpenCE does
not have.

(cherry picked from commit 312960464ca0868746bfea19eb004635a5acbb8e)
2026-10-10 13:59:03 +13:00
xshxdex98 137d15b95f Voice chat: no packet longer than its length's byte
The voice messages give a packet's length in a byte, but a frame could be
encoded up to 400 bytes: at 48 and 64 kbps, a frame past 255 bytes (a loud
one, as Opus's rate varies) had its length cut short, and the host, finding
it unequal to what came, dropped it. The packet's limit is now 255, which
the encoder fits a louder frame to; 64 kbps's frames are 160 bytes.

The voice_audio.h part only (not the commit's d3d8_gl.c comment removal).

(cherry picked from commit 003da0187e1bbd52baad8c3f9c3061c2a61ba55b)
2026-10-10 13:59:03 +13:00
lantos1618 f815e1ccba Zoom effect: no ghosting at high resolutions
The scope's screen effect (a warp: copies of the screen scaled a little
from the center, blended outside the scope's mask) spreads its copies by a
radius in the Xbox's pixels. At 640x480 the few copies blended into a
radial blur; at several screen pixels to the Xbox's one they stood apart as
sharp ghosts. The native builds keep the spread and split it into that
many times the passes (halo_screen_scale), each a step of it.

(cherry picked from commit b7fa7f742a72924514eef69d50daf31ef7f7f836)
(cherry picked from commit d1b40a6fc74ab46641313a6e0095e596494a1e7b)

Without its #ifdef HALO_LINUX, which OpenCE's builds never define (it compiled to nothing), and with the copied parameters declared with the others (gnu89).

(cherry picked from commit 763567dcc7ad8e6a851fec6bb5cb9f9e10452579)
2026-10-10 13:59:03 +13:00
xshxdex98 b5229d7ad5 Custom Edition: linear bitmaps with uneven rows draw with the right pitch
rasterizer_xbox_bitmap_rebuild_hardware_format pads a linear bitmap's rows
to 64 bytes, rounding up; the texture header rounded its pitch down, so a
Custom Edition bitmap whose rows were not a whole multiple was drawn
skewed. The header now rounds up too (Xbox maps' rows always are a
multiple, so they are unchanged).

The texture header's pitch only: OpenCE never had the misalignment log the commit's custom_edition_bitmaps.c part removes.

(cherry picked from commit 90a057778f76eb96d45a112b95be2a0c1e1d69a2)
2026-10-10 13:59:03 +13:00
MrBruh 6b55f5cca8 Network version 25
The host's object states now carry a unit's integrated light, with a bit saying it was sent ("Netcode: carry units' integrated lights in the object states"): a client of version 24 would keep a missed or earlier press's light the wrong way, while one of 25 sets it from every state.
2026-10-10 13:59:03 +13:00
Milenko 2db3174366 Server: a banned machine's join is refused as banned
A machine whose address or hardware id is in bans.txt, or that was dropped
for cheating, was told the game is not open when it tried to join again;
it is told it is kept out (_rejection_code_blacklisted_machine), as at the
ban itself.

(cherry picked from commit 73c78610c67cd3827592673f1b06a1d38babe4ac)

Adapted: without the dedicated server's drop function it was declared
beside (in a server/ folder, not taken), and a machine kept out by a
vote (OpenCE's network_votekick.c) is told it is kept out too.

(cherry picked from commit c0191dd9beb4bfd26dd73ba0923cb7edeea44d49)
2026-10-10 13:59:03 +13:00
Milenko eb0f6e07a7 Game speed: a number from 0 to 100
game_speed takes only finite speeds from 0 to 100.

(cherry picked from commit 4c6e68a6021204563aedb1f91e9a10d90744b5d8)
(cherry picked from commit bdbb182d0fc653960d1baf7a8b46360537eafe32)
2026-10-10 12:53:23 +13:00
Milenko acb71c2f88 Cache file header: its name ends within its field
The header's name is read only within its field.

(cherry picked from commit d4ff4cc226e25d315ca1db853ebef14953521691)
(cherry picked from commit f0dd7c9fa533a95e8e653e203064a985965f4007)
2026-10-10 12:53:23 +13:00
Milenko f7ea5db0e9 Sounds: a range's permutation mask without shifts past its bits
A range with no previous permutation (NONE) set the bit NONE as a byte
shifted to, 255.

(cherry picked from commit a35ddbe6bfcd31d5aa0c501dac52721fc295fa50)
(cherry picked from commit cd87b861b1203df530e22e777219255c21408984)
2026-10-10 12:53:23 +13:00
Milenko f38d526e83 High-res HUD: unfilter each PNG row by its filter
The filter was chosen for every byte. Each row now runs one loop for
its filter; the pixels are the same (all 261 HUD and title PNGs
compared byte for byte).

Decoding the 261 PNGs: 659 ms -> 309 ms (M4, without the GL upload).
b30, the frame where the HUD first shows (18 textures decoded): 123 ms
-> 84 ms of decoding, that frame 122 ms -> 82 ms.

(cherry picked from commit 1213d2475bb9d139e3aeb47426da9fc6190c8de9)
(cherry picked from commit 02e53d4aa030745162e53877fda910d95353c94a)
2026-10-10 12:53:23 +13:00
Milenko 89f83cf08d UPnP: no router address in the error
A router whose internet address is a private one (another NAT beyond
it) was reported with that address in the error text, which goes to the
log and can be shown. Addresses are not shown; the error says what it
means without it.

(cherry picked from commit e59c03e8b399412fb4d4ed6ba7eea1e925451f5b)
(cherry picked from commit 2e6e1270f2b7788a09b7e2e3b76878c88f36e281)
2026-10-10 12:53:23 +13:00
Milenko 56db20924c Audio: a flushed stream's next packet plays from its start
A stream's cursor is the place in its first packet not yet played.
stream_flush completed a partly played head packet, which leaves the
cursor where it was, and let go of the mixer's lock for the game's
callback. A mixer pass in that time took the next packet at the old
cursor: it played from the middle, or was marked finished at once, and
the flush then reported it played (SUCCESS, its whole size) rather than
flushed, so the game queued more as it stopped the stream. Completing a
packet that was not played to its end now resets the cursor.

(cherry picked from commit 3582e112b99d6f47f131ff6ab3439b0439e93c88)
(cherry picked from commit dcbffc6a200b525150c7ea25d9091e3f9cffbfdc)
2026-10-10 12:53:23 +13:00
Milenko d978f66373 Multiplayer: a map with fewer than three multiplayer vehicles
game_engine_predict_resources and game_engine_remap_vehicle took the
globals' multiplayer vehicles 0, 1 and 2 (warthog, ghost, scorpion)
without looking at the block's count. Every retail map has them; a Custom
Edition map's globals may not, and the game then stopped on
tag_block_get_element's assertion or, with assertions off, read past the
block and predicted or compared whatever was there. A missing one is now
NONE: not predicted, and matching no placed vehicle. Maps with all three
play as before.

(cherry picked from commit f1eb47cf3da8cf3e34ad2b6c2e304f994540ab46)
(cherry picked from commit 2f282f1aa2342af9619d5b7fcf0d92d1962b9ee8)
2026-10-10 12:53:23 +13:00
Milenko 412c5224e1 Profiles: a deleted profile's name can be used again
Deleting a saved game file moves the files after it down the memory
unit's list, but the players kept their profiles' old indices. A profile
deleted before the one in use left that one's index naming the next file,
often the next profile made: the profile in use was then saved over it,
taking its name in the list, while its folder kept the new name, which
then read as in use ("a profile with this name already exists") with no
such profile shown. The indices the players and the profile editor hold
now follow their files down the list, and one naming the file deleted is
none.

Fixes #148

(cherry picked from commit 4575f6e6ec7010de1036a3a639ee2794181874d8)
(cherry picked from commit 4bbd4965afbbaa22cb3a655a181c3770382bc505)
2026-10-10 12:53:23 +13:00
Milenko 129d3483dc Multiplayer: the pause menu's controller no longer moves the player
A network game's pause menu pauses nothing, and the controller that moved
about it also moved its player: the D-pad and sticks walked and turned
them as they chose an item. While a menu of a player's own is up
(ui_widgets_active_for_local_player), the player gets no input, as when
the director holds it, and the buttons still held as the menu closes are
let go of first, as before (ui_widget_delete).

Fixes #119

(cherry picked from commit 771a257f25ecf64fe52d84d0dbd77dc11b2a8680)
(cherry picked from commit b5fc042ca61cdfeb9a27693b2cb3f65bf93e2c0c)
2026-10-10 12:53:23 +13:00
Milenko 8fd4f06a90 Windows: the game starts on Windows 7
clock_gettime's wall clock imported GetSystemTimePreciseAsFileTime, which
KERNEL32 has only from Windows 8, so Windows 7 refused to load halo.exe.
It is looked up at run time, with GetSystemTimeAsFileTime where it is
missing. It was the only import newer than Windows 7 in halo.exe; SDL3.dll
has none.

Fixes #77

(cherry picked from commit 8b862537ef55592ae496c2b9dde6f840baa8ebcf)
(cherry picked from commit affe58156dfe9ce6fd4ae46765f50ee43560dfac)
2026-10-10 12:53:23 +13:00
Milenko 0fb9b57605 Telnet console: a client sending an HTTP request is dropped
The console runs what it is sent as scripts. A browser can be made to send
an HTTP request to a local port; such a client is now dropped at its first
line instead of having its lines run.

(cherry picked from commit faa2a331383f4dedce63487d813b7348c00b6f26)
(cherry picked from commit 40a6262e9be05db11317522f9ee9424405124025)
2026-10-10 12:53:23 +13:00
thelinkin3000 1ac8186d6c Scan the motion sensor for up to four local players
The scan kept each local player's blip count and the scan order in one
array of four, the counts first and the order from 2, and had two camera
positions: all the Xbox's two player co-op needed. With three or four
local players in split screen they were written past their ends and into
the function's stack guard, which halted the game as it started drawing
the HUD ("corrupt stack at 0!"). The counts and the order are now arrays of
their own, and there is a camera position for every local player.

(cherry picked from commit 266a556967273bfcf096c1e6009acdb7a5ee045d)
(cherry picked from commit c02cb986c2c5acc6426fcb043f39d1062617f7f7)
(cherry picked from commit ee9cbf17d1422f2b57b5ca07c98010ece1bcbad6)
2026-10-10 12:53:23 +13:00
Milenko d2256722ca Disc image setup: only plain file names written
The maps folder copied out of a disc image takes only plain file names
(printable, no path or drive characters), and on the desktop ports is
never written through a link.

(cherry picked from commit 7deed0430bb01510e4df431fae89a5c9675010d2)
(cherry picked from commit 106f1c6eb6e98646f36c9774a7c1750c78662cfe)
2026-10-10 12:53:23 +13:00
xshxdex98 7df9be7785 Give unset locals values, and drop the decompiler's dated audit notes
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)
2026-10-10 12:53:22 +13:00
xshxdex98 60cd1254d6 Fix the decompiled code's latent bugs, and say plainly what is kept
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)
2026-10-10 12:53:22 +13:00
xshxdex98 e970305724 Profiler: frame descriptions stay inside their buffer
Each of the 25 appends now goes through profile_append, which bounds and
terminates; a 150-tick frame could run past the 512-byte buffer.

(cherry picked from commit ecba1a0cecc7ff5d60d26654bdd50b7330d43f65)
2026-10-10 12:53:22 +13:00
xshxdex98 65e690321d Log and error messages print what their formats ask for
Arguments a format never read are dropped, missing ones passed (the
migration speech type, DirectSound's result), a real printed as %f, a
target AI's name printed for it rather than the source's twice, and two
messages built from saved-game names printed through %s.

(cherry picked from commit dbcf4ab91c2e42c19f57ca436f1d4f2b41964582)
2026-10-10 12:53:22 +13:00
xshxdex98 767a4edb0b Netcode: carry units' integrated lights in the object states
A unit's light is only toggled by presses, so a client that missed one,
or joined after it, kept a vehicle's (or biped's) light the wrong way and
unit_get_current_flashlight_state with it. The host's states now carry it,
with a bit saying so, so older hosts' zeros are not read as "off". What
the client's own player drives is left to its own presses.

(cherry picked from commit 0392b4639e663faa632c928301ff89a425bb0cd0)
2026-10-10 12:53:22 +13:00
xshxdex98 07bb950064 Co-op: the scripts' look tests pass for any player
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)
2026-10-10 12:53:22 +13:00
Milenko 953b0298d4 Scripts: boolean and short results are whole values, not part uninitialized
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)
2026-10-10 12:53:22 +13:00
Milenko 84dd8a2c32 Particles: a fully culled group stores no NaN in its shader
The average radius of a group whose particles were all culled divided
zero by zero, leaving a NaN in the effect shader's secondary map radius.

(cherry picked from commit 11128fd78ade85c50a9a3a5a292bd2bb33bebf30)
(cherry picked from commit b52d79541dc2143de41943367542ae63ad4f384f)
2026-10-10 12:53:22 +13:00
Milenko 794cbb92c5 Bitmap sampling: the compressed-block messages print the lod; an unknown format reads black
bitmap_2d_get_pixel's out-of-range block messages printed a short where
they say lod=%f, and a compressed format it can't decode returned an
uninitialized pixel (release builds carry on past the assertion). Object
lighting is its only caller.

(cherry picked from commit e1169e05a374071e74b3da3d285762c45f1c2a77)
(cherry picked from commit 9d827c0dd9164196da54b10a2fbfdd63bf72a0cd)
2026-10-10 12:53:22 +13:00
Milenko 712d1ebe36 Projectile trails: their bitmaps read before a weapon's first shot
A trail's first draw asks the texture cache for its bitmap without
waiting and is skipped until the read finishes, so a weapon's first shots
could show no trail (the sniper rifle's most of all). A weapon created or
readied now asks the cache, without waiting, for the bitmaps of its
projectiles' contrails. Only the texture cache sees it: nothing in the
game state or on the network changes.

From pfista's halo-og (4e66af91), the source/items/weapons.c part only;
that commit's spawn inventory and teleport prediction changes are not
taken, as they change what a host and its clients exchange.

Co-authored-by: Michael Pfister <pfista@gmail.com>
(cherry picked from commit 7fff24d4843758ddcb6468c9f1febecfbce68aa5)
(cherry picked from commit 965f48f5c5e2063ba025766f1276467708e8112c)
2026-10-10 12:53:22 +13:00
xshxdex98 ab3c388c15 Joining: a connection made is open, whatever came before it
While searching for games the client wrote to its connection before it had a socket (Winsock error 10038); that marked the connection closed, and when the player then picked a game every write of the join request failed, so the first join to a host hung and gave up. Connecting now clears that, and any half-sent data. A second try worked only because it made a new connection.

(cherry picked from commit 4b18b99af6d977707203b304d104c84fb23fecb4)
2026-10-10 12:53:22 +13:00
xshxdex98 ff8fc7a654 Network: log the Winsock error behind a failed send; the connect wait's select
A send that fails with an error the transport doesn't name became endpoint_io with nothing to say why (a first join to a host that then failed). The error is logged now, once for each. The connect wait treats a select that times out as still connecting, and sets its timeout each time (select may leave what is left of it).

(cherry picked from commit 5b02133ce00edcb7d9fd788db459a77cd98e9ac4)
2026-10-10 12:53:22 +13:00
xshxdex98 d5dedf19e0 Rotational particles: don't read past a bitmap's last sequence
Rotational sprites draw their edge-on view from the sequence after the state's. A particle bitmap on foundation has only one sequence, which halted the game in particle_system_render. Without that sequence the face-on sprite is drawn alone.

(cherry picked from commit d436a0c0c3abaf069dd34e4b8d761b49569311e6)
2026-10-10 12:53:22 +13:00
xshxdex98 744040c870 Lens flares out of view are not tested or drawn
A structure BSP's lens flare markers are submitted for every visible
cluster, and each gets an occlusion test every frame, including those
behind the camera or off the screen, whose tests count no pixels (so
they are never drawn). On the Xbox a test was nearly free; here each is
a draw and a query of its own, and a level of many lights (Medical
Block) tested about 460 a frame (debug.gpu_trace_heavy). A flare whose
test could not reach the screen, by the frustum and a sphere taking in
the test's offset point and size, is no longer submitted.

(cherry picked from commit e020bbc58dd08b4bcd609d8cf561c94b87cf4580)
2026-10-10 12:53:22 +13:00
Milenko 7ac5978158 terminal_printf: take its arguments as a va_list
terminal_printf started its variable arguments into a char *, which is
what va_list is on the Xbox, 32-bit x86 and Apple silicon, but not on
x86-64, where va_list is a one-element array of a register-save
structure. On an Intel Mac (or under Rosetta) va_start wrote through an
uninitialised pointer and vsnprintf read garbage, so the first terminal
line printed with arguments crashed the game.

Choosing an empty profile slot is such a line: "player profile begin
editing" fails, as it does on the Xbox, and console_warning reports it
through terminal_printf, so setting up a profile crashed on Intel Macs.

(cherry picked from commit 024e41b8cb1553d7ad9b96233860848d49e5ad13)
2026-10-10 12:53:22 +13:00
justinfilip f75c6dc90d Call the original weapon view Default.
The menu labeled that choice SAME. A chosen angle already sets that field of view.
2026-10-09 18:16:14 -05:00
MrBruh 6a74eab5ad Touch controls review fixes
With input.touch_aim_assist false, walking with the touch stick still let the view follow a target: the touch controls now count as aiming without the assist until a stick aims again (halo_linux_touch_aiming), as the mouse does. The stick's diagonal is full on both axes, as a controller's square stick gives. The gyroscope reaches the game apart from the swipe (nativeGyro), so the profile's invert look turns only the swipe.

The overlay shows once the game has been in play for 200 ms, so it does not flash at a level's first frame. The options dialogs are counted, so a dialog opened from another keeps the controls released, and the layout is saved when the last one closes (a slider moved without a finger was not). A floating stick rests where the thumb lands, even near an edge, and not in the bottom gesture zone. A finger's width is at most 34 logical units, so the D-pad's buttons stay apart on a small dense display. A held button that takes over the swipe from another finger starts from where it is, not from where it went down. The system bars coming and going no longer release every finger. Hide stops the gyroscope.

The libtiff comments are corrected: the screenshot_count and screenshot_size globals also reached tiff_export, but read back-buffer memory the port never draws into, so they never had the picture. Stale comments are fixed and the source exclusion test covers folders.
2026-10-10 12:09:19 +13:00
MrBruh 91a49f898c Touch controls hidden until a game is played
They showed for a moment as the game started: until the main menu's widgets were up nothing said a menu was. They now show only while a game is played (touch_game_playing): not while the game starts, between maps, or at the main menu, whose ui.map is a game too.
2026-10-10 11:19:06 +13:00
MrBruh fc61004479 Touch controls: the profile's buttons and sticks, finger sizes, opacity
The stick moves the player through the game's input state (keyboard_controls_update), as the keys do, so it moves with a southpaw or legacy stick layout too, where it was the left stick. The buttons are named by the profile's button layout, which the game sends the overlay as it changes (host_touch_bindings): with Swap triggers the right trigger's button says Grenade.

No control is smaller than a finger (48 dp across). The overlay's opacity can be set; a floating move stick (off by default) goes where the thumb lands in the lower left. Hide is remembered, and Hide and Options act as the finger lifts on them, so a swipe that starts on them aims rather than opens the options mid-fight. A label too long for its button is drawn smaller.

TouchControls, TouchLayout, GyroscopeAim and HaloActivity's touch layout files in the repository's style; the layout file is version 3 (opacity, floating stick), and versions 1 and 2 still load. CI runs the layout tests.
2026-10-10 11:19:06 +13:00
MrBruh 141e5984f5 Touch aiming with the stick's aim assist and invert
The touch controls' swipe was added after the aim assist, through the mouse's hook: no friction over a target and no adhesion to a moving one, which a controller has. It is now a turn made the stick's rate before the magnetism (halo_linux_touch_look, one port: hook in player_control.c), so the zoom, the stun and the magnetism act on it as on the stick's, and it counts as aiming for them; it follows the profile's invert look as the stick does (input_abstraction_port_look_inverted). input.touch_aim_assist (default true) turns the aim assist off. The mouse's hook is as before the touch controls.
2026-10-10 11:19:06 +13:00
justinfilip acae172b57 Keep the weapon at its original view when the world view is wide.
SAME was drawing the arms and gun with that wide view. Video Setup also packs each platform's own rows, so a hidden setting does not leave a gap.
2026-10-09 12:43:22 -05:00
justinfilip b980207235 Let a frame draw 32768 structure triangles.
Retail stops the list at 16384. The count is a long so it can hold 32768, and the dynamic triangle buffer keeps room for the rest of the frame.
2026-10-09 11:38:27 -05:00
justinfilip d084721fcf Keep an item's resting surface past 32767.
The number stays in the same 16-bit field, and a match can end on a map whose collision surfaces run past that.
2026-10-09 11:02:48 -05:00
saulo benigno c463fabed4 Add in-game Settings to single-player campaign 2026-10-09 12:00:18 -03:00
Paul Hontz fb9f03cf2c Split HALO_ANDROID into the guest's ABI, the ES renderer and the app
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.
2026-10-09 09:05:44 -04:00
eltanschauung cc41b768bb Add opt-in FOV and viewmodel settings 2026-10-09 08:18:29 -04:00
Jonas Volman 1a98636fc0 Replace remaining BSS pragmas with verified zero initialization 2026-10-09 05:14:47 -07:00
thelinkin3000andClaude Opus 5.5 e799d08149 Sleep in the frame throttle instead of spinning
With display.interpolation off the game holds itself to the Xbox's 30
frames a second in main_rasterizer_throttle, which spun on the clock
until the next vertical blank. On the ports each look at the clock is a
system call: a tester's device spent half its game thread there. The loop
sleeps a millisecond between looks now, as it already did while a map
precached (Windows sets its timer to 1 ms, win32_posix.c).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:00:34 +00:00
thelinkin3000andClaude Opus 5.5 59c8d54409 Let any controller drive the menus while one person plays
Each controller's events went only to its own player's screens, and the
main menu is the first player's: a gamepad on any port but the first
did nothing, and a phone can list a device of its own (its touch
controls) before the gamepad. With one person playing, the first
player's screens now read and take every controller's events; with two
or more, each controller keeps to its own player's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:00:34 +00:00
thelinkin3000andClaude Opus 5.5 ee1e169f9b Apply a saved profile's controls to the player using it
Saving the profile being edited wrote it to the disk only: a local
player playing with that profile kept its old button and stick layouts,
sensitivity and inversion until the profile was next chosen (the next
mission, the next start). Now each local player whose active profile it
is takes the saved one at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 12:00:34 +00:00
Milenko d90f84faa3 Add on-screen touch controls for the game on Android
An overlay over the game, from JoshRob297's halo-ce-touch, which ported
theLlamaNet's halo-ce-android: a stick, the controller's buttons with word
labels, swipe aiming, optional gyroscope aiming, the phone's vibration, and
an editor to move, resize, hide, copy, export and import the buttons. It
is player 1's controller, merged into port 0's state as the keyboard is.

Changes from the fork:

- The controls show only in a game. In the menus and during cinematics
  they hide and let the fingers through, so HiIAmMoot's menu touch
  (upstream #50) and its tap to skip work under them. The game tells the
  overlay through a new host import, host_touch_scene.
- They hide while a controller with a stick is connected, such as a
  handheld's built-in one, and never show without a touchscreen (a TV).
  The new setting input.touch_controls ("auto", "on", "off"; default
  "auto") overrides this. Hiding with a controller follows diegotorres00's
  upstream #58.
- Swipes that start in Android's edge-gesture zones do not turn the view,
  and a press shorter than a frame is held long enough for the game to see
  it, as HiIAmMoot suggested in the review of #58.
- The swipe is not the mouse's aiming, so it keeps the controller's aim
  assist.
- The phone vibrates from port 0's XInputSetState, so the game's own
  vibration setting applies and no game source changes. It vibrates only
  while the touch controls show.
- The fork's cheats menu and its built-in cheat buttons are left out.

Co-authored-by: JoshRob297 <99092299+JoshRob297@users.noreply.github.com>
Co-authored-by: theLlamaNet <107216990+theLlamaNet@users.noreply.github.com>
(cherry picked from commit 7bd7481bdede370558a3c93d286e37a9c24237bd)
(cherry picked from commit 5911e2312b6960de58dfa1cad2fefce1a17b97f7)
2026-10-10 00:59:59 +13:00
MrBruh 2aa3897189 Setting rows merged and values widened for a finger only (#50)
A finger's targets: a tap on a setting row's label reaches its value, and the values are wider. The mouse clicks the labels and values where CE's menus have them, as before the PR.
2026-10-10 00:59:22 +13:00
MrBruh f56e120e16 Merge pull request #50 from HiIAmMoot/touch-menus
Operate the Android menus by touch

Conflicts resolved: the pointer's window-to-menu mapping takes main's centering (the scoreboard's) and the PR's missing-back-buffer check, and is shared; the scoreboard's pointer stays the desktop's (an Android stub); FOCUS_LOST drops both the scoreboard's pointer and the touch; main's Steam on-screen keyboard and the PR's Android pointer each keep their side of the #ifndef; CI runs test_touch_menu.py with the harness.
2026-10-10 00:55:30 +13:00
saulo benigno 3ff762a32c Merge branch 'main' into feature/in-game-settings-menu 2026-10-09 08:34:54 -03:00
saulo benigno bfe1d0f2ba Merge branch 'main' into feature/high-res-ui-buttons 2026-10-09 08:33:39 -03:00
MrBruh 42a97d4f07 libtiff left out of the builds
Its only user, tiff_export, wrote the Xbox's movie recorder's frames, which nothing starts: the builds leave out source/bitmaps/libtiff/ and tiff_file.c, and port/linux/game/tiff_file.c's tiff_export says it writes none. port.json's exclusions may name a folder (ending in /).
2026-10-10 00:33:22 +13:00
Jonas Volman 14ec9fc4a2 Remove three no-op BSS pragma blocks without byte changes 2026-10-09 04:23:56 -07:00
MrBruh f479e34914 OpenCE icon for Android, Windows and the desktop window
ORION's icon (port/assets/icon, credited in AUTHORS.md) is the Android launcher's adaptive icon (the render as its foreground, its background's gradient behind it, the armour as its themed layer), halo.exe's icon (port/windows/halo.rc, compiled by llvm-rc) and the window's icon on the desktop builds (SDL_SetWindowIcon). tools/app_icon.py makes them all, in place of tools/android_icon.py and its emblem.
2026-10-10 00:13:07 +13:00
Jonas Volman 73677cf26d Remove empty header stubs and unused shader-stage enums
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.
2026-10-09 02:58:59 -07:00
MrBruh 4505a43e53 Overshield links' comments and lines as the files', part 0 checked (#116)
render_model_parts now follows a link whose previous part is 0, which retail did not: of the 10862 parts of the 24 stock maps' models none has one (the needler's 2 and 3 are their only links), so it changes nothing there. The comments say what the links are for, and the long conditions are wrapped.
2026-10-09 22:34:33 +13:00
MrBruh 801d2e72db Merge pull request #116 from eltanschauung/fix/overshield-glass-compositing
Fix overshield brightness changes caused by transparent draw order
2026-10-09 22:24:40 +13:00
MrBruh c92b9defc2 The secondary target at the screen's scale, on main's targets (#83)
The 320x240 colour target the reflections and active camouflage draw into gets the screen's pixels a unit, as the screen-sized targets do; with resolution_scaling "original" and on Android it is as before. The PR's depth texture of the colour's size is left out: GL draws into attachments of different sizes (their intersection), main already pairs this target with the screen's depth, and it would have bypassed bind_targets' multisampling.
2026-10-09 22:24:30 +13:00
MrBruh 1f63e34e8c Merge pull request #83 from FDervish/fix/mirror-target-resolution
Draw the reflection at the display's pixel density

d3d8_gl.c is main's in this merge: its render target and depth code have changed since (multisampling in bind_targets); the PR's change follows on main's code.
2026-10-09 22:24:11 +13:00
MrBruh 5fb7737797 Screenshot key desktop only, without its test (#109)
The Android guest has none of the SDL calls the PNG takes (SDL_CreateDirectory, SDL_SavePNG...), so its build did not link: the screenshot is written on the desktop builds only, and the key does nothing on Android. Its test, which CI did not run and which no longer built against main's headers, is dropped.
2026-10-09 22:23:29 +13:00
MrBruh 01dbb36693 Merge pull request #109 from eltanschauung/feature/screenshot-action
Add rebindable PNG Screenshot action (default F10)

Conflicts with voice chat's push to talk resolved: SCREENSHOT is after PAUSE MENU and before PUSH TO TALK, in the actions' enum, their settings and Controls Setup alike.
2026-10-09 22:18:42 +13:00
MrBruh 08d91f82fa Animation enhancements behind game.enhanced_animations, node count kept (#144)
The enhancements are on unless config.toml's game.enhanced_animations (HALO_ENHANCED_ANIMATIONS) turns them off; the setting is not in the menus. unit_preprocess_node_orientations sets model_node_count again before the fitting checks use it, as it did: the PR had removed it, which left it unset for every unit. models.h for model_finds_node, and the PR's indentation and spacing as the files'.
2026-10-09 21:48:56 +13:00
MrBruh 31435ab6cc Merge pull request #144 from RockBooch13624/cyborg-animation-enhances
Improve grenade throw and vehicle rider animations
2026-10-09 21:37:30 +13:00
MrBruh 0982cdb93e Steam's on-screen keyboard only, sysroot links kept in, SDL pins noted (#218)
A Wayland touch screen's keyboard types text events, which the menus' fields do not read: text input is started only for Steam's keyboard (SDL_ENABLE_STEAM_SCREEN_KEYBOARD). The sysroot's extraction writes nothing through a link that leads out of it. The three SDL versions (Windows, Android, the Linux sysroot) name each other. #222's test stubs the screen keyboard and passes the text field's new password argument.
2026-10-09 21:27:38 +13:00
MrBruh a2b4c8c5be Merge pull request #218 from startupfoundry/steam-deck
Linux: the release runs on a Steam Deck (SteamOS) and older distributions
2026-10-09 21:14:46 +13:00
MrBruh f6c7f93f98 Looping sounds' replaced components only, no fade endpoints (#187)
A completed fade comes back at full gain only to a second query in the same frame, which the game never makes: update_channels stops a voice whose fade out reaches 0, and sound_start_fade starts a new fade from it. Keeping the endpoint made every new voice's gain depend on fade_interpolation_end being set where it is created. That part is left out, and its test cases with it; replaced looping components are still retired.
2026-10-09 21:14:40 +13:00
MrBruh 6051e598d2 Merge pull request #187 from eltanschauung/fix/looping-audio-lifetime
Preserve looping sound fade endpoints and retire replaced components
2026-10-09 21:13:35 +13:00
MrBruh d518f54a6f No debug keyboard End or Escape restart outside the editor (#178)
The Xbox debug keyboard's End and Escape stop the movie and restart the map; this keyboard reaches the game only through the console and the menus' text boxes, so they only ever did so by accident (End in a text box too). They are left out, and with them the keys the console consumed on closing and the test and CI steps that extracted them; Escape still closes the console, and the held-key guard still keeps it from pausing.
2026-10-09 21:13:08 +13:00
MrBruh 989913d252 Merge pull request #178 from eltanschauung/fix/console-escape-capture
Fix Escape restarting the level from the console

Conflict with #222's typing guard resolved: the held keys are kept over a switch of the menus, typing or the console.
2026-10-09 21:10:19 +13:00
MrBruh 073baa3c69 Scripted first-person cameras blended as before (#189)
A scripted first-person camera looks along its unit's aim, which turns once a tick: placed by the observer and the object's move alone, it turned in 30 Hz steps in a vehicle; it is left to its snapshots' blend.
2026-10-09 21:09:26 +13:00
MrBruh 5a05aefb30 Merge pull request #189 from startupfoundry/camera-interpolation-fixes
High refresh rates: cutscene cameras stay inside the lifepod and dropships, and no camera jump after a hitch
2026-10-09 21:09:01 +13:00
MrBruh 65cd4c29d8 usnprintf as MSVC's again, map formats' widths bounded (#197-#199)
usnprintf terminated every string it wrote, so an exact fit lost its last character on Windows (a score limit of 100 shown as 10); the strings the PRs bound are terminated where they are written, as the rest of the game's are. The title copies are ustrncpy again (title_string[79] is terminated after). A map's format takes flags, then at most three digits of width and three of precision, so %5-d, %1.2.3d and huge widths are not passed to Windows's printf.
2026-10-09 21:06:25 +13:00
MrBruh 5e08c09e78 Merge pull request #199 from ChupathingyCE/upstream-fix/map-format-arguments
Map strings used as formats only with the game's own arguments (with #197 and #198, which it stacks on)
2026-10-09 21:05:46 +13:00
MrBruh db9be6a002 Maps named com or lpt are not devices (#206)
Only con, prn, aux and nul are devices by their three letters; com and lpt are with a digit after them.
2026-10-09 21:05:07 +13:00
MrBruh 7cd56c95e4 Merge pull request #206 from ChupathingyCE/upstream-fix/map-name-devices
Refuse a host's map name that is a Windows device name
2026-10-09 21:05:02 +13:00
MrBruh 66d34b9e65 Keyboard crouch walk at 0.97, without its test (#190)
0.97 is safely below player_control.c's 0.98 cutoff, diagonals too, with no one-float-step margin to guard; the test that extracted the gate by its braces, and the harness flags only it used, are dropped.
2026-10-09 21:04:26 +13:00
MrBruh b24fb497c3 Merge pull request #190 from eltanschauung/fix/keyboard-crouch-walk
Fix keyboard crouch walking
2026-10-09 21:04:19 +13:00
MrBruh 515e6a0ab6 Profiles' Start as CE's again, comment as port's (#222)
main_menu.xml is generated from CE's tags; with the keyboard's typing mode ended as it closes, Enter is A again and CE's own Start handler can stay. Its test is dropped with it.
2026-10-09 21:03:57 +13:00
MrBruh cb66f9ce72 Merge pull request #222 from eltanschauung/fix/profile-keyboard-routing
Fix Profiles opening Multiplayer after profile creation
2026-10-09 21:03:42 +13:00
MrBruh 1254c1661c Merge pull request #142 from kirklandsig/menus-no-save-file-read-each-frame
Menus: no save file read each frame

Conflicts with the Custom Edition map lists resolved: with no last_used, the list is still the Xbox's maps and then the Custom Edition maps (level_count), not the Xbox's 13.
2026-10-09 21:03:21 +13:00
MrBruh eb1b48992c Android quit only, comments as port's (#183)
The side-by-side debug build's Gradle and manifest changes are left out: UpdateProvider's authority stays hard-coded, so its updater would not match them.
2026-10-09 21:02:55 +13:00
MrBruh b292483f35 Merge pull request #183 from FernandolDev/fix/android-main-menu-quit
Android: fix main menu quit
2026-10-09 21:02:50 +13:00
MrBruh c5e4f87b54 Merge pull request #195 from ChupathingyCE/upstream-fix/update-chunk-size
Updater: fail a download whose chunk size is not a number
2026-10-09 20:48:18 +13:00
MrBruh e8b3e07371 Merge pull request #194 from ChupathingyCE/upstream-fix/smbios-bounds
Windows: read an SMBIOS structure only within the firmware table
2026-10-09 20:48:18 +13:00
MrBruh 91351a603d Merge pull request #193 from ChupathingyCE/upstream-fix/env-switch-values
HALO_NO_VSYNC=0 and similar switches no longer turn their setting on
2026-10-09 20:48:18 +13:00
MrBruh f89a6fad45 Merge pull request #192 from ChupathingyCE/upstream-fix/map-name-brackets
Join games on maps whose names have [ ] ( ) or +
2026-10-09 20:48:18 +13:00
MrBruh 772e3423ae Merge pull request #176 from ChupathingyCE/upstream-fix/vertex-shader-once
Compile a vertex shader that fails only once
2026-10-09 20:48:18 +13:00
MrBruh ce31f22e6b Merge pull request #175 from ChupathingyCE/upstream-fix/coop-cluster-negative
Take no co-op activating cluster below zero from the host
2026-10-09 20:48:18 +13:00
MrBruh 496fc4438f Merge pull request #174 from ChupathingyCE/upstream-fix/gametype-edit-none
Choose no gametype in Edit Gametypes when there are none
2026-10-09 20:48:18 +13:00
MrBruh b2d325efac Merge pull request #137 from eltanschauung/feat/mouse-sensitivity-pr
Make low mouse sensitivity increments less restrictive
2026-10-09 20:48:18 +13:00
MrBruh 46c0a8dbfb Merge pull request #138 from eltanschauung/fix/pickup-glass-floor-order-pr
Fix powerup rendering over transparent glass floors
2026-10-09 20:48:18 +13:00
MrBruh 7d821587bf Visibility tests' own scratch query, help text comment
Slot 0 was the tests' scratch query and taken for slot 1, so the game's lens flare 0 read flare 1's visibility; a query of its own is the scratch (#150's full batch too), and every slot is a test's. The query buffer is unbound after it is mapped, so a fallback read is not taken for an offset into it. #171's comment as port: comments are.
2026-10-09 20:09:43 +13:00
MrBruh f1b5c32216 Merge pull request #49 from HiIAmMoot/emulator-tools
Run the Android build on the x86 emulator, build it from WSL, add debug.solo_game
2026-10-09 20:02:39 +13:00
MrBruh 4fe5406c41 Merge pull request #185 from xshxdex98/upstream-spinner-descriptions-index
Spinner descriptions' text indices above the Custom Edition maps'
2026-10-09 20:02:39 +13:00
MrBruh 556841cc18 Merge pull request #171 from innerhat-dev/help-text-custom-icon
Skip a help text custom icon instead of halting on a missing button icon
2026-10-09 20:02:39 +13:00
MrBruh 9342ee83c8 Merge pull request #184 from eltanschauung/fix/audio-retirement-flush
Prevent audio refill after voice retirement or stream cancellation
2026-10-09 20:02:39 +13:00
MrBruh ee93ab9c82 Merge pull request #166 from kirklandsig/pr/dry-voice
Sound: a stream that ran dry plays again once it has data
2026-10-09 20:02:39 +13:00
TwistedFlogandClaude Opus 5.5 a27c205c32 Visibility test results are copied a frame later, all at once (#150)
A copy of each lens flare's result right after its test made the GPU wait
for that test, hundreds of times a frame in The Library (51-61 fps with
drops to 20 on an RX 6700 XT). Each frame's results are now copied
together at the next present, when they are long done, so the GPU never
waits (160-213 fps). Two batches of query objects alternate so a query
is not reused before its count is copied. Flares react a frame later.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 20:02:39 +13:00
MrBruh 94e91c5dc2 Merge pull request #165 from kirklandsig/pr/mirror-unsync
Android renderer: no unsynchronized mirror write while a buffer copy is pending
2026-10-09 20:02:39 +13:00
Mootjuh a510083b49 Add a profiling build: CPU recordings for Perfetto and a report tool 2026-10-09 08:00:11 +02:00
eltanschauung 051b3aa695 Suppress held Enter across typing mode changes 2026-10-09 01:56:44 -04:00
MrBruh 378cb0d427 Refused widget functions fail, resampler comment's figure
A function a map's widget may not run (#203) failed as an invalid one does, so the handler opens and closes no screens after it. The four-taps resampler's difference from tap by tap (#221) is measured at -115 to -126 dB, not -140.
2026-10-09 18:52:31 +13:00
eltanschauung b22dacb026 Fix Profiles routing after creating a profile 2026-10-09 01:51:37 -04:00
MrBruh a43f2de050 Merge pull request #221 from startupfoundry/audio-mixer-perf
Mixer: resampler four taps at a time, same output
2026-10-09 18:46:28 +13:00
MrBruh 82afea59f4 Merge pull request #220 from startupfoundry/gamepad-log
Linux: the log names each gamepad the game opens
2026-10-09 18:46:28 +13:00
MrBruh 1c7f5406b7 Merge pull request #219 from startupfoundry/trace-marker-deck-note
Linux: the trace_marker refusal does nothing on the Steam Deck (docs)
2026-10-09 18:46:28 +13:00
MrBruh cfa1a488ec Merge pull request #216 from eltanschauung/fix/create-game-teardown-delay
Remove redundant 1000ms hang on Create Game options
2026-10-09 18:46:28 +13:00
MrBruh f30db75e49 Merge pull request #211 from startupfoundry/grenade-throw-sync
Network: a client no longer throws grenades it doesn't have right after spawning
2026-10-09 18:46:28 +13:00
MrBruh 86ddd22b29 Merge pull request #210 from startupfoundry/teleport-remote-player
Network: a player who used a teleporter no longer appears stuck on the pad they left
2026-10-09 18:46:28 +13:00
MrBruh cbc8da75f1 Merge pull request #209 from startupfoundry/hit-report-rejections
Network: a client's grenade shock waves are no longer refused (about half its grenade damage)
2026-10-09 18:46:28 +13:00
MrBruh dfc6ac5f9f Merge pull request #207 from ChupathingyCE/upstream-fix/lastmpmp-buffer
lastmpmp.txt written from a buffer of the size written
2026-10-09 18:46:28 +13:00
MrBruh 04f2610f8b Merge pull request #205 from ChupathingyCE/upstream-fix/gl-upload-bytes
GL: vertex and index uploads read only their own bytes
2026-10-09 18:46:28 +13:00
MrBruh dc9b3973f0 Merge pull request #204 from ChupathingyCE/upstream-fix/stb-vorbis-codebooks
stb_vorbis: refuse code books of no dimensions, compare large powers as floats
2026-10-09 18:46:28 +13:00
MrBruh 3bb4866fbf Merge pull request #203 from ChupathingyCE/upstream-fix/widget-menu-functions
Widgets: a map's widgets don't run the main menu's profile or settings functions
2026-10-09 18:46:28 +13:00
MrBruh 9407db4bc3 Merge pull request #202 from ChupathingyCE/upstream-fix/string-list-size
String lists: a one-byte string is not terminated before its data
2026-10-09 18:46:28 +13:00
MrBruh c45f6aefc7 Merge pull request #201 from ChupathingyCE/upstream-fix/tag-name-length
Validator: tag names no longer than a tag path
2026-10-09 18:46:28 +13:00
MrBruh 71f7fd7664 Merge pull request #200 from ChupathingyCE/upstream-fix/tag-name-copies
Bounded copies and formats of tag names
2026-10-09 18:46:27 +13:00
MrBruh f1b3829152 Merge pull request #196 from ChupathingyCE/upstream-fix/wide-format-bounds
Wide printf: bound conversion specifications
2026-10-09 18:46:27 +13:00
MrBruh f5fc3d76ef Scoreboard speaker just right of the name 2026-10-09 18:00:38 +13:00
MrBruh e7e5777a88 Voice chat list in name tag colours
The in-game list of who talks colours each name as the names above players' heads are (hud_player_name_color, now shared): allies in the HUD's blue, enemies red, this machine's own players white; in co-op every other player is an ally.
2026-10-09 17:55:00 +13:00
Paul Hontz d80590c930 Limiter: look for the smallest gain only while limiting
limit looked through its 64 frames ahead for the smallest gain they need
on every output frame, even while nothing is loud enough to need any
(the menus, most of a game). It now counts the frames that need a gain
under 1; while there are none the smallest is 1 without looking.

The output is bit for bit the same (test_mixer.py's scene, whose voices
go over full scale). With no voices playing the mix costs 4.8 instead of
16.9 ns a frame on the desktop bench, 2.7 instead of 20.5 on arm64.
2026-10-09 00:51:21 -04:00
Paul Hontz 81b198b881 Resample four taps at a time
The mixer's audio thread spent about 70% of its time in the resampler's
low pass (perf on a Steam Deck in b30 and d20): 48 taps a voice and output
frame, each blending two phase rows, read one stereo frame at a time from a
ring buffer and added one by one, for both channels even of a mono voice.

Now each voice's history keeps each channel apart and each frame twice,
RESAMPLER_HISTORY apart, so the frames a low pass reaches are side by side
however the ring wraps; a table of each phase row's step to the next makes
the blend a multiply and an add; the taps are summed four at a time in
vector lanes (SSE on x86, NEON on ARM, the compiler's vector extensions,
with a plain loop for any other compiler or with RESAMPLER_SCALAR); a mono
voice works out one channel; and the voice's low pass states stay in
locals while it is mixed rather than in the stream, which the output's
stores could alias. The filter itself (24 zero crossings, cutoff 0.96,
Kaiser 6.5) is unchanged.

Audio thread CPU (per-thread /proc, % of one core):
- Steam Deck, b30 with a bot (30 s): 14.7 -> 9.0, the baseline 15.6-15.8
  over three 90 s runs;
- desktop (Ryzen AI Max+ 395, the game under Xvfb, SDL's dummy audio
  driver, b30 with a bot, 90 s): 6.0 -> 2.0 and 5.5 -> 1.8;
- the mixer bench (tools/harness/tests/test_mixer.py bench, 24 voices, 16
  out of earshot, indoors): desktop x86 32-bit 753 -> 368 ns a frame,
  arm64 (Apple A18 Pro, NEON, as the Android build's) 696 -> 330 ns.

Only the order of the additions changes. Built tap by tap
(RESAMPLER_SCALAR) the mixer's output is bit for bit the old one's, for
every test voice and a whole scene's mix (reverb and limiter); with
vectors it differs from it by at most -126 dB of the peak (-132 to -138
dB RMS), float's rounding. Against the low pass worked out in double
precision both are at -105 dB; a 22 kHz voice is flat to 0.001 dB to 8
kHz, -0.47 dB at 10 kHz, the rest 79 to 89 dB down; a 44.1 kHz voice the
same to 20 kHz. test_mixer.py checks the vector and tap-by-tap mixers
agree.
2026-10-09 00:51:21 -04:00
Paul Hontz c916c1c838 Test the mixer on known signals
tools/harness/tests/test_mixer.py compiles dsound_sdl.c's mixer (its
resampler, voices, reverb and limiter) against a fake world and checks it,
so a faster mixer can be shown to sound the same:
- the resampler against the same Kaiser-windowed sinc worked out in double
  precision tap by tap, on noise and an impulse at 22, 32, 44.1 and 48 kHz
  and pitched up and down (17 to 96 kHz): within -100 dB (it is at -105 dB,
  the table's interpolation);
- sines as heard: a 22 kHz voice flat within 0.5 dB to 10 kHz and a 44.1 kHz
  one to 20 kHz, everything else that comes out at least 70 dB down (80 to
  89 dB in the band); a tone a voice pitched up takes past 24 kHz does not
  come back as an alias (over 60 dB down);
- a voice out of earshot, which is not resampled, moves on as one heard does
  and sounds the same once it is in earshot, also as its packets run out;
- a mono voice is heard as a stereo one with both channels the same.
Three faults (taps one frame off, half the phase rows, no narrowing of the
low pass for a voice played fast) must each fail a case. The harness links
libm for it.

mixer.c also writes voices and a whole scene's mix as raw floats, to compare
one mixer with another (MIXER_SOURCE), prints the figures ("metrics") and
times a game's worth of voices ("bench").
2026-10-09 00:51:07 -04:00
MrBruh 348f8ad4f8 Speaker beside talking players' name tags
A player talking in voice chat has the speaker just left of their name above their head, centred on its capitals, at the name's scale and fade; only where the name itself is shown (display.player_names, and the gametype's motion tracker rules).
2026-10-09 17:44:55 +13:00
MrBruh b08a95b3ba Smaller voice chat list
The in-game list of who talks is drawn at 0.8 times the HUD's text, its speakers with it.
2026-10-09 17:36:47 +13:00
MrBruh b2990d420d Leaving during a votekick passes it
A player who leaves during a vote against them is kept out for network.votekick_ban_minutes, as if the vote passed, and while a vote runs against them the host refuses them joining again, so no one dodges a vote by leaving.
2026-10-09 17:32:15 +13:00
MrBruh a7c8eac379 Lucide speaker icons for voice chat
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).
2026-10-09 17:11:42 +13:00
Paul Hontz 988786d2c2 Linux: the log names each gamepad the game opens
Nothing said which controllers the game had found, so a controller that
did nothing (Steam Input's virtual pad not seen, a pad SDL does not know)
could not be told from one the game ignored. The log now has a line for
each gamepad SDL opens, with SDL's name for it. Android's guest reaches SDL
only through host_imports.list, which has no SDL_GetGamepadName, so it
opens its pads as before.

On a Steam Deck in Game Mode, under Steam Input's default template, the
game sees one pad, Steam's virtual one, which SDL names for the controller
behind it ("gamepad: Steam Deck Controller"); Steam keeps SDL off the Deck's
own controller (SDL_GAMECONTROLLER_IGNORE_DEVICES), so its buttons arrive
once.
2026-10-08 23:24:48 -04:00
Paul Hontz 063c60381a Linux README: playing on a Steam Deck
The README said only how to add the game to Steam. A "Steam Deck" section
now says what to set up in Desktop Mode and what to expect in Game Mode:
the screen (borderless, the 3D view and HUD at the panel's 16:10
resolution), the controls under Steam Input's default template (the right
trackpad as the right stick; the back buttons unbound), text entry (the
game's own keyboard for names, Steam's for the menus' text fields), the
frame rate (one frame each refresh, following Quick Access's refresh rate
and frame limit) and sleep (the game's clocks stop with the Deck; a
network game does not wait).

It suggests no lower display.resolution to save power: on the Deck (b30
with a bot, 60 Hz, 90 s) 960x600 measured 7.8 W from the battery against
8.1 to 8.8 W at the native 1280x800, about the difference between two runs
at the same settings, with the GPU 22% busy against 23 to 26%: the game is
not limited by its pixels there, and the picture is softer.

Checked on a Steam Deck LCD (SteamOS 3.8.16, Game Mode): the game fills
the 1280x800 panel ("screen: 768x480 drawn at 1280x800"); Steam selects
controller_neptune_gamepad_fps ("Gamepad With Joystick Trackpad") for it;
with display_framerate, it shows 60 frames a second at 60 Hz and 40 at
40 Hz (gamescopectl debug_set_fps_limit 40); after the process was stopped
for 8 seconds (SIGSTOP), its sound and frames came back at once.
2026-10-08 23:21:35 -04:00
Paul Hontz 444877f06d Linux: Steam's on-screen keyboard comes up for the menus' text fields
The menus' text fields (Server Setup's name and password, the password a
listed game asks for) take only a keyboard's keys: on a Steam Deck in Game
Mode, with no keyboard, there was no way to type into them but to bring up
Steam's keyboard by hand (Steam + X). The game's own on-screen keyboard
(profile and gametype names) works with the controller and is unchanged.

While a field is typed into, the platform layer now runs SDL's text input
where the system has an on-screen keyboard that text input brings up:
Steam's, which Steam asks for with SDL_ENABLE_STEAM_SCREEN_KEYBOARD in Big
Picture and Game Mode (SDL 3.4 opens steam://open/keyboard, and
steam://close/keyboard when the field ends), or a Wayland touch screen with
no keyboard. The field is single-line: Enter on Steam's keyboard ends it, as
a keyboard's Enter does, and closes it. A password's field asks for a
hidden password's keyboard (SDL_TEXTINPUT_TYPE_TEXT_PASSWORD_HIDDEN; Steam's
is the same). Elsewhere text input stays off, so that no input method takes
the keys the field reads. SDL is called from the window's thread (the event
pump), whichever thread the menus run on.

Steam does not say when its keyboard goes (its Enter, or closed by hand),
so SDL holds it to be up while text input runs. Each field begun brings it
up again, even when the last field ended in the same frame, as the password
screen's does after a wrong password (its field ends and begins again): text
input still running is stopped, which closes Steam's keyboard, and started
half a second later, which opens it (Steam takes each as a URL, each
through a process of its own, which could otherwise arrive the other way
round). A keyboard closed by hand comes back when the field is begun again.

On a Steam Deck (SteamOS 3.8.16, Game Mode, the game started from Steam),
the password screen (debug.menu_open) brings Steam's keyboard up at the
bottom of the screen, below the field; keys pressed on it type into the
field, and its Enter ends the field (the game tries to join) and the
keyboard goes (Steam's log: open/keyboard ... Mode=0, then close/keyboard).
2026-10-08 23:21:35 -04:00
Paul Hontz 041211229a Linux: the portable build starts on SteamOS and older distributions
The Linux release did not start on a Steam Deck: it was linked against the
build machine's glibc (Arch's, so it asked for GLIBC_2.43; SteamOS 3.8 has
2.41) and needed a 32-bit SDL3, which SteamOS, whose system is read-only,
and most distributions do not have.

The portable build (configure.py --portable, which tools/ci_build.py and so
every GitHub Actions build uses) is now compiled and linked against the
32-bit glibc 2.31 of Debian 11, the Steam Runtime 3 ("sniper") baseline,
and brings its own SDL 3.4.16, libSDL3.so.0 beside the executable, which
finds it there first: a DT_RPATH of $ORIGIN (--disable-new-dtags), which
comes before LD_LIBRARY_PATH, as Steam sets one for the games it starts.

tools/linux_sysroot.py downloads the Debian packages and SDL's source
release into build/linux/third_party, from a ninja edge, so only a Linux
build fetches them. Each package comes from archive.debian.org, else
deb.debian.org, else snapshot.debian.org (a permanent URL, at a time when
the package was in the archive); SDL from its GitHub release, else
libsdl.org. Each URL is tried three times, a moment apart, and each file
checked against its SHA-256. tools/linux_sysroot.cmake builds SDL against
the same system root, with X11, Wayland, libdecor (Debian 12's, for
GNOME's window decorations), PipeWire, PulseAudio, ALSA, udev and D-Bus
loaded when it starts, and the build stops if SDL lacks one of them or
would link to libdecor. A build without --portable is as before. The
workflow installs CMake, pkgconf and wayland-scanner instead of lib32-sdl3,
and caches the downloads; the release zip gains libSDL3.so.0 and its
license (SDL3-LICENSE.txt), and the self-updater deletes their .old copies.
With --pgo=train, the instrumented build's copy of libSDL3.so.0 is made
before it plays.

The release build needs GLIBC_2.29 at most (halo and libSDL3.so.0, by
readelf -V), and links only to libSDL3.so.0, libm, libpthread, libdl,
libgcc_s and libc; libSDL3.so.0 only to libm, libdl, libpthread and libc
(libdecor-0.so.0 is a name it loads, not a dependency). It should start on
Debian 11, Ubuntu 20.04, Fedora 32 and later, untested: there, the 32-bit
Wayland libraries are older than the 1.20 that SDL's Wayland driver needs,
so SDL uses X11 (XWayland in a Wayland session). On a Steam Deck (SteamOS
3.8.16, Game Mode, started as Steam starts a non-Steam game), the upstream
release stops at load time (libSDL3.so.0 not found; GLIBC_2.43 not found);
this one loads b30 on the GPU (radeonsi, OpenGL 4.6) and exits with status
0 at debug.exit_after, with its own libSDL3.so.0 though LD_LIBRARY_PATH
names a folder holding another. Debug and release builds from an empty
build folder (every download), and tools/test_linux_port.py,
tools/harness and tools/test_light_storage.py, pass in an Arch container
with the workflow's packages.
2026-10-08 23:21:35 -04:00
Paul Hontz f39f7df586 Linux: the trace_marker refusal does nothing on the Steam Deck
The README and posix_trace_marker.c said the Steam Deck runs the same
gpu-trace service and Mesa as the Steam Frame, so the refusal would help
there too. It runs the same service (tracing on; the trace buffer takes
some 800,000 writes a second), but the Deck's Mesa, 25.3 in both its
32-bit and 64-bit libraries, has no trace marker code, so it writes none
and the refusal changes nothing.

Measured on a Steam Deck LCD (SteamOS 3.8.16, Game Mode, b30 with a bot,
90 s at 60 Hz): with HALO_GPU_TRACE_MARKERS=1, 16.67 ms frames, 0% late,
4.14 W on the APU; refused, 16.69 ms, 0.02% late, 4.12-4.35 W.
2026-10-08 23:03:30 -04:00
MrBruh 75811cc2a3 Co-op setup options in categories
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.
2026-10-09 15:44:49 +13:00
MrBruh 887e2c11c4 Vote kick in Server Setup
Teamplay Options has a VOTE KICK row (on by default) under the voice chat rows, which sets network.votekick for the host's games, as they set theirs.
2026-10-09 15:01:36 +13:00
MrBruh 84cf2f8f19 Include stdio.h for snprintf in the voice code 2026-10-09 14:36:14 +13:00
MrBruh f97b33391b Voice chat
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.
2026-10-09 14:20:57 +13:00
eltanschauung 3a6e146fdb Remove redundant 1000ms hang on Create Game options 2026-10-08 17:25:37 -04:00
MrBruh 3b20ef879a Votes to kick
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.
2026-10-09 10:07:51 +13:00
Stefan MaierandClaude Opus 5.5 d18339be66 VR roadmap: up to date with OpenCE, network version 24
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:29:34 +02:00
Stefan MaierandClaude Opus 5.5 e28767b5e9 Merge merge/upstream-2026-10-08: OpenCE network version 24, Custom Edition maps, positional stereo sound
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:29:34 +02:00
Stefan MaierandClaude Opus 5.5 a167de3873 Merge upstream 2026-10-08: network version 24, Custom Edition maps, positional stereo sound, fixes
Conflicts:
- d3d8_gl.c, xgpu.h: upstream's own snapshot ring for the visibility
  counters left out for ours (host_gl_counters_snapshot, the same idea,
  tried on both headsets, which also halves multiview's counts; its copy
  already has the memory barrier upstream added); both declarations kept.
- rasterizer_xbox.c: the mirrored skinning's winding and upstream's
  many-node matrices, both.
- main.c, network_server_manager.c: both (bots' machines and a lone host
  may count down).
- android_build.py: the OpenXR loader and upstream's SDL mouse patch
  (another SDL file than ours), both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:01:08 +02:00
Paul Hontz 1f87b4ff63 A client's copy of a unit carries no grenades until the host says
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.
2026-10-08 13:29:28 -04:00
Paul Hontz 22fe5628b6 A remote player the host teleported stays where they landed
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.
2026-10-08 13:23:09 -04:00
Paul Hontz 792711ca57 A grenade's shock wave is dealt with its explosion
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.
2026-10-08 11:42:32 -04:00
Milenko 40bb5ea4a6 Map strings used as formats only with the arguments the game gives
The multiplayer messages, scores and saved games' names are formatted
with map strings, and one whose conversions differ from the arguments
passed (a string where the game gives a number) made the game read an
argument as a type it is not. Each site now passes its arguments'
signature to ustring_format_checked: a format whose conversions are not a
prefix of it gives an empty message, logged once. The shipped strings
all pass.
2026-10-08 08:36:36 -04:00
Milenko 5ec67a8db4 String lists: a string of one byte is not terminated before its data
unicode_string_list_get_string terminates a string at its last
character, size / 2 - 1, so a string whose data is one byte long had its
terminator written two bytes before its data. Strings shorter than one
character are now not used.
2026-10-08 08:36:10 -04:00
Milenko ea79111994 Bounded copies and formats of tag names
The scenario's tag name was copied with strcpy into the game state
header's 256-byte map_name, a tag's name was formatted with sprintf into
the out of objects message's 512 bytes, and csprintf bounded its output
at MAXIMUM_STRING_SIZE even when it formats into temporary, which is 256
bytes and where most callers put tag names. All three are bounded by
their buffers' sizes now.
2026-10-08 08:35:50 -04:00
Milenko bcb0e5e632 Scoreboard: bounded copies of map strings into its buffers
The game types' score names (format_score_name) copied a map string into
the callers' 256-character score_string with ustrcpy, and the scoreboard
formatted map strings and player names into row_string with usprintf,
neither bounded. They now use ustrncpy_terminated and usnprintf with the
buffers' sizes, and the title string's copies are terminated.
2026-10-08 08:35:42 -04:00
Milenko c1694f607f HUD item messages: map text formatted only when it takes the count, and bounded
An item's pickup message is a map string that the HUD used as the format
of an unbounded usprintf into a 256-character buffer. It is now used as a
format only if its one conversion is the count the game passes
(ustring_format_takes), with usnprintf, and shown as plain text
otherwise. usnprintf now also terminates a string it cuts short, and
ustrncpy_terminated is a bounded copy that always terminates.
2026-10-08 08:35:30 -04:00
Milenko debf9ca7da Validator: tag names no longer than a tag path
The validator let a tag's or a reference's name be any length, but the
game copies and formats tag names into buffers of 256 bytes (the game
state header's map name, objects.c's messages). A name that does not end
within 256 bytes is now given the empty name, as one outside the tags is.
2026-10-08 08:34:36 -04:00
Milenko 764044d4f2 GL: vertex and index uploads read only their own bytes
stream_upload and index_upload rounded the size up to 16 bytes and read
that many from the caller's data, so a draw whose vertices or indices
were not a multiple of 16 bytes read past their allocation (a quad
list's indices on every frame with transparent geometry). Only the
data's own bytes are uploaded now; the room it takes stays rounded.
2026-10-08 08:34:10 -04:00
Milenko 33804e0fe6 stb_vorbis: refuse code books of no dimensions, and compare large powers as floats
lookup1_values divided by a code book's dimensions, which a stream may
give as 0, and cast powers past an int's range to int; both are undefined.
A code book of no dimensions or entries now has no values (the stream is
refused), and the powers are compared without the cast.
2026-10-08 08:33:44 -04:00
Milenko 99257442f7 Wide printf: bounded conversion specifications
wide_format copied a conversion's flags and width digits into a fixed
buffer as it read them, with no bound, and a large width padded for a
long time. Formats include map text (ui strings, HUD messages), so the
specification is now built from the parsed values, width and precision
are capped at 4096, and a count conversion's argument is passed over
rather than written.
2026-10-08 08:33:28 -04:00
Milenko 6f0f2992cb Widgets: a game map's run none of the main menu's profile functions
A widget's event handlers name their function by its index in the table,
which nothing checked, so a game map's widget could run the main menu's
profile functions (deleting and saving player and playlist profiles, the
demos) and the port's own menu functions (writing config.toml, quitting,
connecting), on its created event too. The shipped maps' widgets run none
of these. Now only the port's own menu tags run the port's functions, and
only the main menu runs its profile functions; a refusal is logged once.
2026-10-08 08:33:20 -04:00
Milenko 22bb651e68 Write the last multiplayer map's name from a buffer of the size written
saved_game_file_remember_last_used_multiplayer_map wrote 256 bytes from
the caller's name, which the menus' map list keeps in 64, so whatever
followed it in memory went into lastmpmp.txt. The name is now copied into
a zeroed buffer of the written size first.
2026-10-08 08:31:13 -04:00
Milenko 5aad51ac18 Refuse a host's map name that is a Windows device name
A host's map name of con, nul, com1 and the like made a client on Windows
open that device as the map, which could hang it. Such names are now
refused with the other invalid map names.
2026-10-08 08:30:56 -04:00
Milenko 9fa925c3b0 Join games on maps whose names have [ ] ( ) or +
A client refused a host's map name with any character outside letters,
digits and _ - . space and backslash, so a game on a Custom Edition map
named like [H2]_Lockout could not be joined. These five characters mean
nothing to a path and are now accepted too.
2026-10-08 08:29:26 -04:00
Milenko cc820fd846 Switches such as HALO_NO_VSYNC=0 no longer turn their setting on
An environment variable that only had to be set took effect whatever its
value, so HALO_NO_VSYNC=0 turned vsync off, HALO_NO_AUDIO=0 turned audio
off and HALO_MOUSE_INVERT=false inverted the mouse. Such a variable that
is empty, 0, false, no or off now counts as not set, as a boolean
setting's value already reads.
2026-10-08 08:29:04 -04:00
Milenko 2b89960b0a Fail a download whose chunk size is not a number
read_body took strtoull's answer for a chunk's size line, and 0, the
last chunk's size, is also what it answers for a line that is not a
number. A broken or cut response then ended as a complete download, cut
short (the update's zip then failed its check, or an update check's JSON
failed to parse). A size line without a hexadecimal number is now an
error.
2026-10-08 08:28:30 -04:00
Milenko a0761c4fd1 Read an SMBIOS structure only within the firmware table
posix_hardware_id_source (Windows) checked that a structure's four-byte
header was within the firmware table, not its formatted part: a table cut
short in a system information structure (type 1) had its UUID read from
up to 20 bytes past the table's end, beyond the buffer. A structure whose
formatted part does not fit now ends the walk.
2026-10-08 08:28:30 -04:00
MrBruh 73dc01d09a Stereo sounds in the world fade as mono ones do
#191's inverse-distance gain replaces the linear fade for a positioned stereo sound rather than adding to it, so it is as loud as a mono sound at the same distance (both curves together left it some 7 dB quieter at 40 units and 15 at 60), and it is the fade the reverb send takes back out, so the reverb follows the 3D room rolloff as a mono sound's does. A 3D sound is not cut off at the tag's maximum distance either: none starts past it.
2026-10-08 23:35:08 +13:00
MrBruh 274282c184 Merge pull request #191 from meowsandstuff/audio/upstream-distancefade-fix 2026-10-08 23:31:48 +13:00
meowsandstuff 0e220b8831 Fix positional stereo audio distance attenuation 2026-10-08 21:19:46 +11:00
Mootjuh 0ca45fb4e7 Build the Android port from WSL 2026-10-08 11:36:39 +02:00
Mootjuh 98a791a7d0 Track writes by page hashes under ARM translation 2026-10-08 11:36:39 +02:00
MrBruh 2d2348cd7d Reverberate and muffle stereo sounds in the world
A stereo channel's positioned sound is now given what a 3D channel's is: the same occlusion, obstruction and underwater listener (as the 3D call passes them) and its class's wet gain make its I3DL2 source, so the mixer muffles its direct path and sends it to the reverb as it does a 3D voice. The room send rolls off with distance as a 3D voice's does, the game's own fade of the stereo voice's volume taken back out of it. Unpositioned stereo stays unpanned and dry.
2026-10-08 22:21:13 +13:00
MrBruh a14401ac7d Pan stereo sounds towards where they are
A sound with a position plays on a 3D channel, but those are mono: a stereo one (a Custom Edition map's gunfire, a bullet's impact) fell back to a stereo channel, which the Xbox never panned, so it was only faded with distance and came from nowhere. update_channels now gives a stereo channel's sound in the world a pan towards it, as the mixer pans a 3D voice; the mixer pans the channels' middle and keeps their difference as wide as the far ear's gain. Unpositioned stereo plays as before, and no channel pools change.
2026-10-08 21:35:40 +13:00
MrBruh 71bf206bd0 Sound tags from files over a map's, and a 16 MB sound cache
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.
2026-10-08 20:22:16 +13:00
eltanschauung f502268853 Fix keyboard crouch walking 2026-10-08 03:12:19 -04:00
Paul Hontz 33c2baa286 Interpolation: a scripted camera that moves with an object drawn with the object
A cinematic's camera point relative to an object (a30's lifepod, a
dropship), or a unit's eyes in one, is placed from the object as it
stands after the last tick. It was blended from its own snapshots, taken
at the first frame after each tick, while the object is blended from its
own, or steps from tick to tick when its pose snaps, so the camera ran
ahead of or behind the object as drawn and showed outside the lifepod's
hull.

Such a camera is now drawn where the observer has it, moved by the
object's drawn root node minus its root node as of the last tick, so it
stays put in the object.

Measured on a30's opening at 144 frames a second, the camera's position
in the lifepod's frame from one frame to the next:
- the lifepod's descent (85 ticks, about 400 frames): 52.6 cm median,
  200 cm at the 95th percentile, now 0.0 cm and 8.5 cm; off a straight
  line through the frames around it, 68 cm median, now 1.8 cm
- the shot before it: 52 cm at the 95th percentile and up to 244 cm, now
  1.4 cm and up to 23 cm
2026-10-08 01:13:17 -04:00
Paul Hontz aa7b107571 Interpolation: the camera's previous snapshot a tick back after a long frame
The camera is blended between snapshots of the observer, taken at the
first frame after each tick, and the objects between their last two
ticks. After a long frame (several ticks before it), the camera's
previous snapshot was several ticks old, so for the rest of the tick the
camera moved several ticks' worth while the world moved one: in a
Pelican, out through its hull for a frame.

The previous snapshot is now placed one tick back from the latest, on a
steady move between the two, so the camera moves with the world. A cut
is still a cut: the test is made on the snapshots as taken.

Measured on b30 at 144 frames a second with a 100 ms stall every 50
frames (3 to 4 ticks in the next frame), first person in the Pelican:
on the frame after a stall, the camera moved in the Pelican's frame by
23.6 cm (median) and up to 208 cm; now 0.06 cm, and 0.5 cm at the 99th
percentile.
2026-10-08 01:13:02 -04:00
MrBruh 544ee497b6 Resampler comments as the file writes them
#186's comment over the constants goes, and the resampling section says again what the low pass does in figures: a 22 kHz voice 0.5 dB down at 10 kHz, its images 80 dB down but for those of the band's last few hundred hertz, about 20 dB.
2026-10-08 17:19:18 +13:00
MrBruh 895d4cb59e Merge pull request #186 from meowsandstuff/audio/sinc-passband-pr 2026-10-08 17:14:34 +13:00
eltanschauung b3a6490f39 Order transparent models next to planar BSP glass 2026-10-08 00:11:54 -04:00
eltanschauung c7c4feab41 Use native transparent part links for enclosed energy at map load 2026-10-07 23:49:07 -04:00
MrBruh 5b3282d465 Merge pull request #188 from xshxdex98/upstream-hud-no-secondary-elements 2026-10-08 16:46:20 +13:00
xshxdex98 57038d51c9 Weapon HUD: no secondary magazine drawn for a weapon without one
#180 gave a weapon without a second magazine the secondary's overlays at
rest (default and always) and states at rest (not disabled). The Xbox
never set those overlays, and an empty magazine's total ammunition state
is disabled, so the stock HUDs' secondary numbers and icons, which never
showed, drew over the campaign HUD's ammunition and grenade panels.

Such a weapon now leaves the secondary overlays unset, as the Xbox did,
and its states read the cleared, empty magazine. #180's fix stays: the
secondary's overlays no longer write over the primary's.
2026-10-07 22:27:32 -05:00
meowsandstuff 9ec3d47cea Preserve source treble in the native sinc resampler 2026-10-08 13:55:24 +11:00
xshxdex98 fc616b8c23 Spinner descriptions' text indices above the Custom Edition maps'
Text boxes take a string index from 0x5000 as a spinner's extra item's
description (kills_to_win_extra_descriptions). Custom Edition multiplayer
maps' display indices now run from 0x4000 to 0x6000 (8192 maps), so a
list of more than 4096 showed the next maps' names as kills-to-win
descriptions. The descriptions move to 0x7000, which custom_edition_maps.c
already keeps every map's display index below.
2026-10-07 21:45:46 -05:00
eltanschauung 3174d332fe Retire replaced looping voices and preserve completed fade endpoints 2026-10-07 22:44:06 -04:00
eltanschauung 78566f5bf6 Stop retired audio voices and cancel SDL packets without refill 2026-10-07 22:36:43 -04:00
MrBruh 6d840473da sv_say: leave out '|'
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.
2026-10-08 15:35:54 +13:00
MrBruh 11b937653c Merge pull request #182 from xshxdex98/upstream-script-sv-say-end-game 2026-10-08 15:34:13 +13:00
MrBruh 66541c670d Merge pull request #181 from xshxdex98/upstream-particle-missing-sequence 2026-10-08 15:34:12 +13:00
MrBruh 1588addbb1 Merge pull request #180 from xshxdex98/upstream-hud-secondary-magazine 2026-10-08 15:34:10 +13:00
MrBruh 3ef06395be Merge pull request #179 from xshxdex98/upstream-script-none-object 2026-10-08 15:34:08 +13:00
MrBruh a1fdbacb8c Merge pull request #157 from xshxdex98/upstream-menu-max-players 2026-10-08 15:34:05 +13:00
Fernando dc71f6820b Fix Android main menu quit 2026-10-07 23:30:29 -03:00
MrBruh 81be9a5dfa Barrier before copying the visibility counters
ES 3.1 makes atomic counter writes visible to a buffer copy only after glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT); Zink and Turnip, where #122 was measured, add the barrier themselves, other drivers need not. glMemoryBarrier joins the Android GL list (a driver without it is only called with atomic counters, which need ES 3.1).
2026-10-08 15:05:30 +13:00
MrBruh e6c970a099 Merge pull request #122 from startupfoundry/es-visibility-snapshots 2026-10-08 15:04:33 +13:00
MrBruh 8fca9ef427 Mark the short camera track fallback as the port's
#152's fallback gets a port: comment and includes cseries/errors.h, as the rest of source/ does, and clamps t with PIN; a pitch not a number still samples the first point.
2026-10-08 15:04:27 +13:00
MrBruh 078901a907 Merge pull request #152 from innerhat-dev/following-camera-track 2026-10-08 15:02:13 +13:00
MrBruh 388cb86aa9 Read open's mode only when it is passed
The trace_marker interposer read a mode whenever any bit of O_TMPFILE was set, which O_DIRECTORY's bit is, so opening a directory read an argument never passed. It tests as glibc's __OPEN_NEEDS_MODE does. The comment no longer says the Steam Deck was measured.
2026-10-08 15:02:09 +13:00
MrBruh 63a758658c Merge pull request #128 from startupfoundry/steamos-trace-marker 2026-10-08 15:00:54 +13:00
MrBruh f4bad008dc Merge pull request #127 from startupfoundry/perf-guest-memcmp 2026-10-08 15:00:54 +13:00
xshxdex98 3fc953136f Scripts: Halo PC's sv_say and sv_end_game for Custom Edition maps
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.
2026-10-07 20:55:11 -05:00
xshxdex98 1f7c5c9d5e Particles: a bitmap without the sequence drawn draws nothing
A particle state names its bitmap's sequence, which a Custom Edition map's
bitmap may not have, or may have with no sprites (Hornets Nest's). The
draw read past the bitmap's sequences, then took the sprite index modulo
a sprite count of zero. Such a particle is now skipped.

Found by ChupathingyCE (MrMilenko).
2026-10-07 20:52:23 -05:00
xshxdex98 f7301fcd06 Weapon HUD: the secondary magazine's overlays and warnings are its own
render_weapon_hud set the secondary magazine's overlay flags into slots 0
and 1, over the primary's, where its states and numbers use 4 and 5: a
weapon of one magazine showed its ammunition overlays as the empty second
magazine's (coldsnap's flamethrower read NO FUEL with a full tank).

A weapon of one magazine also read a second it doesn't have:
weapon_build_weapon_interface_state fills only the weapon's own, into
unset stack memory, so the HUD's secondary flash, reload and fired-empty
warnings followed whatever was there. The state is now cleared first, and
a weapon of one magazine keeps its secondary states and overlays at rest.
2026-10-07 20:51:44 -05:00
Milenko a601ad7119 Scripts: take none as an object name, as Halo PC's compiler does
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.
2026-10-07 20:49:27 -05:00
MrBruh d0b9049e2d Android audio buffer: preprocessor at column 0
The #ifdef from #111's cherry-picked commit goes at column 0, as the rest of port/linux/src puts its directives, and its comment says what the setting is and what 1024 frames costs in latency.
2026-10-08 14:29:23 +13:00
Daniel 423677a14b Give Android audio callback more scheduling headroom
(cherry picked from commit f158218aa6daf8c2cb71852ca1f82e36a4a5ee11)
2026-10-08 14:28:15 +13:00
MrBruh 65a6bb7edd Grow the powerups' render spheres in a port file
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.
2026-10-08 14:15:15 +13:00
eltanschauung 6694d6180e Fix Escape restarting the level from the console 2026-10-07 21:13:27 -04:00
MrBruh ec23cfcf24 Merge pull request #117 from eltanschauung/fix/powerup-render-bounds 2026-10-08 14:08:53 +13:00
MrBruh ebe6917924 Bound the HUD's threshold lookup by its asset count
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.
2026-10-08 14:08:19 +13:00
MrBruh 563f630ea3 Merge pull request #114 from eltanschauung/fix/health-meter-thresholds 2026-10-08 14:06:40 +13:00
MrBruh a6337494d3 Re-patch SDL's mouse listener when the patch changes
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.
2026-10-08 14:06:37 +13:00
MrBruh 0cbad22fc7 Merge pull request #156 from FernandolDev/fix/android-relative-mouse 2026-10-08 14:04:04 +13:00
MrBruh 4646aab649 Merge pull request #118 from ChupathingyCE/pr/uptime-24-days 2026-10-08 14:04:04 +13:00
MrBruh f59c4c15d1 Merge pull request #120 from ChupathingyCE/pr/script-object-lists 2026-10-08 14:04:04 +13:00
MrBruh bad6534e0c Forget the object bounds cache in the game state load procs
The cache is forgotten before every game state load (beside cluster_partitions_port_forget) and at each new map, after the declarations, rather than at the top of four functions.
2026-10-08 13:46:02 +13:00
MrBruh 7e7095b046 Merge pull request #169 from addisonbair/asset-free-tests 2026-10-08 13:42:01 +13:00
MrBruh 11117d7c28 Network version 24
#173's PC vehicle set is a new vehicle set value the host sends with the
gametype, and every machine places the map's vehicles by it: a client of
version 23 would place none of a set it does not know while the host places
them all.
2026-10-08 13:22:39 +13:00
MrBruh ffa4a8ad04 Merge pull request #173 from TwistedFlog/custom-edition-maps
Custom Edition Discrepancy Fixes
2026-10-08 13:18:11 +13:00
Milenko 8d4fe32c0c Merge remote-tracking branch 'upstream/main' into HEAD
# Conflicts:
#	port/linux/include/halo_port_capacity.h
2026-10-07 20:15:42 -04:00
MrBruh ff47e47ad6 Correct negative particle radii as maps load
#151 clamps a radius below zero where point physics takes it. The tag
checks now correct each field it is made of when the map loads, as with
other bad values: particle radius bounds, particle system radius variables
and multipliers, contrail widths, weather particle radius bounds, and the
radius bounds of effects' and breakable surfaces' particles
(tag_validate_non_negative). The clamp stays for radii the game computes.
2026-10-08 13:00:37 +13:00
MrBruh 9e9a1bbf89 Merge pull request #148 from meowsandstuff/error-screen-build-info
Identify native builds and show recent errors on halt screen
2026-10-08 12:46:36 +13:00
MrBruh 760bc907af Merge pull request #151 from innerhat-dev/point-physics-radius
Treat a point physics radius below zero as zero
2026-10-08 12:46:34 +13:00
TwistedFlog 6b1d81c7b1 Custom Edition Map Discrepancy Fixes
Fixed: Weapons whose trigger had flag "analog rate of fire" now read how far the trigger is pulled: fully for mouse/keyboard, the actual pressure for a controller trigger. Before they never fired.
source/input/input_abstraction.c
source/input/input_abstraction.h
source/game/player_control.c

Fixed: Weapon spawn orientation on Custom Edition maps. Multiplayer weapons and items now spawn facing the way the map places them, as in Halo PC.
source/game/game_engine.c

Added: PC vehicle set. There's a new PC option in the vehicle set list, between Gun Turrets and Custom. A team set to PC gets every vehicle the map places, as in Halo PC. On Custom Edition maps that means every placement whose spawn flags name the gametype.
port/assets/menus/ce/strings.xml
port/linux/game/menu_functions.c
source/game/game_engine.h
source/game/game_engine.c

Added: Start LAN, internet and split screen games alone. A game with one machine and one player can now start. Pressing start alone begins the countdown, others can join while the game is in progress, and the lobby no longer says it's waiting for another machine or player.
source/networking/network_server_manager.c
source/interface/ui_widget_game_data_input_functions.c
2026-10-07 17:45:15 -06:00
Milenko 57404d5685 Choose no gametype in Edit Gametypes when there are none
With no gametypes listed (the built-in ones could not be made: a save
root that cannot be written, or its lock timed out), the list's update
set the chosen row to MIN(focused, count - 1), which is -1 and passes
the chosen < count checks: gametypes[-1] was shown, and A or X began
editing or deleting it. The chosen row is now at least 0, as
gametype_edit_read already keeps it, which with no gametypes chooses none.
2026-10-07 19:28:16 -04:00
Milenko 577b45d50c Compile a vertex shader that fails only once
vertex_shader_get kept a failed compile's 0, which it also takes to mean
not compiled yet, so a vertex program the driver refused was translated
and compiled again at every draw that used it, with its whole GLSL logged
each time: a stall every frame, and a debug.txt that grows without end.
Each of a program's four shaders is now tried once, as pixel shaders and
programs already are.
2026-10-07 19:27:56 -04:00
Milenko 44da50e411 Take no co-op activating cluster below zero from the host
objects_port_set_activating_cluster takes the cluster a co-op host sends
in its presentation (network_coop.c), and refused only NONE and indices
past the BSP's clusters. Any other negative value (activating_cluster is
sent plus one, so a host sending -5 or 0x8001) was stored, and the client
then read the cluster PVS before the start of its array. Any negative
index now means none, as NONE already did.
2026-10-07 19:26:59 -04:00
justinfilip 626c42df60 Skip a help text custom icon instead of halting on a missing button icon. 2026-10-07 17:53:37 -05:00
Addison Bair 8ebca844a0 Asset-free regression tests: tools/harness
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.
2026-10-07 22:11:58 +00:00
Addison Bair 04713926d1 Pass over far objects on a packed copy of their bounding spheres
A collision query (collision_get_features_in_sphere) reads the bounding
sphere of every collideable object in every cluster it touches, to pass
over the far ones. Every biped queries each tick, so a crowded cluster
(co-op's extra enemies, a carrier burst) costs the square of the crowd in
scattered object reads.

object_bounds_cache.c keeps the spheres packed by absolute index, written
by their only writer (object_compute_node_matrices). The loop runs the same
point_in_sphere on the copy, so the same objects are passed over and the
same features gathered, in order. game_state.c invalidates the copy when
the game state is replaced (revert, loaded core or game, new map).

Deterministic offline crowd (c20, legendary, extra enemies x8, 24 carriers
burst, seed pinned): game state identical tick for tick; collision queries
4.2 -> 3.0 us (-28%), object updates -15%.
2026-10-07 22:11:47 +00:00
MrBruh 34e2d4fdd8 Network version 23
#154 keeps blocks of a Custom Edition map that version 22 cut to the Xbox
tools' limits (scenario vehicles, animations, seats), so a host and a client
of the two versions would place different objects on such a map; #155's map
version check also only guards clients that have it.
2026-10-08 11:02:06 +13:00
MrBruh 6e015a75ad Merge pull request #168 from xshxdex98/upstream-coop-bringto
Co-op: bringto brings every player to the host
2026-10-08 10:56:51 +13:00
MrBruh bfe812fe10 Merge pull request #164 from xshxdex98/upstream-vehicle-homes
Bring back every vehicle a map places, not the first 64
2026-10-08 10:56:51 +13:00
MrBruh 1617f43fbc Merge pull request #163 from xshxdex98/upstream-client-outside-world
Leave the host's falling bipeds to the host on a client
2026-10-08 10:56:50 +13:00
MrBruh bad4bf455b Merge pull request #162 from xshxdex98/upstream-coop-enemies-cap
Grow a co-op squad to 8 times its size at most, per player
2026-10-08 10:56:49 +13:00
MrBruh 5bf891fd80 Merge pull request #158 from xshxdex98/upstream-oddball-no-spawn
Place the oddball at a starting location on a map with no ball spawn
2026-10-08 10:56:48 +13:00
MrBruh db9b2f4a5b Merge pull request #155 from xshxdex98/upstream-ce-map-version
Refuse a Custom Edition map of another version than the host's
2026-10-08 10:56:47 +13:00
MrBruh 2e8a609e99 Merge pull request #154 from xshxdex98/upstream-ce-tool-limits
Custom Edition maps: blocks past the Xbox tools' limits kept whole
2026-10-08 10:56:47 +13:00
MrBruh 36411f5dea Merge pull request #153 from xshxdex98/upstream-ce-linear-pitch
Custom Edition maps: linear bitmaps of any row length drawn
2026-10-08 10:56:46 +13:00
MrBruh f41c8e977a Merge pull request #145 from addisonbair/widget-pools
Fix invisible plasma bolts in large network games
2026-10-08 10:56:45 +13:00
MrBruh ad26d16a28 Merge pull request #121 from innerhat-dev/coop-follow-camera
Keep a co-op follow camera valid when the host looks up or down
2026-10-08 10:56:44 +13:00
MrBruh 314f576515 Never free NULL in the Custom Edition loader
The game's free (debug_free) reads a header before the pointer, so NULL
faults at 0xffffffe0. A Custom Edition sound that failed to decode freed
its frames with none made, which crashed Linux on loading CMT's a30 with
Custom Edition's later sounds.map; the decoded sounds, model data and
scratch buffer are now freed only when they were made.
2026-10-08 10:20:47 +13:00
xshxdex98 508a9a644b Co-op: bringto brings every player to the host
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.
2026-10-07 15:42:41 -05:00
Kirk LazarusandClaude 17b63c8ac3 Sound: a stream that ran dry plays again once it has data
mix_voice stops mixing a voice once more than 2 * width frames of silence
have been taken (its packets ran out). The centre then never moves, so the
history stays full, take_frame is never called again, and silence, which
only a frame taken resets, stays over the limit. stream_process starts a
stream over only when it has no packets, but the game queues its next
packet before DirectSoundDoWork has completed the finished ones, so the
stream still has some, and the voice stays stopped with its new packets
unplayed. A streamed sound, the music, stopped for good after a hitch
longer than its queued packets, such as a level load.

A voice stopped this way now starts over (resampler_reset, as
stream_process does) as soon as a packet not finished has frames. Packets
with none left are marked finished on the way, as take_frame marks them,
so DirectSoundDoWork completes them.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jhszivGTnJoXcpPBKX3E1
2026-10-07 16:16:16 -04:00
Kirk LazarusandClaude 343eef15cc Android renderer: no unsynchronized mirror write while a buffer copy is pending
mirror_refresh writes a page uploaded for the first time through an
unsynchronized mapping (no queued draw can read it) and a page uploaded
again with glBufferSubData. On Mali, a glBufferSubData into a buffer that
queued draws read gives the buffer new storage at once and copies the old
contents into it on the GPU's timeline. An unsynchronized write made in the
meantime goes into the new storage and is then overwritten by that copy.

After a level change the next level's load does both in the same segments:
it uploads some pages again (the earlier level's) and some for the first
time, and some of the new level's vertices were drawn as the earlier
level's. A level loaded first uploads no page again, so it never showed
there.

Each segment now records device.frame at its latest glBufferSubData. A
first-time page goes in unsynchronized only once STREAM_BUFFER_RING frames
have passed since then (Present waits for each frame's fence when it
reuses that frame's stream buffer), and with glBufferSubData otherwise.
The frames are compared by unsigned subtraction, which keeps a short age
right across the counter's rollover; only an age of a whole counter period
or more could alias, and only towards glBufferSubData.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jhszivGTnJoXcpPBKX3E1
2026-10-07 16:16:14 -04:00
xshxdex98 8b5671b16e Bring back every vehicle a map places, not the first 64
The gametype's vehicle respawn time put back only the first 64 vehicles a
map placed (MAXIMUM_VEHICLE_HOMES): a map places up to 80, and a Custom
Edition map more, and those past the 64th, destroyed or left empty, never
came back. It now keeps up to 1024.
2026-10-07 15:07:23 -05:00
xshxdex98 4935052857 Leave the host's falling bipeds to the host on a client
A client ran biped_falling_damage's outside-the-world erase on the host's
AI bipeds too: object_delete refuses them there (network_objects_may_delete),
so nothing was erased, but each was logged as erased. On a co-op client of a
crowded game that was hundreds of false warnings. The host erases them, and
its delete reaches the clients.
2026-10-07 14:59:38 -05:00
xshxdex98 6b4200e189 Grow a co-op squad to 8 times its size at most, per player
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.
2026-10-07 14:59:03 -05:00
xshxdex98 70a28f42ee Place the oddball at a starting location on a map with no ball spawn
find_position_for_ball returned an uninitialized point when the map has no
oddball ball spawn flag, as a Custom Edition map may not: the original's
assertion there is fatal, but a release build carries on past it, so the
host created the ball wherever the stack left it. It now goes to the
map's first player starting location, or the origin when there is none.
2026-10-07 14:20:02 -05:00
xshxdex98 955bbabd83 Start Server Setup at the most players, not two
The multiplayer menu's state was initialized positionally, and the fields
added before maximum_players_index since made its initializer fill the
gametype list instead: the index started at 0, so MAX PLAYERS opened at 2
and a host who left it there started a game no third player could join.
2026-10-07 14:19:35 -05:00
Fernando 87c953cfd1 Pass captured relative mouse input on Android 2026-10-07 15:49:13 -03:00
xshxdex98 9d309f8d75 Refuse a Custom Edition map of another version than the host's
A client joining a game on a Custom Edition map checked only that it had a
file of that name. With another version of the map (an older release, a
re-edit), its tags are not the host's: the objects, weapons and vehicles the
host sends are other things there, and the game desyncs or crashes.

The host now sends its map file's header checksum as the game record's map
version, which the Xbox game always left 0, and a client whose copy has
another checksum leaves with an error saying so and where to copy the
host's. The record's layout is unchanged: a host that sends 0 (an older
one, or the game's own maps, whose builds of other regions play together)
is not checked, and older clients ignore the field.
2026-10-07 13:13:29 -05:00
xshxdex98 9f57f77ed2 Custom Edition maps: blocks past the Xbox tools' limits kept whole
The tag checks cut every block to its schema's maximum, and some maxima are
the Xbox editing kit's, not the length of any array of the game's: the game
takes those blocks as long as they are. Custom Edition maps' tools go past
them, and the cut took coldsnap 20th XL's last 20 vehicle placements and
bigass_v3's player graph's 42 last animations and 12 last seats (their
references made none). Such a block (_tag_schema_tool_maximum_bit: an
animation graph's animations and unit seats, a scenario's vehicles) is
kept whole on a Custom Edition map; the graph check walks as many
animations as a short indexes. Xbox maps are checked as before.
2026-10-07 12:54:25 -05:00
xshxdex98 303aa43912 Custom Edition maps: linear bitmaps of any row length drawn
A Custom Edition map's linear bitmap rows need not be whole 64-byte steps:
its pixels are padded to them as they load (custom_edition_bitmaps.c). The
texture cache refused one that wasn't (it was never drawn), and since the
tag checks came in, the validator also made it swizzled, which drew a
non-power-of-two bitmap scrambled: sanctuary's kill icons, 24 bitmaps.
Both now take the padded pitch for a Custom Edition map; the Xbox maps'
rows are whole steps, as before.
2026-10-07 12:54:25 -05:00
justinfilip 8fccabed25 Keep a following camera valid when a track has fewer than four control points. 2026-10-07 11:01:36 -05:00
justinfilip f90a8f3bfd Treat a point physics radius below zero as zero. 2026-10-07 10:54:34 -05:00
Stefan MaierandClaude Opus 5.5 ff9cb2b55a VR roadmap: up to date with OpenCE, network version 21
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:55:36 +02:00
Stefan MaierandClaude Opus 5.5 1109660bbe Merge merge/upstream-2026-10-07: OpenCE network version 21, co-op and netcode fixes, a VR crash fix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:55:26 +02:00
Stefan MaierandClaude Opus 5.5 c1b2d34fce VR: no first-person lookups of a weapon or grenade deleted since the tick
On the Steam Frame the game stopped right after a melee: the VR log of the
first-person skeleton's nodes looked up the player's weapon, which had been
deleted since the tick (object #93 unused), before the game's own check
forgets such a weapon ("deleted unexpectedly"). It now skips a weapon
that is gone, and the grenade drawn in the free hand checks its unit and
grenade the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:50:47 +02:00
saulo benigno d412c14f18 Merge branch 'main' into feature/in-game-settings-menu 2026-10-07 09:56:21 -03:00
Codex 48342f528f Identify native builds and show recent errors on halt screen 2026-10-07 23:34:01 +11:00
MrBruh 4e8ed2f196 Tell a joining player which map they are missing
A client joining a game on a map it does not have (a Custom Edition map
not in custom_maps, or one of the game's own not in maps) now leaves with
an error naming the map and where to copy it, instead of the damaged disc
error. It also says when the map cannot run there or the host names a
Custom Edition map as other versions do. The port's error texts wrap to
their dialog, in the smaller font when they would not fit.

Network version 22: hosts now name Custom Edition maps custom_maps\<name>,
which older clients cannot load or explain; they are told to update.
2026-10-07 23:56:33 +13:00
Stefan MaierandClaude Opus 5.5 448a5d5353 Merge upstream 2026-10-07: network version 21, co-op and netcode fixes, renderer speed-ups, hardening
Conflicts: port_config.c (both sides' debug settings), hud_weapon.c (the
VR crosshair filter before upstream's state index check),
first_person_weapons.c (the VR arms kept; upstream's animation node check
added, and its node count limit applied to the VR arms too).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:42:13 +02:00
MrBruh ce41b41d78 List Custom Edition maps as custom singleplayer and multiplayer maps
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.
2026-10-07 22:40:09 +13:00
MrBruh c3657ae91b Harden map loading against hostile maps
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
2026-10-07 20:54:35 +13:00
MrBruh 483a465a5b Remove OpenSauce map support and refuse OpenSauce maps 2026-10-07 19:48:43 +13:00
RockBooch 1860a94813 All animation fixes only applied to cyborg and cyborg_mp
All animation fixes only applied to cyborg and cyborg_mp
2026-10-07 02:16:40 -04:00
eltanschauung 2875b505fc Merge remote-tracking branch 'upstream/main' into resolve/pr117-2026-10-07
# Conflicts:
#	source/cache/cache_files.c
2026-10-07 02:09:33 -04:00
MrBruh 6641c4570c Check Custom Edition maps in map_validate 2026-10-07 19:02:06 +13:00
MrBruh 30cd63678a List Custom Edition maps in the menus 2026-10-07 19:02:06 +13:00
MrBruh f823a18d59 Load, check and run Halo Custom Edition and OpenSauce maps 2026-10-07 19:02:01 +13:00
MrBruh f66885861a Give the desktop builds a 512 MB memory window and texture cache 2026-10-07 19:00:29 +13:00
Addison Bair 35f643f376 Grow the widget and light volume pools for large games
A plasma bolt is drawn by its light volume, a widget, and both pools were
still the Xbox's: 64 widgets and 256 light volumes. Every assault rifle
holds a light volume for its flashlight beam from when it is made, held or
dropped. In network co-op the rifles left lying about are kept as garbage
in proportion to the players, and garbage that any player can see is never
collected, so a large game fills the widget pool and keeps it full; a client
that joins a game in progress makes all the rifles at once and takes every
slot. From then on widgets_new finds the pool full and skips the widget, so
every bolt flies and hits unseen.

- widgets 64 -> 2048 (HALO_PORT_MAXIMUM_WIDGETS)
- light volumes 256 -> 2048 (HALO_PORT_MAXIMUM_LIGHT_VOLUMES); with only the
  widgets grown, the light volumes run out next

At 128 players a game can hold about 1,000: the rifles in hand, the ones on
the ground (garbage collection starts at 400 and keeps those in view) and the
bolts and needles in the air. The CPU part of the game state grows by 38,144
bytes, to 18,242,828 (2,466,548 free). As with any pool size, saved games of
earlier builds no longer load.
2026-10-07 05:23:34 +00:00
RockBooch d07ef9d92b Merge remote-tracking branch 'upstream/main' into cyborg-animation-enhances 2026-10-06 23:14:16 -04:00
RockBooch a2166da352 Update units.c
Improve grenade throw and vehicle rider animations

Keep the legs moving during grenade throws, blended by speed and direction, with smooth stops
Stay crouched when throwing while crouched
Use the floating jump pose when throwing in mid-air
Keep Warthog and Scorpion riders seated when throwing
Release the Scorpion grip IK during throws and reloads
Borrow the standing reload for Scorpion seats that have none
Let players turn their body while throwing (bipeds.c)
2026-10-06 22:53:03 -04:00
MrBruh 76b1898ee1 Visibility tests read on the CPU no longer stall for the GPU
From TwistedFlog's PR #104: Android GPUs without atomic counters (and the desktop when the result buffer cannot be mapped) said a test was incomplete, and the game spun until the GPU caught up. The slot's last known count stands until its query is available.
2026-10-07 15:52:23 +13:00
MrBruh f876a526df Rebuild the water's mip texture only when a level changed
From TwistedFlog's PR #104: the ripple map's levels were copied and the rest mipmapped on every bind, more than once a frame on a30.
2026-10-07 15:47:42 +13:00
MrBruh 43dc11f224 Skip a cloaked model's skinned draw without node matrices
From TwistedFlog's PR #104: with the rasterizer's memory pool full, a partially cloaked model was skinned from NULL node matrices.
2026-10-07 15:27:17 +13:00
MrBruh eb74c9f792 Merge pull request #126 from startupfoundry/perf-fewer-gl-calls
About 40% fewer GL calls a frame
2026-10-07 15:20:19 +13:00
MrBruh 5f1abf161c Survive and report a broken lights array 2026-10-07 15:05:11 +13:00
Kirk LazarusandClaude 7e8c8b1d56 Menus: no save file read each frame
The server and LAN browsers name each visible row's map every frame
(map_display_name), and the lobby shows its map every frame
(lobby_map_show). Both went through ui_widget_port_multiplayer_maps,
which also reads the map used last from z:\lastmpmp.txt. Until a game has
been hosted there is no such file, and each failed read also writes two
lines to debug.txt. The map used last is now looked up only when the
caller asks for it (last_used not NULL), as the menus that open a map
list do.

profile_name_show read the last-used profile at most once a second when
the read worked, but read one that could not be read again every frame.
That one is now tried again a second later too.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-06 21:14:15 -04:00
MrBruh 07302a866f Co-op-only garbage throttle, network version 21 2026-10-07 13:43:50 +13:00
MrBruh e7d8b9fbe3 Merge PR #136 2026-10-07 13:38:30 +13:00
MrBruh 929ae2d5af Merge PR #135 2026-10-07 13:38:30 +13:00
MrBruh 93846725e5 Merge PR #134 2026-10-07 13:38:30 +13:00
MrBruh 633492e4d0 Merge PR #133 2026-10-07 13:38:30 +13:00
MrBruh e4c83398f2 Merge PR #132 2026-10-07 13:38:30 +13:00
MrBruh 54447c7877 Merge PR #131 2026-10-07 13:38:30 +13:00
MrBruh 28dabab5ba Merge PR #130 2026-10-07 13:38:30 +13:00
MrBruh c69c8101ed Merge PR #129 2026-10-07 13:38:30 +13:00
MrBruh 54abd34493 Third hardening round 2026-10-07 13:20:54 +13:00
MrBruh d7efd3934a Validate map tags before loading 2026-10-07 12:01:09 +13:00
eltanschauung 1fe6836960 Add finer mouse sensitivity steps 2026-10-06 18:44:11 -04:00
Jonas Volman b43c70168a Merge reviewed byte-inert cinematic cleanup into halo-1 main 2026-10-06 15:41:45 -07:00
Jonas Volman a71a4ac135 Apply approved byte-inert cinematic shadow cleanup; record upstream exclusions 2026-10-06 15:40:46 -07:00
Stefan MaierandClaude Opus 5.5 c3e8220792 README: what can be played (campaign, co-op, LAN, online, bots, maps, cross-play)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:22:12 +02:00
xshxdex98 7e07506327 Multiplayer: a player joining in progress starts at zero, not with the score of the slot's last player
A player who quits gives up their slot once their body is gone, and the next player to join takes it (network_game_spawn_player). The game types keep scores by slot, and nothing cleared them, so the new player started with the quitter's score (in free for all also the team's, as the team is the slot), and one who left and came back could find their old score again. Each game type's player_added now clears the slot: slayer's and oddball's scores, king's time and hill, race's laps and flags.
2026-10-06 16:14:31 -05:00
Stefan MaierandClaude Opus 5.5 2670b3589f Merge feature/release-ready: README for players, third-party notices, release assets
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:06:46 +02:00
Stefan MaierandClaude Opus 5.5 a950de4b2b Release-ready: README for players, third-party notices, issue template
- README.md: what the mod is (a VR mod for Halo: Combat Evolved), what is
  needed (a disc image of one's own, purchased copy), simple installation
  for the Quest 3 / 3S (SideQuest) and the Steam Frame (Desktop Mode, the
  installer), playing, reporting problems, network use, credits (OpenCE,
  the decompilation, HaloCE-Quest-VR, HaloCE-VR, extract-xiso) and the
  legal notes (unofficial, not affiliated with Microsoft and others).
- THIRD_PARTY_NOTICES.md: the libraries and fonts and their licenses.
- Releases: the APK itself (RingfallMjolnirVR.apk) for sideloading, notes
  that say which file is for which headset, the project's name as title.
- An issue template (headset, build, log; no game files).
- docs/OPENCE_README.md marked as the base port's; the disc images'
  wording everywhere: made from one's own copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:05:29 +02:00
xshxdex98 b214dc1728 Host performance: the garbage collection no longer runs every tick with many enemies
Garbage (bodies, dropped weapons) is kept while a player may see it: in any player's potentially visible clusters, near them or in a 45 degree cone of where they aim, however far. With many players spread over a level and many bodies (co-op's extra enemies), every body was seen by someone, nothing could go, and the whole collection (each garbage object's visibility, the object memory pool compacted) ran again every tick on the host, which also wrote 'garbage collection critical' to debug.txt each tick while short of memory. A player now sees garbage no further than 30 world units off, a collection of active garbage that left too much is tried again only after a second, nothing collected compacts nothing, and the critical line is logged as often as the warning. Running short of memory or objects still collects at once.
2026-10-06 15:59:43 -05:00
Stefan MaierandClaude Opus 5.5 1984996d04 VR roadmap: comfort, buttons, import, installer and the new name done
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:55:46 +02:00
xshxdex98 f20689db44 Co-op: only the host's crossing switches the BSP, and brings the team along
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.
2026-10-06 15:55:44 -05:00
xshxdex98 4d35b9cb10 Netcode: killing blows reach every client, and a player's body dies as the host's did
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.
2026-10-06 15:55:43 -05:00
xshxdex98 9d8e471f78 Netcode: a body come to rest reaches every client, and none hangs in the air
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.
2026-10-06 15:55:43 -05:00
xshxdex98 95bf07a774 Co-op: a gate one player passes brings the rest of the team
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).
2026-10-06 15:55:43 -05:00
Stefan MaierandClaude Opus 5.5 93b96cf5b8 Merge feature/rename-ringfall: Ringfall: Mjolnir VR, comfort and buttons, Frame installer, release signing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:55:25 +02:00
xshxdex98 531e48ce13 Co-op: the campaign has the scoreboard, of names and pings
Holding BACK (or Tab) in network co-op shows the multiplayer scoreboard as it is, with its panel, columns and scrolling, but with no game engine it lists the players as they joined, with only their names and pings: no places, kills or scores.
2026-10-06 15:55:09 -05:00
xshxdex98 0c9f2fcd81 Co-op: a respawn starts behind its teammate, not at the level's start
A network co-op respawn spawned the player at the level's starting location and then teleported them beside a living teammate. When the teleport found no room (a teammate in or by a vehicle), the player was left at the start, outside the loaded BSP in later parts of a level, and the stranded rescue took them to the last checkpoint: the host, who can only respawn beside a client, came back there. The respawn now spawns the player behind the teammate it is for (on the teammate where behind them is outside the BSP), and the teleport places them beside them as before.
2026-10-06 15:54:55 -05:00
Stefan MaierandClaude Opus 5.5 f68db3b03e VR: the flashlight by tapping the head taken out again
Revert of c2aceb64: on the Quest 3 the tap did not toggle the flashlight.
The flashlight's button stays; the on-foot vignette that came after it is
kept. The roadmap and README no longer name it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:53:48 +02:00
Stefan MaierandClaude Opus 5.5 bd08baacac Steam Frame installer: the shortcut added by Steam itself
On the Frame Desktop Mode runs inside Gaming Mode's session: closing Steam
to write shortcuts.vdf ended the session, and the installer with it, before
it wrote the shortcut (the library kept "Halo VR"). Steam now adds the
game itself (steam://addnonsteamgame, from a desktop file beside the game,
as SteamOS's "Add to Steam" does) and picks its id, which the installer
reads back for the library pictures. An old "Halo VR" shortcut is named
for the player to remove.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:32:49 +02:00
Stefan MaierandClaude Opus 5.5 f39126e019 Steam Frame installer: Steam found by its pid file, an install log
Steam's client on SteamOS is not always a process named steam: the
installer may have written the shortcut while Steam ran, and Steam wrote its
own list back when it quit (the library kept "Halo VR"). Steam is now
found by ~/.steam/steam.pid or its ubuntu12_32/steam process, given time
after it ends, and the shortcut checked once written; what the installer
does goes to install.log beside the game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:28:50 +02:00
Stefan MaierandClaude Opus 5.5 f649bd1a3e Release as Ringfall: Mjolnir VR
The product no longer carries Halo in its name; the game it plays is named
only to say which disc image it needs.

- The app: "Ringfall: Mjolnir VR", application id
  io.github.cesarus85.ringfall (the Java classes keep the decompilation's
  package), its update provider's authority from the id; the launcher's,
  updater's, host's and VR options' texts; the OpenXR application name.
- New icon and Steam library pictures, a tilted, broken ring with a falling
  streak (tools/ringfall_art.py, from shapes and Overpass; the helmet and
  the HALO logo gone).
- The Steam Frame: ~/Documents/RingfallMjolnirVR, RingfallMjolnirVR.apk,
  ringfall-mjolnir-vr.sh, Install-Ringfall-Mjolnir-VR.desktop and
  ringfall-mjolnir-vr-steam-frame.zip; the installer renames the folder of
  the builds before (~/Documents/HaloCE) and replaces their "Halo VR"
  shortcut and pictures.
- Releases: only the Android builds, as ringfall-mjolnir-vr-android-*.zip
  (the updater's names), and the Frame's installer; the repository
  Cesarus85/ringfall-mjolnir-vr.
- The signing key's default alias ringfall.
- README.md is the project's own; the port's moved to docs/OPENCE_README.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:16:31 +02:00
Stefan Maier 719563a48c Merge branch 'feature/release-install' into test/comfort-release 2026-10-06 22:08:41 +02:00
Stefan MaierandClaude Opus 5.5 32cc1cb206 VR roadmap: step 2's second part built on feature/vr-comfort
Buttons remapped, the flashlight by tapping the head, finer turning and
vignette steps, the transparent geometry groups raised: what they do, the
settings, what the emulator showed and the headset tests for the Quest 3
and the Steam Frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:07:28 +02:00
Stefan MaierandClaude Opus 5.5 d4c1fbe9a7 Release: Steam Frame installer, release signing, Frame import kept
- A Steam Frame installer for players, opened in Desktop Mode
  (Install-Halo-VR.desktop, a release asset): it downloads the latest
  release's bundle (tools/steam_frame_bundle.py) and runs
  steam_frame_install.sh, with zenity dialogs and no keyboard: the APK and
  launcher in ~/Documents/HaloCE, the maps copied out of a disc image it
  finds or is given (steam_frame_xiso.py, the app's XisoExtractor in
  Python), and Halo VR in the Steam library with its pictures and OpenVR
  set. Running it again updates the game.
- The release job adds the bundle and the desktop file to each release.
- build.gradle signs with port/android/signing.properties (ignored) when
  the workflow's key is not given, so local builds and releases install over
  each other.
- On the Frame the launcher and host take ~/Documents/HaloCE whenever it can
  be written, not only once it holds maps: a disc image imported in the app
  is no longer emptied with Lepton's folders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:06:21 +02:00
Stefan MaierandClaude Opus 5.5 adf220d344 VR: finer turning steps, an on-foot vignette, a COMFORT page
The VR options get a fourth page, COMFORT (PLAY, COMFORT, BUTTONS, MORE;
the tabs beside the title share its width): Turning, Snap turn, Smooth
turn speed and the vehicle vignette move there from PLAY, joined by the
on-foot vignette and its strength. PLAY keeps the hands, laser,
crosshair, motion tracker, body, fingers and the Warthog's wheel.

- Snap turns step through 10 to 45 degrees by 5, then 60, 75 and 90 (were
  30, 45, 60, 90); smooth turning through 30 to 150 degrees a second by
  15, then to 300 by 30 (were 60 to 180 by 30); the vehicle vignette
  through Off, 15, 25, 35, 50, 60, 70, 85 and 100% (were Off, Light 35%,
  Medium 70%, Strong). The old values are among the steps; the defaults
  (snap 30, 120 a second, vignette 70%) are unchanged.
- vr.foot_vignette ("off" default, "turning", "moving") and
  vr.foot_vignette_strength (0.7): on foot the eye images darken from
  their edges while the turning stick turns smoothly and, with "moving",
  while the move stick moves, as far as the sticks are pushed beyond
  their dead zone (fully at three quarters), eased in and out as the
  vehicle vignette is. Not in a vehicle's seat (whether or not its view
  is first person), not in the menus; snap turns have none. The stronger
  of the two vignettes is shown. Off by default: nothing changes.

Checked in the emulator (debug.vr_options): the four tabs and the rows
without overlaps, each row's steps, the values kept over a restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:05:10 +02:00
Stefan MaierandClaude Opus 5.5 32ad9ed6b0 VR: the flashlight by tapping the side or top of the head
Either hand brought to the side or the top of the head and away again, as
a tap, toggles the flashlight (vr.flashlight_gesture, default true; a row
on the VR options' BUTTONS page); the flashlight's button keeps working.
After HaloCE-Quest-VR (CC0), whose off hand near the head's middle
toggles it, made stricter against false toggles:

- The zone is in the head's axes: beside the head (7 to 24 cm out, from
  10 cm below the eyes to 18 cm above) or over it (within 12 cm of its
  middle line, 8 to 30 cm up), never more than 6 cm in front of the eyes,
  so a scope held to the eye, a two-handed weapon, a punch and the wrist's
  motion tracker, all in front of the face, are outside it.
- A tap: the hand comes from 35 cm or more from the head's middle, enters
  slower than 3 m/s (a throw past the ear is faster), stays 40 to 600 ms
  (adjusting the headset takes longer), alone (both hands at the head is
  the headset adjusted) and leaves 4 cm past the zone's edges; toggles are
  at least a second apart.
- None while the menus are up, while recentring, while a grenade is held
  or for 0.6 s after one is thrown, within 0.7 s of a melee swing, with
  both hands on the weapon, with the weapon hand while the scope is drawn,
  or with a hand on a Warthog's wheel; such a hand must leave the head
  before it taps.
- A toggle presses the flashlight's button (white) for 100 ms, as B's tap
  does, and gives the tapping hand a short rumble.

The tap's state machine was run off the headset in a small harness over
taps beside and over the head, a turned head, a second tap within the
cooldown, a long dwell, both hands, a scope at the eye, the scope flag, a
fast throw past the ear, a held grenade and a two-handed grip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 22:00:53 +02:00
Stefan MaierandClaude Opus 5.5 cf19bcfdfa VR: the game's actions moved between the controllers' buttons
A BUTTONS page in the VR options (between PLAY and MORE) puts jump, the
flashlight (held: switch grenades), action/reload, switch weapon, crouch,
zoom and melee on any of seven buttons: A, B, X, Y, the move and turning
sticks' clicks and the weapon hand's grip. Moving an action to a button
swaps it with the action there, so every action always has a button;
"Reset buttons to defaults" puts them back. The triggers, the free hand's
grip (both hands on the weapon), the menu button, the Steam Frame's
bumpers, D-pad and View button keep what they do, and the menus keep
their buttons.

Settings vr.button_jump, _light, _action, _weapon, _crouch, _zoom and
_melee ("a", "b", "x", "y", "move_stick", "turn_stick", "grip"; the
defaults are today's layout), named as right-handed. vr.c reads them with
the live settings and, after the left-handed mirroring
(controllers_mirrored), reads each action's button and hands it on as the
button that action has by default (controls_remapped), so xinput_sdl.c's
table is unchanged and the defaults change nothing. Because the mapping
sits after the mirroring it works the same on the Quest's Touch and the
Steam Frame's controllers (mirrored on the Frame the left D-pad is A, B,
X, Y), and a mirrored player's swaps are the mirror image of the
right-handed ones. The guest/host ABI is unchanged.

- The melee's aim and the weapon's jab (melee_update) follow the melee's
  button, not the grip as such; the grips holding a Warthog's wheel press
  nothing whatever action the grip has (moved from xinput_sdl.c).
- The controller buttons page (now on the BUTTONS page) shows each action
  under the controller and name of its button, for both layouts and the
  mirrored hands; its ON FOOT tab no longer draws the MENUS tab's notes
  over its own.
- debug.vr_options (Android, false): the VR options panel off the
  headset, for checking its pages in the emulator.

Checked in the emulator (debug.vr_options): the rows, swaps, reset, the
controls page right- and left-handed, the settings kept over a restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:57:15 +02:00
Stefan MaierandClaude Opus 5.5 3b5796f1a0 VR roadmap: the keyboard's password in the clear is no open point
Nobody else sees the headset's view (decided 2026-10-06): no masking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:47:26 +02:00
Stefan MaierandClaude Opus 5.5 68135a712d Transparent geometry: room for 2048 groups a window (was the Xbox's 384)
Every transparent thing a window draws is a group of its own: a model's
transparent part (shields, plasma, glass), each particle system, contrail
and light volume, a window's or water's surface. The Xbox had room for 384
(rasterizer_transparent_geometry_new_group); past that new_group returns
NULL and the submitter drops the geometry, logging "### ERROR too many
transparent geometry groups" once (rasterizer_xbox_models.c,
rasterizer_xbox_widgets.c; environment and dynavobgeom drop it silently).
render_window submits the objects first, then the structure's transparent
geometry, weather, particles, particle systems and contrails, so in games
of 30 and more players the late ones vanish: water and glass surfaces,
explosions, muzzle flashes, smoke, plasma glows, contrails.

The groups are a heap array (0xA0 each), a short array of sorted indices
and a bit vector of pending flags, all sized by the constant; indices are
shorts (sorted_index, presorted indices, the draw loop), good to 32767.
Nothing else depends on the number (the debug view's "all groups" index
1000 only colours groups in a debug mode). Upstream (halo-ce-universal)
still has 384.

2048 groups take 320 KB (61 KB before). A one-line log tells when a window
passes 384 and every 128 beyond, for checking big games in the headset.
In the emulator, a 36-player bot game on Blood Gulch had up to 134 groups
a window from a standing player, using under 8 KB of the rasterizer's
768 KB memory pool (the models' queued matrices and lighting), so the
pool is not the next limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:47:22 +02:00
Stefan MaierandClaude Opus 5.5 1f238ff045 Merge feature/vr-lefthanded-graphics: left-handed mode, graphics presets, refresh rate and resolution in VR
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:21:38 +02:00
Stefan MaierandClaude Opus 5.5 88354e5c6c VR roadmap: left-handed mode and graphics presets done on both headsets
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 21:21:35 +02:00
Paul Hontz 7809af0864 Windows: posix_trace_marker.c left to Linux, as the other posix_*.c
The Windows build takes the Linux platform layer's sources but those it replaces; the trace_marker refusal is glibc's open(), which Windows does not have.
2026-10-06 15:20:12 -04:00
Paul Hontz db44dba833 Linux: the GPU driver kept from the kernel's trace_marker (HALO_GPU_TRACE_MARKERS)
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.
2026-10-06 15:10:09 -04:00
Stefan MaierandClaude Opus 5.5 8cdce233e4 Android settings: config.toml written beside itself and renamed over
A config.toml pushed by adb belongs to the shell and the app cannot open it
for writing, so the VR options' changes (handedness, graphics, refresh rate,
resolution) lasted only until a restart. Writing a temporary file and
renaming it over the old one replaces it all the same (the folder is the
app's), and a write cut short leaves the old file whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:55:22 +02:00
Paul Hontz 1d3b8184c3 Renderer: a declaration's vertex array kept for its streams, not looked up each draw
The layout a draw needs follows from its vertex declaration and the streams it has, so each declaration keeps the vertex array its draws last used. Looking it up hashed the whole layout, about 390 bytes, on every draw: 3.8% of the main thread on a Steam Frame.
2026-10-06 14:46:35 -04:00
Stefan MaierandClaude Opus 5.5 81137e3af3 VR roadmap: left-handed mode and graphics presets good on the Steam Frame
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:40:01 +02:00
Stefan MaierandClaude Opus 5.5 fb670e79b9 VR roadmap: the left hand's ammo counter reads the right way round
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:33:11 +02:00
Stefan MaierandClaude Opus 5.5 8022a0b4a1 VR left-handed: the assault rifle's ammo counter reads the right way round
The mirrored first-person model leaves its geometry mirrored; a numeric
display drawn mirrored (rasterizer_set_model_skinning's winding flag) now
shows each digit's bitmap turned over in u (1-u on the first map's texture
transform) and swaps the digits' order across the display (the quad of the
n-th digit from the right shows the n-th from the left; digit count from
the counter limit, the compass's single digit unchanged). Only for 2d,
non-screen-space first maps of numeric chicago/generic shaders, only when
mirrored: right-handed and flat output unchanged.

debug.mirror_first_person (Android, default false): off the headset the
first-person weapon is mirrored across the camera's forward/up plane as
the left hand's is in VR, for checking such things in the emulator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:33:11 +02:00
Stefan MaierandClaude Opus 5.5 696383f191 VR roadmap: the left hand's mirrored ammo display, an open point
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:01:39 +02:00
Stefan Maier 9a33ecd2d3 Revert "VR left-handed: the assault rifle's ammo display reads the right way round"
This reverts commit 530a32b64889c24e64c5e805949e31df1c68fb73.
2026-10-06 20:01:19 +02:00
Stefan Maier 9447bc2ae4 Revert "VR left-handed: the ammo display's subtree found without hanging"
This reverts commit 8e40d6ecacd92f3938e329e528a27c3eddd8dbe1.
2026-10-06 20:01:18 +02:00
Stefan Maier f04c8c4045 Revert "VR left-handed: a part is drawn mirrored only where all its nodes are"
This reverts commit 1a6061b932e7f293cc40351f90f4d0d68edb2bd2.
2026-10-06 20:01:18 +02:00
Stefan MaierandClaude Opus 5.5 b026590824 VR left-handed: a part is drawn mirrored only where all its nodes are
The assault rifle's ammo display, mirrored back, vanished: its part's
winding was judged by its first node alone, which may be the mirrored gun,
so its triangles were turned over and culled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:54:01 +02:00
Stefan MaierandClaude Opus 5.5 2a2b2cdbf3 VR left-handed: the ammo display's subtree found without hanging
Walking up from a node to the display through parent_node_index never
ended where the root names itself as its parent: the game hung loading a
level left-handed (Steam Frame). Each step now goes only to a parent
before its child, as vr_skeleton_build does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:50:52 +02:00
Stefan MaierandClaude Opus 5.5 5861e57d99 VR left-handed: the assault rifle's ammo display reads the right way round
Its node ("... display") and what hangs from it are mirrored back about
the display's own centre after the pose is mirrored: it stays where the
mirrored gun has it, reads as in the right hand, and its triangles keep
their winding (rasterizer_xbox.c decides per skinned part). After
HaloCE-Quest-VR (CC0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:47:53 +02:00
Stefan MaierandClaude Opus 5.5 50d685e465 VR roadmap: step 2 in part built (left-handed mode, graphics presets, refresh rate)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:33:27 +02:00
Stefan MaierandClaude Opus 5.5 0e6b006878 VR options: graphics, refresh rate and resolution rows
The panel's second page (MORE) gets "Graphics" (vr.graphics: Low,
Medium, High, Max), "Refresh rate" and "Resolution" rows, with a note
of what they do. The refresh rate steps through the rates the runtime
offers (XR_FB_display_refresh_rate: host_xr_refresh_rates, new host
imports with host_xr_set_refresh_rate) and changes at once (vr.c asks
for it when vr.refresh_rate changes); where the runtime offers none
(SteamVR on the Steam Frame, whose rate is SteamOS's) the row shows the
rate it runs at, from its frames' period, and stays as it is. The
resolution (vr.resolution_scale, 70 to 120%) is read as the app starts:
the row says when a restart is needed. vr.log_performance's late frames
follow the rate as it changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:30:32 +02:00
Stefan MaierandClaude Opus 5.5 ca73ba7909 VR: graphics presets (vr.graphics)
port/linux/game/vr_graphics.c (after HaloCE-Quest-VR's, CC0) sets the
game's own effect switches each frame before drawing: "high" (the
default) draws everything as before; "medium" no object shadows,
environment reflections or lens flares; "low" nor specular light, bump
maps, detail objects (grass), the screen's fog layer, contrails, weather
or camouflage's second pass; "max" is "high" with per-pixel model
lighting and 512x512 shadow maps (d3d8_gl.c asks halo_vr_graphics). A
switch the game changes (scripts, console) is taken as its wish and
combined with the preset's. A client in another's game keeps grass, lens
flares and the fog layer as the host's rules have them; cheats.c leaves
the environment parts the preset may keep off there (shadows, specular,
reflections).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:28:48 +02:00
Paul Hontz b34e56fa0c Guest runtime: memcmp eight bytes at a time 2026-10-06 13:27:06 -04:00
Paul Hontz ef6ea26fd8 Renderer: a draw's changed texture units bound in one call (desktop GL) 2026-10-06 13:26:57 -04:00
Paul Hontz 9c6ed2481e Renderer: a vertex array object for each vertex layout (desktop GL) 2026-10-06 13:26:42 -04:00
Stefan MaierandClaude Opus 5.5 c254d6df05 VR: left-handed, the rumble's motors follow the hands
The game's light (right) motor rumbles the weapon hand, the heavy one the
free hand (halo_vr_weapon_hand), as right-handed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:26:40 +02:00
Paul Hontz bf4d5d8faf Renderer: a sampler object for each sampler state, bound rather than changed 2026-10-06 13:26:17 -04:00
Stefan MaierandClaude Opus 5.5 9910c4a9dc VR options: weapon hand and mirrored controls, two pages
The VR options panel gets a "Weapon hand" row (vr.handedness) and a
"Mirrored controls" row (vr.mirror_controls, dim when right-handed). Its
rows now go on two pages with tabs beside the title (PLAY, MORE; up and
down run on across them, a tab clicked shows its page). The controller
buttons page is built from the hands' jobs: the weapon hand's trigger
and grip, the free hand's, the move and turn sticks and the face
buttons (Quest X/Y for A/B; Steam Frame D-pad for A, B, X, Y) on the
sides the settings put them; the wrist, wheel and aiming notes name the
hands. network_vr.c's notes name the weapon and the free hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:26:03 +02:00
Stefan MaierandClaude Opus 5.5 b1d59e654d VR: left-handed, the motion tracker on the right wrist
The wrist layer goes on the free hand's wrist: halo_xr_submit.wrist_hand
(new, after wrist_down); host_xr.c keeps both grips and places the layer
on the right wrist as the mirror image of the left one's, read the right
way round.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:22:25 +02:00
Stefan MaierandClaude Opus 5.5 1f3d1be207 VR: left-handed controls, mirrored with vr.mirror_controls
Left-handed, in play, vr.c hands xinput_sdl.c the controllers with the
weapon hand's trigger and grip as the right side's (fire, melee, the
wheel's throttle) and the free hand's as the left's (grenade, foregrip,
brake). vr.mirror_controls (default true) mirrors the sticks (move on
the right, turn and steer on the left), their clicks, the bumpers and
the face buttons too: on the Quest X/Y for A/B; on the Steam Frame, whose
A, B, X and Y are all on the right controller, the left D-pad takes them
as their mirror image (down A, left B, right X, up Y) and they take the
D-pad, so every function stays reachable. The menus keep the buttons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:21:36 +02:00
Stefan MaierandClaude Opus 5.5 d97de6d499 VR: left-handed mode, the weapon in the left hand
vr.handedness = "left" puts the weapon in the left hand: aim, muzzle,
laser, crosshair, scope and turret aim follow the left controller; the
grenade, the foregrip and the fingers' closing round a grenade move to
the right hand, the grip's melee to the left. The first-person model is
posed as the right hand's in a mirror image of the room ("model space":
vr.c mirrors the controllers into it, first_person_weapons.c the body)
and the posed skeleton mirrored back, after HaloCE-Quest-VR (CC0); a
model whose node matrices mirror has its triangles' winding turned over
(rasterizer_set_model_skinning -> d3d8_gl.c, ended with the model and
each transparent group). halo_vr_left_hand is now halo_vr_free_hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:20:59 +02:00
Stefan MaierandClaude Opus 5.5 fbe48a869f VR roadmap: steps 1 and 3a done, in main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:53:21 +02:00
Stefan MaierandClaude Opus 5.5 206eb0b52b VR roadmap: the Steam Frame's tests done
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:46:55 +02:00
Stefan MaierandClaude Opus 5.5 1c66715a19 VR roadmap: the Quest's tests done
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:34:15 +02:00
Stefan MaierandClaude Opus 5.5 87f336df78 VR roadmap: 3a tried on the Quest
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:30:49 +02:00
saulo benigno d2e3fcfea6 Merge branch 'main' into feature/high-res-ui-buttons 2026-10-06 12:58:15 -03:00
Stefan MaierandClaude Opus 5.5 0a86e406c8 VR arms: the hand's roll leads the elbow; the free arm bent by default
The elbow could not be lifted out to the side: it bent only towards the
fixed pole (down, out, back). An elbow lifted out rolls the controller's
top in, so the pole now leans towards the hand's underside (the weapon's
for the right arm, the free left controller's for the left; the hand on
the foregrip keeps the fixed pole). With the free arm solved before its
hand is turned it looked right on the Quest: vr.arm_ik defaults to true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 17:56:47 +02:00
Stefan MaierandClaude Opus 5.5 c09b40a2ef VR arms: the free left arm solved first, its hand turned after
As HaloCE-Quest-VR does it: the free arm is bent to the controller's grip
from the animated pose (the hand free to swing with it), and only then is
the hand turned to the controller and the roll shared along the forearm.
Turning the hand first left the solve to swing it on, which stretched the
arm's skin like a rubber band (Quest, 2026-10-06). vr.arm_ik stays off by
default until it is seen right.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 17:54:15 +02:00
saulo benigno ec28d223ff Merge branch 'main' into feature/in-game-settings-menu 2026-10-06 12:49:08 -03:00
justinfilip 140f0298d7 Keep a co-op follow camera valid when the host looks up or down.
The upright direction is not square to that look, and the camera check halted the game.
2026-10-06 10:42:57 -05:00
Paul Hontz 3ddec63920 The visibility tests' counts on ES without waiting for the GPU
On OpenGL ES 3.1 and later, D3DDevice_GetVisibilityTestResult read the lens
flares' atomic counters with a blocking map: the CPU stopped every frame
until the GPU had finished the draws that counted, which halved the frame
rate on drivers that queue frames (Turnip, Zink).

Now the GPU copies the counters into the frame's snapshot buffer at the end
of each frame (a ring of three, with the stream buffers), and the frame's
tests are listed with it, each a result slot and its counter. Once the
frame's fence has passed, two frames on, the CPU reads the snapshot once
and gives each listed slot its count. A result is the latest count the GPU
has finished, as with the desktop's query buffer.

host_gl_read_buffer_word becomes host_gl_read_buffer, which reads a range;
glCopyBufferSubData joins the ES function list.
2026-10-06 11:41:39 -04:00
Stefan MaierandClaude Opus 5.5 1e2006d06c VR arms: the elbow bends in the arm's plane, the shoulders higher
Second Quest test: with the elbow out to the side the right elbow's skin
turned inwards: the shortest turn of the solve left the upper arm's hinge
where it was. vr_align_elbow now rolls the upper arm about its length so
the animation's bend lies in the plane the arm bends in, and the upper
arm takes no share of the hand's roll any more. The open sleeve end could
still be seen: the shoulders are 5 cm higher (20 cm below the neck).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 16:47:45 +02:00
Stefan MaierandClaude Opus 5.5 e69e087602 VR arms: the free arm rigid by default, the shoulders further back
First Quest test: the free left arm's skin stretched like a rubber band
with the new elbow IK, so vr.arm_ik now defaults to false (the arm's shape
as before, pointed at the shoulder); and the open end of the right
sleeve came into view looking down to the side: the shoulders now stand
4 cm behind the neck instead of 3 cm in front of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 15:59:41 +02:00
Milenko cdfb64e831 Scripts: room for a tick's object lists of a heavily scripted map
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)
2026-10-06 09:46:24 -04:00
Milenko e9746abbee Hosting on machines up for more than 24.8 days
GetTickCount and QueryPerformanceCounter counted from the host's boot; the
Xbox's count from power-on, and the game keeps times in signed 32-bit
variables. Past 2^31 ms of uptime (24.8 days), a hosted game timed out its
own client after about 15 s ("timeout in network_connection_idle", "the
game host went down"). Both clocks now count from when the game started,
beginning at 10 s.
2026-10-06 09:29:39 -04:00
MrBruh 76addf661f Second hardening round 2026-10-07 00:01:28 +13:00
Stefan MaierandClaude Opus 5.5 87a38e2895 VR roadmap: the pending headset tests, performance among them
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:45:44 +02:00
MrBruh 7ea610770c Gamepad layout preview in profile settings 2026-10-06 22:41:37 +13:00
Stefan MaierandClaude Opus 5.5 788ab74a6b VR roadmap: step 3a built in feature/vr-arms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:37:07 +02:00
Stefan MaierandClaude Opus 5.5 6d2dcfaaec VR arms 3a.6: vr.body (arms, hands, off) and vr.fingers in the VR options
- vr.body = "arms" (default): arms and hands as before; "hands": the arms'
  bones shrunk to a point 3.5 cm behind each wrist along the hand's line
  so the cuff closes behind the hand (vr_hide_arm, after HaloCE-Quest-VR's
  vr_hide_forearms, CC0), no arm IK solved; "off": the hands model is not
  drawn (one model draw less an eye) and nothing of the arms posed; the
  weapon, its aim with both hands, barrel, laser and crosshair stay.
- vr.c reads vr.body with the live settings (halo_vr_body_mode).
- The VR options panel gets "Body" (Arms and hands / Hands only / Weapon
  only) and "Fingers" (Tracked / Off) rows; its twelve rows are 22
  pixels apart (were 26) so the panel keeps its box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:35:07 +02:00
Stefan MaierandClaude Opus 5.5 98e848de39 VR arms 3a.5: the fingers posed
- vr_arms.c: vr_finger_nodes, vr_pose_finger (after HaloCE-Quest-VR, CC0,
  nearly verbatim): each finger's three joints found by name under the
  hand, posed by absolute directions in the palm's frame (a little bent
  at rest, a fist at full curl; the thumb up the handle or across the
  palm), each segment's subtree turned with its length kept. Its touch
  of the world (rays per finger) is left out for the Frame's processor.
- The free left hand takes its controller's curls (halo_vr_finger_curls:
  index from the trigger, the others from the grip, the thumb from where
  it rests; round a held grenade). On the foregrip the hand keeps the
  weapon's pose.
- The hand on the weapon keeps its grip; only its index finger is lifted
  part way off the trigger while the controller says it is (touch known
  and the finger off it), never where touch is unknown.
- Joints found once per skeleton; the cost is a few dozen node turns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:33:12 +02:00
Stefan MaierandClaude Opus 5.5 7754532683 VR arms 3a.4: the IK core from HaloCE-Quest-VR, the free left arm bent
- port/linux/game/vr_arms.c: vr_solve_arm and vr_distribute_arm_twist,
  after HaloCE-Quest-VR (CC0), nearly verbatim: the shoulder reaches 8 cm
  and the arm stretches up to 18% out of reach (further, the shoulder
  follows the held hand, as this port had it, so the arm stays whole),
  bends no further than 145 degrees, the elbow's swing eased per arm
  (vr_limb_history), the hand's roll shared by forearm (85%) and upper
  arm (25%) within 110 degrees, continuous across half a turn. The
  skeleton is read through the graph's index; the nodes under each node
  come from one pass a frame (vr_skeleton_build) rather than walking
  ancestors per node; time is the headset's frame's (halo_vr_pose_time).
- Both arms use it; the right hand and a left hand on the foregrip stay
  where the weapon puts them (hand_stays), so shots, barrel and laser do
  not move.
- The free left hand: its wrist and fingers are set on the controller as
  before, and the arm now bends to it from the shoulder instead of being
  carried rigidly. The ribbons of e377e4dc did not come from skin on the
  root (3a.0: none hangs there), so no triangle filter is needed; the
  rigid arm stays behind vr.arm_ik = false, read live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:31:16 +02:00
Stefan MaierandClaude Opus 5.5 19b0bcb42e VR arms 3a.3: the shoulders follow the torso, not the head
- vr.c keeps the torso's heading in LOCAL space (torso_update, after
  HaloCE-Quest-VR's vr_body_heading, CC0): the head's heading, steady at
  any pitch, drawn halfway towards the hands when both are ahead (at most
  45 degrees), followed beyond a 15 degree cone, eased at 10 a second;
  reset on a recentre or a gap. In LOCAL space the stick's snap and
  smooth turns carry it at once (HaloCE-Quest-VR's "arm IK lags as the
  body turns").
- halo_vr_body_frame gives the torso's heading and the neck the head
  turns about (9 cm behind the eyes); the shoulders hang from the neck,
  square to the torso, so turning the head no longer swings them (as the
  head faces the torso, where they were before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:26:24 +02:00
Stefan MaierandClaude Opus 5.5 f30dd50317 VR arms 3a.2: the fingers' input (touch, trigger, grip)
- host_xr: two actions, the thumb's touch (stick, buttons, thumb rest,
  the Frame's D-pad and View) and the index finger's (trigger), bound
  for the Touch, Index and Steam Frame profiles (paths after
  HaloCE-Quest-VR, CC0, and Valve's "Steam Frame Input" page) as optional
  bindings: a profile refusing them is suggested again without them.
- ABI: HALO_XR_BUTTON_THUMB_TOUCH, _INDEX_TOUCH and _TOUCH_KNOWN (the
  runtime gives the thumb's touch at all) in the buttons' free bits; the
  structures keep their size (android_abi_check: none unsafe).
- vr.c: each hand's thumb, index, middle and last two fingers, curled
  from those (the thumb half curled where touch is unknown, the hand
  round a held grenade), eased by HaloCE-Quest-VR's critically damped
  spring; halo_vr_finger_curls() hands them to the game; vr.fingers
  (default on) turns it off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:24:58 +02:00
Stefan MaierandClaude Opus 5.5 07a15190b4 host_xr: suggest() takes up to 64 bindings, says when it cannot, and retries
suggest() copied a profile's bindings into an array of 32 and silently
dropped the rest; the Steam Frame's profile already has 26, so a few
more would have lost bindings without a word. It takes 64 now and logs
an error past that, logs the runtime's result when a profile is
refused, and suggest_bindings() takes optional bindings besides: should
the runtime refuse the profile with them (one unknown path refuses the
whole suggestion), it is suggested again without them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:23:11 +02:00
Stefan MaierandClaude Opus 5.5 f254be4b45 VR arms 3a.1: the first-person pose built once a frame, not per eye
Posed from the camera at the right controller, the first-person weapon
and the arms are the same for both eyes and the scope's view, yet were
built (animation, interpolation, arm IK) for each: twice a frame on
SteamVR, three times zoomed in. The pose is kept for the rest of the
headset's frame (halo_vr_frame_serial) once built from the controller,
for the same weapon; posed from an eye's own camera (the hand not
tracked), each eye still builds its own. vr.log_performance counts the
builds and the poses kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:22:45 +02:00
Stefan MaierandClaude Opus 5.5 a6350d207a VR arms 3a.0: time the first-person pose, log the hands' skin
- vr.log_performance reports how long the first-person pose takes a frame
  (its animation and the arms), how often it is built, and the arms' part
  of it (halo_vr_timing_start/_end).
- Once per hands model it logs how many triangles hang from each node and
  how many of those from the skeleton's root ('frame bone24'), read from
  the part's own buffers (rasterizer_model_buffer_data, after
  HaloCE-Quest-VR, CC0). In the emulator the cyborg's hands have none on
  the root: the ribbons of e377e4dc did not come from root skin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 11:22:08 +02:00
Stefan MaierandClaude Opus 5.5 1929702f5b Steam Frame CPU: Mesa's trace markers quiet, no glGet in the counters' copies; VR roadmap
- Valve's Mesa in Lepton writes a kernel trace marker around every flush
  and context switch; its descriptors on trace_marker are pointed at
  /dev/null once the GL context and the OpenXR session exist (after
  astromaddie/HaloCE-VR, CC0)
- host_gl_counters_snapshot/_read no longer ask GL for the copy bindings
  (each glGet waits for Mesa's GL thread): they are left unbound
- port/android/VR_ROADMAP.md: what is done and the agreed next steps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:58:04 +02:00
MrBruh 22249cf192 Windows crash reports 2026-10-06 20:32:36 +13:00
Stefan MaierandClaude Opus 5.5 9d6b980ba6 Merge upstream (OpenCE 9e4f8a2d): performance with many enemies, co-op fixes, anti-aliasing, per-pixel lighting, shadow resolution, reverb, password lobbies, network version 20
Conflicts: d3d8_gl.c (multiview variants kept beside the per-pixel lit
shaders: shader[4] and lit_shader[4], vertex_shader_get takes both;
multiview targets before multisampling; the picture read resolves both),
nv2a_vsh.c/xgpu.h (lighting and multiview parameters), render.c (no
anti-aliasing pass on VR eye passes), dsound_sdl.c, p2p.h.
VR: anti-aliasing off (the eyes go to the headset from their textures);
the VR online list skips password games (the server browser asks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 09:29:14 +02:00
RockBooch abae0d0928 Update bipeds.c
Turn around while throwing grenade no longer locks until animation is over
2026-10-06 02:47:45 -04:00
RockBooch fd7f2d30b6 Update units.c
Blend leg directions and ease the stop during grenade throws
2026-10-06 02:27:29 -04:00
MrBruh 9e4f8a2dcb Merge pull request #115 from eltanschauung/fix/attached-light-storage
Fix missing dynamic lights and attached glows on Windows
2026-10-06 19:09:18 +13:00
eltanschauung 16b5436796 Test powerup mesh bounds, fallback cases and frustum edge sweeps 2026-10-06 01:29:53 -04:00
eltanschauung 77efb01b00 Grow culling spheres with enlarged objects 2026-10-06 01:29:48 -04:00
eltanschauung 201874d783 Derive conservative render spheres from rigid powerup meshes 2026-10-06 01:29:42 -04:00
eltanschauung 859e423cbc Derive powerup render bounds when loading map tags 2026-10-06 01:29:35 -04:00
eltanschauung 53c268b8a6 Preserve existing workflow line endings 2026-10-06 01:18:49 -04:00
eltanschauung 592be6270a Run light storage regressions in Windows and Linux CI 2026-10-06 01:17:37 -04:00
eltanschauung 8817c37470 Test native light storage with ordinary and LTO links 2026-10-06 01:17:23 -04:00
eltanschauung d635d83725 Fix dynamic lighting storage corruption on Windows 2026-10-06 01:16:42 -04:00
MrBruh 2a6dc2b738 Show the server browser's lock icon 2026-10-06 18:08:45 +13:00
MrBruh 6112dcfc5a Password-protected public lobbies 2026-10-06 17:51:54 +13:00
MrBruh 62fa7e13ef Co-op player collisions option 2026-10-06 17:03:38 +13:00
RockBooch d25fa0fbaf Merge remote-tracking branch 'upstream/main' into cyborg-animation-enhances 2026-10-05 23:40:45 -04:00
MrBruh 9a12828762 Play MP maps alone 2026-10-06 16:27:27 +13:00
eltanschauung 9990227153 Merge branch 'main' into fix/health-meter-thresholds 2026-10-05 23:22:44 -04:00
RockBooch 29a2a1945a Merge remote-tracking branch 'upstream/main' into cyborg-animation-enhances 2026-10-05 23:13:09 -04:00
MrBruh 7e82e7128e New settings in the settings menus
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.
2026-10-06 15:36:22 +13:00
MrBruh 94882796b7 Anti-aliasing
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.
2026-10-06 15:24:59 +13:00
RockBooch 102e093436 Update units.c
Keep legs moving during grenade throws
2026-10-05 22:20:57 -04:00
MrBruh 3dba558e4f Per-pixel model lighting
display.per_pixel_lighting (off by default) lights the models for each
pixel by the same ambient, distant and point lights the model lighting
programs (9, 10, 17 and 27) sum for each vertex. The normal and position
are found in each program's instructions and the program is checked to
light as the pixel shader does; the lights come in a small uniform of
their own, and a shader that fails falls back to per-vertex lighting.

Based on #48 by Tyberious.
2026-10-06 15:08:32 +13:00
MrBruh 1dc533fe18 Shadow map resolution setting
display.shadow_resolution (128 by default, as on the Xbox; 256, 512 or
1024) draws the two shadow maps larger. The blur's taps move to half a
texel of the larger map and two passes more for each doubling widen it
to the Xbox's tent, so the shadows keep their softness.

Based on #47 by Tyberious.
2026-10-06 14:59:21 +13:00
MrBruh 8a8e70599f I3DL2 reverb and muffled sounds
Based on #44 and #46 by Tyberious. 3D voices send to an I3DL2 reverb of the camera's sound environment, and obstruction and occlusion muffle their direct path above the HF reference (occlusion their send too). Both use the fixed distance; a maximum distance under the minimum is held at it. A change of environment crossfades the delays and gains over 85 ms, the filters' coefficients ramp over each mix, the diffusers, lines and filters flush denormals, and the reverb stops while it is quiet. audio.reverb (default true) is read again when the settings change.
2026-10-06 14:39:41 +13:00
MrBruh b115a41263 Resample with a table per phase
Fixup for #42: a voice at the output rate or slower blends two rows of weights by its phase instead of working out each tap's (the same low pass, a third of the work), and a voice turned all the way down only moves on.
2026-10-06 13:58:46 +13:00
eltanschauung be69b5abcb Generate categorical health thresholds and test real GPU rendering 2026-10-05 20:57:53 -04:00
eltanschauung a56488379c Store exact segment thresholds separately from health artwork 2026-10-05 20:57:19 -04:00
MrBruh 320464258a Merge pull request #42 from Tyberious/audio-resampling 2026-10-06 13:57:04 +13:00
MrBruh f5bb75c27c Widen the constant checkpoint serial
Fixup for #52: the checkpoint serial main added since is 64-bit too, as the serials it is compared with are.
2026-10-06 13:56:58 +13:00
eltanschauung c40d7b22fc Read discrete health thresholds independently of filtered HUD artwork 2026-10-05 20:56:45 -04:00
MrBruh 5cd1daff77 Merge pull request #52 from Tyberious/constant-serial-wrap 2026-10-06 13:56:35 +13:00
MrBruh 6329be8aa1 Merge pull request #53 from Tyberious/interpolation-snaps 2026-10-06 13:56:35 +13:00
MrBruh 7a2f4caa50 Merge pull request #43 from Tyberious/audio-limiter 2026-10-06 13:56:35 +13:00
MrBruh 938c37a669 Merge pull request #45 from Tyberious/audio-distance 2026-10-06 13:56:35 +13:00
MrBruh fae3fbad69 Merge pull request #41 from Tyberious/adpcm-decoding 2026-10-06 13:56:35 +13:00
MrBruh 5a35ea2b20 Network version 18
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.
2026-10-06 13:39:26 +13:00
MrBruh 2e179ea90f Merge pull request #112 from innerhat-dev/networked-break-and-frame-scratch 2026-10-06 13:34:53 +13:00
MrBruh 9b517e554c Merge pull request #99 from xshxdex98/coop-shader-permutation 2026-10-06 13:29:02 +13:00
MrBruh 595bbc5611 Merge pull request #113 from xshxdex98/quiet-garbage-collection 2026-10-06 13:29:02 +13:00
MrBruh 761210d405 Harden map and network input 2026-10-06 13:26:43 +13:00
xshxdex98 7ba780ef59 Garbage collection's notices are chatter: on screen only with console_log = "all"
With many enemies (co-op's extra enemies) object memory runs low and the
garbage collector prints "garbage collection critical" and "removing
objects" at every pass, filling the screen for the host and every client.
They are the game's own chatter, so they show as game.console_log says, as
the sound and texture caches' already do; the log keeps every line.
2026-10-05 18:55:53 -05:00
justinfilip d9c43e3478 Keep a networked glass break from reading the scenario as a damage effect.
The host's notice of a broken surface carries no damage effect. The client filled a damage record with zeros and then read tag 0, which is the scenario, and halted. Shards still spawn. They skip the kick from an effect this machine never received.

The frame scratch pool was still the Xbox size after the particle pool grew eight times, so a busy level spent it every frame. It now grows with that particle pool, and a frame that still runs out is logged once.

A console line longer than the buffer halted inside the error printer, before the reason was written to the log. Long lines are cut to fit, and the reason is written first.
2026-10-05 17:19:35 -05:00
Stefan MaierandClaude Opus 5.5 c04ce7fd00 Steam Frame: each eye drawn apart (no multiview on SteamVR)
With both eyes drawn at once (multiview) the eyes' pictures did not fuse
on the Steam Frame, now and then: each eye alone sharp, both together
doubled as the head turned, in the menus too. With vr.multiview = false
the user saw no doubling, at the same frame time (the Frame's time goes
to the GL driver either way). debug.multiview_steamvr brings multiview
back there for finding the cause; the Quest keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:54:53 +02:00
Stefan MaierandClaude Opus 5.5 3f06b1b9d8 Revert "Steam Frame: the layers' images acquired before the frame waits"
This reverts commit 24e583c0. With the images acquired before xrWaitFrame
the frames came fast (about 8 ms of work) but the picture doubled again
on the Steam Frame as the head turned (181 of 3753 frames reprojected);
without it (673b32b1) it did not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:40:35 +02:00
Stefan MaierandClaude Opus 5.5 1b2d058a04 VR multiview: lens flares placed by their depth in each eye
A lens flare is projected on the CPU (w 1, its depth in z) and drawn
without the depth test, which multiview took for something at infinity,
as the sun's flares are. A weapon's own lights have flares too, at its
markers half a metre from the eyes (the plasma pistol's at its prongs,
the plasma rifle's at its muzzle): each eye saw its flare where infinity
puts it, a green ball at the barrel the eyes could not fuse. While flares
are drawn (rasterizer_lens_flares_draw), each eye's is now placed by the
flare's depth, as its occlusion test already was; the sun's are far
enough for infinity either way. Whether a flare shows at all depends on
its occlusion test, which is why the balls came and went, and showed on
the Frame more than on the Quest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:34:56 +02:00
Stefan MaierandClaude Opus 5.5 b5fa53ed95 Steam Frame: the layers' images acquired before the frame waits
With the copies deferred the wait only moved: acquiring the images took
8-10 ms a frame, the left eye's 6-8 of it. SteamVR's acquire (or its wait
for the image) waits for the GL work queued before it, the eyes' whole
drawing when the left eye's copy asked, the HUD's for the overlay. The
images the last frame filled are now acquired before xrWaitFrame, when
little is queued and the frame would wait for the headset anyway; one a
frame needs that was not acquired ahead is acquired as before. The
performance log shows the time acquiring ahead apart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:34:56 +02:00
Stefan MaierandClaude Opus 5.5 1f2c10a3ca Menus' art: a texture outside the art's range is no menu bitmap
Every texture the renderer binds asks whether it is a menu bitmap
standing for its PNG (menu_art_texture), in a game too, where the PC
menus' bitmaps are loaded for the pause menu's settings: a walk over the
150-odd registered bitmaps for each texture of each draw. Their textures
lie together, so one lowest/highest check answers for the map's own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:11:05 +02:00
Stefan MaierandClaude Opus 5.5 12f1155752 Steam Frame: the headset's layer copies without waits in the frame
The Frame's game thread spent most of each 13.9 ms frame waiting, not
working: the left eye's copy into its swapchain took 4.5-7 ms, the right
one's right after it 0.7, each later copy 0.7-3. Every copy read the GL
state back to restore it (glGet*, glIsEnabled, the source's swizzle) and
released its image at once; with Mesa's GL thread (zink) each read waits
for the thread to run everything queued, each release for the GPU, so the
thread stood still for the eyes' whole drawing mid-frame, and frames came
late enough for SteamVR to reproject 2-8% of them (the doubled picture
when turning the head).

Deferred copies (SteamVR by default; debug.xr_deferred_copies 0 or 1 says
otherwise; the Quest's path is unchanged):
- nothing read back: a copy leaves GL as it drew and returns 2; vr.c has
  the renderer set its state again (d3d8_host_state_changed: the state
  cache invalidated, its vertex arrays bound again)
- no swizzle round trip: copies read render targets, which have none
- the images kept until the frame ends and released together before
  xrEndFrame; the eyes' work flushed to the GPU once they are copied
- the performance log times acquiring and releasing apart

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:11:05 +02:00
MrBruh 2b0327bc80 Network version 17
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.
2026-10-06 08:59:22 +13:00
MrBruh 58aa352705 Merge pull request #107 from xshxdex98/sync-glass-and-scenery 2026-10-06 08:40:24 +13:00
MrBruh 59c4b2160e Merge pull request #106 from xshxdex98/coop-scripts-for-everyone 2026-10-06 08:40:24 +13:00
MrBruh 1cf336f0ff Merge pull request #105 from xshxdex98/coop-enemy-spread 2026-10-06 08:40:24 +13:00
MrBruh d10969dc83 Merge pull request #103 from xshxdex98/coop-backtrack-team 2026-10-06 08:40:24 +13:00
MrBruh fe1d51dd58 Merge pull request #102 from xshxdex98/coop-bsp-wait 2026-10-06 08:40:24 +13:00
Stefan MaierandClaude Opus 5.5 24e4123882 VR performance diagnostics: the frame's parts, the layer copies, Mesa's GL thread switch
- vr.log_performance: frames with game ticks and without, how many late,
  and their time by part (input, ticks, camera, draw, present)
- with XR_META_performance_metrics: per frame, each layer copy's time, the
  waits for swapchain images and xrEndFrame
- debug.gl_thread: Mesa's GL thread on or off (mesa_glthread) for trying

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:33:00 +02:00
MrBruh 197c199473 Improve performance by ~80% with many enemies
The Silent Cartographer with 32x co-op enemies (484 actors) went from 53
to about 97 fps on the Linux PC, with the same game and the same picture:
cluster list removals start where the reference was put, full data arrays
are not walked again, object lighting keeps each cluster's drawn lights
and passes over far ones without their distance, the per-texture profile
timer only runs while something reads it, and the renderer binds streams
once per draw, caches its texture, target and shader lookups, and no
longer makes Mesa's GL thread wait on large uploads or visibility tests.
2026-10-06 08:13:44 +13:00
eltanschauung 0db37662aa Test rebindable screenshot input and real GL-to-PNG capture 2026-10-05 14:42:15 -04:00
eltanschauung 8d2d81179e Document Screenshot controls and timestamped PNGs 2026-10-05 14:41:35 -04:00
eltanschauung 15566ec76b Define the rebindable Screenshot action 2026-10-05 14:41:08 -04:00
eltanschauung c6959530d5 Expose Screenshot below Pause Menu in Controls Setup 2026-10-05 14:40:23 -04:00
eltanschauung 58a6bd2f5e Add rebindable PNG Screenshot action, default F10 2026-10-05 14:39:42 -04:00
xshxdex98 0acc3b1381 Network damage: one place fills a damage event
damage_event_new fills the fields every damage event has, for scenery's
and units' alike.
2026-10-05 13:11:01 -05:00
xshxdex98 1e74e9b03e Co-op scripts: follower actions named, the skip's revert plainer
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.
2026-10-05 13:10:29 -05:00
xshxdex98 5d108579ed Co-op extra enemies: one function judges a spot
coop_enemies_spot_free makes every check of a spot (way, floor, headroom,
nobody there) in one place, each constant has its own comment, and the
file comment's wrapping is fixed.
2026-10-05 13:09:58 -05:00
xshxdex98 7a1ffca28b Network: glass and destructible scenery break the same for everyone
Breakable surfaces (glass) are broken by each machine's own projectiles
with its own random damage, so a pane could shatter on the host and stay
whole, still blocking, on a client. In co-op the host now sends each break
(and a few again every refresh, for late joiners), and a client breaks the
same surface with its shards.

Destructible scenery and devices (lights, panels) are the map's, at the
same index on every machine, but only damage to units was synced: a
client's shots never damaged them, and what the host broke didn't break
or play its effects on the clients. A client now reports its hits on them
like any other, the host checks and deals them, and every client deals the
host's damage to its own copy, which breaks, plays its effects and is
destroyed as the host's was.

Both are new message kinds that a client without them ignores, so the
network version is unchanged.
2026-10-05 13:00:01 -05:00
xshxdex98 b843156fe8 Network: a client never reverts its game on its own
The pause menu's revert (main_revert_map) reverted a network client's own
game state, leaving it out of step with the host. A client now ignores it,
and the cutscene skip's local revert, which a client only reaches outside
the co-op vote, is the host's or a local game's alone.
2026-10-05 12:59:00 -05:00
xshxdex98 99a31ca82d Co-op: a cutscene waiting until it's safe waits for one safe player
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.
2026-10-05 12:58:59 -05:00
xshxdex98 9666575b39 Co-op extra enemies: same floor, clear of crates, room to stand
A spot's ground was looked for up to 2.6 m below it, so an extra enemy
could land under a ledge or catwalk, and only walls were in the way, so
it could stand inside a crate or a machine.

A spot now needs a floor within 0.5 m of the starting location's, nothing
in the way from beside the starting location (level, scenery, machines,
vehicles), and 1.6 m of room above its floor.
2026-10-05 12:58:21 -05:00
Stefan MaierandClaude Opus 5.5 8466846d45 Steam Frame: the frames' thread kept on the fast cores
Without XR_EXT_performance_settings (SteamVR in Lepton) the scheduler may
put the game's busy thread on the 2.3 GHz cores; frames then come late to
the compositor and the picture doubles as the head turns, now and then,
at a steady 72 fps. On SteamVR the thread that ends the frames is kept on
the cores no slower than 90% of the fastest (HALO_XR_ANY_CORE: not); the
metrics line says which core it is on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:53:47 +02:00
Stefan MaierandClaude Opus 5.5 bc580cf1cf VR: the compositor's performance counters in the log (XR_META_performance_metrics)
Every five seconds, where the runtime offers them (SteamVR on the Steam
Frame): to tell frames the compositor showed late or reprojected from the
game's own pace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:46:54 +02:00
xshxdex98 8e6b9a211e Co-op: triggers, cutscenes and failed missions work for every player
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.
2026-10-05 12:43:48 -05:00
xshxdex98 d0016bca89 Co-op extra enemies: spread onto free ground, never stacked
An extra enemy was placed on a ring around its starting location, trying
one spot per ring, and checked only against walls. When the 8 tries failed
(tight corridors, the Maw's interiors) it went on the starting location
itself, on top of the squad's own actor, and nothing stopped it landing on
another starting location's actor a metre away.

Now every spot of each ring is tried (a ring holds more spots the further
out it is), and a spot needs ground under it, no wall in the way and no
biped within 0.6 m or vehicle around it. A starting location with no room
passes its enemy to the squad's next one, and with no room anywhere the
enemy isn't placed. Dropship riders released beside the ones who jumped
out are spread the same way.
2026-10-05 12:41:08 -05:00
Stefan MaierandClaude Opus 5.5 3d575f32ed Bots on foot: ramps, rails, door frames and things out of reach
- a way is clear only at the knees and the chest (a rail with room under
  it, a slit in a wall), and for a shortcut along a path at the shoulders
  too (a door frame)
- stuck below a near point (a step, a ramp's lip): a jump on towards it
- a thing to pick up it comes no nearer to for 2.5 s is given up
- stuck three times in five seconds: the goal given up, somewhere away
- at the point it walks to: arrived, not stuck

Emulator, Blood Gulch, 15 bots: CTF stuck events 264 -> 153, mid-field
26% -> 32%, kills 26 -> 33; team slayer stuck events 860 -> 560.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:33:16 +02:00
Stefan MaierandClaude Opus 5.5 727b9ca80d Bots on foot: no crowding at things out of reach, spread over the enemies, hold spots they can reach
- a thing to pick up that it stands by and is not offered (on a ledge,
  out of reach) is given up for 20 s; up to four remembered, in the
  fight too (the crowds jumping at the base's weapons)
- stuck: the side step goes to the open side
- goals: each bot one of the three nearest enemies, and beside them while
  far, so a team spreads out
- holding a spot: stops near it, and one it cannot reach falls back to
  the place's centre

Emulator, Blood Gulch, 15 bots: CTF stuck events 1793 -> 264; team slayer
stuck windows 32% -> 9-15%, time by the bases 85% -> 68-73%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 18:52:30 +02:00
Stefan MaierandClaude Opus 5.5 99d7c924db Bots: a person joining a lobby the bots fill takes a bot's place
The bots fill the game up to its maximum; a machine joining the lobby
keeps its place (no bot added for it) and its player takes the last
bot's; the game lists as open while a bot could leave.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 18:00:31 +02:00
Stefan MaierandClaude Opus 5.5 57d96e4681 VR PC menus: fixes from the first Frame test
- bots: no more than the game's MAXIMUM PLAYERS has room for beside the
  people (instead of raising it); the host options show and change the
  bots there are
- VR options hint on the PC main menu drawn whole (left-aligned)
- the online and host panels: a click beside them or on their key's B
  closes them, on the online panel's X enters a code
- spinners' arrows (drawn just outside them) clickable
- Android flat (the emulator): the mouse drives the menus' pointer, for
  testing the menus' clicks with adb shell input mouse
- Frame launcher: Lepton's logcat of the game's container in logcat.txt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:45:41 +02:00
Stefan MaierandClaude Opus 5.5 177ad4d40d VR: the PC version's menus (vr.menus, on by default)
- menu_tags.c builds the PC menus in VR too unless vr.menus = "xbox"
- their text fields (Server Setup's game name) typed on the Xbox's
  on-screen keyboard in VR
- join code / online panel on the PC Server Browser (X or click its hint),
  host options (Y or click) and join code on the PC lobby, VR options on
  the PC main menu
- bots: room made for them beside the people (Server Setup's maximum
  players), none in co-op
- player_name_valid accepts trailing spaces (Xbox menus pad new profile
  names, "New001     ", which the PC lobby refused)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:17:16 +02:00
Stefan MaierandClaude Opus 5.5 bfb668d498 Merge branch 'merge/upstream-2026-10-05'
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:00:26 +02:00
Stefan MaierandClaude Opus 5.5 fc2b6551ca Merge upstream (OpenCE 13c14df9): online co-op campaign, online split screen, network version 16
Upstream's online co-op (network_actors.c, network_coop.c) ticks beside the
VR port's VR relay and bots; its message types end at 77, below the VR
port's from 240. The look and move flags the action test reads moved into
player_control_action_test_note: the head's turning goes there too. The
vertex shader's screen-space undo is upstream's on every platform now, with
the multiview eye offset after it on Android. The APK carries upstream's
brokers.txt beside the FDM layer stub; the updaters keep the VR port's own
repository (upstream's now points at OpenCommunityEdition/OpenCE).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:22:05 +02:00
Stefan Maier 5316a6b145 Merge branch 'frame/launcher' 2026-10-05 16:19:02 +02:00
Stefan MaierandClaude Opus 5.5 59b1fb422b Steam Frame: a library button that starts, updates and quits the game
The launcher gives Lepton what Steam gives an Android title of its own (a
compat data path, which the app's data links to after an install, and the
shader path), keeps a container id of its own, tries a start that ends at
once again (a new APK's first start only installs it), and stops when the
game was quit. Steam's "Exit game" cannot reach the game in Lepton's
container: the VR options get "Quit Halo" (asked twice), which on Android
now ends the process and leaves quit_requested.txt for the launcher. The
library's pictures are in port/android/steam_frame_grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:17:35 +02:00
xshxdex98 dc383d10da Co-op: clients draw AI units with their variant's shader permutation
An actor variant sets its unit's forced shader permutation after the unit
is made, and only the host runs the AI, so on clients Elite majors and
commanders drew the minor's armor (bitmap 0 of the cube maps instead of 1
and 2). The object creation message carries it in its spare byte, which
builds without this send as 0; a client applies only one that isn't 0, so
the message keeps its size and games hosted on builds without this keep
each tag's own.
2026-10-05 09:04:54 -05:00
xshxdex98 a143d9490d Co-op: tell a player held back at a loading zone what it waits for
Going back to a BSP the team has been in waits for two thirds of the team,
and a player on the trigger was never told so. They now see "Waiting for
your team to go back: 1 of 2 here", at once and every three seconds while
they wait. Each machine works it out for its own players with the count the
host decides by, now one function, so nothing new goes over the network.
2026-10-05 08:51:49 -05:00
xshxdex98 e55c29a97e Co-op: going back counts the team within 45 metres, not 12
Two thirds of the players had to be in the loading zone or within 4 world
units of whoever was in it. A team walking back through a doorway in a line
is longer than that, so going back almost never switched: the first player
walked on past the loaded level, fell and was brought back to the team.
15 world units fits a team moving together and still not a player who ran
back alone.
2026-10-05 08:51:49 -05:00
xshxdex98 16361bdef1 Co-op: a client's player waits on its floor for the host's BSP
The host's switch and its players' new positions travel separately, so a
client could hear where the host moved its player, in the new BSP, before
the switch itself, and drop through the floor until the BSP loaded. A
client now keeps its player on the floor of the BSP it has until the
host's position is somewhere it has loaded. Nothing changes on the wire.
2026-10-05 08:29:25 -05:00
Stefan MaierandClaude Opus 5.5 5ecd3075a0 Steam Frame: a launcher that starts the game as Steam starts Android titles
Lepton runs the APK in a container of its own, kept between starts, and
installs it anew whenever the APK is newer; the script starts it a second
time after a new APK's first start, which only installs it. With the APK in
~/Documents/HaloCE, updating the game is replacing that file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 15:17:54 +02:00
MrBruh 13c14df95c Merge pull request #101 from xshxdex98/coop-bsp-sync
Co-op: every machine stays on the host's BSP (network version 16)
2026-10-06 02:13:10 +13:00
xshxdex98 b525bf8514 Network version 16
A client's input says which structure BSP it has loaded (co-op).
2026-10-05 08:09:47 -05:00
xshxdex98 087ca8a875 Co-op: every machine stays on the host's BSP
Players in big sessions ended up on different BSPs:

- The host told clients about a switch only every half second, and not
  reliably, so a client could predict its player for a while in a BSP it
  hadn't loaded. A switch is now sent at once, reliably, ahead of the
  players' new positions.
- A client still loading reports so: its input carries the BSP it has
  loaded. Until it has the host's, the host takes none of its movement
  (which falls there with no floor) and none of its loading zones.
- After a switch, by a trigger, a script or a skipped cutscene, no trigger
  can switch again until every machine has loaded the new BSP (at most
  ten seconds), so no client falls two switches behind.
- A player the host can't place beside whoever crossed, when a big team
  runs out of room there, is placed beside another teammate instead of
  being left where they were.
2026-10-05 08:09:46 -05:00
MrBruh 24957ff6a0 Network version 15 2026-10-06 01:28:04 +13:00
MrBruh 402d4b8351 Merge pull request #97 from xshxdex98/coop-dead-vehicles 2026-10-06 01:24:23 +13:00
MrBruh d1825ab08d Merge pull request #96 from xshxdex98/coop-backtrack-gate 2026-10-06 01:24:22 +13:00
MrBruh 268ec84b74 Merge pull request #95 from xshxdex98/coop-allegiances 2026-10-06 01:24:21 +13:00
xshxdex98 8dc796028e Co-op: going back to a visited BSP needs two thirds of the team
One player who stayed behind or went AFK no longer blocks the team.
2026-10-05 07:14:28 -05:00
xshxdex98 8aa381ed14 Co-op: vehicles the host destroys explode on clients
Clients only had a kill path for bipeds, so a Ghost, Banshee or Wraith the
host destroyed stayed intact on clients. A vehicle the host's state says
is dead is now destroyed there too, with its explosion.
2026-10-05 07:01:37 -05:00
xshxdex98 c3f82fa9b2 Co-op: going back to a BSP already visited needs the whole team there
A loading zone brings the whole team to whoever crosses it. Into a new part
of the level that's what you want, but it let one player drag everyone
back. A switch back to a BSP the team has been in now waits until every
living player is in the trigger or near the player in it.
2026-10-05 07:01:32 -05:00
xshxdex98 5ff4b6ee8d Co-op: clients take the host's allegiances
The scripts make allegiances (ai_allegiance player human) and only the host
runs them, so on clients marines stayed enemies of the players: red reticles
and the wrong team. The host sends its ally and friend tables with the
presentation each tick.
2026-10-05 07:01:26 -05:00
MrBruh 91e838e2f6 Network version 14 2026-10-06 00:54:11 +13:00
MrBruh 520b8401db Merge pull request #93 from xshxdex98/test-death-timing-coop 2026-10-06 00:51:37 +13:00
MrBruh 2ed4a7b49d Merge pull request #92 from xshxdex98/coop-flood-dropships 2026-10-06 00:51:36 +13:00
MrBruh 503992d544 Merge pull request #91 from xshxdex98/coop-device-states 2026-10-06 00:51:35 +13:00
MrBruh ccc7df6611 Merge pull request #90 from xshxdex98/coop-loading-zones 2026-10-06 00:51:35 +13:00
xshxdex98 ddcbe79283 test_death_timing.py: stubs for the network co-op paths
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.
2026-10-05 06:41:27 -05:00
xshxdex98 862d88737e Co-op: Flood bodies get up on clients; dropship doors open and close there
- A body the host's unit got back up from (a Flood combat form feigning
  death) gets up on the clients too, instead of its ragdoll being dragged
  about. The resurrection is one function both use (unit_port_resurrect).
- The scripts' unit_open and unit_close go to the clients, so dropships'
  doors aren't left open there.
2026-10-05 06:36:27 -05:00
xshxdex98 8610b28376 Co-op: doors and elevators are where the host has them
The host sends each device's position and power when they change (and with
the resent state), and a client puts its device there when the host's is at
rest and they differ, or when they drift apart while it moves. Clients only
had the device groups' values, which aim a device: a door could stop half
open, and an elevator be somewhere else on a client, so its players fell.
2026-10-05 06:36:27 -05:00
xshxdex98 9e6247e0cb Co-op: a loading zone brings the team along; only someone falling is rescued
- Crossing a BSP loading zone moves every other player to whoever crossed,
  as split screen does. Moving only some players left the rest where the
  next switch threw them back to a hallway, or under a floor.
- The rescue for a player outside the loaded BSP waits two seconds and
  only takes someone falling: a player riding an elevator through its
  shaft is left alone.
2026-10-05 06:34:00 -05:00
MrBruh dea5fe3b1e Fix crash past 256 actors 2026-10-06 00:29:42 +13:00
MrBruh f0804aa587 Extra enemies: per player or static multiplier 2026-10-06 00:08:26 +13:00
MrBruh 16ad4cc900 25% extra enemies option 2026-10-05 23:45:31 +13:00
MrBruh a38ede078a Co-op extra enemies per player 2026-10-05 23:25:15 +13:00
MrBruh fa85aa53f5 Kick command; co-op friendly fire on by default 2026-10-05 22:51:05 +13:00
MrBruh 8be9a038a1 Co-op friendly fire setting 2026-10-05 22:40:34 +13:00
MrBruh 783b591a6a Co-op defaults to 16 players and private 2026-10-05 22:27:36 +13:00
MrBruh e82d880060 Singleplayer and multiplayer map choice 2026-10-05 22:17:22 +13:00
MrBruh 7d0e587c51 Co-op review fixes 2026-10-05 21:02:04 +13:00
MrBruh 01586cfebf Merge pull request #89 from xshxdex98/coop-campaign 2026-10-05 20:29:19 +13:00
MrBruh de52060644 Host takes grenade damage only from grenades thrown 2026-10-05 19:54:33 +13:00
MrBruh ab7c507a29 Leaving the lobby after a game goes to the main menu 2026-10-05 19:39:57 +13:00
xshxdex98 ba6fe0a826 AI sync: any team the host's unit has goes with it, not only the campaign's 2026-10-05 00:59:33 -05:00
MrBruh 3f6c0c37a4 Friendly fire stops teammates' instant kills 2026-10-05 18:47:39 +13:00
xshxdex98 606109551d Co-op: friendly reticles, skipped cutscenes' sound, the end of a level, tutorial prompts
- AI units' teams go with their states, so friendlies aren't red on
  clients' reticles.
- A skipped cutscene's music and dialogue stop on the clients.
- Winning a level returns every machine to the lobby with the next level
  set up, instead of leaving them on its last screen.
- Help text carries whether the host shows it, so no prompt is dropped.
2026-10-05 00:46:51 -05:00
xshxdex98 8194c5a00b Online co-op: play the campaign together over LAN and the internet
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.
2026-10-05 00:31:30 -05:00
MrBruh 737551fd51 Resolution, scaling and window size settings 2026-10-05 18:28:58 +13:00
xshxdex98 7a56c624f5 Merge upstream main 2026-10-05 00:07:13 -05:00
MrBruh 3ae09c3d2c Fade the radar wedge out fully 2026-10-05 17:23:47 +13:00
MrBruh 3a22816c2f Merge pull request #81 from FDervish/fix/antenna-stepped-per-tick 2026-10-05 17:15:38 +13:00
MrBruh 2853a42253 Clear an antenna's shape record when it is created 2026-10-05 17:13:27 +13:00
Fadi DerouicheandClaude Opus 5 39ece781ee Step the antenna chain in the tick and draw the frames between the ticks
An antenna's chain is a simulation of the Xbox's own step, an update a tick,
and the port was stepping it once a frame with the frame's own time. Its
springs, its carry and the points' physics then acted against the frames'
movement of it, so how far the chain moved per update depended on the frame
rate, and a vehicle at speed swung it far wider than a tick's step does.

- game.c steps the chain in the tick, beside the effects, which are already
  stepped there.
- antenna.c records the shape each tick leaves, relative to the point the
  vehicle's marker carries the base by. A frame drawn between two ticks is the
  part of the way between those two shapes that it is, carried to where the
  marker is now, so the chain stays on the vehicle it hangs from instead of
  lagging a tick behind it. The gate that left a chain that had not been drawn
  for five updates unstepped counted frames; it counts ticks now, which is
  what it counted on the Xbox.
- widgets.c no longer steps the chain from the widget update, which ran once a
  frame.
- game_time.c draws the per-frame effects and widgets that hang off objects
  (game_frame) with the pose between the last two ticks, the pose the renderer
  draws with, so what they place on an object is drawn where they put it.
- objects.c hands markers the matrices the renderer draws with, so an effect,
  a particle system or a contrail attached to a moving object follows it
  between the ticks too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-05 17:13:27 +13:00
MrBruh cf17201ec8 Split screen divider spans wide screens 2026-10-05 17:13:27 +13:00
MrBruh 45de17116b Merge pull request #82 from addisonbair/gamescope-fullscreen
Count a fullscreen window that gamescope never confirms as fullscreen
2026-10-05 17:01:51 +13:00
xshxdex98 7d56f34b83 Map screen: choose co-op campaign or multiplayer, then vanilla or custom maps
Hosting over LAN or the internet, the Map screen's list first offers CO-OP
CAMPAIGN or MULTIPLAYER. CO-OP CAMPAIGN offers CAMPAIGN or CUSTOM CAMPAIGN,
then a level and its difficulty. MULTIPLAYER offers VANILLA MAPS or CUSTOM
MAPS, then the maps, and goes on to the gametypes as before. B steps back
through the choices and leaves the screen from the first. Split screen
starts at VANILLA MAPS / CUSTOM MAPS.

The custom lists stay empty and a co-op level cannot be hosted yet: network
co-op and Custom Edition maps come in their own pull requests, which fill
them in.
2026-10-04 22:54:24 -05:00
MrBruh 3c7ba95ff5 Merge pull request #70 from startupfoundry/windows-no-console
Release builds of Windows without a console window
2026-10-05 16:51:22 +13:00
MrBruh 092505224a Merge pull request #25 from garfbargle/macos-android-build
Build the Android port on macOS hosts
2026-10-05 16:48:39 +13:00
MrBruh 079883d640 Merge pull request #59 from saulob/fix/startup-color-flash
Fix incorrect Colors on the initial menu frame
2026-10-05 16:39:03 +13:00
MrBruh 54faa1dab6 Merge pull request #87 from eltanschauung/fix/death-camera-timing-pr
Fix death camera and co-op respawn timing at high frame rates
2026-10-05 16:33:14 +13:00
MrBruh 273152cc25 Merge pull request #80 from FDervish/fix/vertex-explosion
Fix vertices exploding to the screen centre near the camera plane
2026-10-05 16:07:31 +13:00
MrBruh 9bdd7f30c0 Merge pull request #86 from thelinkin3000/fix/host-again-after-backing-out
Make a new game each time Create is chosen
2026-10-05 15:53:59 +13:00
saulo benigno 423d0104d5 Merge branch 'main' into feature/in-game-settings-menu 2026-10-04 23:17:48 -03:00
saulo benigno 3f2795a8ee Merge branch 'main' into feature/high-res-ui-buttons 2026-10-04 22:57:59 -03:00
MrBruh 685bf260a0 Either co-op player picks the level 2026-10-05 14:47:11 +13:00
MrBruh 933ed2b7b8 Point updates at OpenCommunityEdition/OpenCE 2026-10-05 14:21:03 +13:00
MrBruh c115f5292d Split screen joins of games under way 2026-10-05 14:06:51 +13:00
eltanschauung 0f22c1d592 Add standalone death and co-op regression tests 2026-10-04 20:33:17 -04:00
eltanschauung fb1abedd8b Keep dead camera target when no living player exists 2026-10-04 20:32:59 -04:00
eltanschauung cd345aac7c Count death and co-op respawn delays in game ticks 2026-10-04 20:32:31 -04:00
MrBruh 7e00135d31 Fix menu crash with no network client 2026-10-05 12:59:15 +13:00
thelinkin3000andClaude Opus 5.5 95416b2cfc Make a new game each time Create is chosen
Backing out of a hosted game keeps its server: the lobby's last local
player leaving pauses it (netgame_unjoin_player). Choosing Create again
threw the host's client away, but network_game_start_new_server makes and
joins a server only when there is none, so the new client never joined the
one left over. The lobby then had nobody in it: START added no player, B
could not leave (not in pregame), and the server's map changes failed to
reach the client that was gone.

Create now disposes the client and server first, so it always makes a new
game and joins it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 23:37:36 +00:00
MrBruh ffc1512dcb Read brokers.txt without SDL on Android 2026-10-05 11:27:05 +13:00
MrBruh 88a7c07ec7 Brokers in brokers.txt 2026-10-05 11:15:11 +13:00
Stefan Maier 8e8713681d Merge remote-tracking branch 'origin/fix/multiview-desktop-compile' 2026-10-04 22:59:55 +02:00
Stefan Maier d7099f3e40 Fix desktop link: bot machine check and glClearDepthf were Android-only
network_game_server_is_bot_machine (cea7abfc, Bots as players of the
host) was written into the HALO_ANDROID block of network_server_manager.c
while bots.c calls it on every platform, and so was the array it reads;
both move out of the guard (on the desktop nothing marks a bot machine,
so the check is always false there, the writes stay Android-only).

glClearDepthf (dc21f4b4, Play in VR on OpenXR headsets) is ES-only:
GL_FUNCTIONS declares it for Android's loader and the define at the top
maps glClearDepth to it, but the desktop context links against GL's
glClearDepth. The clear now calls glClearDepth, which the Android define
turns into glClearDepthf as before.
2026-10-04 22:37:17 +02:00
Fadi DerouicheandClaude Opus 5 f2aec7bee3 Draw the reflection at the display's pixel density
The Xbox draws the mirror into a 320x240 target
(RASTERIZER_TARGET_RENDER_SECONDARY_WIDTH/HEIGHT), a quarter of the
picture's linear resolution at 1080p, and gives its shaders that size:
that is the softness the reflection has always had. The game's size
stays - its viewport, its projection, and the 320.0f/240.0f it puts in
c[-84] for the mirror's projective texture coordinate are untouched, so
nothing about the reflection's mapping moves - and the texture it is
drawn into gets the display's own pixels, the viewport and the clear
following it (target_pixel).

A colour and its depth must be the same size in GL, and the game pairs
the reflection's target with the screen's depth (the reflection's target
has no Z of its own, so rasterizer_set_target uses the primary's), so
draw into a depth of the colour's own size: the game clears the depth of
every target it draws the scene into, so one of its own behaves as the
shared one did.

The mirror's mapping was checked over a play session - the surface's
plane, the mirrored camera it gives, and the surface's own screen
position all come out exact - so the reflection is where it was, in more
pixels.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 21:32:34 +01:00
Stefan Maier 4ba36d7305 VR: fix desktop build, targets_multiview was Android-only
6c3f4ce2 (VR: draw both eyes in one pass) changed prepare_draw to pass
targets_multiview to vertex_shader_get, but the variable is declared and
kept inside the HALO_ANDROID guard (L887): Linux and Windows failed to
compile with 'use of undeclared identifier targets_multiview', Android
was fine. On the desktop there are no multiview layers to stand in for
the back buffer, so the desktop call passes FALSE.
2026-10-04 22:25:21 +02:00
MrBruh e3389991ac Online split screen 2026-10-05 09:18:40 +13:00
Stefan Maier 855f083117 Merge branch 'frame/performance' 2026-10-04 22:04:33 +02:00
Stefan Maier 6f3786de91 Merge branch 'fix/vr-shot-origin' 2026-10-04 22:04:33 +02:00
Stefan MaierandClaude Opus 5.5 493713b0a6 Steam Frame: the refresh rates offered in the log; its rate and speed noted
The runtime offers only the rate SteamOS is set to. At 72 Hz the game holds
it with multiview (about 11.6 ms a frame); at 90 Hz it cannot, and the
frames SteamVR makes up showed as a doubled picture when the head turned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:47:56 +02:00
Stefan Maier bf356fc58a Merge branch 'fix/vr-shot-origin' into frame/controllers 2026-10-04 21:42:21 +02:00
Stefan MaierandClaude Opus 5.5 6a7b919522 VR: shots fly along the aim, not above it
A shot fired from the muzzle of the weapon in the hand was kept inside the
level by the game's test of the line from the eyes to it, which in VR runs
down through the player's own body: the test hit it and moved the shot up
towards the eyes, from where it flew parallel to the aim, a few centimetres
above the crosshair and the laser (measured: up to 7 cm at 3 m). In VR the
line is tested against the level alone (a hand through a wall still does
not fire through it); measured after: the impacts on their rays, on
average within a millimetre.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:42:07 +02:00
Addison Bair 20861c06d2 Count a fullscreen window that gamescope never confirms as fullscreen
gamescope (the Steam Deck's Game Mode) makes the window the size of the
display but never confirms the fullscreen request, so SDL leaves
SDL_WINDOW_FULLSCREEN unset. platform_screen_mode then reported a window,
the game drew the Xbox's 640x480, and gamescope stretched it to the
screen, soft and 4:3. A window that was asked to be fullscreen and covers
its display now counts as fullscreen too, so the game draws at the
display's resolution and shape (1280x800 on the Deck).
2026-10-04 19:30:07 +00:00
Fadi DerouicheandClaude Opus 5 fc7271fd52 Step the antenna chain in the tick and draw the frames between the ticks
An antenna's chain is a simulation of the Xbox's own step, an update a tick,
and the port was stepping it once a frame with the frame's own time. Its
springs, its carry and the points' physics then acted against the frames'
movement of it, so how far the chain moved per update depended on the frame
rate, and a vehicle at speed swung it far wider than a tick's step does.

- game.c steps the chain in the tick, beside the effects, which are already
  stepped there.
- antenna.c records the shape each tick leaves, relative to the point the
  vehicle's marker carries the base by. A frame drawn between two ticks is the
  part of the way between those two shapes that it is, carried to where the
  marker is now, so the chain stays on the vehicle it hangs from instead of
  lagging a tick behind it. The gate that left a chain that had not been drawn
  for five updates unstepped counted frames; it counts ticks now, which is
  what it counted on the Xbox.
- widgets.c no longer steps the chain from the widget update, which ran once a
  frame.
- game_time.c draws the per-frame effects and widgets that hang off objects
  (game_frame) with the pose between the last two ticks, the pose the renderer
  draws with, so what they place on an object is drawn where they put it.
- objects.c hands markers the matrices the renderer draws with, so an effect,
  a particle system or a contrail attached to a moving object follows it
  between the ticks too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:45:28 +01:00
Stefan MaierandClaude Opus 5.5 c1305fa94f Steam Frame: its controllers as themselves, with the Xbox's layout
SteamVR offers the Frame's controllers by their own profile
(XR_VALVE_frame_controller_interaction); without bindings for it, it mapped
the Quest's onto them, and X and Y landed on the left one's D-pad. Now: the
right one's A, B, X, Y and menu button are the game's A, B, X, Y and Start
(the Xbox's layout), the left one's View is Back and its D-pad the D-pad,
the bumpers switch grenades and the flashlight. The VR options' page of
buttons shows the Frame's layout there. The runtime's extensions and the
controllers' profile are in the log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:28:13 +02:00
Fadi DerouicheandClaude Opus 5 3d2c04d675 Fix vertices exploding to the screen centre near the camera plane
The screen-space conversion that the Xbox's vertex shaders do is undone by
multiplying the screen position by w again, which is lossy near the camera
plane: the conversion takes the reciprocal of w (rcc), which clamps there, and
1/w rounds differently on each GPU.

Worse, where w is zero or not a number the reconstruction leaves the vertex at
the clip-space origin. That point is inside the frustum, so nothing clips the
triangle away, and OpenGL's zero divided by zero puts the vertex in the middle
of the screen: the triangle is then drawn from wherever its other vertices are
out to there. On the first-person weapon, whose pose follows the camera and so
reaches the camera plane, that is a streak across the whole view. The Xbox's
own divide sends such a vertex to infinity and its clipper discards the
triangle.

Compute the final position from the clip-space position that the shader keeps
whenever it takes the reciprocal of w, so that nothing is reconstructed by
dividing; and where w is zero or not a number, put the vertex behind the
camera (w = -1), which the clipper also discards, as the Xbox did.

Both paths were behind the Android build's #ifdef, so desktop GL only ever
used the lossy reconstruction. The y flip and depth range that follow remain
per platform.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 19:27:35 +01:00
Stefan MaierandClaude Opus 5.5 0fc85d6c67 Steam Frame: the overlays' alpha from their colour on SteamVR
SteamVR draws a quad's texels by their alpha alone, and the game's menus,
HUD and text have none in their additive parts: on the Frame nothing showed.
On SteamVR (the runtime's name says) the copy into the quads' images takes
their alpha from their colour where it is higher; HALO_XR_QUAD_ALPHA
(colour, keep) overrides, for trying. The Frame's notes updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 20:17:43 +02:00
Stefan MaierandClaude Opus 5.5 c0a08f395e Steam Frame: the game's data in the SteamOS user's Documents when it is there
Lepton empties the app's own folders (and uninstalls the app) whenever it
starts again; Documents is the SteamOS user's and kept. When
/home/steamos/Documents/HaloCE holds the maps and is writable, the launcher
and the game take it as the data folder (maps, config.toml, saves, logs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 19:51:08 +02:00
Stefan MaierandClaude Opus 5.5 06cb70fb58 Steam Frame: notes, and no variable the Android loader never reads
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:27:28 +02:00
Stefan MaierandClaude Opus 5.5 1709698053 Start on the Steam Frame (Lepton)
Lepton has no clipboard service, and SDL3's activity died taking one for
granted: the build now patches SDL's clipboard handler to do without.
The Frame lists an implicit OpenXR layer for Vulkan apps whose library an
app cannot load, which failed xrCreateInstance (and the Android loader
reads no variable to turn it off): the app now carries a pass-through
layer under that library's name. With both, the game runs on the Frame
and its OpenXR session starts (SteamVR, 1728x1728 an eye, 72 Hz).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 18:26:39 +02:00
Stefan Maier cb72dca40f Merge branch 'merge/upstream-2026-10-04' 2026-10-04 14:00:26 +02:00
Stefan MaierandClaude Opus 5.5 bbea2a2bff The online panel's game by its row past the fixed rows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:47:56 +02:00
Stefan MaierandClaude Opus 5.5 57cdbd8f80 Merge upstream (cybersecurity/halo-ce-universal 193cbf59): server browser, split-screen co-op
Upstream's server browser of public internet games (p2p_lobby.c) and the VR
port's own public list share the brokers: its topics are a table now, and
the VR port's (join codes' and public games') are entries of it; the VR
port's retained publishes carry MQTT 5's empty properties. The VR port's
public toggle lists the game in the server browser too, and the VR online
panel shows the server browser's games (PC, phones, VR) first, joined by
their invites, then its own list's that it has not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:47:29 +02:00
Stefan Maier c5c5a8eaeb Merge branch 'feature/bots-vehicles' 2026-10-04 13:33:07 +02:00
Stefan Maier a3ee36b5ef Merge branch 'feature/vr-hud-fixes' 2026-10-04 13:33:07 +02:00
Stefan Maier 30a1e0f40e Merge branch 'feature/vr-menu-lists' 2026-10-04 13:33:07 +02:00
Stefan Maier 327cf96d6f Merge branch 'feature/vr-options' 2026-10-04 13:33:07 +02:00
Stefan MaierandClaude Opus 5.5 867855d388 Bot drivers see walls and rocks ahead, back off when stuck, go round teammates
Driving a jeep or a scout, a bot looks ahead (farther the faster) along the
way it turns: a wall or a rock there (not ground it can drive up) is passed
on the most open side nearest its way, slower, and boxed in it backs off.
Stuck is now making no way along its heading for a second and a half, which
spinning wheels no longer hide. A teammate in the way is gone round slowly
on the other side instead of waited for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:25:15 +02:00
Stefan MaierandClaude Opus 5.5 4af570bc08 VR: the online games' hint further left and shorter, clear of Y = Create Game
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 13:13:27 +02:00
Stefan MaierandClaude Opus 5.5 7a1cf79396 VR: teammates' triangles over their heads, a click joins on the profile screen
The triangle the game draws over each teammate goes into the name's slot
over the head (shown in the world there, not on the HUD before the eyes,
where it hung distorted), under the name, or alone when names are off. On
the screen where controllers join by pressing A, a click is the pad's A. On
the screen of games to join, the online games' hint goes into the key bar's
free left and the join status under it, clear of the map's picture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:00:41 +02:00
Stefan MaierandClaude Opus 5.5 9e7fd75f57 Bots: a vehicle left stuck is left alone a while; every third bot follows a person
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:41:40 +02:00
Stefan MaierandClaude Opus 5.5 6754403d68 VR: the menu pointer leaves the stick's choice be; clicks beside a list step it
A choice the stick moved to (a map among the tiles) is no longer taken back
by the pointer resting on another, until the pointer moves well away; and a
click beside a list's slots or a value's box, along the way it steps (where
its arrows are drawn), steps it back or on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:41:09 +02:00
Stefan MaierandClaude Opus 5.5 aedf5ff9bc Bot drivers wait for those coming to get in, and brake for teammates
Just in, a bot driver waits up to six seconds while a bot goes for its
vehicle or a person of its team stands near it with a gun's seat free; it
brakes (backing, if fast) for a teammate on foot in its way. A bot takes a
vehicle only when the nearest enemy (or the objective) is a long way off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:39:24 +02:00
MrBruh 193cbf59c7 Split-screen co-op 2026-10-04 22:27:13 +13:00
Stefan MaierandClaude Opus 5.5 fe160e9dbe Bots man turrets while an enemy is about
A turret near a bot (15 world units) is manned while an enemy is within 35
of it, and left once there has been no one to shoot for ten seconds; at it
the bot aims and fires as at a vehicle's gun. The map's vehicles are named
in the log once a game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:23:49 +02:00
MrBruhandClaude Opus 5.5 d578f88bd0 Validate server browser names as player names
The server browser cleans each game's name as a host cleans the names
of the players and machines that join it, and leaves out a game whose
name has nothing left that names it. A host lists a Latin letter with
a mark as its plain letter, as player names are read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 20:53:00 +13:00
MrBruhandClaude Opus 5.5 d3fc997fdc Ready walked-over weapons, swap out the selected one
In multiplayer a second weapon is picked up into the empty slot on
walking over it and readied, as in campaign; a client readies the
weapon the host says it picked up once it has it. A swap drops the
weapon the player selected, not the one still in hand mid-switch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 20:47:06 +13:00
MrBruh a227872ccb Merge pull request #73 from thelinkin3000/fix/unarmed-melee-campaign
Leave an unarmed melee alone where the map has no multiplayer weapons
2026-10-04 20:28:01 +13:00
MrBruh 812ffeea33 Outline the focused server browser row 2026-10-04 20:19:12 +13:00
MrBruh c05864b3fc Leave quit players off the scoreboard 2026-10-04 20:06:28 +13:00
Mootjuh 55c43670a4 Merge upstream main 2026-10-04 08:41:04 +02:00
Mootjuh 1b585ba499 Keep the mutation script out of the PR 2026-10-04 08:39:14 +02:00
Mootjuh d1da19316e Trim comments to the repository's style 2026-10-04 08:39:12 +02:00
thelinkin3000andClaude Opus 5.5 ce77db8404 Leave an unarmed melee alone where the map has no multiplayer weapons
A player with no weapon, whose biped has no blow of its own, borrows the
melee of the globals' first multiplayer weapon. A campaign map's globals
list no multiplayer weapons, and a10 starts the player unarmed, so a melee
in the cryo tube's tutorial asked for element 0 of an empty block and
halted in tag_groups.c. With no multiplayer weapon there is no blow to
borrow, and the melee does what the game's always did there: nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 06:22:42 +00:00
Mootjuh 38a85a9a1a Let a touch drag leave a PC screen's button bar 2026-10-04 08:04:07 +02:00
MrBruh 39350fb81f Enemy names to the motion sensor's range and setting 2026-10-04 18:57:11 +13:00
MrBruh 1a15a171ac No unarmed grenades or melee from vehicle seats 2026-10-04 17:54:34 +13:00
MrBruh 2e3841ff7d Left-align the shotgun's ammo meter 2026-10-04 17:47:55 +13:00
MrBruh 46edf53657 Settings and End Game in the pause menu 2026-10-04 17:20:32 +13:00
Paul Hontz cd747c23c5 Log of the port to halo.log without a console
The release build no longer has a console, and the platform layer's log
(platform_log to stderr: invite links, the data root, crash reports)
went only there. When stderr goes nowhere, halo.log next to halo.exe
gets it instead.
2026-10-03 22:43:14 -04:00
Paul Hontz f148dbd6d9 Release builds of Windows without a console window
The release halo.exe was a console-subsystem PE, so Windows and Wine
open an empty console window next to the game. Under Proton in
gamescope the console can take focus and leave a black screen. Nothing
reads the console (the log is debug.txt), so release builds now link as
GUI with mainCRTStartup; debug builds keep the console.
2026-10-03 22:26:20 -04:00
MrBruh c3adcfe5bf Deflate the release zips again 2026-10-04 14:22:11 +13:00
MrBruh 3304965653 Lobby check test past the compiler launcher 2026-10-04 13:43:13 +13:00
MrBruh c04765d7f4 Server browser of public internet games 2026-10-04 13:34:23 +13:00
Mootjuh 124a3cc584 Stop a touch drag at a PC list's first and last usable row 2026-10-04 00:50:13 +02:00
Mootjuh 25bc2f5ae7 Merge upstream main 2026-10-04 00:20:59 +02:00
saulo benigno 31e54eba1b Add in-game settings menu 2026-10-03 19:07:13 -03:00
MrBruh 933aac6175 No map weapons option, grenades with no weapon 2026-10-04 10:44:37 +13:00
MrBruh 809408c663 Survive kills and joins in departed players' slots 2026-10-04 10:12:32 +13:00
Mootjuh f48031f15f Correct two comments on the list band's hover and click 2026-10-03 22:55:26 +02:00
MrBruh 601a4c1f50 Unload the menu map before a client's next game 2026-10-04 09:51:47 +13:00
Mootjuh 349c110d74 Step a full-screen list by the band beside its slots 2026-10-03 22:41:04 +02:00
Mootjuh b8ac7b4b8d Settle tap targets once a frame; stop legend strips at targets below 2026-10-03 22:22:49 +02:00
MrBruh 133d6a5dc1 Free quit players' slots 2026-10-04 09:07:47 +13:00
Stefan MaierandClaude Opus 5.5 12c97ffa85 Bots take vehicles: drive, man the gun, ride along; and race
A bot whose way is long takes an empty vehicle it can drive near it (a
Warthog, a Ghost, a Scorpion), walking to the driver's door and standing
there until the game offers the seat. Driving, it follows a vehicle's path
(from the vehicle's surface, wider, its steps further apart), turning a
Warthog or a Ghost by facing where it goes and pushing a Scorpion there from
where its gun faces; it runs an enemy down, circles one with a gunner
aboard, fires a Ghost's and a Scorpion's guns, backs off what it is stuck
on, roams between the map's spawn points, and gets out at the objective,
overturned, stuck again and again, or with an enemy close while it stands.
In a game of teams a bot mans the gun of a teammate's vehicle, a person's
first (or rides along with a person), aims and fires it, and gets out once
no one drives. Overturned vehicles near it are turned upright.

In a race a bot heads for the flag it is to touch next
(game_engine_race_next_flag), in a vehicle if it can have one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 16:48:59 +02:00
saulo benigno 61623e68d5 Fix texture bindings after first upload 2026-10-03 11:41:54 -03:00
MrBruh 23b542601f Unarmed melee and infinite grenades for any player count 2026-10-04 03:22:16 +13:00
MrBruh 62b630a261 Remove the flamethrower and gravity rifle 2026-10-04 03:05:59 +13:00
Stefan MaierandClaude Opus 5.5 0e6327cadf The controls page lists switching grenades (B held) on a line of its own
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:41:56 +02:00
Stefan MaierandClaude Opus 5.5 5f82df71f0 VR options: Y on the main menu or the pause menu
A panel of the VR settings one may change while playing (turning, snap
angle, smooth speed, the vehicles' comfort vignette, the Warthog's wheel,
the weapon laser, the crosshair, the motion tracker on the wrist), written
to config.toml, which vr.c reads again as they change; and a page of what
the controllers' buttons do, on foot, in vehicles and in the menus. The
pointer works both, and its hint opens the panel. The menus' screens are
named in the log as they come up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:37:10 +02:00
Stefan Maier cfdd821b45 Merge branch 'merge/upstream-v11' 2026-10-03 15:21:57 +02:00
Stefan MaierandClaude Opus 5.5 a9bbce5f7a Merge upstream (cybersecurity/halo-ce-universal c9ee319a): PC menus, network version 11
Upstream's PC menus are kept for the desktop; in VR the Xbox's stay, which
the VR port's join codes, game list, host options and keyboard pointer are
built into. config_write_integer now writes through upstream's config_write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:11:07 +02:00
MrBruh e8e0c2215a Find the port's headers in Windows game units 2026-10-04 02:08:45 +13:00
MrBruh 383355381c Kills to win up to 500 in the PC menus 2026-10-04 02:04:14 +13:00
Stefan Maier e94906100c Merge branch 'feature/warthog-wheel' 2026-10-03 14:58:32 +02:00
Stefan Maier 57add5ed71 Merge branch 'feature/vr-menu-pointer' 2026-10-03 14:58:32 +02:00
Milenko 2b0103a0ef Slayer's kills to win up to 500, for big games
The game type editor's kills to win (Slayer and Team Slayer) went up to
50, which a game of many players reaches in moments: it now also offers 75,
100, 150, 200, 250 and 500. The spinner's items are its string list's
(ui.map's 5, 10, 15, 25, 50) and then strings of the port's own, each with
its description; the options list's descriptions count only a spinner's
items of the tag's own, so those of the rows after it are where they were.
A spinner with extra items is a little wider, for three digits.
2026-10-04 01:58:26 +13:00
MrBruh c9ee319ab5 Add the PC menus 2026-10-04 01:56:45 +13:00
Stefan MaierandClaude Opus 5.5 770f8881e0 At the wheel, the right trigger is the throttle and the left the brake
Holding a Warthog's wheel, the triggers drive as the left stick pushed up
or down would (backwards once stopped), unless the stick is pushed further;
they neither fire nor hold a grenade then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:50:41 +02:00
Stefan MaierandClaude Opus 5.5 38ab192749 Steer a Warthog by its wheel with the hands
At an open vehicle's wheel, a hand that grips near it (where the driver's
drawn hands hold it) takes hold, and the hands turn it about its column:
both by the line between them, one about the wheel's middle. Turned
vr.vehicle_wheel_degrees (90) either way it steers fully; let go, the right
stick steers as before. Holding the wheel, the right grip is no melee.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:00:26 +02:00
Stefan MaierandClaude Opus 5.5 827eb0fc1d The keyboard gets the menu pointer's clicks; its BACK and ENTER hints click
The menus' mouse handling took the pointer's clicks each frame before the
keyboard asked for them, so the keyboard never saw one: with the keyboard
up the pointer is now the keyboard's alone. Clicking the hints under it
(B = BACK, A = ENTER, in its picture) cancels or finishes as Done does.
The pointer counts as moved past 3 pixels, not a hand's tremor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:47:51 +02:00
Stefan MaierandClaude Opus 5.5 a482a5f5bb The on-screen keyboard follows the menu pointer in VR
The key under the controller's pointer is picked and a click types it.
In the menus the triggers no longer reach the game as the gamepad's (they
click the pointer; on the keyboard they also moved its cursor).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:42:57 +02:00
Stefan MaierandClaude Opus 5.5 81f321f959 Of overlapping screen keys, the one whose icon is nearest on the left
A key's label is a text box often far wider than its text, reaching over
the next key (Back pressed Delete). Of the keys under the pointer, the one
whose icon begins nearest on its left is taken: each key from its icon to
the next one's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:30:20 +02:00
Stefan MaierandClaude Opus 5.5 f97ff9219f Menu pointer: narrower keys, the click where the trigger began to move
The screen keys' margin in VR was wide enough to reach the next key (Back
pressed Delete): now 2 pixels to the sides and 4 above and below, with a
label at most 25 pixels off. A click is taken where the controller
pointed as its trigger began to move (past a tenth), not a fixed time
before, which could still be on the key it just left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:27:28 +02:00
Stefan MaierandClaude Opus 5.5 85e79a067a Steadier menu pointer clicks in VR
A click is taken where the controller pointed a few frames before the
trigger went down (pulling it tips the controller), and in VR a screen
key's button takes a label up to 40 pixels off and a margin of 6 about it,
so the keys under the menus (A, B, Back) are hit as readily as items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 12:23:28 +02:00
Stefan MaierandClaude Opus 5.5 effeda1b7c Point at the menus with a controller in VR
The desktop builds' mouse in the menus (halo_ui_pointer.h) now comes, on
Android in VR, from a controller: where its aim's line meets the menus'
screen fixed in the room is the pointer, the trigger clicks (the hand
pressed last points; a click ticks its rumble), and a ringed dot shows
where it points. The item under it takes the focus and a click presses A,
as with the mouse; the online games and host options panels take it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 11:56:05 +02:00
Stefan Maier 6299503191 Merge branch 'feature/bots-team' 2026-10-03 11:10:25 +02:00
Stefan MaierandClaude Opus 5.5 c5d86c8d8b Bots spread out about their objective and keep to it
Each bot has a direction of its own (the golden angle on from the last):
far from a shared goal (the enemy's flag, the ball, the hill) it comes by
a way of its own, out to its side of the straight way, straight only for
the last 10 world units; holding a place it takes a spot of its own: on
the hill, about its flag's stand (every other guard ahead of it towards
the enemy's), about a teammate it escorts. With no way to its spot it
takes the point itself. A bot with an objective no longer leaves it in a
fight for a weapon close by.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:55:28 +02:00
Stefan MaierandClaude Opus 5.5 7242151017 Names over the heads at 60% of their size on the HUD
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:48:01 +02:00
Stefan MaierandClaude Opus 5.5 1c8e651bb1 Bots play with their team
In a game of teams a bot holds its fire while a teammate stands in the
line of it (and steps aside for a clear shot), takes on first the enemies
near its teammates (people above all), escorts a teammate carrying the
enemy's flag or the ball a few world units off on its own side, and, with
nothing to do, every other bot keeps near the nearest person on its team.
A goal it finds no way to three times running (a person up where only a
teleporter goes, a thing out of reach) is given up for a while, and a
roaming bot then goes somewhere near at random.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:41:08 +02:00
Stefan MaierandClaude Opus 5.5 4b8f8dde8b Players' names over their heads in VR, each on a quad of its own
The HUD in VR is a screen at one distance before the eyes, where the names
upstream draws on it hung. Each name is now drawn into a slot of a region
of the back buffer before the HUD (up to eight, one under another), taken
into a names layer, and shown on a quad of its own over its player's head,
facing the eyes, as wide as it would look on the HUD: in depth, for both
eyes. Who is named, and when, is as upstream has it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 10:31:36 +02:00
Stefan Maier 920b1bfea9 Merge branch 'feature/vr-scope' 2026-10-03 10:18:30 +02:00
Stefan Maier 09f869e44c Merge branch 'feature/bots-modes' 2026-10-03 10:18:30 +02:00
Stefan MaierandClaude Opus 5.5 bc45338214 Bots get stuck in corners less
A path the game finds but cannot steer round the objects on it (the other
players among them: obstacle avoidance failed) is walked as smoothed,
instead of none (which had bots walk straight into walls); a step aside
settles a bump. A bot goes on to the next point of its path when it is
in plain view on its own level, walks the last 3 world units straight
only when nothing is in the way, and counts itself stuck when it makes no
way to its point for 6 ticks (only while it walks one): then it steps
aside, finds its way anew, skips a corner's point it cannot reach, jumps
at 12 and goes elsewhere later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 09:33:58 +02:00
Stefan MaierandClaude Opus 5.5 a663d1b100 Bots take good weapons in a fight too, and value strong ones more
Within 10 world units a bot goes for a weapon or powerup worth at least 4
while it fights. Strong weapons are worth a longer way (worth squared over
distance, falling off over 15 world units), and with two weapons one is
worth what it betters the worse by and 1.5 more, so rocket launchers and
sniper rifles are taken, not only the shotgun close by. The pistol counts
for a little less up close, the assault and plasma rifles more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:27:24 +02:00
Stefan MaierandClaude Opus 5.5 d8034d37fd Raise the rocket launcher's scope disc 3.5 cm more (18 cm)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:03:19 +02:00
Stefan MaierandClaude Opus 5.5 f8a39df0cd No names over the heads in VR for now
In VR the HUD is a screen at a fixed distance before the eyes, so the
names upstream draws on it hung there instead of over the players' heads.
Off until they are drawn into each eye's picture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:03:10 +02:00
Stefan Maier 6057146244 Merge remote-tracking branch 'upstream/main' into merge/upstream-2026-10-03
# Conflicts:
#	port/linux/game/network_distributed.c
#	port/linux/game/network_distributed.h
2026-10-03 07:38:52 +02:00
saulo benigno c72962a73d Add high-resolution UI controller buttons 2026-10-02 21:10:04 -03:00
Jonas Volman 1b25ea9a37 Merge three independently verified biped and avoidance function gains into halo-1 main 2026-10-02 17:05:13 -07:00
Jonas VolmanandClaude Opus 5.5 fccf773db3 Reconcile actor test avoidance match (+1 function, +743 meaningful bytes)
Independently verified donor c694a43d4e3343eedebac48fe6f2b6052f679778 under Q-UP21 after the separately gated helper restoration. Fresh clean full gate against 09255f0e: cumulative +3 functions, +1238 meaningful / +1264 padded bytes, zero exact losses; 7483 of 7573 Halo functions, 1603071 of 1770166 code bytes, data 3311591 and objects 396 unchanged, parks 59 valid. Tools tests: 1663 passed, 5 skipped, 159 subtests. Full /W3 and unit /W4 multisets unchanged. All twenty cast ablations and layout controls passed; helper identity, real-call schedule and 284 ordered provider-link probes passed. The disclosed residual caller changes 4144 to 4128 bytes.

Retain the narrow per-function parentheses ruling, inferred scopes and collision_result placement, S6 correction, single-variable alternative and compiler-version caveat. No general hold, object admission, research candidate, scorer/verifier or compiler flag change. Fresh canonical receipts and source-policy boundaries are recorded in docs/object_matching_logs/biped_avoidance_canonical_reconciliation_20261002.md. Unrelated README changes and research preserved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:04:04 -07:00
Jonas VolmanandClaude Opus 5.5 201442d377 Restore actor avoidance transform helper calls with the approved guard removal (zero credit)
Independently reconciled donor 41d3b992a5e5c0fbbb5f76c476d27f9c68031e82 without candidate F. Fresh clean full gate: no additional gains or losses, all prior function sections identical, one new uncredited 48-byte SELECT_ANY point_from_line3d copy identical to January, unchanged real-call schedule, warnings, data and object credit. Current-compiler cast ablation and layout controls pass, 284 ordered provider-link probes pass, 1663 tools tests pass. Six inferred casts and three byte-inert per-site parentheses are disclosed; all other actor_moving holds stay in force.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:49:29 -07:00
Jonas VolmanandClaude Opus 5.5 164026a7f1 Restore biped_jump helper call under the admitted two-site vector view (zero credit)
Independently reconciled donor 82ea0d5e9bcee8a1a48775aab6f0c351f5ee5e2a after the biped packet. Fresh clean full gate: zero additional gains or losses, all normalized objects unchanged, parks valid, warnings unchanged and 1663 tools tests passed. Current-compiler cast ablation is byte-inert; nine layout/alignment checks pass and a deliberately false control fails. Both current selected-provider link orders pass. Cast spelling remains inferred; no new parenthesis or general view-cast exception.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:47:08 -07:00
Jonas VolmanandClaude Opus 5.5 606d0a2458 Reconcile approved biped sight and placement matches (+2 functions, +495 meaningful bytes)
Independently verified donor edbdbf729e9b7c0c09557f1ccb45f94ad6c8a06a against canonical 09255f0e: fresh clean full build, strict whole-board sweep with zero losses, parks 61 to 59, unchanged data/object credit and warnings, 1663 tools tests passed. Folded helper identity and all selected-provider pair links passed. Per-site parentheses, descriptive constants and coupled jumping form remain narrowly disclosed as inferred; no general hold lifted. Unrelated canonical biped_accelerate shape and README edit preserved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:44:38 -07:00
Stefan MaierandClaude Opus 5.5 94b17a7913 Bots play capture the flag, king of the hill and oddball (phase 4)
The flags and balls (weapons that are flags) are gathered each tick with
where they are and who carries them. In CTF a bot carrying the enemy's
flag runs it to its own stand; the rest hunt whoever carries their flag,
touch it home where it lies dropped, or go for the enemy's (every third
guards its own). In king of the hill they go to the hill (the game's goal)
and keep moving in it. In oddball they go for the ball, taken as a weapon
is, hunt its carrier, and carrying it keep away. Carrying, a bot does not
fire, and looks where it runs. The last 3 world units to a goal are
walked straight, over what the path goes round (a flag on its stand).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:22:31 +02:00
Stefan MaierandClaude Opus 5.5 30f362ff3d Bots pick up weapons, powerups and grenades, and throw grenades (phase 2)
Out of a fight a bot looks every second for what lies within 25 world
units worth having: a weapon better than the worse of its two (it changes
to that one and holds the swap while the game offers the weapon), ammo for
an empty weapon it carries, an overshield, camouflage, a health pack when
hurt, grenades when it has few. In a fight it takes the weapon that suits
the distance (shotgun near, sniper rifle far, no rocket up close), at
most every two seconds, changes from an empty one, and now and then lobs
a grenade at an enemy 6 to 20 world units off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 20:41:48 +02:00
Stefan MaierandClaude Opus 5.5 23d14319a7 Raise the rocket launcher's scope disc 3.5 cm more
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:14:34 +02:00
Stefan MaierandClaude Opus 5.5 ff5b91f120 Raise the VR scope's disc over the rocket launcher's tube
The disc sits 7 cm over the hand, where a rifle's or pistol's scope is;
over the rocket launcher, carried higher on the shoulder, it was inside
the tube. A weapon may raise it (halo_vr_set_scope_raise): the rocket
launcher by 11 cm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:07:26 +02:00
Stefan MaierandClaude Opus 5.5 f6c2322df9 Host options in the lobby: public, bots and their skill
Y in a hosted game's lobby opens a panel to make the game public, choose
how many bots join (none by default, up to 15) and how well they play
(bots.skill: easy, normal, hard; hard is the first bots' play, normal a
little slower to notice, turn and aim). Integer settings can now be
written to config.toml as booleans could.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 18:05:34 +02:00
Stefan MaierandClaude Opus 5.5 39fbaefaac Keep the new settings out of the network table
The settings file is written in the table's order, so settings of another
section between network.* entries wrote [network] twice, and the whole
file was then refused (defaults used). They now follow the network
settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:52:14 +02:00
Stefan MaierandClaude Opus 5.5 c6397cb515 Bots find their way with the game's pathfinding (phase 1)
A bot walks to its goal along a path from ai/path.c (the maps' pathfinding
surfaces, as the campaign's actors use them), found again every half
second: in a fight towards its target, weaving; out of sight hunting the
nearest enemy, or roaming near someone; looking where it goes. With no
path it walks straight at the goal, and when stuck picks another.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:05:12 +02:00
MrBruh 80d30410c8 Flush the GPU every 3 draws on Mesa Intel 2026-10-03 02:56:11 +13:00
MrBruh d1c7243cb2 Rework the multiplayer scoreboard 2026-10-03 01:16:09 +13:00
Mootjuh 7ad54bb012 Keep a widened setting's split at its centre; widen against the noted boxes 2026-10-02 12:33:02 +02:00
Mootjuh 2ee4fd2cc2 Double the width of each setting 2026-10-02 11:16:52 +02:00
MrBruh fd3d62f960 Keep player names bannable 2026-10-02 22:12:16 +13:00
Stefan MaierandClaude Opus 5.5 5ba6c13097 Bots as players of the host (phase 0)
bots.count bots are added in the lobby as players of machines of the
host's own that nothing connects to (four a machine, from the top slot
down): new connections skip their slots, and they count as machines for
the countdown, so a game of the host and bots starts. Every machine sees
them as players; each tick the host thinks for them (bots.c) and takes
the result as their input through update_server_handle_distributed_input.
The first brain finds the nearest visible enemy, turns at a player's rate
with some aim error, walks at it weaving and fires on target, and else
wanders, jumping and turning when stuck. Every multiplayer map has
pathfinding surfaces for the next phase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:08:09 +02:00
Mootjuh c4cf55458e Merge a setting row only when it holds one value; fit legends first 2026-10-02 10:35:11 +02:00
Mootjuh 8fda769765 Let a tap on a setting row reach its value 2026-10-02 10:24:55 +02:00
Jeff ClarkandClaude Opus 5.5 e44619818b Snap poses that changed too much in a tick
Frame interpolation blends each node between its last two tick snapshots.
Blending two snapshots of different poses node by node stretches vertices
across the screen for a tick, then they snap back:
- the first-person pose blended from whatever was drawn last, which after
  being zoomed, in a vehicle, dead or in a cinematic is seconds old, and
  after a weapon switch may be another skeleton with as many nodes;
- an object's pose was only checked by its root node's move (10 units), so
  the game changing a pose at once (an actor waking from dormancy, a model
  swapped) was swept through poses it never had.
Now the first-person pose blends only from the tick just before, and not
when a node moved more than 0.25 units in the camera's frame; and an object
snaps when a node moved more than 0.4 units in its root node's frame (in
that frame, so the whole object turning or moving counts for nothing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:11:46 -04:00
Jeff ClarkandClaude Opus 5.5 b449c43e22 Keep vertex constant serials from wrapping
Each changed vertex constant register takes the next serial, and a program
uploads the registers changed since the serial it last saw. The serials
were 32-bit; a skinned model's draw changes up to 132 registers, so at a
high frame rate they wrapped within minutes to an hour. After a wrap, the
log walk (serial <= constants_serial) and the full scan (register serial >
program's serial) both found nothing changed, and every program's next
draw used the registers it last uploaded: often another object's node
matrices, so its vertices flew across the screen for a frame. The serials
are 64-bit now and never wrap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:11:35 -04:00
Stefan MaierandClaude Opus 5.5 66c0d33e34 List public online games and join them from a panel
A host can make its game public (Y in the lobby, or network.public_game):
it then announces name, map, game type, players and join code every 10 s,
retained, on hcvr/1/g<network version>/<id> through the signalling
brokers, and clears it when it stops. On the system link list, X opens an
online games panel listing the public games heard lately (stale ones
dropped after 45 s) and a row to type a code; A joins by the game's code.
Announcements carry no addresses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:00:57 +02:00
MrBruh b438cea479 Cap enemy name range and shrink names 2026-10-02 20:59:36 +13:00
Jonas Volman 862c393cd8 Merge five independently verified function gains into halo-1 main 2026-10-02 00:45:39 -07:00
Jonas Volman 09255f0e78 Reconcile main static storage: +1 exact function, zero losses
Approved coherent packet 41640b4d, independently clean-built after transport and plane-helper reconciliation. +538 meaningful / +544 padded bytes; main 95/95. Replace the invented packed aggregate with January's separate storage, add exactly eight private symbol rows and retire the frame-rate park. Names, six static scopes and exact main_game_render placement remain disclosed as inferred. Full stable sweep, parks, admission, fake scan and 1663 tools tests pass; no new warnings, data, objects or providers. Cumulative reconciliation +5 functions, +2121 meaningful / +2144 padded bytes, zero exact losses. Preserve dirty README and unrelated research. No PCH, scorer, verifier, status or held-source changes. Detailed independent receipts and limits recorded in docs/object_matching_logs/upstream_tmp_reconcile_20261002.md.
2026-10-02 00:44:12 -07:00
MrBruh 634b4197f9 Limit enemy names to the weapon's red reticle range 2026-10-02 20:36:46 +13:00
Jonas Volman 38fdd8cb24 Reconcile plane-helper pair: +2 exact functions, zero losses
Approved coherent packet 78294dd3 (Q-UP1), independently gated on canonical. Remove only outer helper parentheses, preserving /Od argument order, together with the attested float argument repair in item_accelerate. Items object remains identical. +1126 meaningful / +1136 padded bytes; data/object/provider sets unchanged. Fresh full build, stable sweep, admission, fake scan (26), parks and tests (1663 passed, 5 skipped, 159 subtests) pass; no new warnings. Retire only two exact parks. Remeasure four residuals on canonical, preserving previous measurements/rejection history: frustum 88.8082->90.11173; pill 97.21691->98.086395; polygon clip 80.76382->82.37186; lens flares 99.29274->99.198944 (disclosed decrease). Do not import unrelated lane fidelity changes or held parentheses/DD2/DD3/casts.
2026-10-02 00:34:46 -07:00
MrBruh ab96597321 Fix the Android build 2026-10-02 20:34:00 +13:00
Jonas Volman 25f719236d Reconcile transport pair: +2 exact functions, zero losses
Independently reconciled lane e191c0f1 under Q-UP1. Header and definition return long together; January startup-store and cleanup topology retained. Retire only the two verified parks. Fresh clean build and stable sweep: +457 meaningful / +464 padded bytes, 0 losses, unchanged data and object status. Affected /W4 warning counts unchanged; inherited WSA narrowing moves to initialization. Admission/parks/fake scan pass; tools 1663 passed, 5 skipped, 159 subtests. No new providers or section/data changes beyond the two intended functions. README and unrelated research preserved.
2026-10-02 00:30:24 -07:00
MrBruh f0dfb58c94 Cap the frame rate with vsync off 2026-10-02 20:26:22 +13:00
Mootjuh 430f3d8287 Read the gesture insets at every finger down 2026-10-02 09:11:15 +02:00
Mootjuh f32a6a791e Ignore touches in the system gesture zones 2026-10-02 09:11:15 +02:00
Mootjuh 4712923c67 Add debug.touch_targets 2026-10-02 09:11:15 +02:00
Mootjuh 798f388732 Skip cinematics with a tap 2026-10-02 09:11:15 +02:00
Mootjuh 3373a3556d Operate the Android menus by touch 2026-10-02 09:11:15 +02:00
Mootjuh 74ec92e787 Add the menus' touch gesture unit 2026-10-02 09:11:14 +02:00
Jeff ClarkandClaude Opus 5.5 991ab41016 Fix the distance of 3D sounds
The game gives positions and each sound's minimum and maximum distances in
world units, and sets DirectSound's distance factor to 3.048, the meters in
a unit (10 feet). The mixer multiplied the distance by that factor before
the rolloff, but not the minimum distance, so every 3D sound was heard 3
times as far away as it was: up to 10 dB quieter against the 2D sounds,
and panned as if farther. The distance factor is DirectSound's for Doppler;
the rolloff is in the units the distances are given in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 02:04:57 -04:00
Jeff ClarkandClaude Opus 5.5 5f9748d736 Resample voices with a windowed sinc
The mixer resampled each voice to 48 kHz by linear interpolation, which
left the images of a 22 kHz voice's band only 3 to 21 dB down (an 8 kHz
tone's image at 14 kHz was 10 dB under it) and dulled the band itself
(-4 dB at 8 kHz): voices sounded gritty and low-fi. It also held a packet's
last frame instead of reading on into the next packet. Now each output
sample is band-limited interpolation of the frames around it (a Kaiser-
windowed sinc over 16 zero crossings, J. O. Smith's method): flat to 8 kHz
at 22 kHz, the images 66 to 98 dB down, the low pass narrowed for voices
played faster than the output rate, and the frames taken from the packets
in turn, so it reads across their boundaries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:50:39 -04:00
Jeff ClarkandClaude Opus 5.5 56b2a0e8e3 Fix Xbox ADPCM decoding
An Xbox ADPCM block's header holds its first sample, and its nibbles code
the 63 after it; the 64th nibble only pads the block. The decoder dropped
the header's sample and decoded the padding in its place, so every 64th
sample was wrong: a buzz at 344 Hz under every 22 kHz sound, voices most
of all. In the maps' sounds the last nibble is always 0, and the next
block's header follows the 63rd nibble's sample by one ordinary step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:47:20 -04:00
Jeff ClarkandClaude Opus 5.5 5f388f7818 Replace the soft clipper with a limiter
The voices sum at their full level, as the Xbox's mix bins did, and the
mixer bent every sample over 0.8 with tanh. When loud voices piled up, as
dialogue over gunfire does, that distorted the sound: a 2x full scale sine
came out with 23% harmonic distortion. A look-ahead limiter now turns the
whole mix down for as long as it would go over -1 dBFS, both channels
alike, and brings it back over 100 ms. The output is 1.3 ms later, so the
gain comes down before each peak; below full scale nothing changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 01:29:45 -04:00
MrBruh 9f3e8c92de Add high-res postgame carnage report title 2026-10-02 16:24:30 +13:00
Jonas Volman 356a8d412c Merge verified triangle and nav-point matches into halo-1 main
Preserve main README unchanged. Source/config/verification tree equals canonical 5e55d8f8; two exact Halo gains, zero regressions, +1998 meaningful bytes. No object or data-credit changes.
2026-10-01 19:41:01 -07:00
Jonas Volman 5e55d8f83a Reconcile exact triangle predicate and nav-point renderer (+1998 Halo bytes)
Cleaned punpck donor reconstructions; strict bytes and all relocations match January. Fresh cold whole-board gate: 2 gains, 0 losses; 1663 tests pass. Retire the two parks. Disclose six HUD /W3 C4244 narrowing warnings and six identical shared-inline helper copies; provider links pass both orders. No object or data credit changes; inherited README and research excluded.
2026-10-01 19:40:22 -07:00
MrBruh f2ba71d9af Show player names above heads in multiplayer 2026-10-02 15:24:40 +13:00
MrBruh 0182da8172 Add high-res text and OpenCE titles 2026-10-02 13:39:17 +13:00
Stefan MaierandClaude Opus 5.5 6b77fe428a Show how joining an online game goes on the system link list
p2p_join_state tells the menus whether the host is being reached, was
reached (its game is in the list), did not answer, could not be reached
(both routers too strict: allow UPnP) or was lost; the join screen shows
it under the code's status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:37:25 +02:00
Stefan MaierandClaude Opus 5.5 0badc451cb Join online games with a short code in VR
A hosted game gets an 8-character join code (Crockford base 32) besides
its invite link, shown under the lobby's buttons. On the system link list,
X opens the game's keyboard to type a code in; the code's topic and key
come from a slow PBKDF2 derivation, the host answers an ask on that topic
through the signalling brokers with its invite sealed with the key, and
the joiner joins as with the link. Codes in join_link.txt work too.

Also: the clipboard text last seen is kept from run to run, so a run's
own invite is not joined again at the next start; in menus a held stick
repeats after 350 ms and then every 90 ms, and its threshold is 70%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:32:50 +02:00
Stefan MaierandClaude Opus 5.5 39a6cc9794 Hold a VR player's relayed aim for a second
Where another machine's VR player aims and fires from is kept for 30
ticks after their hands last came (was 10), over networks that deliver
in bursts; a held grenade still lets go after 10, and a throw is taken
within its own 30.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:36:34 +02:00
Stefan MaierandClaude Opus 5.5 35b90f26fc Send VR players' hands to the other machines
A new distributed message (_distributed_message_vr_poses, last in the
list so builds without it drop it) carries a VR player's controller aim,
muzzle offset, held grenade and last hand throw every tick: clients send
their own to the host, the host relays them with its own. Other machines
aim that player's weapon where the controller points, fire from the hand,
hold the grenade while the hand does and throw it as the hand threw it.
Damage stays the shooter's checked report; network version unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:58:52 +02:00
MrBruh c55e4e2b9d Widen only flat UI fills on widescreen 2026-10-02 02:21:22 +13:00
bfenty 7cd1724fe5 Widen full-screen UI widget backgrounds to the display width
A widget background bitmap whose bounds cover the whole 640x480 design
space (the pause menu dim, for example) stopped at the edges of the
centered 640 column on widescreen displays, leaving the side bands
undimmed. Widen the bounds and the clip symmetrically to the full
screen width, mirroring the existing fade_to_black special case in
render_ui_widgets().

Tested on a Retroid Pocket Flip2 (854x480 render target).
2026-10-02 02:16:14 +13:00
MrBruh bd8ec3c798 Add high-res sniper ladder and fix meter fringes 2026-10-02 02:09:45 +13:00
MrBruh 4799980e8a Draw the whole HUD in high resolution 2026-10-02 01:24:38 +13:00
MrBruh f8937c6179 Add high-res HUD textures 2026-10-01 22:33:25 +13:00
Stefan MaierandClaude Opus 5.5 02316c7f35 Merge upstream (92 commits) into the VR port
Upstream's own fixes replace two of ours: reclaiming ART's idle large
object space for the guest's fixed ranges (host_memory.c), and the audio
callback's gathering (host_sdl.c). Its consolidated headers take the VR
declarations (model_animation_definitions.h, render/render.h). The
updaters look at this repository's releases: the original's builds, made
without VR, must never replace this one (they share the package name).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:30:05 +02:00
MrBruh bfbac35761 Fix text glyph edge artifacts 2026-10-01 16:57:38 +13:00
MrBruh 8fb1647e18 Fix weapon swap crash and log spam 2026-10-01 16:20:28 +13:00
MrBruh 943abae15a Fix Windows update unpacking 2026-10-01 14:55:29 +13:00
MrBruh cdd0291a3f Add console log verbosity setting 2026-10-01 14:17:47 +13:00
MrBruh 083eef0c42 Validate player and machine names 2026-10-01 13:45:46 +13:00
MrBruh c5fcfbd46a Kick speed hacks and add bans 2026-10-01 13:12:54 +13:00
MrBruh 71ae1f47bc Fix seventh netcode review 2026-10-01 12:03:52 +13:00
MrBruh a6ca914b8a Fix sixth netcode review 2026-10-01 10:08:53 +13:00
MrBruh e233e1c1bb Fix fifth netcode review 2026-10-01 09:12:07 +13:00
MrBruh 671cc9ed91 Fix fourth netcode review 2026-10-01 06:23:16 +13:00
MrBruh 4f3ce00a8e Fix third netcode review 2026-10-01 04:57:51 +13:00
MrBruh 0d548798e5 Fix second netcode review 2026-10-01 03:54:02 +13:00
MrBruh aad8719ffc Fix client flags in the second game 2026-10-01 02:37:43 +13:00
MrBruh e7d1ed3a67 Harden and fix the netcode 2026-10-01 01:22:46 +13:00
MrBruh c68db56144 Fix the mouse aim merge 2026-09-30 22:38:44 +13:00
MrBruh f100438431 Merge Englis666 mouse-aim-fixes 2026-09-30 22:36:59 +13:00
MrBruh 4885431af9 Merge pull request #27 from Englis666/dedicated-gpu
Ask laptops for the dedicated GPU on Windows
2026-09-30 22:16:25 +13:00
MrBruh a3ed50c56d Remove the lockstep netcode 2026-09-30 22:12:05 +13:00
MrBruh 4adc3a87a5 Remove matching tooling and HALO_LINUX ifdefs 2026-09-30 21:49:46 +13:00
MrBruh b47f237dc1 Convert any listed PAL build 2026-09-30 21:07:47 +13:00
MrBruh a05253c181 Lock multiplayer to known map builds 2026-09-30 21:02:34 +13:00
MrBruh 8fce8887d5 Smooth PAL first-person animations 2026-09-30 20:04:36 +13:00
MrBruh 758f07ea5f Retry the Discord post when rate limited 2026-09-30 19:32:03 +13:00
MrBruh ef9b9e9b1f Fix stale player input after the first game 2026-09-30 19:17:50 +13:00
Englis666andCursor 5ac71baf94 Keep the bullets' autoaim for the mouse
Only the view's magnetism is turned off for the mouse: the bullets still bend toward the target, as with a controller, so shots land as they did.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-30 00:19:37 -05:00
MrBruh a17d89c6f1 Merge bnunu/halo-1 main 2026-09-30 18:13:05 +13:00
Eclisse f3b46607e7 Ask laptops for the dedicated GPU on Windows 2026-09-29 23:38:59 -05:00
Englis666 b9f222222b Enhance direct camera configuration for desktop platforms
Add a new configuration setting for 'display.direct_camera' to enable first-person view adjustments based on player aiming. This update clarifies the behavior of the camera during gameplay and ensures that the setting is applicable only to desktop environments. Additionally, minor adjustments were made to the render_interpolation module to accommodate this feature.
2026-09-29 23:38:59 -05:00
Englis666 eb76e6fb4f Add direct camera rendering for first-person view
Implement direct camera rendering logic for first-person perspective in the render_interpolation module. This includes a new function to handle direct camera updates based on player control and cinematic states, ensuring proper camera behavior across different gameplay scenarios. Additional includes and static variables were added to support this functionality.
2026-09-29 23:38:59 -05:00
Englis666andCursor 767613a3cf Turn off aim assist for the mouse
When the mouse moved after the right stick, the controller's magnetism and autoaim leave the player be (input.mouse_aim_assist, off by default). In lockstep games the autoaim stays, since every machine simulates every shot alike.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 23:37:43 -05:00
MrBruh c26f2e84f8 Fix dirty disc error when joining a game 2026-09-30 17:24:48 +13:00
MrBruh 6feea5dbaa Only reclaim ART's idle large object space 2026-09-30 16:31:05 +13:00
Brandon Fenty 114e4a7635 Android: reclaim idle ART reservations squatting on the Xbox memory window
On the Retroid Pocket Flip2 (kona, kernel 4.19) ART reserves its free-list large object space across 0x754d0000-0x954d0000 at zygote start, covering the fixed Xbox window at 0x80000000, so MAP_FIXED_NOREPLACE fails with EEXIST and the game image never loads.

When the fixed reservation hits EEXIST, munmap exactly the intersection of the requested range with anonymous [anon:dalvik-*] space reservations (idle: those spaces commit lazily from their bottom and this app allocates no large objects) and retry. Nothing changes when the window is already free.
2026-09-30 16:29:26 +13:00
MrBruh f15c1e26b0 Log failed asserts in release builds 2026-09-30 16:18:57 +13:00
MrBruh 5a23aafc3a Merge pull request #24 from ShaneParslow/iso_dialog_hang_fix
Fix Linux hang on data extraction file dialog
2026-09-30 16:02:46 +13:00
MrBruh b739e0a56c Center the zoomed HUD on wide screens 2026-09-30 15:55:13 +13:00
codi 557c0b486c Build the Android port on macOS hosts
configure.py hardcoded the NDK's linux-x86_64 toolchain folder, so the
Android build could not find the NDK on a Mac. Pick the host's folder
(darwin-x86_64, windows-x86_64 or linux-x86_64), or the only one the NDK
has.

On a case-insensitive file system, the game's <StdDef.h> shim found itself
again through its own <stddef.h> inclusion and recursed. Use include_next,
as the port's other header shims do.
2026-09-29 20:51:24 -06:00
Shane Parslow 164d5aebdc Fix Linux hang on data extraction file dialog
SDL_WaitEvent with NULL event does not consume events, so it never
waits, spins, never pumps, never checks dbus. Use PumpEvents and Delay
instead. This also fixes the hot spin that used to occur here.
2026-09-29 19:42:35 -07:00
MrBruh fd8fc72693 Count down the respawn timer on clients 2026-09-30 15:13:55 +13:00
Jonas Volman 8db3414dcf Merge verified canonical reconciliation into halo-1 main
Bring main through 6bfd9567, preserving its existing README exactly. All other tracked content equals the independently gated canonical reconciliation. No force push or source changes beyond the verified canonical commits.
2026-09-29 19:06:08 -07:00
MrBruh 30709dbf0f Fix late joiners missing players and crashing 2026-09-30 13:53:49 +13:00
Jonas Volman 6bfd956737 Reconcile approved Halo fidelity packets and bounded data credit
Recover transparent geometry submit (+1,285 meaningful code, zero exact losses); reconcile COMMON ownership and Q10 85+EXT1 credit, actions/main data, and separately disclosed denominator corrections. Preserve canonical-only repairs and all held research. C09 is ordinary disclosed cleanup, not a claim of proven scaffolding intent. Clean rebuild, 1,663 tests, parks, audits, provider links and independent EXT1 check pass. Local reconciliation only.
2026-09-29 13:52:37 -07:00
MrBruh 0ef2ed7dc7 Extend the network test bots 2026-09-30 03:29:38 +13:00
MrBruh 045d67f861 Close the virtual keyboard with the menus 2026-09-30 03:29:38 +13:00
MrBruh f427d058af Fix statistics refresh order 2026-09-30 03:29:38 +13:00
MrBruh 10644b0f07 Fix client grenade count assert 2026-09-30 03:29:38 +13:00
MrBruh ffa981253c Fix stale action prompt and fallen bodies 2026-09-30 03:29:38 +13:00
MrBruh 2ff6516f02 Fix joining games in progress 2026-09-30 03:29:38 +13:00
MrBruh 84ed39e257 Fix crosshair sprite crash and client update spam 2026-09-30 01:00:43 +13:00
MrBruh 01298f74e9 Send idle actions for empty player slots 2026-09-30 00:33:26 +13:00
MrBruh c43e1ee461 Honour the base vertex index of indexed draws 2026-09-29 23:44:32 +13:00
MrBruh 8f3e7de9ca Play PAL maps as the NTSC maps 2026-09-29 22:07:10 +13:00
Jonas Volman f3aadeab98 Reconcile five Halo functions and approved declaration batches
Integrate coherent B3, P2/K17 prerequisites, completed K1-K16 + S, and the independent profile/effects rulings. Five gains and two explicitly approved B3 debits: net +3 strict functions, +2770 meaningful/+2800 padded bytes (+2774 progress credit). No additional losses; Halo objects 394 to 396 and data unchanged.

Clean rebuild, identical 8252-row stable sweep, 68 valid parks, admission/provider checks, 467-unit warning comparison, unchanged fake scan, and 1318 tests passed. Preserve canonical helper recoveries and unrelated work. Full evidence and disclosures in docs/object_matching_logs/astra_five_function_reconciliation_20260928.md.
2026-09-28 23:17:10 -07:00
MrBruh d8f48a2a34 Fix crosshair drawn before its first update 2026-09-29 17:13:33 +13:00
MrBruh 21714ac086 Name single-player machines after their player 2026-09-29 16:10:02 +13:00
MrBruh 5c867dc46e Fix teams and player slots for mid-match joins 2026-09-29 15:49:22 +13:00
MrBruh caa249457a Install Android maps from a disc image 2026-09-29 15:01:08 +13:00
MrBruh 6013deec33 Fix split-screen scissor and flare counts 2026-09-29 14:35:53 +13:00
MrBruh 1555f895a8 Merge pull request #6 from thelinkin3000/fix/splitscreen-viewport
Fix split-screen showing only the last player's view
2026-09-29 14:31:18 +13:00
MrBruh 8fda61312f Merge pull request #7 from thelinkin3000/fix/android-upload-flag
Stop invalidating streamed buffer ranges on Android
2026-09-29 14:27:40 +13:00
MrBruh a643adea30 Add UPnP for internet play 2026-09-29 14:23:21 +13:00
MrBruh c0c86bda26 Allow joining games in progress 2026-09-29 13:53:13 +13:00
thelinkin3000 96a48d9daa Stop invalidating streamed buffer ranges on Android
The Android port writes each streamed vertex and index range with a
glMapBufferRange call that set both GL_MAP_UNSYNCHRONIZED_BIT and
GL_MAP_INVALIDATE_RANGE_BIT. The two contradict each other: the first
promises that no queued draw is reading the range, the second tells the
driver to discard whatever the range held, which is the work the first exists
to avoid.

Adreno charges for that discard, around 0.85 ms per call whatever the range
written, and the renderer makes one call per draw. On a Galaxy Z Flip 4
(Snapdragon 8 Gen 1) a few hundred of those a frame came to 96% of a 550 ms
frame and left the game running at 1.8 fps with the GPU idle the whole time.
Dropping the invalidation takes the same call to well under a microsecond and
the same game to the display's refresh rate.

Mali is unaffected either way, and its memory use is flat before and after.
2026-09-28 23:37:19 +00:00
MrBruh dddaffd494 Prioritise player updates per client 2026-09-29 12:20:59 +13:00
MrBruh 087c1f5685 Improve netcode, add network versions 2026-09-29 11:32:57 +13:00
Jonas Volman 51cd4d8b67 Reconcile exact AI actor talk-weight chain (+904 bytes) 2026-09-28 13:41:27 -07:00
thelinkin3000 b6ef765346 Fix split-screen showing only the last player's view
In a local two-player match the first player's half of the screen came
out as a flat grey rectangle while the second player's half rendered
correctly; both players' controls still worked.

Each player's window starts by clearing the framebuffer to the colour of
its own fog, expecting the clear to be confined to that window's
rectangle. The port's Direct3D layer instead cleared the whole buffer,
so the second player's setup wiped the picture the first player had
just finished drawing, leaving only its fog colour behind.

Clip those clears to the active window, and make fragment clipping
follow the window too, so a window's effects cannot spill across the
divider. Visibility queries are rescaled to the window as well, since
they now cover a quarter of the area they used to in a two-player game.

Verified on-device in a two-player match.
2026-09-28 17:32:16 +00:00
MrBruh cd47170f39 Fix Android audio deadlock 2026-09-29 03:19:11 +13:00
MrBruh 3c9cd59c1b Add Android app icon 2026-09-29 03:09:03 +13:00
MrBruh c4425807c5 Mark Android app as a game 2026-09-29 03:05:24 +13:00
MrBruh 8994176637 Add auto-updater 2026-09-29 02:51:49 +13:00
MrBruh ed28cb2fa1 Rewrite READMEs, add rolling releases 2026-09-29 01:59:09 +13:00
MrBruh 2055d7216b Add Discord build notifications 2026-09-29 01:40:17 +13:00
MrBruh ef1c625058 Fix guest powerups and pickups 2026-09-29 01:28:47 +13:00
MrBruh 8fcfe9e653 Finish distributed netcode 2026-09-29 00:27:28 +13:00
MrBruh 391e83b11f Add distributed netcode 2026-09-28 22:27:37 +13:00
MrBruh b7874714ae Deterministic FP math 2026-09-28 19:56:00 +13:00
Jonas Volman 5a83a7234a Reconcile exact flag update and multiplayer directions with zero losses 2026-09-27 22:29:15 -07:00
Jonas Volman 709bfc3a3c Reconcile approved frontier ownership and admission packets
Import only independently reviewed production changes. Preserve canonical physics and virtual-keyboard repairs; keep B3 and all unresolved holds out.

Record exactly four approved canary debits (6987 meaningful bytes), six Halo admissions and two revocations. Keep vendor ioinit/storage work separate. Full build, 8252-row sweep, 1318 tests, park/admission/fake audits and 621-object binding pass within approved debit bounds.

Do not publish donor research dumps or private assets. README and unrelated user research remain untouched. See frontier_publication_reconciliation_20260927.md for complete evidence limits and accounting.
2026-09-27 17:12:48 -07:00
Jonas VolmanandClaude Opus 5.5 e59d5ca926 Correct stale vertex-shader initializer matching comment
Owner-approved f0941b08 comment-only reconciliation. Rebuilt object unchanged in every nondebug section, resolved relocation and symbol including line-derived literals. Zero credit/status changes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:38:04 -07:00
Jonas VolmanandClaude Opus 5.5 646da5e6f7 Reconcile RIFF byte-swap statics without an alignment directive
Approved CH-P1 plus private-storage prerequisite from005f965f/af5c0f7b. Eight later-demo-attested names at January offsets, fourth riff_format_info name disclosed as later only; original riff chunk literal unchanged. Removes invented wrapper, aliases, align directive. Fresh resolved bytes/relocations identical; full build/stable/parks/admission,1318 tests pass. Zero credit.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:36:57 -07:00
Jonas VolmanandClaude Opus 5.5 57591011c5 Reconcile AIFF's six attested private byte-swap objects
Approved CG-P105 plus static prerequisite, from2c6b0292/dac1af35. Later demo PDB names disclosed; January offsets/layout align4 independently checked. All resolved bytes/relocations preserved, alignment8 to4 only. Full build, stable sweep zero losses, parks/admission,1318 tests pass. Zero credit.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:34:21 -07:00
Jonas VolmanandClaude Opus 5.5 09bf46b57a Reconcile key-agreement's six attested private data objects
Approved CG-P07 plus private-storage prerequisite, from c8ea5bf1/faa01ee8. Later demo PDB names disclosed; January offsets0,40,60,80,100,116 and align4 prove layout. Independent resolved relocation checks preserve code/data, only alignment8 to4; no headers/new providers. Production full build, stable sweep zero losses, parks/admission and1318 tests pass. Zero credit.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:32:16 -07:00
Jonas VolmanandClaude Opus 5.5 ef3ec0606c Credit independently verified HS data under approved Q11 model
PD2 from e3c053cd, HS-only append. +53122 raw data+1658 padding=54780, no code/functions/objects. Current provider seals and negative tests pass; full build, stable sweep, parks, admission, 1318 tools tests pass. No scorer upgrade or actions credit.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:29:49 -07:00
Jonas VolmanandClaude Opus 5.5 d7c5c52411 Reconcile reviewed Q11 extent verifier without credit
PB from dccd2e95, including coverage/substitution hardening. Current canonical independent 910-section and20-provider review passed; existing report unchanged without new entry. Full build/stable/parks/admission clean; 1318 tests,5 skipped,100 subtests. Scorer remains pinned3.3.1; no actions credit.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:25:38 -07:00
Jonas VolmanandClaude Opus 5.5 baab1f8efa Reconcile Q11 grouped-data regression gate without credit
Owner-approved PA+PC from c32e53c2. Independent current-object review and negative controls passed. Full build, stable sweep zero gains/losses, parks/admission unchanged; 1199 tests pass. Extent credit and verifier are separate commits.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:22:34 -07:00
Jonas VolmanandClaude Opus 5.5 632bebab28 Reconcile approved main_crash command against canonical
Source from Claude donor d18a7cee; independent stock and W3 builds, full build and stable sweep: +11 meaningful /16 padded Halo code, +1796 data, zero exact losses. 1161 tools tests pass. No declaration-batch or general UB exception.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:15:02 -07:00
Jonas Volman e3aea795e8 Recover private virtual-keyboard helper use and storage 2026-09-27 13:14:25 -07:00
Jonas Volman 25da8a5e32 Restore January water-depth predicate selection in physics 2026-09-27 13:04:02 -07:00
Jonas Volman 30f71413c1 Restore evidenced physics predicates and observer time powers 2026-09-27 12:42:44 -07:00
Jonas Volman 9e851ed4e5 Recover January lift absolute-value operations in physics 2026-09-27 12:25:27 -07:00
Jonas Volman a659684885 Restore two January public BSS definitions with proven owner storage 2026-09-27 12:00:52 -07:00
Jonas Volman ebd24ae1e7 Recover source-backed vector avoidance timer statement without exact credit 2026-09-27 11:44:36 -07:00
7750 changed files with 614613 additions and 1628249 deletions

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
# Files that a Linux shell runs directly must keep LF line endings in a
# Windows checkout (core.autocrlf): a CRLF shebang line does not start.
*.sh text eol=lf
port/android/gradlew text eol=lf
+38
View File
@@ -0,0 +1,38 @@
name: Problem report
description: Something does not work as it should
body:
- type: markdown
attributes:
value: |
Thanks for reporting! Please do **not** ask for, link to or attach game files or disc images; such issues are closed and deleted.
- type: dropdown
id: headset
attributes:
label: Headset
options:
- Meta Quest 3
- Meta Quest 3S
- Steam Frame
- Other
validations:
required: true
- type: input
id: build
attributes:
label: Build number
description: The release you installed, e.g. "build 180".
validations:
required: true
- type: textarea
id: what
attributes:
label: What happened
description: What you did, what you expected and what happened instead.
validations:
required: true
- type: textarea
id: log
attributes:
label: Log
description: |
Attach `debug.txt`. Quest: `Android/data/io.github.cesarus85.ringfall/files/debug.txt` (SideQuest's file manager). Steam Frame: `Documents/RingfallMjolnirVR/debug.txt`, and `logcat.txt` and `install.log` beside it.
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: How to install
url: https://github.com/Cesarus85/mjolnir-vr#install-on-meta-quest-3--3s
about: Step-by-step installation for the Quest 3 / 3S and the Steam Frame.
+475 -14
View File
@@ -1,26 +1,41 @@
name: Build
# Debug and release builds of the native ports (Linux, Windows, Android) for
# every pushed commit, uploaded as artifacts. tools/ci_build.py does each
# build; see it for the options.
# Debug and release builds of the native ports (Linux, Windows, Android) and
# the web port for every pushed commit, uploaded as artifacts.
# tools/ci_build.py does each build; see it for the options.
#
# Kept cheap:
# - one job per platform builds both configurations, so its set-up (tools,
# downloads, caches) is paid once;
# - compiles go through ccache, whose cache carries over from run to run, so
# a commit only recompiles what it changed; downloads (SDL, musl, Gradle)
# are cached too;
# a commit only recompiles what it changed; downloads (SDL, musl, Gradle,
# the Linux build's system root) are cached too;
# - a newer push to the same branch cancels a run still going;
# - pushes that only change documentation do not build;
# - Windows, whose minutes cost twice Linux's, builds only Windows;
# - Mjolnir VR builds only Android (the VR app): the Linux and
# Windows jobs are kept, off (if: false), so that upstream's changes to
# them still merge;
# - artifacts are kept for three days.
#
# A successful build of main is also published as a GitHub release, the
# newest marked latest, so that
# https://github.com/<repository>/releases/latest/download/<file> always
# gives the latest build; the last five are kept. Its web build is then the
# site on GitHub Pages (https://halocombatevolved.com/, port/web/README.md).
on:
# (Mjolnir VR: main only; its branches are tried on the headsets
# from local builds)
push:
branches: [main]
paths-ignore:
- "**.md"
- "docs/**"
- "research/**"
workflow_dispatch:
inputs:
profile:
description: "Also build the profiling flavour (configure.py --profile) on Linux"
type: boolean
default: false
concurrency:
group: build-${{ github.ref }}
@@ -37,8 +52,14 @@ env:
jobs:
linux:
# (off: Mjolnir VR ships the Android app only)
if: ${{ false }}
# Arch Linux has what the build needs as packages: clang 22 (for the
# profile-guided optimisation in pgo/) and 32-bit SDL 3.
# profile-guided optimisation in pgo/), and CMake, pkg-config and
# wayland-scanner for the SDL 3 that the portable build brings. The build
# links against Debian 11's 32-bit glibc (tools/linux_sysroot.py), not
# Arch's, so that it starts on SteamOS and older distributions; Arch's
# 32-bit glibc is for the tests.
runs-on: ubuntu-24.04
container: archlinux:latest
timeout-minutes: 40
@@ -47,7 +68,7 @@ jobs:
run: |
printf '\n[multilib]\nInclude = /etc/pacman.d/mirrorlist\n' >> /etc/pacman.conf
pacman -Syu --noconfirm --needed git zstd python python-pytest ninja ccache \
clang lld gcc lib32-glibc lib32-gcc-libs lib32-sdl3
clang lld gcc lib32-glibc lib32-gcc-libs cmake pkgconf wayland
- uses: actions/checkout@v4
@@ -57,14 +78,29 @@ jobs:
key: ccache-linux-${{ github.sha }}
restore-keys: ccache-linux-
# the system root and SDL's source, which the build downloads
- uses: actions/cache@v4
with:
path: build/linux/third_party
key: linux-third-party-${{ hashFiles('tools/linux_sysroot.py') }}
- name: Debug
run: python tools/ci_build.py linux debug
# (the profiling build, configure.py --profile, which no other step
# compiles: only when a run started by hand asks for it, as it costs a
# build of its own)
- name: Profile
if: github.event_name == 'workflow_dispatch' && inputs.profile
run: python tools/ci_build.py linux profile
- name: Release
run: python tools/ci_build.py linux release
- name: Test
run: python -m pytest -q tools/test_linux_port.py
run: |
python -m pytest -q tools/test_linux_port.py tools/test_android_asm_convert.py tools/test_android_mirror_uploads.py tools/test_android_ce_texture_cache.py tools/test_host_watch_hash.py tools/test_touch_menu.py tools/harness tools/test_profile.py tools/stagekit/tests tools/test_net_compare.py
python tools/test_light_storage.py
- name: Compile cache
run: ccache --show-stats
@@ -89,10 +125,15 @@ jobs:
- name: Install the toolchain
run: |
mkdir -p "$RUNNER_TEMP/tools"
# (each download checked against the hash it had when it was pinned)
curl -fsSL -o ninja.zip https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-linux.zip
curl -fsSL -o ccache.tar.xz https://github.com/ccache/ccache/releases/download/v4.10.2/ccache-4.10.2-linux-x86_64.tar.xz
sha256sum -c - <<'EOF'
6f98805688d19672bd699fbbfa2c2cf0fc054ac3df1f0e6a47664d963d530255 ninja.zip
80cab87bd510eca796467aee8e663c398239e0df1c4800a0b5dff11dca0b4f18 ccache.tar.xz
EOF
unzip -q ninja.zip -d "$RUNNER_TEMP/tools" && rm ninja.zip
curl -fsSL https://github.com/ccache/ccache/releases/download/v4.10.2/ccache-4.10.2-linux-x86_64.tar.xz \
| tar -xJ -C "$RUNNER_TEMP/tools" --strip-components=1 ccache-4.10.2-linux-x86_64/ccache
tar -xJf ccache.tar.xz -C "$RUNNER_TEMP/tools" --strip-components=1 ccache-4.10.2-linux-x86_64/ccache && rm ccache.tar.xz
echo "$RUNNER_TEMP/tools" >> "$GITHUB_PATH"
- uses: actions/checkout@v4
@@ -107,7 +148,7 @@ jobs:
- uses: actions/cache@v4
with:
path: build/android/third_party
key: android-third-party-${{ hashFiles('tools/android_build.py') }}
key: android-third-party-${{ hashFiles('tools/android_build.py', 'port/android/patches/**') }}
- uses: actions/cache@v4
with:
@@ -116,12 +157,36 @@ jobs:
~/.gradle/wrapper
key: gradle-${{ hashFiles('port/android/**/*.gradle*', 'port/android/gradle/wrapper/gradle-wrapper.properties') }}
# the app's signing key, from the ANDROID_KEYSTORE_BASE64 secret (a
# keystore, base64) and ANDROID_KEYSTORE_PASSWORD (its password, and
# the key's, alias halo): every build signed with it installs over the
# one before, as the app's self-updater (Updater.java) needs. Without
# them each build is signed with the runner's own debug key.
- name: Signing key
env:
KEYSTORE: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
if [ -n "$KEYSTORE" ]; then
printf '%s' "$KEYSTORE" | base64 -d > "$RUNNER_TEMP/halo.keystore"
echo "HALO_ANDROID_KEYSTORE=$RUNNER_TEMP/halo.keystore" >> "$GITHUB_ENV"
else
echo "::warning::no ANDROID_KEYSTORE_BASE64 secret: the Android builds cannot update over each other"
fi
- name: Debug
env:
HALO_ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
run: python3 tools/ci_build.py android debug
- name: Release
env:
HALO_ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
run: python3 tools/ci_build.py android release
# the touch controls' layout and gyroscope maths (the runner's JDK)
- name: Test the touch layout
run: python3 tools/test_touch_layout.py
- name: Compile cache
run: ccache --show-stats
@@ -138,6 +203,8 @@ jobs:
retention-days: 3
windows:
# (off: Mjolnir VR ships the Android app only)
if: ${{ false }}
# The runner image has LLVM, Python and Visual Studio's x86 libraries and
# Windows SDK.
runs-on: windows-2025
@@ -150,8 +217,11 @@ jobs:
shell: pwsh
run: |
Invoke-WebRequest -OutFile ninja.zip https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-win.zip
Expand-Archive ninja.zip -DestinationPath "$env:RUNNER_TEMP\tools"
Invoke-WebRequest -OutFile ccache.zip https://github.com/ccache/ccache/releases/download/v4.10.2/ccache-4.10.2-windows-x86_64.zip
# (each download checked against the hash it had when it was pinned)
if ((Get-FileHash ninja.zip -Algorithm SHA256).Hash -ne "F550FEC705B6D6FF58F2DB3C374C2277A37691678D6ABA463ADCBB129108467A") { throw "ninja.zip: unexpected SHA-256" }
if ((Get-FileHash ccache.zip -Algorithm SHA256).Hash -ne "6252F081876A9A9F700FAE13A5AEC5D0D486B28261D7F1F72AC11C7AD9DF4DA9") { throw "ccache.zip: unexpected SHA-256" }
Expand-Archive ninja.zip -DestinationPath "$env:RUNNER_TEMP\tools"
Expand-Archive ccache.zip -DestinationPath $env:RUNNER_TEMP
Copy-Item "$env:RUNNER_TEMP\ccache-4.10.2-windows-x86_64\ccache.exe" "$env:RUNNER_TEMP\tools"
Remove-Item ninja.zip, ccache.zip
@@ -177,6 +247,9 @@ jobs:
- name: Release
run: python tools/ci_build.py windows release
- name: Test light storage
run: python tools/test_light_storage.py
- name: Compile cache
run: ccache --show-stats
@@ -191,3 +264,391 @@ jobs:
name: halo-windows-release
path: dist/halo-windows-release
retention-days: 3
# the PDBs, for crash reports' backtraces (tools/ci_build.py)
- uses: actions/upload-artifact@v4
with:
name: halo-windows-debug-symbols
path: dist/halo-windows-debug-symbols
retention-days: 3
- uses: actions/upload-artifact@v4
with:
name: halo-windows-release-symbols
path: dist/halo-windows-release-symbols
retention-days: 3
web:
# (off: Mjolnir VR ships the Android app only)
if: ${{ false }}
# Emscripten (emsdk, the version below, cached) builds the web port's
# site (port/web/README.md); the tests check its graph, the page's
# importer and service worker, its sockets, that it starts in a
# headless Chromium (with no game data, as far as the menus' map), and
# that two browsers join a room through a local MQTT broker.
runs-on: ubuntu-24.04
timeout-minutes: 40
env:
EMSDK_VERSION: 6.0.10
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: ~/emsdk
key: emsdk-${{ env.EMSDK_VERSION }}-${{ hashFiles('port/web/halo_sdl3.py') }}
- name: Install the toolchain
run: |
if [ ! -x ~/emsdk/upstream/emscripten/emcc ]; then
rm -rf ~/emsdk
git clone --depth 1 https://github.com/emscripten-core/emsdk.git ~/emsdk
~/emsdk/emsdk install "$EMSDK_VERSION"
fi
~/emsdk/emsdk activate "$EMSDK_VERSION"
sudo apt-get update -q
sudo apt-get install -y -q ninja-build ccache python3-pytest mosquitto
- uses: actions/cache@v4
with:
path: .ccache
key: ccache-web-${{ github.sha }}
restore-keys: ccache-web-
- name: Debug
run: |
source ~/emsdk/emsdk_env.sh
python3 tools/ci_build.py web debug
- name: Release
run: |
source ~/emsdk/emsdk_env.sh
python3 tools/ci_build.py web release
- name: Test
run: |
source ~/emsdk/emsdk_env.sh
node --test "port/web/tests/*.test.js"
python3 -m pytest -q tools/test_web_build.py
- name: Test in Chromium and Firefox
run: |
npm install --no-save --prefix "$RUNNER_TEMP/playwright" playwright@1.58
"$RUNNER_TEMP/playwright/node_modules/.bin/playwright" install --with-deps chromium firefox
NODE_PATH="$RUNNER_TEMP/playwright/node_modules" node port/web/tests/smoke.mjs dist/halo-web-release
NODE_PATH="$RUNNER_TEMP/playwright/node_modules" node port/web/tests/rooms.mjs dist/halo-web-release
# (the native builds' WebRTC, port/linux/src/p2p_webrtc.c, with both)
NODE_PATH="$RUNNER_TEMP/playwright/node_modules" node port/web/tests/webrtc_native.mjs chromium firefox
- name: Compile cache
run: ccache --show-stats
- uses: actions/upload-artifact@v4
with:
name: halo-web-debug
path: dist/halo-web-debug
include-hidden-files: true
retention-days: 3
- uses: actions/upload-artifact@v4
with:
name: halo-web-release
path: dist/halo-web-release
include-hidden-files: true
retention-days: 3
release:
# The eight builds (and the Windows builds' PDBs) as a release, tagged
# build-<run number> and marked latest
# (every file keeps its name from release to release, for the README's
# latest/download links); then all but the newest five build-* releases
# go, tags and all. Only for main, and only when every platform built.
needs: [android]
if: ${{ github.ref == 'refs/heads/main' }}
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: write
outputs:
tag: ${{ steps.publish.outputs.tag }}
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
port/android
tools
- uses: actions/download-artifact@v4
with:
pattern: halo-*
path: artifacts
- name: Package
run: |
mkdir release
# (Mjolnir VR publishes its Android builds only, under
# its own name: Updater.java looks for them; the desktop ports are
# built to keep them building)
for config in debug release; do
(cd "artifacts/halo-android-$config" &&
zip -qr "$GITHUB_WORKSPACE/release/mjolnir-vr-android-$config.zip" .)
done
# the Steam Frame's installer (port/android/STEAM_FRAME.md): its
# bundle with the release APK, and the desktop file that fetches it
python3 tools/steam_frame_bundle.py artifacts/halo-android-release/app-release.apk release "$GH_REPO"
# the APK itself, for sideloading on the Quest (SideQuest, adb)
cp artifacts/halo-android-release/app-release.apk release/MjolnirVR.apk
# the same under the names before "Mjolnir VR" (Ringfall: Mjolnir
# VR, builds 302 to 313): their updaters (Updater.java, the Frame's
# launcher) and the links already shared ask for them
cp release/mjolnir-vr-android-debug.zip release/ringfall-mjolnir-vr-android-debug.zip
cp release/mjolnir-vr-android-release.zip release/ringfall-mjolnir-vr-android-release.zip
cp release/mjolnir-vr-steam-frame.zip release/ringfall-mjolnir-vr-steam-frame.zip
cp release/Install-Mjolnir-VR.desktop release/Install-Ringfall-Mjolnir-VR.desktop
cp release/MjolnirVR.apk release/RingfallMjolnirVR.apk
ls -l release
- name: Publish
id: publish
env:
MESSAGE: ${{ github.event.head_commit.message }}
COMMIT_URL: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
tag="build-$GITHUB_RUN_NUMBER"
subject="$(printf '%s' "${MESSAGE:-manual run}" | head -n 1)"
readme="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
{
printf '%s\n\n' "$subject"
printf '**Mjolnir VR** is an unofficial VR mod for Halo: Combat Evolved. It needs a disc image of your own, purchased Xbox copy of the game; no game files are included.\n\n'
printf '| Headset | Download | How to install |\n| --- | --- | --- |\n'
printf '| Meta Quest 3 / 3S | `MjolnirVR.apk` | [Install on Quest](%s#install-on-meta-quest-3--3s) |\n' "$readme"
printf '| Steam Frame | `Install-Mjolnir-VR.desktop` (open it in Desktop Mode) | [Install on Steam Frame](%s#install-on-steam-frame) |\n\n' "$readme"
printf 'The other files are for the built-in updater and the Steam Frame installer (those named `ringfall-…` / `Ringfall…`: the same, for the builds before the name Mjolnir VR).\n\n'
printf 'Commit: %s\nBuild: %s\n' "$COMMIT_URL" "$RUN_URL"
} > notes.md
gh release create "$tag" release/* --target "$GITHUB_SHA" --latest \
--title "Mjolnir VR, build $GITHUB_RUN_NUMBER (${GITHUB_SHA:0:7})" --notes-file notes.md
echo "tag=$tag" >> "$GITHUB_OUTPUT"
# The Windows builds' executables and PDBs to Sentry, which turns the
# crash reports of these builds (port/windows/src/win32_crash.c) into
# backtraces with function names and lines. Needs the SENTRY_AUTH_TOKEN
# secret: an organization auth token of the Sentry organization, which
# names the organization and its region itself. The SENTRY_PROJECT
# variable can name the project (its slug), else the id in the DSN of
# win32_crash.c; SENTRY_ORG and SENTRY_URL (https://de.sentry.io) are
# for a personal token instead. A failed upload does not stop the
# release.
- name: Upload Windows symbols to Sentry
continue-on-error: true
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT || '4512207917744208' }}
ORG: ${{ vars.SENTRY_ORG }}
URL: ${{ vars.SENTRY_URL }}
SENTRY_CLI_VERSION: 2.46.0
run: |
if [ -z "$SENTRY_AUTH_TOKEN" ]; then
echo "::warning::no SENTRY_AUTH_TOKEN secret: crash reports of this build will have no function names"
exit 0
fi
if [ -n "$ORG" ]; then export SENTRY_ORG="$ORG"; fi
if [ -n "$URL" ]; then export SENTRY_URL="$URL"; fi
curl -fsSL -o sentry-cli "https://github.com/getsentry/sentry-cli/releases/download/$SENTRY_CLI_VERSION/sentry-cli-Linux-x86_64"
chmod +x sentry-cli
./sentry-cli debug-files upload --wait \
artifacts/halo-windows-release artifacts/halo-windows-release-symbols \
artifacts/halo-windows-debug artifacts/halo-windows-debug-symbols
- name: Keep the last five
run: |
gh release list --limit 100 --json tagName,createdAt \
--jq '[.[] | select(.tagName | startswith("build-"))] | sort_by(.createdAt) | reverse | .[5:] | .[].tagName' |
while read -r tag; do
gh release delete "$tag" --cleanup-tag --yes
done
pages:
# (off: Mjolnir VR ships the Android app only)
if: ${{ false }}
# The release's web build to GitHub Pages, which serves it at
# https://halocombatevolved.com/ (port/web/README.md, "Hosting"). Only
# main, only once every platform built and the release is out (the site
# and the release are one build), and a deployment is never cancelled
# halfway. The repository's Pages source must be GitHub Actions.
needs: [release]
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: write
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- uses: actions/download-artifact@v4
with:
name: halo-web-release
path: site
# The game's wasm, most of a build, from jsDelivr rather than GitHub
# Pages, which serves 100 GB a month: the wasm alone in a commit of the
# web-wasm branch (pushed over the last, so the repository keeps one),
# which jsDelivr serves by its hash (with CORS; GitHub's release
# downloads have none, so a page cannot fetch them), and version.json
# names that copy and its SHA-256. The site's service worker takes the
# wasm from there, checked, and from the site only when it cannot
# (port/web/site/sw.js). A failure here leaves the site serving it.
- name: The wasm on jsDelivr
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
sha256="$(sha256sum site/halo.wasm | cut -d' ' -f1)"
mkdir wasm
cp site/halo.wasm wasm/
cd wasm
git init -q -b web-wasm
git -c user.name="github-actions[bot]" -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \
add halo.wasm
git -c user.name="github-actions[bot]" -c user.email="41898282+github-actions[bot]@users.noreply.github.com" \
commit -q -m "The web build's wasm (build $GITHUB_RUN_NUMBER, $GITHUB_SHA)"
git push -q --force "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:refs/heads/web-wasm
url="https://cdn.jsdelivr.net/gh/${GITHUB_REPOSITORY}@$(git rev-parse HEAD)/halo.wasm"
# (jsDelivr fetches it now, and it must be the same file: it serves
# files of at most 20 MB from GitHub)
curl -fsSL --retry 6 --retry-delay 10 --retry-all-errors -o served.wasm "$url"
echo "$sha256 served.wasm" | sha256sum -c -
cd ..
python3 - "$url" "$sha256" <<'EOF'
import json, sys
with open("site/version.json") as file:
stamp = json.load(file)
stamp["wasm"] = {"url": sys.argv[1], "sha256": sys.argv[2]}
with open("site/version.json", "w") as file:
json.dump(stamp, file, indent=1)
EOF
echo "the wasm from $url"
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:
path: site
- id: deploy
uses: actions/deploy-pages@v4
notify:
# A Discord embed for the run, to the DISCORD_WEBHOOK secret (nothing
# without it): green when every platform built, red when one did not,
# with each platform's result, this run's release (main only), and links
# to the latest release's builds.
needs: [android, release]
if: ${{ !cancelled() }}
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- name: Post to Discord
env:
WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
LINUX: ${{ needs.linux.result }}
ANDROID: ${{ needs.android.result }}
WINDOWS: ${{ needs.windows.result }}
WEB: ${{ needs.web.result }}
PAGES: ${{ needs.pages.result }}
SITE_URL: https://halocombatevolved.com/
RELEASE_TAG: ${{ needs.release.outputs.tag }}
SERVER: ${{ github.server_url }}
REPOSITORY: ${{ github.repository }}
BRANCH: ${{ github.ref_name }}
SHA: ${{ github.sha }}
ACTOR: ${{ github.actor }}
MESSAGE: ${{ github.event.head_commit.message }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
COMMIT_URL: ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}
run: |
if [ -z "$WEBHOOK" ]; then
echo "no DISCORD_WEBHOOK secret: nothing to post"
exit 0
fi
status() {
case "$1" in
success) echo "✅ Built" ;;
failure) echo "❌ Failed" ;;
cancelled) echo "⏹️ Cancelled" ;;
*) echo "➖ Skipped" ;;
esac
}
if [ "$ANDROID" = success ]; then
title="Build succeeded"; color=5763719
else
title="Build failed"; color=15548997
fi
latest="$SERVER/$REPOSITORY/releases/latest"
downloads="[Quest]($latest/download/mjolnir-vr-android-release.zip) · [Steam Frame]($latest/download/Install-Mjolnir-VR.desktop) ([all builds]($latest))"
if [ -n "$RELEASE_TAG" ]; then
release="[$RELEASE_TAG]($SERVER/$REPOSITORY/releases/tag/$RELEASE_TAG)"
if [ "$PAGES" = success ]; then
release="$release, on [the site]($SITE_URL)"
fi
else
release="None (only a successful build of main makes one)"
fi
subject="$(printf '%s' "${MESSAGE:-manual run}" | head -n 1 | cut -c1-200)"
jq -n \
--arg title "$title" --argjson color "$color" --arg url "$RUN_URL" \
--arg description "[\`${SHA:0:7}\`]($COMMIT_URL) $subject — $ACTOR ($BRANCH)" \
--arg release "$release" --arg downloads "$downloads" \
--arg linux "$(status "$LINUX")" --arg windows "$(status "$WINDOWS")" --arg android "$(status "$ANDROID")" \
--arg web "$(status "$WEB")" \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
'{embeds: [{
title: $title, url: $url, color: $color, description: $description, timestamp: $timestamp,
fields: [
{name: "Linux", value: $linux, inline: true},
{name: "Windows", value: $windows, inline: true},
{name: "Android", value: $android, inline: true},
{name: "Web", value: $web, inline: true},
{name: "Release", value: $release},
{name: "Latest release builds", value: $downloads}
]
}]}' > payload.json
# Discord rate limits webhooks (HTTP 429, with how many seconds to
# wait in retry_after): wait and try again, a few times. A post
# still limited, or a network or Discord error, is a warning rather
# than a failed run; any other answer (a deleted webhook) fails. (At
# most 5 tries of 15 seconds and 4 waits of 30: inside the job's 5
# minutes.)
for attempt in 1 2 3 4 5; do
code=$(curl -sS --max-time 15 -o response.json -w '%{http_code}' \
-H "Content-Type: application/json" -d @payload.json "$WEBHOOK") || code=000
case "$code" in
2??)
exit 0
;;
429)
wait=$(jq -r '.retry_after // 5' response.json 2>/dev/null || echo 5)
wait=$(awk -v w="$wait" 'BEGIN { w += 0; if (w < 1) w = 1; if (w > 30) w = 30; printf "%d", w + 0.999 }')
echo "Discord is rate limiting the webhook; trying again in ${wait}s (attempt $attempt of 5)"
;;
000|5??)
wait=5
echo "Discord answered HTTP $code; trying again in ${wait}s (attempt $attempt of 5)"
;;
*)
echo "::error::Discord answered HTTP $code: $(cat response.json 2>/dev/null)"
exit 1
;;
esac
[ "$attempt" -lt 5 ] && sleep "$wait"
done
echo "::warning::The Discord post did not go through after 5 attempts (HTTP $code)"
+199
View File
@@ -0,0 +1,199 @@
name: Server image
# The dedicated server's container image (deploy/docker), made from a
# release's halo-linux-release.zip, and the Helm chart's checks
# (deploy/helm/opence-server).
#
# - After each successful Build of main (which publishes release build-<n>):
# the image of that release, checked, pushed to
# ghcr.io/opencommunityedition/opence-server as sha-<commit>, build-<n> and
# latest, and signed keyless with cosign (this workflow's GitHub identity).
# - Weekly, the latest release's image again, for Debian's fixes.
# - By hand, any release's (latest moves only for the newest).
#
# The checks (no game data in CI: it is never published): the image runs as
# 10001:10001; started on a read-only root without maps it writes its
# settings to /state, says "server: no usable maps" and exits 2; halo
# --version answers; the health check fails with nothing listening.
#
# Verify:
# cosign verify ghcr.io/opencommunityedition/opence-server:latest \
# --certificate-identity-regexp '^https://github\.com/OpenCommunityEdition/OpenCE/\.github/workflows/server-image\.yml@' \
# --certificate-oidc-issuer https://token.actions.githubusercontent.com
on:
workflow_run:
workflows: [Build]
types: [completed]
branches: [main]
schedule:
- cron: "23 5 * * 1"
workflow_dispatch:
inputs:
release:
description: "The release to build the image of: a tag (build-206) or latest"
default: latest
concurrency:
group: server-image-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
env:
IMAGE: ghcr.io/opencommunityedition/opence-server
jobs:
chart:
# (off: Mjolnir VR ships no dedicated server image)
if: ${{ false }}
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
- uses: azure/setup-helm@v4
- name: Lint
run: |
set -euo pipefail
chart=deploy/helm/opence-server
for values in "$chart"/ci/*-values.yaml; do
helm lint "$chart" --strict -f "$values"
helm template ci "$chart" -f "$values" > /dev/null
done
# the maps are never in the image: without game data the chart refuses to render
if helm template ci "$chart" > /dev/null 2>&1; then
echo "::error::the chart renders without gameData"; exit 1
fi
- name: Unit tests
run: |
set -euo pipefail
# (Helm 4 checks plugins' signatures, which helm-unittest does not publish)
if helm version --short | grep -q '^v3'; then verify=; else verify=--verify=false; fi
helm plugin install https://github.com/helm-unittest/helm-unittest $verify
helm unittest deploy/helm/opence-server
image:
# (off: Mjolnir VR ships no dedicated server image)
if: ${{ false }}
runs-on: ubuntu-24.04
timeout-minutes: 20
permissions:
contents: read
packages: write
# keyless signing: the workflow's OIDC token gets a short-lived Sigstore certificate
id-token: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
- name: Release
id: release
env:
GH_TOKEN: ${{ github.token }}
EVENT: ${{ github.event_name }}
RUN_NUMBER: ${{ github.event.workflow_run.run_number }}
WANTED: ${{ inputs.release }}
run: |
set -euo pipefail
case "$EVENT" in
workflow_run) tag="build-$RUN_NUMBER" ;;
workflow_dispatch) tag="${WANTED:-latest}" ;;
*) tag=latest ;;
esac
newest="$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName -q .tagName)"
if [ "$tag" = latest ]; then
tag="$newest"
fi
commit="$(gh release view "$tag" --repo "$GITHUB_REPOSITORY" --json targetCommitish -q .targetCommitish)"
gh release download "$tag" --repo "$GITHUB_REPOSITORY" --pattern halo-linux-release.zip --dir deploy/docker
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "commit=$commit" >> "$GITHUB_OUTPUT"
echo "newest=$([ "$tag" = "$newest" ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
echo "Release $tag (commit $commit; the newest: $newest)"
- name: Build
env:
TAG: ${{ steps.release.outputs.tag }}
COMMIT: ${{ steps.release.outputs.commit }}
run: |
set -euo pipefail
docker build --pull -t opence-server:ci \
--build-arg RELEASE=halo-linux-release.zip \
--label org.opencontainers.image.source="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" \
--label org.opencontainers.image.revision="$COMMIT" \
--label org.opencontainers.image.version="$TAG" \
--label org.opencontainers.image.licenses=AGPL-3.0-or-later \
--label org.opencontainers.image.title="OpenCE dedicated server" \
deploy/docker
- name: Smoke test
run: |
set -euo pipefail
[ "$(docker image inspect -f '{{.Config.User}}' opence-server:ci)" = 10001:10001 ] \
|| { echo "::error::the image does not run as 10001:10001"; exit 1; }
docker run --rm --read-only --entrypoint /opt/opence/halo opence-server:ci --version | tee version.txt
grep -Eq '^OpenCE build-[0-9]+ \(network version [0-9]+\)$' version.txt \
|| { echo "::error::halo --version did not answer"; exit 1; }
# without maps, on a read-only root with /state the one writable folder
docker volume create smoke-state > /dev/null
status=0
timeout 60 docker run --name smoke --read-only --cap-drop ALL --security-opt no-new-privileges \
--tmpfs /tmp -v smoke-state:/state -e HALO_SERVER_WEB_PASSWORD=ci-smoke-password \
opence-server:ci > server.log 2>&1 || status=$?
cat server.log
docker rm -f smoke > /dev/null
[ "$status" = 2 ] || { echo "::error::the server without maps exited with $status, not 2"; exit 1; }
grep -qF 'server: no usable maps' server.log \
|| { echo "::error::no 'server: no usable maps' line"; exit 1; }
if grep -qF 'server: ready' server.log; then echo "::error::the server said it was ready"; exit 1; fi
docker run --rm -v smoke-state:/state --entrypoint /bin/bash opence-server:ci -c 'test -s /state/config.toml' \
|| { echo "::error::no /state/config.toml"; exit 1; }
# the health check, with nothing listening
if docker run --rm --entrypoint /opt/opence/healthz.sh opence-server:ci; then
echo "::error::the health check passed with no server"; exit 1
fi
docker volume rm smoke-state > /dev/null
- uses: docker/login-action@v3
if: github.repository == 'OpenCommunityEdition/OpenCE'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: sigstore/cosign-installer@v3
if: github.repository == 'OpenCommunityEdition/OpenCE'
- name: Push and sign
if: github.repository == 'OpenCommunityEdition/OpenCE'
env:
TAG: ${{ steps.release.outputs.tag }}
COMMIT: ${{ steps.release.outputs.commit }}
NEWEST: ${{ steps.release.outputs.newest }}
IDENTITY: ^https://github\.com/${{ github.repository }}/\.github/workflows/server-image\.yml@
run: |
set -euo pipefail
tags=("sha-${COMMIT:0:7}" "$TAG")
if [ "$NEWEST" = true ]; then
tags+=(latest)
fi
for tag in "${tags[@]}"; do
docker tag opence-server:ci "$IMAGE:$tag"
docker push "$IMAGE:$tag"
done
ref="$(docker image inspect -f '{{index .RepoDigests 0}}' "$IMAGE:$TAG")"
test -n "$ref"
cosign sign --yes "$ref"
cosign verify "$ref" --certificate-identity-regexp "$IDENTITY" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com > /dev/null
{
echo "Pushed and signed $ref as ${tags[*]}."
echo
echo "Verify: cosign verify $IMAGE:$TAG --certificate-identity-regexp '$IDENTITY' --certificate-oidc-issuer https://token.actions.githubusercontent.com"
} >> "$GITHUB_STEP_SUMMARY"
+9 -17
View File
@@ -9,37 +9,29 @@ __pycache__
# Build files
build/
projects/
.ninja_*
build.ninja
*.vcxproj*
*.sln*
# decompctx output
ctx.*
*.ctx
# Generated configs
objdiff.json
compile_commands.json
# Miscellaneous
# The January build and the Xbox SDK (tools/xdk_headers.py reads the
# build's PDB); never commit
*.exe
*.pdb
*.idb
*.i64
*.id0
*.id1
*.id2
*.nam
*.til
*.bndb
xbox/
!source/**/xbox/
# Locally extracted proprietary game data (never commit)
assets/
# (but the high-res assets, drawn for the port: tools/hud_assets.py)
!port/assets/
original/
# CI build outputs (tools/ci_build.py)
dist/
# The app signing key's local settings and keys (port/android/app/build.gradle; never commit)
port/android/signing.properties
*.jks
*.keystore
+251
View File
@@ -0,0 +1,251 @@
# Notes for agents
This repository ports the Halo: Combat Evolved decompilation (Xbox build
01.01.14.2342, `cachebeta.exe`) to Linux, Windows, Android and the web. The game's C
sources are the decompilation; the port adds a platform layer that
implements the Xbox APIs the game calls, and features the Xbox game did not
have (a new netcode, internet play, the PC version's menus, Custom Edition
maps, touch controls). The project is published as OpenCE.
Start with [README.md](README.md). The other documents are listed at the
end.
## Layout
| Path | Contents |
| --- | --- |
| `source/` | The game: 466 C files of the decompilation, compiled as they are with few changes (each marked, below) |
| `port/linux/src/` | The platform layer the three ports share: Direct3D 8 on OpenGL (`d3d8_gl.c`), DirectSound and XInput on SDL3, files, threads, memory, sockets, settings (`port_config.c`), the mods folder (`port_mods.c`), internet play (`p2p*.c`), the dedicated server's platform side (`server_*.c`: the mode, the log, the rotation's grammar, HTTP, RCON, the master servers' publisher) and the master servers' list (`master_list.c`) |
| `port/linux/game/` | The port's game-side code, compiled as the game's sources are: the distributed netcode (`network_*.c`), co-op, OpenCE's gametypes (`game_modes.c`, `infection.c`, `gun_game.c`), map sharing (`map_share*.c`), the menus (`menu_tags.c`, `menu_functions.c`), Custom Edition maps (`custom_edition_*.c`, `cache_file_formats.c`), the tag validator (`tag_validate.c`, `tag_schema_*.c`), mods (`mods.c`, `mod_*.c`), streamer mode's word filter (`text_filter*.c`), the dedicated server's director, commands, gametypes and status (`server_*.c`), touch, FOV and more |
| `port/linux/include/` | The port's headers: the limits (`halo_port_limits.h`, with `HALO_PORT_NETWORK_VERSION`), capacities (`halo_port_capacity.h`), the prefix header, `halo_linux_source_fixups.h` (port functions the game's sources call) |
| `port/include/xdk/` | Stand-ins for the Xbox SDK's headers (declarations only) |
| `port/windows/` | The Windows build's own files (`win32_*.c`, headers) |
| `port/android/` | The Android app: the guest runtime, the host library (`host/`), the Java app (`app/`), host imports (`host_imports.list`) |
| `port/web/` | The web build: its runtime (`src/`: entry, sockets inside the page, memory watch), the page and its service worker (`site/`), the SDL port, tests |
| `port/assets/` | What the builds embed or ship: high-res HUD (`hud/`), fonts, titles, menus (`menus/`), icons, network brokers and master servers, the dedicated server's web page (`server_ui/`) |
| `port/third_party/` | Vendored libraries, each with a README naming its upstream, version and checksum |
| `port/tools/` | Stand-alone tools (`cache_file_report.c`) |
| `tools/` | Build scripts (`linux_build.py`, `windows_build.py`, `android_build.py`, `web_build.py`, `ci_build.py`, `web_serve.py`), generators (`ce_menus.py`, `port_settings.py`, `hud_assets.py`, `title_assets.py`, `embed_assets.py`, `server_ui_embed.py`, `xdk_headers.py`), the dedicated server's protocol simulator and remote console (`server_sim.py`, `opence_rcon.py`), tests, and `stagekit/` (scenarios staged and instrumented on the real engine, no retail assets) |
| `docs/` | Longer design notes (`custom_edition_caches.md`, `mods.md`, `dedicated_server.md`, `master_protocol.md`) |
| `pgo/` | Profile-guided optimisation profiles |
| `deploy/` | Running a dedicated server: systemd, Docker, Pterodactyl, Helm, PufferPanel, MCSManager, example gametypes |
| `assets/` | Game data for local runs (gitignored: `maps/`, `custom_maps/`) |
| `.github/workflows/` | CI: builds every platform on each push, publishes releases from `main` |
## Building
```sh
python configure.py # debug build; add --release, --portable, --profile, --pgo=off, --lto=off
ninja linux # build/linux/halo
ninja android # the Android guest image and native libraries
ninja android_apk # the APK (Gradle)
ninja web # build/web/site (Emscripten: port/web/README.md)
ninja windows # on Windows, or on Linux after configure.py --windows-cross
python tools/ci_build.py linux release # what CI builds
```
- `configure.py` writes `build.ninja`; `configure_args` at its top records
the options it was run with. Restore them after trying other options.
- The Linux and Windows builds are 32-bit x86 with clang, in MSVC's ABI
(`-fms-extensions -fshort-wchar -malign-double -fcommon`), C89 (`gnu89`):
declarations come before statements.
- Every native build compiles without fused multiply-add and with the
vendored musl maths (`halo_math.h`), so that every machine of a network
game computes the same results. Do not call the system's maths functions.
- Android: the game is an ILP32 AArch64 guest (`arm64_32`) in a 64-bit app.
Guest code can call only the host imports declared in
`port/android/host_imports.list` and `port/android/guest/runtime/guest_host.h`,
and only the SDL functions `guest_sdl.c` bridges. Run `ninja android`
after changing anything it compiles: `port/linux/src`, `port/linux/game`,
`port/android`, `source/`, the build scripts.
- Web: the game is `wasm32` (also ILP32), run on a Web Worker; calls must
match their callee's type exactly (the link fails on a mismatch), and
WebGL 2 is ES 3.0. Run `ninja web` after changing what it compiles (as
for Android, and `port/web`). Refer to [port/web/README.md](port/web/README.md).
### Defines
| Define | Set by | Meaning |
| --- | --- | --- |
| `HALO_ANDROID` | Android | The app: its display, input, files and lifecycle |
| `HALO_GLES` | Android, web | The OpenGL ES renderer |
| `HALO_ARM64_GUEST` | Android | The guest's ABI (ILP32 AArch64) |
| `HALO_WEB` | Web | The browser: its frames, storage, sockets and WebGL 2's limits (with `HALO_GLES`, not `HALO_ANDROID`) |
| `HALO_WINDOWS` | Windows (`halo_windows_prefix.h`) | The Windows build |
| `HALO_RELEASE` | `--release` | No assertions checked |
| `HALO_PROFILE` | `--profile` | The profiling build; a normal build must be unchanged by it |
`HALO_LINUX` and `HALO_CUSTOM_EDITION` are **not** defined by any build:
code guarded by them compiles to nothing. Custom Edition support is a
setting (`game.custom_edition`), checked at run time with
`custom_edition_cache_tags_loaded()`.
## Testing
```sh
python -m pytest -q tools/harness tools/test_touch_menu.py # the engine's functions in a fake world
python -m pytest -q tools/stagekit/tests # scenarios on the real engine, no retail assets (Linux)
python -m pytest -q tools/test_linux_port.py # the build, the maps' checks (needs assets/maps)
python -m pytest -q tools/test_mods.py # mods played over a map (needs assets/maps)
python -m pytest -q tools/test_bots.py # the bots in local games (needs assets/maps)
python -m pytest -q tools/test_cache_file_formats.py tools/test_bmp_files.py tools/test_profile.py
python tools/test_touch_layout.py # the Android touch layout (JDK 17+)
python tools/test_light_storage.py
python tools/test_death_timing.py
python -m pytest -q tools/test_web_build.py # the web build's graph, sockets, menus
node --test "port/web/tests/*.test.js" # the web page's importer and service worker
node port/web/tests/webrtc_native.mjs chromium firefox # the native builds' WebRTC with browsers (Playwright)
python -m pytest -q tools/test_dedicated_server.py # the dedicated server with clients (needs assets/maps)
python -m pytest -q tools/test_server_sim.py tools/test_egg.py # the server's web page and protocol simulator, the egg
python -m pytest -q tools/test_net_compare.py # net_compare.py's judging of logs, its copies and their processes
python tools/net_compare.py --a build/linux/halo --b <other build> --data <game data> --swap
# two builds in one network game, in step (retail maps: local only)
```
`tools/harness/README.md` explains how to add an asset-free test: the code
under test is taken from the sources, not copied. `tools/stagekit/README.md`
explains how to add a scenario: the real engine, with no retail assets.
Running the game without a person at it:
- `debug.network_test` (`HALO_NETWORK_TEST=host:<map>` or `join`) hosts or
joins a game without the menus and logs each player's state every second;
several copies play on one machine on different loopback addresses
(`HALO_NET_ADDRESS`, `HALO_NET_BROADCAST`). `NETCODE.md` describes the
test settings.
- `halo --server` is a dedicated server (`docs/dedicated_server.md`), and
`halo --probe [<invite>]` prints a game's advertisement as JSON and exits;
`tools/test_dedicated_server.py` runs both with clients.
- `debug.exit_after`, `debug.hidden_window`, `debug.null_renderer`,
`debug.menu_open` (start on one menu screen), `debug.telnet_console`.
- Use a virtual display (Xvfb, with `SDL_VIDEO_DRIVER=x11`) for windowed
runs, never the desktop of the person you work for, and stop only the
processes you started.
## Rules of the project
### The game's sources
- Change `source/` as little as possible, and mark each change from the
original with a comment that starts `port:`. Write like the surrounding
code: its naming, its comment density, and the project's plain,
descriptive comment style.
- A port function the game's sources call is declared in
`halo_linux_source_fixups.h` or included with a note naming its file
(`#include "view_fov.h" /* port: port/linux/game/view_fov.c */`).
- Notes such as `BUG (original, preserved for exact matching)` mark the
original's bugs. Fixing one is allowed; say so in a `port:` comment.
### The Xbox SDK
`port/include/xdk` was written without the SDK's headers, from the January
build's debug information and public documentation. Never read the SDK's
headers or anything derived from them, never copy from them, and do not
read `xbox/include`. To add a name, follow `port/include/xdk/README.md`.
### Networking
- Raise `HALO_PORT_NETWORK_VERSION` (`port/linux/include/halo_port_limits.h`)
with any change to what the machines send each other, or to anything they
must agree on, and say why in the commit. Machines of different versions
do not play together.
- Only the host decides; clients predict their own players. Read
`port/linux/NETCODE.md` before changing the netcode.
- Internet play is the same code in every build, the web's too: a browser's
tunnel is a WebRTC data channel (`port/web/src/web_p2p.c`), which the
native builds take on their tunnel's socket (`p2p_webrtc.c`, `posix_dtls.c`).
Check a change with both (`port/web/tests/webrtc_native.mjs`, and with the
maps `port/web/tests/internet.mjs`).
- Anything another machine sends is untrusted: check sizes, indices and
rates, as the existing handlers do. `debug.network_corrupt` tests this.
### Menus
- The menus must match Halo Custom Edition's. Their XML (`port/assets/menus/ce/`)
is generated by `tools/ce_menus.py` from the PC version's tags; the
settings screens come from `tools/port_settings.py`. Do not edit the XML
by hand: change the generator and run it (the command is in
`port/assets/menus/README.md`, "Writing them again"), then check that
`git status` shows only the screens meant to change.
- Functions the menus call but the port has not wired are listed in
`port/assets/menus/UNWIRED.md`.
### Art and credit
- Replacement HUD, title and menu art is made of hand-drawn redraws.
- Prefer an existing redraw (the ui-svg-handmade set), then a new redraw. A
Custom Edition picture that cannot reasonably be redrawn (a 3D render, a
screenshot, a logo) may be used only if it is listed in
`port/assets/menus/NON_HANDDRAWN.md`.
- `AUTHORS.md` credits art only.
- Fonts: Overpass and OpenCE (Newtown, respaced) only; see
`port/assets/fonts/README.md`. Never ship the maps' commercial typefaces.
### Custom Edition maps
Custom Edition maps live in the data root's `custom_maps` folder with
`bitmaps.map`, `sounds.map` and `loc.map`; their level names are
`custom_maps\<name>`. OpenSauce features are not supported and must not be
added: a map that needs them is refused, and one that only carries
OpenSauce's header runs as stock Custom Edition runs it. Map files are
untrusted input: read them through bounds-checked readers. See
`docs/custom_edition_caches.md`.
### Settings
A setting is an entry in `port/linux/src/port_config.c`, with a default, an
environment variable and a comment that `config.toml` gets. Document it in
the settings table of `port/linux/README.md` (and the Android README for an
Android-only one). A departure from the stock game's look or behaviour
should be a setting, off or stock by default, unless the project decides
otherwise.
### Third-party code and licences
The repository is `AGPL-3.0-or-later` (`LICENSE`); the commits before the
change, and the releases built from them, stay CC0 (`LICENSES/CC0-1.0.txt`).
The README's "Licence" section says what each part is under. Vendored code
goes in `port/third_party/<name>`, copied unchanged, with a README naming
the upstream, the version or commit, the licence, the checksum and what uses
it. Its licence must let it be combined with the AGPL 3.0: MIT, BSD (without
an advertising clause), ISC, zlib, Apache 2.0, MPL 2.0, LGPL or GPL 3.0 or
later, public domain. GPL-2.0-only code and the 4-clause BSD licence cannot
be. Code taken from another project, GPL 3.0 code included, keeps its
notices and its licence, and says where it came from.
The decompilation's upstream, punpckhdq/halo, is AGPL 3.0 from 17 October
2026; bnunu/halo-1 is CC0.
### Commits and pull requests
- Commit subjects are plain sentences saying what changed; the body says
why and what was checked.
- A pull request is merged with a merge commit (`Merge pull request #N from
<owner>/<branch>`), and follow-up changes go in separate commits whose
subject ends with `(#N)`.
- A commit taken from a fork keeps its author and is made with
`git cherry-pick -x`.
## Documents
| Document | About |
| --- | --- |
| [README.md](README.md) | Downloads, game data, building, build options, profiling builds |
| [port/linux/README.md](port/linux/README.md) | The Linux build, controls, menus, every setting, updates, frame rate, field of view, system link, internet play, voice chat, the server browser, map sharing, security, map checks, how the port operates, the game source changes |
| [port/linux/NETCODE.md](port/linux/NETCODE.md) | The distributed netcode, network versions, joining a game in progress, map sharing, transport, testing |
| [port/windows/README.md](port/windows/README.md) | The Windows build, its headers, inline functions, crash reports |
| [port/android/README.md](port/android/README.md) | The Android build, the guest and host, touch controls, Android settings, finding problems |
| [port/web/README.md](port/web/README.md) | The web build, the page, the game data in the browser, limits, how it operates, hosting on GitHub Pages |
| [port/include/xdk/README.md](port/include/xdk/README.md) | How the SDK declarations were written and are checked |
| [docs/custom_edition_caches.md](docs/custom_edition_caches.md) | Loading and converting Custom Edition maps, and the evidence for each layout |
| [docs/dedicated_server.md](docs/dedicated_server.md) | The dedicated server: its command line, exit codes, commands, HTTP API, RCON, events and files |
| [docs/master_protocol.md](docs/master_protocol.md) | The master servers' protocol: publishing, the probe, the signed list |
| [docs/mods.md](docs/mods.md) | Mods: folders of Halo PC tag files whose sounds, bitmaps, text and models play over a map's on one machine |
| [port/assets/text_filter/README.md](port/assets/text_filter/README.md) | Streamer mode's words: Surge AI's list, the review, the allowed words |
| [port/assets/menus/README.md](port/assets/menus/README.md) | The menu files' format and how to write them again |
| [port/assets/menus/NON_HANDDRAWN.md](port/assets/menus/NON_HANDDRAWN.md) | Menu pictures that are not redraws |
| [port/assets/menus/UNWIRED.md](port/assets/menus/UNWIRED.md) | Menu functions that do nothing yet |
| [port/assets/fonts/README.md](port/assets/fonts/README.md) | The fonts and their licences |
| [tools/harness/README.md](tools/harness/README.md) | Asset-free regression tests |
| [tools/stagekit/README.md](tools/stagekit/README.md) | Scenarios staged and instrumented on the real engine, no retail assets |
| `port/third_party/*/README.md` | Each vendored library |
| [AUTHORS.md](AUTHORS.md) | Art credits |
+25
View File
@@ -0,0 +1,25 @@
# Authors
## Art
- **The game's icon** (the Android app's launcher icon, `halo.exe`'s icon
and the desktop windows' icon): by ORION (`port/assets/icon`).
- **The controller's A, B, X and Y buttons** (the menus' button icons and
the HUD messages'): redraws by saulob (`port/assets/buttons`).
- **The high-res HUD** (its meters, icons, scopes and messages, and the
weapons' reticles): redraws by MrBruh (`port/assets/hud`), based on
reference photos by Aerocatia. The sniper rifle's zoom labels (2x, 10x)
are set in Overpass (`port/assets/fonts`).
- **The menus' mouse cursor** (the blue arrow the menus show for the
mouse): by pugzysixtyfour (`port/assets/cursor`).
## Third-party art
- **Voice chat's speaker icons** (who talks, and who is muted, in the lobby,
on the scoreboard and in the game): `volume-2` and `volume-x` from
[Lucide](https://lucide.dev) 1.53.0, copyright (c) 2026 Lucide Icons and
Contributors, under the ISC licence
(`port/assets/icons/lucide/LICENSE`).
- **The Mods menu's warning icons** (a mod out of its requested order, and
a mod that cannot be played): `triangle-alert` and `circle-x` from
[Lucide](https://lucide.dev) 1.53.0, under the same licence.
+661
View File
@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.
View File
File renamed without changes.
+196 -46
View File
@@ -1,77 +1,227 @@
Halo 1 xbox decomp, ported to Linux, Windows and Android
=============
<p align="center">
<img src="port/android/steam_frame_grid/library.png" width="640" alt="Mjolnir VR">
</p>
This is a port of the decompilation of Halo: Combat Evolved build 2342 (`cachebeta.exe`, sha256 `4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`) to Linux, Windows and Android.
# Mjolnir VR
<img width="1289" height="995" alt="image" src="https://github.com/user-attachments/assets/0d3ad50f-f8b8-46cf-aef8-e3661da2a7d7" />
**A VR mod for Halo: Combat Evolved** (the original Xbox game) on the
**Meta Quest 3 / 3S** and the **Steam Frame**. Play the campaign and
multiplayer in full 6DoF virtual reality, with your hands in the game.
This is based on [bnunu](https://github.com/bnunu/halo)'s decompilation project, which itself is a fork of [punpckhdq/halo](https://github.com/punpckhdq/halo).
Mjolnir VR runs on your headset by itself, without a PC. It
brings no game files: it uses a disc image of **your own, purchased copy**
of the game.
## Agent quick references
> **Unofficial fan project.** Mjolnir VR is a free,
> non-commercial fan project. It is not affiliated with, endorsed,
> sponsored or approved by Microsoft, Xbox Game Studios, Halo Studios or
> Bungie. See [Legal](#legal).
- [Current campaign house rules and batch/treemap cadence](docs/campaign_house_rules.md)
- [Common constants, types, float patterns, tag IDs, and flag conventions](docs/common_constants.md)
- [Shared assertion macros and byte-matching examples](docs/assertions.md)
- [Supplied CE source maps, recovered names, and next reconstruction packets](docs/user_source_reconstruction_map_20260906.md)
- [Matching methodology and source-credibility rules](docs/matching_methodology.md)
Until build 313 it was called *Ringfall: Mjolnir VR*. Installed builds
update to the new name by themselves, and the old links lead here.
## Build instructions
## Contents
You need Python and [ninja-build](https://ninja-build.org/) on your PATH; run `configure.py` from the repository root, then `ninja` with the target below (plain `ninja` builds the one for the computer you are on). No part of the Xbox SDK is needed: the SDK declarations the game uses are in [port/include/xdk](port/include/xdk/README.md).
- [What you get](#what-you-get)
- [What you can play](#what-you-can-play)
- [What you need](#what-you-need)
- [Install on Meta Quest 3 / 3S](#install-on-meta-quest-3--3s)
- [Install on Steam Frame](#install-on-steam-frame)
- [Playing](#playing)
- [Problems and questions](#problems-and-questions)
- [Credits](#credits)
- [Legal](#legal)
Every pushed commit is built by GitHub Actions ([.github/workflows/build.yml](.github/workflows/build.yml)): debug and release builds for Linux, Windows and Android, made by `tools/ci_build.py` (which also works locally, e.g. `python tools/ci_build.py linux release`) and kept as artifacts for three days.
## What you get
### Native Linux build
- **Full VR**: stereo 6DoF at your headset's refresh rate (72 to 120 Hz);
the HUD, menus, scope and crosshair float as layers of their own.
- **Your hands in the game**: the weapon sits in your hand and fires
where you point it; arms and fingers follow your controllers;
two-handed weapons, melee by swinging, grenades thrown with your hand,
crouching and leaning with your body.
- **Vehicles**: steer the Warthog by turning its wheel with both hands;
turrets with handles.
- **Comfort**: snap or smooth turning in fine steps, comfort vignettes,
left-handed mode, buttons remapped freely, graphics presets, refresh
rate and resolution, all in the in-game VR options (**Y**).
- **The PC version's menus**, clicked with a laser pointer, and an
on-screen keyboard for names, codes and passwords.
`ninja linux` compiles the game with clang into a native 32-bit Linux executable, `build/linux/halo`. It needs clang, 32-bit glibc development files and 32-bit SDL3. It renders with OpenGL, plays sound through SDL3 audio, and takes keyboard, mouse and gamepad input. Put the PAL game data (build 01.01.14.2342) under `assets/` so that `assets/maps` exists, then run `build/linux/halo`. See [port/linux/README.md](port/linux/README.md) for controls and settings.
## What you can play
### Debug and release
- **The whole campaign**: all ten missions, on every difficulty, saved as
you go.
- **Co-op campaign** with friends, online or on your local network, with
settings for friendly fire and extra enemies per player.
- **LAN multiplayer** (system link) with everyone on your network.
- **Online multiplayer**, no server of your own needed: host a game and
share its join code, find public games in the server browser, or protect
a lobby with a password. Up to 128 players.
- **Bots** to fill a game, on foot, in vehicles and at turrets; they make
room when a person joins.
- **All 13 multiplayer maps** and the Xbox game's game types: Slayer,
Capture the Flag, King of the Hill, Oddball and Race.
- **Cross-play**: VR players on the Quest and the Steam Frame, together with
players of the PC and phone builds of
[OpenCE](https://github.com/OpenCommunityEdition/OpenCE).
The native builds (Linux, Windows, Android) are debug builds by default: like the build the decompilation reproduces, they stop at the first failed assertion and log it. `python configure.py --release` configures release builds instead, which, like the retail game, do not check assertions. The byte-matching build is unaffected.
## What you need
### Optimisation
- A **Meta Quest 3 / 3S** or a **Steam Frame**.
- A **disc image** (`.iso` or `.xiso`) of **Halo: Combat Evolved for the
original Xbox**, made from **a disc you own**. Any region and version
works. The game plays in the disc's language; on the European disc in
Spanish and Italian it takes your headset's language (else the first).
The Linux and Windows builds are optimised for the processor of the computer that builds them (`-march=native`), and may not start on another. `python configure.py --portable` targets the x86-64 baseline instead (SSE2, which every 64-bit x86 processor has): use it for builds you share.
This project does not include, link to or help find game files. Please
do not ask for or share disc images in issues or discussions; such posts
are deleted.
They also use full link-time optimisation by default, which makes the final link take a minute or so; `--lto=thin` links in parallel and incrementally, `--lto=off` not at all.
## Install on Meta Quest 3 / 3S
They are also optimised with profiles of the game at play, recorded by an instrumented build playing the main menu and the opening minute of every campaign level by itself: `pgo/halo_linux.profdata` for Linux (and Android, whose code is almost the same) and `pgo/halo_windows.profdata` for Windows. The profiles need clang 22 or later; with an older clang, and with `--pgo=off`, the builds do without. A profile stays useful as the code changes (functions it does not know simply go without). To record a new one, delete it and configure with `--pgo=train`: `ninja linux` or `ninja windows` then builds the instrumented game and plays the levels (about fifteen minutes, in a window, silently; it needs the game data in `assets/`) before the real build. Over ssh, Windows plays them on the logged-in user's desktop, through a scheduled task. `--pgo-profile <file>` uses another profile.
You install the game once with a computer (Windows, macOS or Linux); after
that it updates itself.
With Mesa drivers the Linux build makes its GL calls through Mesa's GL thread (`mesa_glthread`), which takes them off the game's thread.
1. **Turn on Developer Mode** for your Quest, once: in the Meta Horizon
app on your phone, open your headset's settings, then *Developer
Mode* (Meta asks you to create a free developer account first).
2. **Install [SideQuest](https://sidequestvr.com/setup-howto)** (the
"Advanced Installer") on your computer and connect the Quest with a
USB cable. In the headset, allow USB debugging for this computer.
3. **Download `MjolnirVR.apk`** from the
[latest release](../../releases/latest) and drag it onto the SideQuest
window. It installs the game.
4. **Copy your disc image** to the Quest's `Download` folder: in
SideQuest, open the file manager (the folder icon), go to `Download`
and drag the `.iso` file in.
5. **In the headset**, open your *Library*, choose *Unknown Sources* and
start **Mjolnir VR**. Select **Choose disc image** and pick
your file in `Download`. The game copies what it needs (about 1.8 GB,
a minute or two) and starts.
Frames per second at the opening of a30, uncapped (`vsync = false` in `config.toml`, or `HALO_NO_VSYNC=1`), about 510 draws per frame; each row adds one change to the one above (Linux: a laptop with an Intel Core i7-1355U and Iris Xe graphics, median of three runs; Android: a Pixel 9 Pro XL, Tensor G4, median of three runs):
You can delete the disc image from `Download` afterwards. When a new
version comes out, the game asks you at start whether to update.
| Change | Linux | Android |
| --- | ---: | ---: |
| Original | 103 | 132 |
| GL state set only when it changes | 139 | 156 |
| Vertex and index buffers from a GL copy of the Xbox memory | 171 | 193 |
| Full link-time optimisation | 189 | not applicable |
| Profile-guided optimisation | 201 | 197 |
| `-march=native` (`--portable`, x86-64: 212; Android: `-mcpu=cortex-x3`, not used) | 224 | 191 |
| Mesa's GL thread | 257 | not applicable |
| Sound obstruction tested once per game tick, cheaper per-draw bookkeeping | 303 | 220 |
## Install on Steam Frame
Android has no equivalent of `-march=native`: the build runs on any 64-bit phone, and compiling for the Pixel's big cores made it slower there anyway, so its last row builds on the profile-guided one. On the laptop the game's own thread no longer sets the frame rate: it spends about as long waiting for the GL and driver threads and the GPU (which the laptop's power management keeps at a low clock for this load) as they spend waiting for it. The same holds in heavier scenes such as b30, at about 190.
No computer and no keyboard needed.
Unity ("jumbo") builds, which compile many files as one, would give the compiler nothing full link-time optimisation does not already see, and the decompiled sources declare too many conflicting local types for it anyway. `-O3` and profile-driven function splitting measured no faster than `-O2`.
1. **Switch to Desktop Mode**: press the Steam button, then *Power* >
*Switch to Desktop*.
2. **Put your disc image** in the `Downloads` folder (for example from a
USB stick or memory card with the file manager, Dolphin).
3. **Download `Install-Mjolnir-VR.desktop`** from the
[latest release](../../releases/latest) with the web browser.
4. **Open it**: in Dolphin, open `Downloads` and double-click the file.
If you are asked whether to run it, choose to continue.
5. **Answer the dialogs.** The installer finds your disc image, copies
what the game needs, adds **Mjolnir VR** to your Steam
library and offers to switch back to Gaming Mode.
6. Start **Mjolnir VR** from your library (under *Non-Steam*).
### Frame rate
The game updates itself: when a new version is out, it is downloaded
while you play and used from the next start on. Your settings, saves and
game data stay. More about the Steam Frame:
[port/android/STEAM_FRAME.md](port/android/STEAM_FRAME.md).
The native builds draw a frame at every refresh of the display (60, 90, 120, 240 Hz, ...), paced by vsync, while the game still simulates at 30 Hz as on the Xbox: each frame blends the last two ticks. To see the frame rate, open the developer console (the \` key) and enter `display_framerate true`; the frames per second, averaged over half a second, appear at the bottom right of the screen. `interpolation = false` in `config.toml` (written next to the executable, or in the data folder on Android, on the first run) restores the original 30 frames per second. See [port/linux/README.md](port/linux/README.md#frame-rate).
## Playing
### Native Windows build
- **Y** opens the VR options in the main and pause menus: turning,
comfort, weapon hand, buttons, graphics, refresh rate and resolution.
*Quit the game* is on its last page.
- In the menus, point with a controller and pull the trigger to click.
- The controller layout is on the VR options' *Controller buttons* page.
- **Voice chat** in network games: hold the grip of the hand without the
weapon to talk (not while it holds the weapon or the Warthog's wheel).
The *BUTTONS* page can put push to talk on another button.
`ninja windows`, run on Windows, compiles the game with clang into a native 32-bit Windows executable, `build/windows/halo.exe` (with `SDL3.dll`), sharing the Linux build's platform layer. It needs LLVM, Python and ninja, plus Visual Studio's x86 C++ libraries and a Windows SDK. Put the game data under `assets/` as for Linux. See [port/windows/README.md](port/windows/README.md).
## Problems and questions
### Android build
Please open an [issue](../../issues) and include:
`ninja android_apk` builds an arm64 Android app (`port/android/app/build/outputs/apk/debug/app-debug.apk`) that runs the game natively on 64-bit ARM phones, with OpenGL ES 3 rendering at the device's aspect ratio, SDL3 audio and game controller support (including a PS5 DualSense over Bluetooth). It needs the Android NDK and a clang with the `arm64_32` target in addition to the Linux build's requirements. The game data goes in the app's storage (the app offers to import it). See [port/android/README.md](port/android/README.md).
- your headset and the build number of the release you installed;
- what you did and what happened;
- the game's log, `debug.txt`:
- Quest: `Android/data/io.github.cesarus85.ringfall/files/debug.txt`
(SideQuest's file manager);
- Steam Frame: `Documents/RingfallMjolnirVR/debug.txt` (and
`logcat.txt` and `install.log` beside it).
### Matching build
What is done and what comes next: [port/android/VR_ROADMAP.md](port/android/VR_ROADMAP.md).
This fork builds only the native ports. The upstream project's byte-matching build, which compiles the game with the Xbox SDK's own compiler and compares it with `cachebeta.exe`, needs the August 2001 Xbox SDK, which cannot be redistributed, so `configure.py` no longer writes it. Its sources, configuration and `#ifdef`s are kept as they are, so that upstream's matching work still merges; `SolutionConfig.matching` in `tools/project_x86.py` turns it back on, for a checkout with the SDK's `XDK/xbox` folder extracted to `xbox/` and `cachebeta.exe` in the repository root. See [port/linux/README.md](port/linux/README.md#the-matching-build-on-a-linux-host) for running it on Linux.
**Network use.** The game connects to the internet only for what you
use: the update check (GitHub), online games (to other players
directly), and join codes and lobby lists through public MQTT brokers
(listed in [brokers.txt](port/assets/network/brokers.txt)). There is no
telemetry.
## Where's all the type information?
## Credits
We use debug symbols from later Halo games to help map structures, enums, type definitions, function signatures and some variable names. You should obtain a copy of the Halo CEA beta and run it through [pdb-decompiler](https://github.com/camden-smallwood/pdb-decompiler) to obtain debug info for yourself. Note that CEA is based off of Halo PC and has its own modifications which make it not 100% accurate to the original Xbox title.
Mjolnir VR stands on the work of others:
- **[OpenCE](https://github.com/OpenCommunityEdition/OpenCE)** (Open
Community Edition): the port of the decompiled game to Linux, Windows
and Android, with its renderer, netcode, PC menus and multiplayer, that
this project builds on and follows.
- **The decompilation**: [bnunu/halo-1](https://github.com/bnunu/halo-1),
a fork of [punpckhdq/halo](https://github.com/punpckhdq/halo).
- **[HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR)**
by moistman42069 (CC0): the arm IK, the forearm's twist and the
fingers' poses are taken from it (`port/linux/game/vr_arms.c`); the
left-handed mode and the graphics presets follow its ideas.
- **[HaloCE-VR](https://github.com/astromaddie/HaloCE-VR)** by astromaddie
(CC0): the Steam Frame work, whose findings (such as quieting Mesa's
trace markers) are used here.
- **[xboxdevwiki](https://xboxdevwiki.net/XDVDFS)**: its description of
the Xbox's disc file system, which the disc image readers are written
from.
- The libraries and fonts in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
Thank you all.
## Building it
`python3 configure.py --release`, then `ninja android_apk` (Android SDK
and NDK needed). See [port/android/README.md](port/android/README.md) for
the details and [docs/OPENCE_README.md](docs/OPENCE_README.md) for the
port it is built on.
## Legal
Mjolnir VR is an unofficial, free, non-commercial fan project.
It is not affiliated with, endorsed, sponsored or approved by Microsoft
Corporation, Xbox Game Studios, Halo Studios or Bungie. Halo, Halo: Combat
Evolved and Xbox are trademarks of Microsoft Corporation; they are named
here only to say which game this mod works with. Meta Quest is a trademark
of Meta Platforms, Inc.; Steam and Steam Frame are trademarks of Valve
Corporation.
No game data, art, music or other content of the game is included or
distributed. You need your own, legally purchased copy of the game. If
you enjoy this mod, please support the creators of the original by buying
their games.
### Licence
Mjolnir VR is free software: you can give it to others and change
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, version 3 of the License or (at your option)
any later version (`AGPL-3.0-or-later`), as OpenCE, which it builds on, is.
It comes with no warranty. The full text is in [LICENSE](LICENSE).
- **From build 312 on.** The AGPL applies from the commit that took in
OpenCE's change of licence (19 October 2026), and to each release built
from it or a later one. Earlier releases, and the source at every earlier
commit, were released under [CC0 1.0](LICENSES/CC0-1.0.txt) and stay
that way.
- **Hosts and servers.** If you change the game and let other persons play
with your version over a network (for example, as the host of an
internet game), the AGPL asks you to offer them the source of your
version. The game as released has its source here, and each release's
source is its tag (`build-<n>`).
- **Third-party code and fonts** keep their own licences: see
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
- **Halo is not covered.** The licence covers the work of this project's
and OpenCE's authors. It gives no rights in Halo, its maps or anything
else that Microsoft owns.
+48
View File
@@ -0,0 +1,48 @@
# Third-party notices
Mjolnir VR is released under the GNU Affero General Public
License, version 3 or later ([LICENSE](LICENSE)); releases before build 312
were released under [CC0 1.0](LICENSES/CC0-1.0.txt). It includes or builds
on the following, under their own licenses; each license's full text is in
the file named.
## Code
| Component | Used for | License | Text |
| --- | --- | --- | --- |
| [OpenCE](https://github.com/OpenCommunityEdition/OpenCE) and the decompilation ([bnunu/halo-1](https://github.com/bnunu/halo-1), [punpckhdq/halo](https://github.com/punpckhdq/halo)) | the game's code, the port | AGPL 3.0 or later (OpenCE from 19 October 2026, punpckhdq/halo from 17 October 2026; CC0 1.0 before); bnunu/halo-1: CC0 1.0 | [LICENSE](LICENSE), [LICENSES/CC0-1.0.txt](LICENSES/CC0-1.0.txt) |
| [HaloCE-Quest-VR](https://github.com/moistman42069/HaloCE-Quest-VR) | the arm IK, forearm twist and finger poses (`port/linux/game/vr_arms.c`) | CC0 1.0 | (its repository) |
| [HaloCE-VR](https://github.com/astromaddie/HaloCE-VR) | Steam Frame findings | CC0 1.0 | (its repository) |
| [SDL 3](https://libsdl.org) | windows, input, sound | zlib | (in SDL's source, `LICENSE.txt`) |
| [Khronos OpenXR loader](https://github.com/KhronosGroup/OpenXR-SDK) | VR runtime access | Apache 2.0 | (in the loader's package) |
| [musl](https://musl.libc.org) | the C library of the game's code on Android, its maths | MIT | [port/third_party/musl-math](port/third_party/musl-math/README.md) |
| [Expat](https://libexpat.github.io) | the PC menus' definitions | MIT | [port/third_party/expat/COPYING](port/third_party/expat/COPYING) |
| [Mbed TLS](https://www.trustedfirmware.org/projects/mbed-tls/) | HTTPS for the desktop builds' updater (not in the Android app) | Apache 2.0 | [port/third_party/mbedtls/LICENSE](port/third_party/mbedtls/LICENSE) |
| [miniupnpc](https://miniupnp.tuxfamily.org) | router port forwarding for online games | BSD 3-Clause | [port/third_party/miniupnpc/LICENSE](port/third_party/miniupnpc/LICENSE) |
| [KCP](https://github.com/skywind3000/kcp) | reliable transport for online games | MIT | [port/third_party/kcp/LICENSE](port/third_party/kcp/LICENSE) |
| [Monocypher](https://monocypher.org) | signing public games' listings (Ed25519); online play's other cryptography is the port's own | CC0 1.0 or BSD 2-Clause | [port/third_party/monocypher/LICENCE.md](port/third_party/monocypher/LICENCE.md) |
| [tomlc17](https://github.com/cktan/tomlc17) | reading `config.toml` | MIT | [port/third_party/tomlc17/LICENSE](port/third_party/tomlc17/LICENSE) |
| [stb](https://github.com/nothings/stb) | drawing the game's text sharply (stb_truetype) | MIT or public domain | [port/third_party/stb/LICENSE](port/third_party/stb/LICENSE) |
| [SMAA](https://www.iryoku.com/smaa/) | anti-aliasing (flat screens; off in VR) | MIT | [port/third_party/smaa/LICENSE](port/third_party/smaa/LICENSE) |
The disc image readers (`XisoExtractor.java`, `steam_frame_xiso.py`,
`xiso.c`) are written from a description of the Xbox's disc file system
([xboxdevwiki's XDVDFS page](https://xboxdevwiki.net/XDVDFS)), not from
extract-xiso, which earlier releases followed.
## Fonts
| Font | Used for | License | Text |
| --- | --- | --- | --- |
| Overpass (Red Hat, Delve Withrington) | the game's text, the library pictures | SIL Open Font License 1.1 | [port/assets/fonts/Overpass-OFL.txt](port/assets/fonts/Overpass-OFL.txt) |
| OpenCE (after Newtown) | the menus' titles | SIL Open Font License 1.1 | [port/assets/fonts/OpenCE-OFL.txt](port/assets/fonts/OpenCE-OFL.txt) |
| Newtown (Roger White) | OpenCE's source | public domain | [port/assets/fonts/Newtown-LICENSE.txt](port/assets/fonts/Newtown-LICENSE.txt) |
See [port/assets/fonts/README.md](port/assets/fonts/README.md) for where
each font is used.
## Not included
No file of Halo: Combat Evolved (maps, sounds, art, music, fonts or
executables) is part of this repository or its releases. The game reads
them from the player's own disc image.
-1304
View File
File diff suppressed because it is too large. Load diff
-22457
View File
File diff suppressed because it is too large. Load diff
-45
View File
@@ -1,45 +0,0 @@
{
"version": 1,
"entries": [
{
"unit": "source/rasterizer/xbox/rasterizer_xbox_profile",
"class": "source-layout-incomplete",
"symbol": "_rasterizer_profile_globals",
"reason": "All 16 target functions and all 2234 target data bytes are exact. The three stock SDK select-any tables (D3DTEXTUREDIRECTENCODE 16 bytes, D3DSIMPLERENDERSTATEENCODE 328, D3DPRIMITIVETOVERTEXCOUNT 88) come from D3D8.h's unconditional extern const selectany definitions, which January's TU included (its object imports _D3DDevice_InsertCallback@12 and QueryPerformance*). Each copy is byte-identical to January's selected provider and passes the selected-provider pair links in both orders, the standard under which collision_debug and the other Matching emitters carry the same tables. Canonical's dynavobgeom reconciliation (641e1466, docs/object_matching_logs/claude_fifty_objects_canonical_reconciliation.md) still leaves stock SDK-table coalescing open pending a uniform owner policy audit, so this object inherits that decision. Whole-object admission also fails source authenticity. The private state is written as four invented file-static aggregates (rasterizer_profile_globals, rasterizer_profile_elapsed_state, rasterizer_profile_frame_state, rasterizer_profile_state). Their pad02/pad06/pad12/pad16 members only spell out VC7's per-static 4-byte alignment padding, and reserved06[6] hides one unattested 4-byte object at .bss +0x45c. Loose file statics reproduce every code byte and every other data offset, and the later /Od build places elapsed_times before callback_start_times, contradicting the elapsed_state member order. The rasterizer_profile_check NONE branch reproduces January's format/varargs defect (%s consumes the NONE profile value), which awaits the owner's original-bug ruling. Three load-bearing volatile qualifiers have no first-party attestation. The TU also still declares global_window_parameters with a consumer-local extern: moving it to the owner header rasterizer_xbox_internal.h keeps every exact section byte-identical but declares the name earlier in rasterizer_xbox_models, which moves the residual __rasterizer_model_draw by one instruction.",
"evidence": "research/fifty_objects_20260925/w/review3_rasterizer_xbox_profile/REVIEW.md",
"reopen": "Recover the genuine January private state shape: replace the invented aggregates with authentic separate file statics, or first-party-attested structures, without fabricated pad/reserved members. Identify the unreferenced 4-byte January object at .bss +0x45c from first-party evidence rather than a filler. Obtain the owner's rulings on the BUG-marked NONE-branch varargs defect and the three volatile qualifiers. Any new private names need the owner's descriptive-name policy (as for the held rasterizer_xbox .bss split). Preserve all 16 target functions and all data sections, land the pinned 132-byte .data entry only with the Matching flip, move the window extern to its owner header with a full consumer sweep (re-baselining the rasterizer_xbox_models __rasterizer_model_draw park it perturbs), and apply the owner's uniform stock SDK-table decision from the open dynavobgeom reconciliation."
},
{
"unit": "source/rasterizer/xbox/rasterizer_xbox_dynavobgeom",
"class": "candidate-only-comdat-owner",
"symbol": "_D3DSIMPLERENDERSTATEENCODE",
"reason": "All 17 January code functions and all 31 January-owned sections/symbols match after replacing local debug, window and pixel-shader views with genuine owner types. Nine D3DINLINE wrappers have January's file-static storage. The two surplus color-conversion helpers are byte-identical to January's selected providers, and all 17 surplus symbols pass selected-provider links in both orders. The global_window_parameters extern still lives in consumer C files, with incompatible partial views in other consumers rather than a common owner declaration. Three stock SDK tables and other candidate-only constants/literals remain absent from January's split; the selected-provider test ignores unrelated unresolved symbols. A complete ordinary final-link ownership proof has not been established, so exact function credit does not imply Matching.",
"evidence": "docs/object_matching_logs/rasterizer_xbox_dynavobgeom_owner_layout_reconciliation.md",
"reopen": "Move global_window_parameters to its authentic owner declaration and reconcile the partial consumer views with a full regression sweep. Establish selected ownership/coalescing of candidate-only SDK tables and helper data through a complete compatible ordinary link or equally decisive January source/link evidence, without losing target exactness. Do not suppress genuine header emissions, alter selection metadata, force-inline, hand-copy helpers or count candidate-only bytes as January-owned."
},
{
"unit": "source/interface/hud_draw",
"class": "candidate-only-comdat-owner",
"symbol": "_fast_ftol",
"reason": "The exact flash-duration leaf uses the authentic shared conversion helper and naturally emits an additional unreferenced select-any32-byte fast_ftol copy absent from January HUD Draw. January's identical selected Actor Combat copy is no-duplicates; its ordinary XDK duplicate-owner failure is already documented. The two newly exact drawing helpers receive valid target-function credit, but no whole-object or linked-coalescing claim follows.",
"evidence": "docs/object_matching_logs/hud_draw_obj_two_exact_leaves_integration_20260904.md#complete-owner-boundary",
"reopen": "Require authentic source topology or an ordinary base/base link with an authenticated compatible canonical selected helper before clearing this veto. Do not force inline, manually expand math, alter target/COMDAT metadata, invent a discard pass, or credit candidate-only helper bytes. All remaining HUD Draw functions/data/linkage/source checks must independently pass before whole-object Matching."
},
{
"unit": "source/effects/decals",
"class": "candidate-only-comdat-owner",
"symbol": "_vector_from_points2d",
"reason": "The exact typed decal_projection_create body naturally inlines vector_from_points2d as January requires, but VC7 also emits an unreferenced select-any helper body absent from the January Decals split. January's selected Action Obey copy is no-duplicates. The exact target function receives valid credit; the surplus helper receives none and blocks whole-object Matching.",
"evidence": "docs/object_matching_logs/decals_obj_color_projection_packet_20260912.md#inline-helper-ownership-boundary",
"reopen": "Remove only when authentic source topology or an ordinary compatible canonical link preserves decal_projection_create while preventing or safely coalescing the extra helper. Do not force inline, manually expand the helper for code shaping, alter COMDAT metadata, or count the candidate-only bytes."
},
{
"unit": "source/effects/decals",
"class": "candidate-only-comdat-owner",
"symbol": "_cross_product2d",
"reason": "The exact typed decal_projection_create body naturally inlines cross_product2d as January requires, but VC7 also emits an unreferenced select-any helper body absent from the January Decals split. January's selected Actor Combat copy is no-duplicates. The exact target function receives valid credit; the surplus helper receives none and blocks whole-object Matching.",
"evidence": "docs/object_matching_logs/decals_obj_color_projection_packet_20260912.md#inline-helper-ownership-boundary",
"reopen": "Remove only when authentic source topology or an ordinary compatible canonical link preserves decal_projection_create while preventing or safely coalescing the extra helper. Do not force inline, manually expand the helper for code shaping, alter COMDAT metadata, or count the candidate-only bytes."
}
]
}
-1162
View File
File diff suppressed because it is too large. Load diff
@@ -1,20 +0,0 @@
{
"debug_sections": [
{
"after_fingerprint_sha256": "8e07b48ebedfa52de730be35f0b56d2fadb9b72e51a1edd029e20b5bb5cddc23",
"before_fingerprint_sha256": "92288b56cfbca8760857b38d5fb34cdfe3b56c092891ebfa6c00dc9cfc68b395",
"recipe": "xdk-stock-d3dinline",
"section": ".debug$F|anonymous=2",
"unit": "source/rasterizer/xbox/rasterizer_xbox_plasma_energy"
},
{
"after_fingerprint_sha256": "5b79f3d7f6d7cd9eca0a4324f66baf87dff523b69e917689707607178a0b48ab",
"before_fingerprint_sha256": "fd054221f7d9970be62ba370272c65e7c78a8a116110be3a5796fb06a8127ece",
"recipe": "xdk-stock-d3dinline",
"section": ".debug$S|anonymous=0",
"unit": "source/rasterizer/xbox/rasterizer_xbox_plasma_energy"
}
],
"functions": [],
"schema_version": 1
}
@@ -1,102 +0,0 @@
{
"debug_sections": [
{
"after_fingerprint_sha256": "8e07b48ebedfa52de730be35f0b56d2fadb9b72e51a1edd029e20b5bb5cddc23",
"before_fingerprint_sha256": "92288b56cfbca8760857b38d5fb34cdfe3b56c092891ebfa6c00dc9cfc68b395",
"recipe": "xdk-stock-d3dinline",
"section": ".debug$F|anonymous=2",
"unit": "source/rasterizer/xbox/rasterizer_xbox_text"
}
],
"functions": [
{
"after_comdat_selection": 2,
"after_fingerprint_sha256": "458fb5bd5e078ea76d95e47dd9632bb3bb0b3a25c8aa64efba41fd359a99b2cf",
"before_comdat_selection": 1,
"before_fingerprint_sha256": "9ff19872aa908361445bece0a21cab0ca4c93f68cb7bad1bcc70e77f0de6c6c8",
"function": "_D3DDevice_SetRenderState",
"recipe": "xdk-stock-d3dinline",
"source_recipe": "stock XDK D3DINLINE (static __forceinline)",
"target_evidence_sha256": "ef767bb47681ea399daf6f8720f056e523790d105bcea1f634d71c3e3611d192",
"unit": "source/rasterizer/xbox/rasterizer_xbox_text"
},
{
"after_comdat_selection": 2,
"after_fingerprint_sha256": "459f5d33ded34255e79f255c42a193c74066bdad08fc9116b2dd82eaf16ee094",
"before_comdat_selection": 1,
"before_fingerprint_sha256": "142d63782a1e9e3a41a5f722084b8681b1bf765912c4dd91784299a280112204",
"function": "_D3DDevice_SetTextureStageState",
"recipe": "xdk-stock-d3dinline",
"source_recipe": "stock XDK D3DINLINE (static __forceinline)",
"target_evidence_sha256": "ef19cb2f99aa7c1ad0f06f57c44a2dbace4b75ba6c779e68a1885e832d50d6c4",
"unit": "source/rasterizer/xbox/rasterizer_xbox_text"
},
{
"after_comdat_selection": 2,
"after_fingerprint_sha256": "5382805b884f0d40e945936f88f42d2b3c9f0c5ca0f83098312bc5a8444c3d66",
"before_comdat_selection": 1,
"before_fingerprint_sha256": "8f5385e5727b96b51ea06b98ff74fc9ba2f57802f0118f6f759e32ed3bb60797",
"function": "_IDirect3DDevice8_Begin@8",
"recipe": "xdk-stock-d3dinline",
"source_recipe": "stock XDK D3DINLINE (static __forceinline)",
"target_evidence_sha256": "f9f8776473104153fbb44b071659641050b0ef17ffde33740e053f1d31598d86",
"unit": "source/rasterizer/xbox/rasterizer_xbox_text"
},
{
"after_comdat_selection": 2,
"after_fingerprint_sha256": "a6c5208bf1949404ef5f4340bb14eff5ffab46a37c3aa041c2d5e4523b1dd8c2",
"before_comdat_selection": 1,
"before_fingerprint_sha256": "7916c2492802dcf917429ec316aefe67fbd8c96ef60bad99e12e26c38271342a",
"function": "_IDirect3DDevice8_End@4",
"recipe": "xdk-stock-d3dinline",
"source_recipe": "stock XDK D3DINLINE (static __forceinline)",
"target_evidence_sha256": "2acf41df227cc5ab56a634bc22b9a1af2f3594aa640b8a26e2126783c6802ed2",
"unit": "source/rasterizer/xbox/rasterizer_xbox_text"
},
{
"after_comdat_selection": 2,
"after_fingerprint_sha256": "09a181160ca4a0cec88563ed68f170bb7562d98e07cae82eb6ede383067e867c",
"before_comdat_selection": 1,
"before_fingerprint_sha256": "41c0a05e408be665a1bb43ea64bc56c1dbca0c5ef460f81b4248f6d02a2999d3",
"function": "_IDirect3DDevice8_SetTextureStageState@16",
"recipe": "xdk-stock-d3dinline",
"source_recipe": "stock XDK D3DINLINE (static __forceinline)",
"target_evidence_sha256": "b53ab8eb325c14f08df0e98df7b272a3dceb09921ed0d1dbeea49b69e07557f2",
"unit": "source/rasterizer/xbox/rasterizer_xbox_text"
},
{
"after_comdat_selection": 2,
"after_fingerprint_sha256": "13ff5c4aa2773bc0df32f62fce0afe58261ebabf2b2ca76426ef01a613fa7bf0",
"before_comdat_selection": 1,
"before_fingerprint_sha256": "7bb9a9967e3ddbbadfd0667b85c8f52ee06137a1f0b0cd4a0e45a4e1247d1815",
"function": "_IDirect3DDevice8_SetVertexData2f@16",
"recipe": "xdk-stock-d3dinline",
"source_recipe": "stock XDK D3DINLINE (static __forceinline)",
"target_evidence_sha256": "1303752bb188cf6b5a00adb8c2c49092f22408c2c38203e8daddf5b61aa61153",
"unit": "source/rasterizer/xbox/rasterizer_xbox_text"
},
{
"after_comdat_selection": 2,
"after_fingerprint_sha256": "78b5829871e22251b4e266bd81caa2eda338a6e93f43c2a592c93eaad8808e6b",
"before_comdat_selection": 1,
"before_fingerprint_sha256": "464ecff7dc1e2d1a1c18d02c467dcbe893baedfebfa42920922583cde75b9331",
"function": "_IDirect3DDevice8_SetVertexDataColor@12",
"recipe": "xdk-stock-d3dinline",
"source_recipe": "stock XDK D3DINLINE (static __forceinline)",
"target_evidence_sha256": "e4a0591ba36b4fdad4a24f1009fed7b141c8acf919c8a4bca421435760c2e38a",
"unit": "source/rasterizer/xbox/rasterizer_xbox_text"
},
{
"after_comdat_selection": 2,
"after_fingerprint_sha256": "c936826f1cdc47b8e52662c867164d8fa9063b86bd7050db06a3992a0414dba7",
"before_comdat_selection": 1,
"before_fingerprint_sha256": "db0e0cd1dc136a05abf97c28072edfda4b9cd8de3a0bf1e45fd45747655dcef5",
"function": "_IDirect3DDevice8_SetVertexShaderConstant@16",
"recipe": "xdk-stock-d3dinline",
"source_recipe": "stock XDK D3DINLINE (static __forceinline)",
"target_evidence_sha256": "cc42796796437d1eaee75aa6b6a300dc712d6d48cb2571820adf13f5eea7a3bd",
"unit": "source/rasterizer/xbox/rasterizer_xbox_text"
}
],
"schema_version": 1
}
-113827
View File
File diff suppressed because it is too large. Load diff
-4
View File
@@ -1,4 +0,0 @@
{
"version": 1,
"entries": []
}
-581
View File
@@ -1,581 +0,0 @@
[
{
"unit": "source/shell/shell_xbox",
"group": "aligned-rdata",
"reason": "the complete rebuilt object owns the exact two string COMDATs and compiler-generated SEH scope table; the grouped verifier proves each payload, alignment-derived padded extent, flags, owner snapshot, relocation address/type, and source-relative handler destination while objdiff merges the aligned sections and attributes the private scope-table labels differently",
"members": [
{
"symbol": "??_C@_06GAOAIMNN@xdemo?5?$AA@",
"measurements": {
"target": {
"section": ".rdata",
"size": 7,
"padded_size": 8,
"flags": 1076891712,
"relocation_count": 0,
"normalized_sha256": "2d636beaf6ef18262eb352ea8a00f3b1b0ee3628666eb07e679892eca5f08b5e",
"owner": { "value": 0, "type": 0, "storage": 2 }
},
"base": {
"section": ".rdata",
"size": 7,
"padded_size": 8,
"flags": 1076891712,
"relocation_count": 0,
"normalized_sha256": "2d636beaf6ef18262eb352ea8a00f3b1b0ee3628666eb07e679892eca5f08b5e",
"owner": { "value": 0, "type": 0, "storage": 2 }
}
}
},
{
"symbol": "??_C@_06PFFNFMJI@XDEMOS?$AA@",
"measurements": {
"target": {
"section": ".rdata",
"size": 7,
"padded_size": 8,
"flags": 1076891712,
"relocation_count": 0,
"normalized_sha256": "798ec2f79c908eed7153344688b0def452386891f2deae50826a2658182e9bd5",
"owner": { "value": 0, "type": 0, "storage": 2 }
},
"base": {
"section": ".rdata",
"size": 7,
"padded_size": 8,
"flags": 1076891712,
"relocation_count": 0,
"normalized_sha256": "798ec2f79c908eed7153344688b0def452386891f2deae50826a2658182e9bd5",
"owner": { "value": 0, "type": 0, "storage": 2 }
}
}
},
{
"symbol": "_rdata_002a0a88",
"base_symbol": "$T18302",
"source_function": "_main",
"measurements": {
"target": {
"section": ".rdata",
"size": 12,
"padded_size": 16,
"flags": 1077940288,
"relocation_count": 2,
"normalized_sha256": "3414421b183188342209566fad4e0c0723070b4030630dde685ba70281e92725",
"owner": { "value": 0, "type": 0, "storage": 2 }
},
"base": {
"section": ".rdata",
"size": 12,
"padded_size": 16,
"flags": 1077940288,
"relocation_count": 2,
"normalized_sha256": "3414421b183188342209566fad4e0c0723070b4030630dde685ba70281e92725",
"owner": { "value": 0, "type": 0, "storage": 3 }
}
}
}
]
},
{
"unit": "libs/libcmt/crt0msg",
"symbol": "_rterrs",
"reason": "The first-party XDK names the private runtime-error table rterrs. January and the rebuilt object have identical 144-byte data, all 18 relocation sites and types, and independently resolved string destinations; objdiff undercredits the aggregate despite exact COFF section comparison.",
"measurements": {
"size": 144,
"relocation_count": 18,
"normalized_sha256": "25298d4d1ea85dfa6a4696577658216429876d7b7b59ed245667db872d9d79d2"
}
},
{
"unit": "source/camera/editor_flying_camera",
"group": "camera-static-sections",
"allow_incomplete_unit": true,
"credit_raw_size": true,
"reason": "January and the rebuilt object have identical 124-byte private .bss and 52-byte .data payloads, flags, alignments, and owner storage. The nine .data relocations resolve to the same image destinations. HCEX names the separate file statics and their definition order; the January .bss offsets corroborate their exact layout. Objdiff undercredits the two sections after the authentic private-state split. Credit raw sizes because its report scores .bss as 124 bytes, not the 128-byte alignment-derived padded extent.",
"members": [
{
"symbol": "_is_scripted",
"measurements": {
"target": {
"section": ".bss", "size": 124, "padded_size": 128,
"flags": 3225419904, "relocation_count": 0,
"normalized_sha256": "7b8ec8dd836b564f0c85ad088fc744de820345204e154bc1503e04e9d6fdd9f1",
"owner": { "value": 0, "type": 0, "storage": 3 }
},
"base": {
"section": ".bss", "size": 124, "padded_size": 128,
"flags": 3225419904, "relocation_count": 0,
"normalized_sha256": "7b8ec8dd836b564f0c85ad088fc744de820345204e154bc1503e04e9d6fdd9f1",
"owner": { "value": 0, "type": 0, "storage": 3 }
}
}
},
{
"symbol": "_editor_camera_speed",
"measurements": {
"target": {
"section": ".data", "size": 52, "padded_size": 52,
"flags": 3224371264, "relocation_count": 9,
"normalized_sha256": "22486dfe6b955caf557fcbca418621432912c2d3012afd4e6031fe22b880d07b",
"owner": { "value": 0, "type": 0, "storage": 3 }
},
"base": {
"section": ".data", "size": 52, "padded_size": 52,
"flags": 3224371264, "relocation_count": 9,
"normalized_sha256": "22486dfe6b955caf557fcbca418621432912c2d3012afd4e6031fe22b880d07b",
"owner": { "value": 0, "type": 0, "storage": 3 }
}
}
}
]
},
{
"unit": "source/units/unit_dialogue",
"symbol": "_global_speech_priority_names",
"reason": "January and the rebuilt 44-byte speech-name table have identical payload, 11 relocation sites and types, and independently resolved literal destinations; objdiff undercredits the table because the executable split assigns nine folded SELECT_ANY strings to other objects",
"measurements": {
"size": 44,
"relocation_count": 11,
"normalized_sha256": "85759b3811ff7dc47b03792ac85317be51431a3f9e01dcafce317ed736a391b0"
}
},
{
"unit": "source/game/game_allegiance",
"symbol": "_global_game_team_names",
"reason": "The 40-byte January and rebuilt team-name tables have identical normalized payload, owner storage and flags, and all ten relocations resolve independently to the same image destinations; objdiff undercredits this sole unmatched section.",
"measurements": {
"size": 40,
"relocation_count": 10,
"normalized_sha256": "2c34ce1df23b838c5abf2a7f6437cca3d3067ed509ff25f11df6b11b582b51eb"
}
},
{
"unit": "source/game/game_engine_slayer",
"symbol": "_slayer_engine",
"reason": "The 136-byte January and rebuilt engine tables have identical normalized payload, external owner storage and section flags, and all 24 relocations resolve independently to the same image destinations; objdiff undercredits this sole unmatched section.",
"measurements": {
"size": 136,
"relocation_count": 24,
"normalized_sha256": "cd47b29508fea4e4c11451792c17f629a9ad240b3c3df5c889c0d416c378bc78"
}
},
{
"unit": "source/interface/hud_definitions",
"symbol": "_global_hud_anchor_names",
"reason": "The 20-byte January and rebuilt HUD anchor-name tables have identical normalized payload, owner storage and flags, and all five relocations resolve independently to the same image destinations; objdiff undercredits this sole unmatched section.",
"measurements": {
"size": 20,
"relocation_count": 5,
"normalized_sha256": "de47c9b27eb8d300dbb5f2c353e632c393262cf06340c4fa7f1b40c4cbd36f90"
}
},
{
"unit": "source/structures/leaf_map",
"symbol": "_leaf_map_initialize_section",
"allow_incomplete_unit": true,
"reason": "January and rebuilt profile-section data have identical 2512-byte payload, section flags and static owner, with all 28 relocations independently resolving to the same image destinations; objdiff undercredits this sole unmatched section.",
"measurements": {
"size": 2512,
"relocation_count": 28,
"normalized_sha256": "34570fc43b6cae2795503a6a62861e9a42ce856cb74d895ba7fa6174f1bda68b"
}
},
{
"unit": "source/game/game_engine_race",
"symbol": "_race_engine",
"reason": "January and rebuilt race-engine tables have identical 136-byte payload, section flags and external owner, with all 25 relocations independently resolving to the same image destinations; objdiff undercredits this sole unmatched section.",
"measurements": {
"size": 136,
"relocation_count": 25,
"normalized_sha256": "97a4c632a978acafd20933da4504d8ec3fdf3c5afd2f78934bced57dc8f746f4"
}
},
{
"unit": "source/game/game_engine_ctf",
"symbol": "_ctf_engine",
"reason": "January and rebuilt CTF-engine tables have identical 136-byte payload, section flags and external owner, with all 28 relocations independently resolving to the same image destinations; objdiff undercredits this sole unmatched section.",
"measurements": {
"size": 136,
"relocation_count": 28,
"normalized_sha256": "45c41730d038fd3f985f7ccfd8a7e04aa02e2db549ac941386e7cd04c1e1d8c1"
}
},
{
"unit": "source/bitmaps/libtiff/tif_print",
"symbol": "_tif_print_rcsid",
"reason": "The 164-byte January and rebuilt libtiff data section has identical payload, flags and external owner, and all 18 relocations independently resolve to the same image destinations; objdiff undercredits its sole unmatched section.",
"measurements": {
"size": 164,
"relocation_count": 18,
"normalized_sha256": "a81bb4fa00f599a245c1e6b636a2a630629557f2c86f92e2960cf3e16ed94e23"
}
},
{
"unit": "source/memory/zlib/zutil",
"symbol": "_z_errmsg",
"reason": "The 44-byte January and rebuilt zlib error table has identical payload, flags and external owner, and all ten relocations independently resolve to the same image destinations; objdiff undercredits its sole unmatched section.",
"measurements": {
"size": 44,
"relocation_count": 10,
"normalized_sha256": "966ef500de11387122d16ed112b63eb8562f81fc0e67d6f8f4d190766cf1e6ae"
}
},
{
"unit": "source/memory/zlib/deflate",
"symbol": "?my_version@?1??deflateInit2_@@9@9",
"reason": "The four-byte January and rebuilt zlib version pointer has identical payload, flags and static owner; its relocation independently resolves to the same image destination despite objdiff's alias attribution.",
"measurements": {
"size": 4,
"relocation_count": 1,
"normalized_sha256": "df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"
}
},
{
"unit": "source/networking/network_server_manager",
"symbol": "_network_game_server_memory_do_not_use_directly_in_use",
"reason": "January and rebuilt eight-byte BSS owner have identical extent, flags, external storage and section-relative layout with no relocations; objdiff undercredits this sole unmatched section.",
"measurements": {
"size": 8,
"relocation_count": 0,
"normalized_sha256": "af5570f5a1810b7af78caf4bc70a660f0df51e42baf91d4de5b2328de0e83dfc"
}
},
{
"unit": "source/game/game_engine_oddball",
"symbol": "_oddball_engine",
"reason": "January and rebuilt Oddball engine tables have identical 136-byte payload, flags and owner storage, with all 27 relocations independently resolving to the same image destinations; objdiff undercredits the sole unmatched data section.",
"measurements": {
"size": 136,
"relocation_count": 27,
"normalized_sha256": "ffe1ef0aa827017276299f3a497655019d91f94374129c31634061500e5ae4b9"
}
},
{
"unit": "source/game/game_globals",
"symbol": "_global_material_type_strings",
"reason": "January and rebuilt material-name tables have identical 132-byte payload, flags and owner storage, with all 33 relocations independently resolving to the same image destinations; objdiff undercredits the sole unmatched data section.",
"measurements": {
"size": 132,
"relocation_count": 33,
"normalized_sha256": "115bad14f1c9f2c027a84de21b107015722cb76be8d0abf3760ad8e00d6c24a5"
}
},
{
"unit": "source/items/projectile_definitions",
"symbol": "_default_projectile_material_response",
"reason": "January and rebuilt projectile material tables have identical 160-byte payload, flags and owner storage, with all three relocations independently resolving to the same image destinations; objdiff undercredits the sole unmatched data section.",
"measurements": {
"size": 160,
"relocation_count": 3,
"normalized_sha256": "b71e2246c2d39831ad65f2b9570fecb59a1a0d743278b63cc125b118c148f6b9"
}
},
{
"unit": "source/memory/byte_swapping",
"symbol": "_byte_bs_codes",
"reason": "January and rebuilt byte-swap tables have identical 144-byte payload, flags and owner storage, with all eight relocations independently resolving to the same image destinations; objdiff undercredits the sole unmatched data section.",
"measurements": {
"size": 144,
"relocation_count": 8,
"normalized_sha256": "fc892f5c359e0165aa8cdbd745b191928baac3213935c79d8699cdcba32468d0"
}
},
{
"unit": "source/physics/collision_bsp_definitions",
"symbol": "_global_collision_bsp_definition_data",
"reason": "January and rebuilt collision BSP definition tables have identical 900-byte payload, flags and owner storage, with all 68 relocations independently resolving to the same image destinations; objdiff undercredits the sole unmatched data section.",
"measurements": {
"size": 900,
"relocation_count": 68,
"normalized_sha256": "3ed97b7cf09b09837ca36461d74c21909d34106d563a7b914aaffd7c51cf3e56"
}
},
{
"unit": "source/physics/collision_usage",
"symbol": "_global_collision_function_names",
"reason": "January and rebuilt collision function-name tables have identical 138-byte payload, flags and owner storage, with all 30 relocations independently resolving to the same image destinations; objdiff undercredits the sole unmatched data section.",
"measurements": {
"size": 138,
"relocation_count": 30,
"normalized_sha256": "804797955ced17a1f6b1e4b86c0a83a5d25fcb03fa846e690dae51ef13bc5c2f"
}
},
{
"unit": "source/render/render_particles",
"symbol": "_render_particles_section",
"reason": "The January and rebuilt render-particles profile-section owners have equal 1528-byte payload, section flags and external storage, and their sole relocation independently resolves to the same image string; objdiff undercredits the complete section. All three target functions are strict exact, and base-only SELECT_ANY literals have byte-identical January-selected providers with passing ordinary pair links.",
"measurements": {
"size": 1528,
"relocation_count": 1,
"normalized_sha256": "806638334fff1341360b4dec7db0987871f376bc3999e26ff4bcd3a1755d2955"
}
},
{
"unit": "source/bitmaps/bitmaps",
"symbol": "_global_vector_palette",
"reason": "January and rebuilt 1116-byte palette data have equal payload, section flags, owner storage and offsets, and all 21 relocations resolve to the same image destinations. All 34 target functions are strict exact; the sole extra fast_ftol COMDAT is byte-identical to its selected January provider and pair-links in either order.",
"measurements": {
"size": 1116,
"relocation_count": 21,
"normalized_sha256": "443063631e6958ff5026be3038f8785d61eee4073cb01b8eaa4fdfcc3b620002"
}
},
{
"unit": "source/game/cheats",
"symbol": "_cheat_strings",
"reason": "January and rebuilt cheats BSS have equal 3201-byte extent, section flags, external owner, zero relocations and matching section-relative symbol layout. The two private function names come from the first-party HCEA SHIP PDB; the string array has a descriptive inferred name, and the trailing unreferenced byte is disclosed as name-unknown rather than credited as a separate owner. All 14 target functions are strict exact and candidate-only providers pass bounded links in both orders.",
"measurements": {
"size": 3201,
"relocation_count": 0,
"normalized_sha256": "76d6c353527d212ec3799ea375f945e3ae3760257bd2ef5a75441090df0ddfac"
}
},
{
"unit": "source/items/items",
"symbol": "_item_update_section",
"reason": "January and rebuilt item-update profile owners have equal 1528-byte payload, section flags, external owner and one resolved image relocation. All 18 target functions and eight target-owned noncode symbols are strict exact. The only former link conflict was the hand-written add_vectors3d provider in ActorCombat; its genuine shared-header inline now emits the same code as SELECT_ANY without changing any ActorCombat function or park. All 42 candidate-only helper/literal copies equal their January-selected providers and 26 ordinary provider-pair links pass in both orders.",
"measurements": {
"size": 1528,
"relocation_count": 1,
"normalized_sha256": "806638334fff1341360b4dec7db0987871f376bc3999e26ff4bcd3a1755d2955"
}
},
{
"unit": "source/interface/hud",
"symbol": "_default_string",
"reason": "January and rebuilt HUD own the same four-byte external data pointer, section flags and relocation destination: the two-zero-byte string selected from GameEngine. All 22 target functions are strict exact. Five candidate-only genuine shared-header inline helpers match their January-selected copies byte-for-byte and ordinary provider-pair links pass in both orders, so these folded copies receive no separate credit.",
"measurements": {
"size": 4,
"relocation_count": 1,
"normalized_sha256": "df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119"
}
},
{
"unit": "source/effects/particles",
"symbol": "_particles_update_section",
"reason": "January and rebuilt particles own the same 1536-byte static data section with three image-equivalent relocations, flags and payload. Its eight-byte collision-scale bounds also have identical payload, flags and the external linkage January requires. All 20 target functions are strict exact; genuine shared-header helper providers now link in both orders after removing duplicate hand-written bodies from DebugMemory, Contrails and BoredCamera. Candidate-only folded helper copies receive no separate credit.",
"measurements": {
"size": 1536,
"relocation_count": 3,
"normalized_sha256": "6d9398e131ef72b9a6ffc2321c22d27a3e2db1a458385b77b08ba0657a56c74f"
}
},
{
"unit": "source/ai/ai_scenario_definitions",
"symbol": "_global_ai_default_state_names",
"reason": "January and rebuilt 48-byte AI default-state name table (sole .data section) have identical normalized payload and flags (c0300040), a PDB-public external owner at offset 0, and all 12 relocations resolve to the same image addresses. The whole object otherwise matches: 4/4 functions strict exact, every January-owned section and symbol equal, and all candidate-only literals pair-link with their selected providers in both orders. objdiff 3.3.1 misreads the identical '$' literal names.",
"measurements": {
"size": 48,
"relocation_count": 12,
"normalized_sha256": "17b0761f87b081d5cf10757ccc89f12be355c70e2e29df288b65b30710dcbcd1"
}
},
{
"unit": "source/ai/ai_profile",
"symbol": "_global_ai_meter_definitions",
"reason": "January and rebuilt 236-byte AI meter/profile-name .data section (sole data section; _ai_profile_render_spray_names static at +224 on both sides) have identical normalized payload and flags (c0400040), and all 10 relocations resolve to the same image addresses. The whole object otherwise matches: 23/23 functions strict exact, every January-owned section and symbol equal and consistent with cachebeta.pdb publics, and candidate-only literals pair-link with their selected providers in both orders.",
"measurements": {
"size": 236,
"relocation_count": 10,
"normalized_sha256": "3a69a8b8a83037c3277fa86c09c7aa4e9b9da8b480e3facb87275548ee4c88b8"
}
},
{
"unit": "source/shaders/shader_definitions",
"symbol": "_global_shader_effect_additive",
"reason": "January and rebuilt 364-byte shader-effect .data section (the additive and alpha-blended effect definitions, the latter at +184 on both sides) have identical normalized payload and flags (c0400040), and both relocations resolve to the same image addresses. The whole object otherwise matches: 1/1 function strict exact, every January-owned section and symbol equal and consistent with cachebeta.pdb publics, and candidate-only literals pair-link with their selected providers in both orders.",
"measurements": {
"size": 364,
"relocation_count": 2,
"normalized_sha256": "8bbb21a994d2ec5051de3ea76497f648469fe42d5f6aa7da85f3b598bf16bcd0"
}
},
{
"unit": "source/units/dialogue_definitions",
"symbol": "_dialogue_vocalization_type_name",
"reason": "January and rebuilt 1,672-byte dialogue .data section have identical normalized payload and flags (c0400040), and all 418 relocations resolve to the same image addresses. The whole object otherwise matches: 2/2 functions strict exact, all 298 January symbols equal in section, offset and storage and consistent with cachebeta.pdb publics, and all candidate-only literals pair-link with their selected providers in both orders.",
"measurements": {
"size": 1672,
"relocation_count": 418,
"normalized_sha256": "256984cb62bc879971463ff0ff7567042dc95d8b0fb2bc676f246c2293c77ae8"
}
},
{
"unit": "source/ai/actor_types",
"symbol": "_global_actor_type_names",
"reason": "January and rebuilt 128-byte actor-type .data section (the static actor_type_definitions table at +0 and the public global_actor_type_names at +64) have identical normalized payload and flags (c0400040), and all 32 relocations resolve to the same image addresses. Ownership was corrected first: cachebeta.pdb publics prove actor_type_definitions and its accessor actor_type_definition_get file-static. January's own assert strings name actor_type_definitions; the Halo CE atlas names the accessor at the same 0x140-byte span. All 13 functions are strict exact and every January-owned section and symbol matches.",
"measurements": {
"size": 128,
"relocation_count": 32,
"normalized_sha256": "38723a2e5e8a17aa7950dc008209944e898f69a7bd10a23c839d341e935fd5ca"
}
},
{
"unit": "source/cutscene/recorded_animation_definitions",
"symbol": "_recorded_animation_event_stream_data",
"reason": "January and rebuilt 180-byte recorded-animation tag .data section have identical normalized payload and flags (c0400040), and all 11 relocations resolve to the same image addresses. Ownership was corrected first: byte_swap_recording is absent from cachebeta.pdb publics and referenced by no other January object, so it is file-static. Both functions are strict exact and every January-owned section and symbol matches.",
"measurements": {
"size": 180,
"relocation_count": 11,
"normalized_sha256": "c83e71991a0ac6309173102325bf6a194d27d5d80cdc9a395ed954c80bae3d5d"
}
},
{
"unit": "source/models/model_animation_definitions",
"symbol": "_weapon_type_animation_list",
"reason": "January and rebuilt 1,288-byte animation-list .data section have identical normalized payload and flags (c0400040), and all 170 relocations resolve to the same image addresses. Ownership was corrected first: the seven *_animation_list_entries arrays are absent from cachebeta.pdb publics and referenced by no other January object, so they are file-static beside their public animation_list wrappers. All three functions are strict exact and all 139 January symbols match.",
"measurements": {
"size": 1288,
"relocation_count": 170,
"normalized_sha256": "76f074030d3cb6940a74673504f664555bd5d02087282c01338c8b83fe3d1654"
}
},
{
"unit": "source/sound/sound_classes",
"symbol": "_sound_classes",
"reason": "January and rebuilt 2,452-byte sound-class .data section have identical normalized payload and flags (c0400040), and all 51 relocations resolve to the same image addresses. Ownership was corrected first: the static accessor at 0x1b84c0 is absent from cachebeta.pdb publics and is named sound_class_datum_get by the Halo CE atlas, anchored on six already-exact sound_classes symbols. All 12 functions are strict exact and every January-owned section and symbol matches.",
"measurements": {
"size": 2452,
"relocation_count": 51,
"normalized_sha256": "1ae64de7abb95c40952d3aacf88c68232070d4c19906bf32dfc8877bd6725abc"
}
},
{
"unit": "source/objects/object_types",
"symbol": "_object_data_definition",
"reason": "January and rebuilt 2,608-byte object-type tag .data section have identical normalized payload and flags (c0400040), and all 190 relocations resolve to the same image addresses. Ownership was corrected first: HCEX.pdb names the sole object_types file static processed_bsp_flags (unsigned short), absent from cachebeta.pdb publics. All 31 functions are strict exact and all 66 January symbols match. All eight candidate-only helper COMDATs are byte-identical to January's selected copies and pair-link in both orders; items' hand-written valid_real_matrix4x3/valid_real_vector3d_axes3 NODUP providers were replaced by the genuine real_math.h inlines.",
"measurements": {
"size": 2608,
"relocation_count": 190,
"normalized_sha256": "205043014fcdf8e45147d168ae70e43cda7814dfc63f1c1dade93f2c0f9f4341"
}
},
{
"unit": "source/hs/hs_scenario_definitions",
"symbol": "_hs_script_types_enum",
"reason": "January and rebuilt HS scenario .data have identical 628-byte normalized payload, all 33 relocation identities, and 8-byte section alignment (c0400040). The original nine public definitions retain their target offsets and storage. Separate typed field and byte-swap arrays replace invented packed aggregates and padding; the byte-swap function is genuinely file-static. The one target function remains strict exact, and there are no surplus external definitions.",
"measurements": {
"size": 628,
"relocation_count": 33,
"normalized_sha256": "a1add12fc2ddd7afb3935997f9e1532b27a3887f8e85e914db954d1665547c09"
}
},
{
"unit": "source/hs/hs_globals_external",
"symbol": "_screenshot_size_definition",
"reason": "January and rebuilt HS external-global .data have identical 7,076-byte normalized payload, section flags (c0400040), and all 1,322 relocation identities. The invented 442-element definition aggregate has been replaced by 442 separate file-static typed definitions in January's proven 12-byte record order; the public hs_external_globals table remains at +5304. The 2011 first-party HCEX PDB independently attests 427 unique definition names and their type; the 15 older-only names are descriptive names derived from their January string records, not claimed originals. January and rebuilt first-record storage are file-static, and the unit emits no code or candidate-only external owner.",
"measurements": {
"size": 7076,
"relocation_count": 1322,
"normalized_sha256": "1e4346aef4491a55772f4f51a6989e06074dc5245a2c01aa1be97fb2e221ae8c"
}
},
{
"unit": "source/interface/ui_widget_event_handler_functions",
"symbol": "_event_handler_functions",
"reason": "January and rebuilt 934-byte event-handler .data section (the static event_handler_function_list name/function table at +0 and the public event_handler_functions at +816) have identical normalized payload and flags (c0400040), and all 228 relocations resolve to the same image addresses. The 366-byte .bss is also identical. Ownership was corrected first: 98 handler functions, event_handler_function_list and new_campaign_profile_name are absent from cachebeta.pdb publics and referenced by no other January object, so they are file-static. All 100 functions are strict exact and all 452 January symbols match. The three stock D3D SDK .rdata tables it also emits are byte-identical to January's selected copies and pair-link in both orders, as in the 54 already-admitted objects that emit them.",
"measurements": {
"size": 934,
"relocation_count": 228,
"normalized_sha256": "c1040d667c392fd772fab34335ade456dac49ae864a8200c4560df63cec186fd"
}
},
{
"unit": "source/objects/objects",
"symbol": "?section@?1??objects_update@@9@9",
"reason": "objects_update's function-static section table: January and the rebuilt object have identical 1,530-byte .data, relocation site/type and independently resolved string destination; objdiff 3.3.1's '$'-literal relocation-name defect undercredits it despite exact COFF section comparison. Whole object admitted after the Layer 1 provider repairs (action_charge, player_control, bored_camera, path_obstacles, decals) replaced hand-written NODUP helper copies with the genuine real_math.h inlines: all 121 functions strict exact, 225 symbols match storage/offsets and cachebeta.pdb publics, 26 candidate-only helper COMDATs byte-identical to January's selected copies and pair-linking in both orders.",
"measurements": {
"size": 1530,
"relocation_count": 1,
"normalized_sha256": "e94bb79478c95dce11c4a1f720a07ccbaa3a8d18b84acbe5605472d6f8635b74"
}
},
{
"unit": "source/units/units",
"symbol": "_magic_base_animation_seat_index",
"reason": "units' writable .data section (magic_base_animation_seat_index, unit_update_section, unit_update's last_time and base_seat_labels): January and the rebuilt object have identical 1,564-byte payload, 7 relocation sites/types and independently resolved string destinations (5 of the 7 base_seat_labels strings are selected in other January objects and pair-link cleanly); objdiff 3.3.1's '$'-literal relocation-name defect undercredits it despite exact COFF section comparison. Whole object admitted: all 189 functions strict exact; the 25 code_ placeholders carry their first-party names (HCEX_Release.pdb units compiland, Sept-2001 map, January atlas) and all 50 file-statics carry static storage per cachebeta.pdb publics, so 321 symbols match storage/offsets; 97 candidate-only surplus definitions (31 helper COMDATs byte-identical to January's selected copies, SDK tables, literals) pair-link with their selected providers in both orders; _point_from_line3d is referenced out of line by January's units.obj (owner ruling 20260921 #1).",
"measurements": {
"size": 1564,
"relocation_count": 7,
"normalized_sha256": "6ed61c39a648905cf23b12b916e2adadfe97b6e5aa492c29a14c7265809ef1a7"
}
},
{
"unit": "source/items/projectiles",
"symbol": "_projectile_update_section",
"allow_incomplete_unit": true,
"reason": "January and rebuilt projectile-update profile-section data have identical 1548-byte payload, section flags and static owners (_projectile_update_section at 0, _effect_marker_names at 1528), with all six relocations independently resolving to the same image destinations; January's split leaves the pooled normal and gravity literals undefined while the rebuilt object emits identical select-any copies, and objdiff 3.3.1's '$'-literal name defect undercredits this sole unmatched section. The unit's .rdata is already 100%; the parked _projectile_new residual keeps the unit incomplete and receives no credit from this entry.",
"measurements": {
"size": 1548,
"relocation_count": 6,
"normalized_sha256": "7bfaf5fb0b0c9e0593153885a81788bc8b118a9651a60a8a94e5ffd9fd203cf2"
}
},
{
"unit": "source/math/periodic_functions",
"symbol": "_global_periodic_functions_enum_strings",
"allow_incomplete_unit": true,
"reason": "The January and rebuilt 96-byte periodic/transition enum .data have identical normalized payload, owner storage and flags, and all 20 relocations resolve to the same image destinations. Objdiff 3.3.1 undercredits folded string literals. The variable-period builder remains parked and earns no code credit.",
"measurements": {
"size": 96,
"relocation_count": 20,
"normalized_sha256": "8fdd4bf9e8be002fe8938ee0849dfec244c27138f7b77eff28ec8ba7b4538614"
}
},
{
"unit": "source/objects/object_lights",
"symbol": "_lights_section",
"allow_incomplete_unit": true,
"reason": "object_lights' writable .data (the static lights_section profile section at +0 and the four public object_light_* tuning globals at +1528..+1540): January and the rebuilt object have identical 1,541-byte payload, owner storage and flags (c0400040), and its single relocation (the 'render_lights' string, selected in hs) resolves independently to the same image destination; objdiff 3.3.1's '$'-literal relocation-name defect undercredits it despite exact COFF section comparison. All 43 functions are strict exact; should_render_lights, light_attenuation and cluster_get_first/next_light are file-static per cachebeta.pdb publics (no January reference outside the object; should_render_lights is the /Od-attested call in lights_render_diffuse and lights_render_specular), so all 71 January symbols match storage and offsets; the 12 candidate-only helper COMDATs pair-link with their selected providers in both orders. The unit is not marked Matching only because its _object_get_bounding_sphere COMDAT differs from action_vehicle's selected copy in its two __FILE__ literal relocations (January object_lights' own 'c:\\halo\\source\\objects\\objects.h' literal; held owner item), so this sole unmatched section is credited with an explicit incomplete-unit opt-in.",
"measurements": {
"size": 1541,
"relocation_count": 1,
"normalized_sha256": "527b43a173f528444fa4423af92d7d58a83f2afe7fc1661b6d297401b2760f71"
}
},
{
"unit": "source/bitmaps/bitmap_group",
"symbol": "_global_bitmap_reference",
"allow_incomplete_unit": true,
"reason": "January and rebuilt bitmap_group writable .data (the public global_bitmap_reference/_optional, bitmap_pixel_data, color_plate_data and bitmap_group tag definitions plus their static field/enum tables, 31 owners) have identical 1,424-byte payload, flags (c0400040) and owner storage, and all 152 relocations independently resolve to the same image destinations. January's split leaves the empty-string and 'default' literals undefined (linker-selected in other objects) while the rebuilt object emits identical select-any copies, so objdiff 3.3.1's '$'-literal name defect undercredits this sole unmatched section; renaming only the '$' inside literal names on both sides scores it 100.0 with the production objdiff binary. The parked _bitmap_group_add_bitmap residual keeps the unit incomplete, so this entry opts in explicitly as an incomplete unit and grants no function or object credit.",
"measurements": {
"size": 1424,
"relocation_count": 152,
"normalized_sha256": "df5c6d69f4e13ea6df9398e2b5cc5c4455410b077b5ba6763930c4dd748505cb"
}
},
{
"unit": "source/ai/ai_communication",
"symbol": "_global_communication_priority_names",
"allow_incomplete_unit": true,
"reason": "January and rebuilt ai_communication .data (the public global_communication_priority_names, global_communication_team_names and global_communication_type_names string tables at +0/+32/+48) have identical 276-byte payload, flags (c0400040) and owner storage, and all 69 relocations independently resolve to the same image destinations. January's split leaves six literals ('none', 'shout', 'damage', 'cover', 'flee', 'berserk') undefined because the linker selected other objects' copies, while the rebuilt object emits identical select-any copies, so objdiff 3.3.1's '$'-literal name defect undercredits this sole unmatched section; renaming only the '$' inside literal names on both sides scores it 100.0 with the production objdiff binary. _ai_communication_actor_talk_weight (parked) and _ai_communication_event remain residual, so this entry opts in explicitly as an incomplete unit and grants no function or object credit.",
"measurements": {
"size": 276,
"relocation_count": 69,
"normalized_sha256": "d489001ff88ed206b56f44bd0789a8332510f33bf40a312a7b2e5d60c3fe3e03"
}
},
{
"unit": "source/ai/ai_debug",
"symbol": "_global_ai_debug_firing_position_color_count",
"allow_incomplete_unit": true,
"reason": "January and rebuilt ai_debug .data (the public global_ai_debug_firing_position_color_count at +0, the file-static postcombat type-name table at +4 and the two file-static selected-squad globals at +48/+52) have identical 56-byte payload, flags (c0300040) and owner storage, and all 11 relocations independently resolve to the same image destinations. January's split leaves the 'none' literal undefined because the linker selected another object's copy, while the rebuilt object emits an identical select-any copy, so objdiff 3.3.1's '$'-literal name defect undercredits this sole unmatched section; renaming only the '$' inside literal names on both sides scores it 100.0 with the production objdiff binary. _ai_debug_render_actor remains residual, so this entry opts in explicitly as an incomplete unit and grants no function or object credit.",
"measurements": {
"size": 56,
"relocation_count": 11,
"normalized_sha256": "3ba0a54a0875e71a01972b13dcf574a4d16d0f1ea3531ef51d98311b5ac7eee5"
}
},
{
"unit": "source/game/game_engine_king",
"symbol": "_king_engine",
"allow_incomplete_unit": true,
"reason": "January and rebuilt King engine tables have identical 136-byte payload, section flags (c0400040) and public owner storage (_king_engine is a cachebeta.pdb public), and all 23 relocations (22 engine callbacks and the 'king' name literal) independently resolve to the same image destinations. January's split leaves the 'king' literal undefined because the linker selected game_engine's copy, while the rebuilt object emits an identical select-any copy, so objdiff 3.3.1's '$'-literal name defect undercredits this sole unmatched section; renaming only the '$' inside literal names on both sides scores it 100.0 with the production objdiff binary. All 29 functions are strict exact; the unit stays NonMatching until the owner rules on the whole-object packet (owner queue #13: _find_next_hill's preserved uninitialised no-candidate return, plus the cachebeta storage fix), so this entry opts in explicitly as an incomplete unit, credits only the table's data bytes and certifies nothing about _find_next_hill or object completion.",
"measurements": {
"size": 136,
"relocation_count": 23,
"normalized_sha256": "4009f5b6117e9f00cb001a23b9a56b9dc51a694206c442ce715e2bed2d5465a7"
}
}
]
-587
View File
@@ -1,587 +0,0 @@
[
{
"unit": "source/devices/devices",
"function": "_device_export_function_values",
"reason": "The scoped Opus reconciliation proves all 512 padded bytes, 21 ordered relocations and normalized SHA-256 7bf0963941ed36b13676536592a3af9d2786f5d4970b56f1129d24c3a35cc968 exact. Objdiff's 94.405594 percent is a compiler-local switch-label representation false negative; credit only the target report's 505 meaningful bytes. The ordinary typed mode loop preserves January's unordered delay predicate and uses the proper devices.h public declaration. No comparator relaxation, byte patch, extra helper, parameter-width trick or whole-object completion claim. See docs/object_matching_logs/devices_obj_export_function_values_opus_reconciliation_20260906.md."
},
{
"unit": "source/rasterizer/xbox/rasterizer_xbox",
"function": "_rasterizer_set_stencil_mode",
"reason": "The scoped Opus reconciliation proves all 784 padded/meaningful bytes, 78 ordered relocations and normalized SHA-256 903201d61d15f0c4dcad96fd81acb8049e8bfc201883061924dc95f3eaea8852 exact. Objdiff's 96.65072 percent is a compiler-local switch-label representation false negative. Source applies the debug policy in the public long input domain before narrowing once to the short cached mode, as January's entry dataflow corroborates; no declaration permutation or type/ABI change. Existing data and helper ownership remain separate from this function credit. See docs/object_matching_logs/rasterizer_xbox_obj_three_natural_opus_closures_20260906.md."
},
{
"unit": "source/ai/action_obey",
"function": "_action_obey_command_end",
"reason": "Independent frozen January/actual COFF comparison proves all 560 padded bytes, 24 ordered relocations and normalized SHA-256 f59f0185a672ce6409fe3d3c3a439e517483593f6ffc9bf5eff4b18dd4420a61 exact. Objdiff reports 85.26786 percent because compiler-local switch labels are represented differently. The ledger credits only the target's 549 meaningful bytes, with no byte/comparator exception. The genuine private body is retained through individual_end, not a fake external ABI. Fable's earlier case-order correction remains documented; this is independently authenticated reconstruction, not a claim of first-shot source. Whole-object math-owner and remaining-source boundaries are separate. See docs/object_matching_logs/action_obey_obj_partial_graph_reconciliation_20260905.md."
},
{
"unit": "source/ai/action_obey",
"function": "_action_obey_directmovement_update_facing",
"reason": "Independent frozen January/actual COFF comparison proves all 336 meaningful/padded bytes, 17 ordered relocations and normalized SHA-256 17b28fabb3ad4366074689d2f9be1364ccf07cdc0c7a185dbd4dff25b00e3f97 exact. Objdiff reports 96.77419 percent due to compiler-local switch labels. This is strict target-function credit only; no comparator weakening or surplus helper credit. Its real individual_update caller naturally preserves the target cross_product3d/negate_vector3d owners after removing their manual wrapper aliases. Five additional header math owners are explicitly audited and remain a whole-object link/admission boundary. See docs/object_matching_logs/action_obey_obj_partial_graph_reconciliation_20260905.md."
},
{
"unit": "source/input/input_xbox",
"function": "_input_key_is_down",
"reason": "Independent root and reviewer COFF gates prove all 192 meaningful/padded bytes and 19 ordered relocations exact, normalized SHA-256 c1ab3b0043e411c7e6695fb129837501c3bca12a2477d9497b5123f3eaf17805. Objdiff reports 93.93939 percent because compiler-local switch labels are represented differently. A natural result variable and single terminal return satisfy the house rules without a bogus unreachable fallback and reproduce January; no target-byte exception or declaration-order tuning. See docs/object_matching_logs/input_xbox_obj_public_leaves_independent_review_20260904.md."
},
{
"unit": "source/interface/player_ui",
"function": "_set_local_player_controls_from_player_profile",
"reason": "Independent strict COFF comparison proves all 528 meaningful/padded bytes and 14 ordered relocations exact, normalized SHA-256 ff47c65c6eda0c31b625817bb914f77ad790244d2be44109e484d10664685ee5. Raw objdiff reports 95.89041 percent due to compiler-local switch labels. This repairs existing function credit only; it does not count fresh reconstruction or waive the object's public playlist-layout/coalescing boundary. See docs/object_matching_logs/player_ui_obj_owner_house_rule_reconciliation_20260904.md."
},
{
"unit": "source/interface/hud_unit",
"function": "_hud_render_damage_indicators",
"reason": "Independent root/worker COFF gates prove all 688 padded bytes and 39 ordered relocations exact, normalized SHA-256 cf7cb8af78f4f4f9fb2a11c7bb67c2e5fb0e31cedcbe8b062ef7aa7fad4cacd0. Objdiff reports 95.85253 percent because compiler-local switch labels are represented differently; no byte or target exception. The independently PDB/type/ABI-reviewed source and complete emitted-owner audit are documented in docs/object_matching_logs/hud_unit_obj_damage_indicators_exact_20260904.md."
},
{
"unit": "source/networking/network_client_message_handler",
"function": "_network_game_client_handle_message",
"reason": "Audited January dispatcher is strict exact: 1088 padded/logical bytes, 84 ordered relocations, normalized SHA-256 32ca599a7f9f3eb44546bcd119ce77eb705e9fc745f9d852c16295cbe1f3850f. Objdiff differs only on compiler-local switch labels; no byte or target exception. See docs/object_matching_logs/network_client_message_handler_obj_admission_review_20260904.md."
},
{
"unit": "source/cache/cache_files",
"function": "_cache_files_map_directory",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/units/units",
"function": "_unit_is_busy",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/text/international_strings",
"function": "_double_byte_character",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/shaders/shaders",
"function": "_numeric_countdown_timer_get",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/shaders/shaders",
"function": "_shader_get_vertex_shader_permutation",
"reason": "hardened COFF comparison proves all 288 padded bytes and all 18 semantic relocations exact; objdiff reports a local jump-table label encoding false negative"
},
{
"unit": "source/shaders/shaders",
"function": "_shader_is_decal",
"reason": "hardened COFF comparison proves all 144 padded bytes and all 10 semantic relocations exact; objdiff reports a local jump-table label encoding false negative"
},
{
"unit": "source/rasterizer/xbox/rasterizer_xbox_errors",
"function": "_rasterizer_error",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/networking/network_messages",
"function": "_create_network_game_message",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/networking/network_game_globals",
"function": "_network_game_client_start_frame",
"reason": "hardened COFF comparison proves all 560 padded bytes and all 57 semantic relocations exact; objdiff reports a local jump-table label encoding false negative"
},
{
"unit": "source/memory/data_encoding",
"function": "_data_decode_array",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/memory/data_encoding",
"function": "_data_decode_memory",
"reason": "hardened COFF comparison proves all 304 padded bytes and all 19 semantic relocations exact; objdiff reports a local jump-table label encoding false negative"
},
{
"unit": "source/memory/data_encoding",
"function": "_data_encode_memory",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/memory/data_encoding",
"function": "_data_encode_array",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/memory/data_packets",
"function": "__data_packet_verify",
"reason": "hardened COFF comparison proves all 464 padded bytes and all 27 semantic relocations exact; objdiff reports a local jump-table label encoding false negative"
},
{
"unit": "source/memory/data_packets",
"function": "__data_packet_encode",
"reason": "hardened COFF comparison proves all 768 padded bytes and all 44 semantic relocations exact; objdiff reports a local jump-table label encoding false negative"
},
{
"unit": "source/memory/data_packets",
"function": "__data_packet_decode",
"reason": "hardened COFF comparison proves all 672 padded bytes and all 28 semantic relocations exact; objdiff reports a local jump-table label encoding false negative"
},
{
"unit": "source/memory/byte_swapping",
"function": "__byte_swap_data",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/memory/zlib/inflate",
"function": "_inflate",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/main/console",
"function": "_console_update",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/interface/ui_widget_text_search_and_replace_functions",
"function": "_widget_controller",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/interface/attract_mode",
"function": "_attract_mode_get_localized_movie_path",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/game/game_time",
"function": "_game_time_start",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/game/game_time",
"function": "_game_time_update",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/devices/device_controls",
"function": "_code_00083e30",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/cseries/cseries_windows",
"function": "_code_0007cb70",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bungie_net/network/transport_address",
"function": "_transport_error_to_string",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_print",
"function": "_TIFFPrintDirectory",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_packbits",
"function": "_PackBitsEncode",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_next",
"function": "_NeXTDecode",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_dir",
"function": "_code_00053960",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_dir",
"function": "_code_00054160",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_dirwrite",
"function": "_TIFFWriteNormalTag",
"reason": "hardened COFF comparison proves all 800 padded bytes and all 30 semantic relocations exact; objdiff reports an internal jump-table relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_dirwrite",
"function": "_TIFFWriteDirectory",
"reason": "hardened COFF comparison proves all 1760 padded bytes and all 56 semantic relocations exact; objdiff reports internal jump-table relocation encoding false negatives"
},
{
"unit": "source/bitmaps/libtiff/tif_aux",
"function": "_TIFFVGetFieldDefaulted",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_lzw",
"function": "_horizontalAccumulate8",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_lzw",
"function": "_horizontalAccumulate16",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_lzw",
"function": "_horizontalDifference8",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_lzw",
"function": "_horizontalDifference16",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_getimage",
"function": "_setorientation",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_getimage",
"function": "_code_00058a20",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_getimage",
"function": "_code_00058c40",
"reason": "hardened COFF comparison proves all 832 padded bytes and all 18 semantic relocations exact; objdiff reports static BSS symbol/relocation ownership false negatives"
},
{
"unit": "source/bitmaps/libtiff/tif_getimage",
"function": "_code_00059180",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_getimage",
"function": "_put1bitbwtile",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_getimage",
"function": "_code_00059df0",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_getimage",
"function": "_code_0005a6f0",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_getimage",
"function": "_TIFFReadRGBAImage",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_dirread",
"function": "_TIFFFetchData",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_dirread",
"function": "_TIFFFetchByteArray",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_dirread",
"function": "_TIFFFetchNormalTag",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_dirread",
"function": "_TIFFReadDirectory",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/ai/actor_type_sentinel",
"function": "_sentinel_decide_action",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/ai/actor_type_mounted_weapon",
"function": "_mounted_weapon_decide_action",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/ai/actor_type_marine",
"function": "_marine_decide_action",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/ai/actor_type_jackal",
"function": "_jackal_decide_action",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/ai/actor_type_infection",
"function": "_infection_decide_action",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/ai/actor_type_hunter",
"function": "_hunter_decide_action",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/ai/actor_type_grunt",
"function": "_grunt_decide_action",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/ai/actor_type_flood",
"function": "_flood_decide_action",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/ai/actor_type_engineer",
"function": "_engineer_decide_action",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/ai/actor_type_elite",
"function": "_elite_decide_action",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/ai/actor_type_crew",
"function": "_crew_decide_action",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/ai/actor_type_carrier",
"function": "_carrier_decide_action",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/ai/action_guard",
"function": "_action_guard_speak_post_combat",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/ai/action_guard",
"function": "_action_guard_setup_postcombat",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/ai/action_guard",
"function": "_action_guard_control",
"reason": "hardened COFF comparison proves exact bytes and semantic relocations; objdiff reports a local-label/relocation encoding false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_fax3",
"function": "_skiptoeol",
"reason": "hardened COFF comparison proves exact bytes and internal jump-table relocations; objdiff reports a local-label ownership false negative"
},
{
"unit": "source/bitmaps/libtiff/tif_fax3",
"function": "_Fax3Decode2DRow",
"reason": "hardened COFF comparison proves exact bytes and uniquely anchored non-code relocation destinations; objdiff reports csplit data-anchor naming false negatives"
},
{
"unit": "source/objects/objects",
"function": "_object_compute_node_matrices",
"reason": "hardened COFF comparison proves all 6560 padded bytes and all 318 semantic relocations exact; objdiff reports a local-label encoding false negative"
},
{
"unit": "source/objects/objects",
"function": "$L11841",
"owner_function": "_attachments_new",
"reason": "the exact owning COMDAT and an internal relocation prove this differently named jump-table label is the same destination"
},
{
"unit": "source/objects/objects",
"function": "$L9776",
"owner_function": "_object_export_function_values",
"reason": "the exact owning COMDAT and an internal relocation prove this differently named jump-table label is the same destination"
},
{
"unit": "source/objects/objects",
"function": "$L9764",
"owner_function": "_object_export_function_values",
"reason": "the exact owning COMDAT and an internal relocation prove this differently named jump-table label is the same destination"
},
{
"unit": "source/objects/objects",
"function": "$L10620",
"owner_function": "_attachments_delete",
"reason": "the exact owning COMDAT and an internal relocation prove this differently named jump-table label is the same destination"
},
{
"unit": "source/memory/zlib/infblock",
"function": "$L2587",
"owner_function": "_inflate_blocks",
"reason": "the exact owning COMDAT and an internal relocation prove this differently numbered jump-table label is the same destination"
},
{
"unit": "source/rasterizer/xbox/rasterizer_xbox_vertex_shaders_runtime",
"function": "_rasterizer_set_vertex_shader_permutation",
"reason": "hardened COFF comparison proves all 1376 padded bytes and all 133 semantic relocations exact; objdiff reports a switch-table/local-label encoding false negative"
},
{
"unit": "source/networking/telnet_console",
"function": "_process_telnet_client_buffer",
"reason": "hardened COFF comparison proves all 496 padded bytes and all 30 semantic relocations exact; objdiff reports a switch-table/local-label encoding false negative"
},
{
"unit": "source/interface/ui_widget_event_handler_functions",
"function": "_playlist_profile_set_game_engine",
"reason": "hardened COFF comparison proves exact bytes and relocation destinations; objdiff reports a local-symbol ownership false negative"
},
{
"unit": "source/interface/ui_widget_event_handler_functions",
"function": "_playlist_profile_change_ctf_rules",
"reason": "hardened COFF comparison proves exact bytes and relocation destinations; objdiff reports a local-symbol ownership false negative"
},
{
"unit": "source/interface/ui_widget_event_handler_functions",
"function": "_playlist_profile_change_koth_rules",
"reason": "hardened COFF comparison proves exact bytes and relocation destinations; objdiff reports a local-symbol ownership false negative"
},
{
"unit": "source/interface/ui_widget_event_handler_functions",
"function": "_playlist_profile_change_slayer_rules",
"reason": "hardened COFF comparison proves exact bytes and relocation destinations; objdiff reports a local-symbol ownership false negative"
},
{
"unit": "source/interface/ui_widget_event_handler_functions",
"function": "_playlist_profile_change_oddball_rules",
"reason": "hardened COFF comparison proves exact bytes and relocation destinations; objdiff reports a local-symbol ownership false negative"
},
{
"unit": "source/interface/ui_widget_event_handler_functions",
"function": "_playlist_profile_change_racing_rules",
"reason": "hardened COFF comparison proves exact bytes and relocation destinations; objdiff reports a local-symbol ownership false negative"
},
{
"unit": "source/interface/ui_widget_event_handler_functions",
"function": "_playlist_profile_change_player_options",
"reason": "hardened COFF comparison proves exact bytes and relocation destinations; objdiff reports a local-symbol ownership false negative"
},
{
"unit": "source/interface/ui_widget_event_handler_functions",
"function": "_playlist_profile_change_item_options",
"reason": "hardened COFF comparison proves exact bytes and relocation destinations; objdiff reports a local-symbol ownership false negative"
},
{
"unit": "source/interface/ui_widget_event_handler_functions",
"function": "_playlist_profile_initialize_game_engine",
"reason": "hardened COFF comparison proves exact bytes and relocation destinations; objdiff reports a local-symbol ownership false negative"
},
{
"unit": "source/interface/ui_widget_event_handler_functions",
"function": "_playlist_profile_initialize_ctf_rules",
"reason": "hardened COFF comparison proves exact bytes and relocation destinations; objdiff reports a local-symbol ownership false negative"
},
{
"unit": "source/interface/ui_widget_event_handler_functions",
"function": "_playlist_profile_initialize_koth_rules",
"reason": "hardened COFF comparison proves exact bytes and relocation destinations; objdiff reports a local-symbol ownership false negative"
},
{
"unit": "source/interface/ui_widget_event_handler_functions",
"function": "_playlist_profile_initialize_slayer_rules",
"reason": "hardened COFF comparison proves exact bytes and relocation destinations; objdiff reports a local-symbol ownership false negative"
},
{
"unit": "source/interface/ui_widget_event_handler_functions",
"function": "_playlist_profile_initialize_oddball_rules",
"reason": "hardened COFF comparison proves exact bytes and relocation destinations; objdiff reports a local-symbol ownership false negative"
},
{
"unit": "source/interface/ui_widget_event_handler_functions",
"function": "_playlist_profile_initialize_racing_rules",
"reason": "hardened COFF comparison proves exact bytes and relocation destinations; objdiff reports a local-symbol ownership false negative"
},
{
"unit": "source/interface/ui_widget_event_handler_functions",
"function": "_playlist_profile_initialize_player_options",
"reason": "hardened COFF comparison proves exact bytes and relocation destinations; objdiff reports a local-symbol ownership false negative"
},
{
"unit": "source/interface/ui_widget_event_handler_functions",
"function": "_playlist_profile_initialize_item_options",
"reason": "hardened COFF comparison proves exact bytes and relocation destinations; objdiff reports a local-symbol ownership false negative"
},
{
"unit": "source/interface/ui_widget_event_handler_functions",
"function": "_player_profile_initialize_controller_settings",
"reason": "hardened COFF comparison proves exact bytes and relocation destinations; objdiff reports a local-symbol ownership false negative"
},
{
"unit": "source/interface/ui_widget_event_handler_functions",
"function": "_player_profile_change_controller_settings",
"reason": "hardened COFF comparison proves exact bytes and relocation destinations; objdiff reports a local-symbol ownership false negative"
},
{
"unit": "source/render/render_contrails",
"function": "_render_contrail",
"reason": "hardened COFF comparison proves all 1952 padded bytes and all 57 semantic relocations exact (SHA-256 70d28d083222f55ac171cb1aa9676d018fb2bcae9ee54f09facee54755cef5e8); objdiff reports a switch-table/local-label encoding false negative"
},
{
"unit": "source/items/weapons",
"function": "_weapon_set_state",
"reason": "hardened COFF comparison proves all 432 padded bytes and all 21 semantic relocations exact (SHA-256 667efc085661d6b50a6fb3c778be5f3b44e05acfca0132acf3aa527b21f20b74); objdiff reports a switch-table/local-label encoding false negative"
},
{
"unit": "source/ai/path_smoothing",
"function": "_surface_is_walkable",
"reason": "hardened COFF comparison proves all 128 padded bytes and all 5 semantic relocations exact (SHA-256 a58fa4f6cf9598874c2a0d1fb60f226f65dea7f5cbc7182a682b2faefc62580f); the private name is recovered from the source-backed path-smoothing helper cluster"
},
{
"unit": "source/rasterizer/rasterizer_debug",
"function": "_rasterizer_debug_line",
"reason": "hardened COFF comparison proves all 32 padded bytes and the REL32 relocation to _rasterizer_debug_line_shaded exact (SHA-256 60f8ba4857cfb47aa739f77d4df9844fae5d61fb4292d7d92b25c28166a7a0d7)"
},
{
"unit": "source/rasterizer/rasterizer_debug",
"function": "_rasterizer_debug_triangle",
"reason": "hardened COFF comparison proves all 32 padded bytes and the REL32 relocation to _rasterizer_debug_triangle_shaded exact (SHA-256 1b0df8cc752bec5820c858223f553d6f62fc7e39bac45bbb9b209d0f980a148a)"
},
{
"unit": "source/cutscene/recorded_animation_playback",
"function": "_recorded_animation_apply_event_stream",
"reason": "hardened COFF comparison proves all 496 padded bytes and all 34 semantic relocations exact (SHA-256 4e8a5bd6f5b73b358df9e82beed6a1a08aa23ee6f84b353aa3026df37f1b441b); objdiff reports a switch-table/local-label encoding false negative"
},
{
"unit": "source/camera/camera_scripting",
"function": "_scripted_camera_update",
"reason": "hardened COFF comparison proves all 1664 padded bytes and all 82 semantic relocations exact (SHA-256 cb504e5b44213920854f97d593ab2361bda073fcb0ffef531347b0c4b8565e06); objdiff reports a switch-table/local-label encoding false negative"
},
{
"unit": "source/game/game_engine_ctf",
"function": "_ctf_engine_display_score",
"reason": "hardened COFF comparison proves all 928 padded bytes and all 78 semantic relocations exact (SHA-256 df4518d31867315a05039fdcf8679ba2476011eeb70122ac00c6b6efef13f148); objdiff reports a switch-table/local-label ownership false negative"
},
{
"unit": "source/game/game_engine_oddball",
"function": "_oddball_engine_display_score",
"reason": "hardened COFF comparison proves all 736 padded bytes and all 56 semantic relocations exact (SHA-256 6ad4051ea66ca25449fb855b7cb0076efef3e056e80fa1a6566d7dc5fc2a2e38); objdiff reports pooled-string COMDAT ownership as a false negative"
},
{
"unit": "source/interface/first_person_weapons",
"function": "_first_person_weapon_state_from_weapon_message",
"reason": "hardened COFF comparison proves all 192 padded bytes and all 19 semantic relocations exact (SHA-256 f551efe027b59b504571439a15088c0065c23383298acbcdb85aeb04f10d3947); objdiff reports a private switch-table/local-label ownership false negative"
},
{
"unit": "source/interface/first_person_weapons",
"function": "_first_person_animation_type_from_weapon_state",
"reason": "hardened COFF comparison proves all 272 padded bytes and all 25 semantic relocations exact (SHA-256 ea1838b37d9d2a3d39b80a5118d966f264e0daecbff3a47c9f41a6968006b499); objdiff reports a private switch-table/local-label ownership false negative"
},
{
"unit": "source/interface/first_person_weapons",
"function": "_first_person_weapon_set_state",
"reason": "hardened COFF comparison proves all 688 padded bytes and all 26 semantic relocations exact (SHA-256 965964af9115913e36fa06e152828b2e857e7ac59e2cc0e6fdcff29ddebcb91f); objdiff reports a private switch-table/local-label ownership false negative"
},
{
"unit": "source/interface/ui_widget_game_data_input_functions",
"function": "_mutliplayer_settings_select_list_update_displayed_items",
"reason": "hardened COFF comparison proves all 1424 padded bytes and all 73 semantic relocations exact (SHA-256 adce7f9ab32aee72ec9d542efc297df9295a839a27406848eb8467ffea97594f); objdiff reports compiler-local switch-table labels as a false negative; January's original misspelling of multiplayer in the owner name is preserved"
},
{
"unit": "source/interface/ui_widget_game_data_input_functions",
"function": "_server_list_menu_update",
"reason": "hardened COFF comparison proves all 2016 padded bytes and all 115 semantic relocations exact (SHA-256 f7a5f8c39ab8f6200fa4dfe798c533c40d76e0f8acd06935cea2d5a696c73334); objdiff reports this complete typed server-list graph as a false negative"
},
{
"unit": "source/interface/ui_widget",
"function": "_remap_sticks_for_local_player",
"reason": "hardened COFF comparison proves all 304 padded bytes and all 31 semantic relocations exact (SHA-256 963b6756ac2f3b70faf18955bad37bac6ed40bbe366baa697feccc61e7050ce8); objdiff reports a control-flow pairing false negative after the natural switch assigns the remapped icon and reaches the shared return"
}
]
-68
View File
@@ -1,68 +0,0 @@
[
{ "file_offset": 1536, "name" : ".text" },
{ "file_offset": 1926336, "name" : "D3D" },
{ "file_offset": 2005192, "name" : "D3D_RD" },
{ "file_offset": 2011016, "name" : "D3D_RW" },
{ "file_offset": 2011136, "name" : "D3D_URW" },
{ "file_offset": 2024576, "name" : "D3DX" },
{ "file_offset": 2044112, "name" : "D3DX_RD" },
{ "file_offset": 2044320, "name" : "DSOUND" },
{ "file_offset": 2092088, "name" : "DSOUND_RD" },
{ "file_offset": 2171408, "name" : "DSOUND_RW" },
{ "file_offset": 2173056, "name" : "DSOUND_URW" },
{ "file_offset": 2173248, "name" : "XNET" },
{ "file_offset": 2218120, "name" : "XNET_RD" },
{ "file_offset": 2218496, "name" : "BINK" },
{ "file_offset": 2308672, "name" : "BINK32" },
{ "file_offset": 2313408, "name" : "BINK32A" },
{ "file_offset": 2318912, "name" : "BINK16" },
{ "file_offset": 2323808, "name" : "BINK4444" },
{ "file_offset": 2329344, "name" : "BINK5551" },
{ "file_offset": 2333760, "name" : "BINK16MX" },
{ "file_offset": 2334080, "name" : "BINK16X2" },
{ "file_offset": 2335456, "name" : "BINK16M" },
{ "file_offset": 2335968, "name" : "BINK32MX" },
{ "file_offset": 2336416, "name" : "BINK32X2" },
{ "file_offset": 2337856, "name" : "BINK32M" },
{ "file_offset": 2338208, "name" : ".XPP$ClassA" },
{ "file_offset": 2338212, "name" : ".XPP$ClassHub" },
{ "file_offset": 2338216, "name" : ".XPP$ClassMU" },
{ "file_offset": 2338220, "name" : ".XPP$ClassXID" },
{ "file_offset": 2338228, "name" : ".XPP$ClassZ" },
{ "file_offset": 2338232, "name" : ".XPP$Data" },
{ "file_offset": 2338948, "name" : ".XPPCINIT" },
{ "file_offset": 2342115, "name" : ".XPPCODE" },
{ "file_offset": 2370500, "name" : ".XPPRDATA" },
{ "file_offset": 2370592, "name" : ".idata$5" },
{ "file_offset": 2371276, "name" : ".rdata" },
{ "file_offset": 2845216, "name" : ".rdata$debug" },
{ "file_offset": 2845312, "name" : ".CRT$RIA" },
{ "file_offset": 2845316, "name" : ".CRT$RII10" },
{ "file_offset": 2845320, "name" : ".CRT$RII20" },
{ "file_offset": 2845324, "name" : ".CRT$RIZ" },
{ "file_offset": 2845328, "name" : ".CRT$XCA" },
{ "file_offset": 2845332, "name" : ".CRT$XCU" },
{ "file_offset": 2845340, "name" : ".CRT$XCZ" },
{ "file_offset": 2845344, "name" : ".CRT$XIA" },
{ "file_offset": 2845348, "name" : ".CRT$XIC" },
{ "file_offset": 2845356, "name" : ".CRT$XIZ" },
{ "file_offset": 2845360, "name" : ".CRT$XPA" },
{ "file_offset": 2845364, "name" : ".CRT$XPX" },
{ "file_offset": 2845368, "name" : ".CRT$XPZ" },
{ "file_offset": 2845372, "name" : ".CRT$XTA" },
{ "file_offset": 2845376, "name" : ".CRT$XTZ" },
{ "file_offset": 2845392, "name" : ".data" },
{ "file_offset": 3251360, "name" : ".bss" },
{ "file_offset": 6401024, "name" : "DOLBY" },
{ "file_offset": 6429088, "name" : "BINKDATA" },
{ "file_offset": 6446080, "name" : ".idata$2" },
{ "file_offset": 6446120, "name" : ".idata$3" },
{ "file_offset": 6446140, "name" : ".idata$4" },
{ "file_offset": 6446824, "name" : ".idata$6" },
{ "file_offset": 6446848, "name" : ".tls" },
{ "file_offset": 6446852, "name" : ".tls$" },
{ "file_offset": 6446860, "name" : ".tls$ZZZ" },
{ "file_offset": 6446880, "name" : ".XBLD" },
{ "file_offset": 6446896, "name" : ".XBLD$A" },
{ "file_offset": 6446912, "name" : ".XBLD$V" }
]
-39
View File
@@ -1,39 +0,0 @@
[
{
"unit": "source/objects/objects",
"section": ".bss",
"reason": "January layout, PDB-public evidence, and module-local references prove three source-private object globals followed by the externally visible debug byte",
"snapshot": {
"size": 13,
"flags": 3224371328,
"relocation_count": 0,
"normalized_sha256": "dd46c3eebb1884ff3b5258c0a2fc9398e560a29e0780d4b53869b6254aa46a96",
"symbols": [
{
"name": "_object_name_list",
"value": 0,
"type": 0,
"storage": 3
},
{
"name": "_object_memory_pool",
"value": 4,
"type": 0,
"storage": 3
},
{
"name": "_object_globals",
"value": 8,
"type": 0,
"storage": 3
},
{
"name": "_debug_objects_position_velocity",
"value": 12,
"type": 0,
"storage": 2
}
]
}
}
]
-23861
View File
File diff suppressed because it is too large. Load diff
+137 -164
View File
@@ -1,70 +1,28 @@
#!/usr/bin/env python3
# root build script
# root build script: writes build.ninja for the native ports (Linux, Windows,
# Android) and the web port
import argparse
import json
from pathlib import Path
from typing import (
IO,
Any,
Callable,
Dict,
Iterable,
List,
Optional,
Set,
Tuple,
TypedDict,
Union,
cast,
)
from tools.project_x86 import (
Object,
ObjectStatus,
ProjectConfig,
SolutionConfig,
calculate_progress,
generate_build,
is_windows,
)
import io
import os
import subprocess
import sys
from pathlib import Path
from types import SimpleNamespace
from tools import ninja_syntax
from tools.android_build import android_configure_inputs, generate_android_build
from tools.linux_build import check_profile_options, generate_linux_build, linux_configure_inputs
from tools.web_build import generate_web_build, web_configure_inputs
from tools.windows_build import generate_windows_build, windows_configure_inputs
def is_windows() -> bool:
return os.name == "nt"
# arguments
parser = argparse.ArgumentParser()
parser.add_argument(
"mode",
choices=["configure", "progress"],
default="configure",
help="script mode (default: configure)",
nargs="?",
)
parser.add_argument(
"--objdiff",
metavar="BINARY",
type=Path,
help="path to objdiff-cli binary (optional)",
)
parser.add_argument(
"--csplit",
metavar="BINARY",
type=Path,
help="path to csplit binary (optional)",
)
parser.add_argument(
"--ninja",
metavar="BINARY",
type=Path,
help="path to ninja binary (optional)",
)
parser.add_argument(
"--ml",
metavar="BINARY",
type=Path,
help="path to Microsoft Macro Assembler for authentic CRT .asm units",
)
parser.add_argument(
"--linux-cc",
metavar="BINARY",
@@ -73,26 +31,36 @@ parser.add_argument(
parser.add_argument(
"--compiler-launcher",
metavar="BINARY",
help="native ports: a program that runs each compile, such as ccache "
help="a program that runs each compile, such as ccache "
"(links and the Android host's few units run the compiler directly)",
)
parser.add_argument(
"--release",
action="store_true",
help="release builds of the native ports (Linux, Windows, Android): assertions are not checked",
help="release builds (Linux, Windows, Android, web): assertions are not checked",
)
parser.add_argument(
"--profile",
action="store_true",
help="profiling builds (Linux, Windows, Android): CPU scopes recorded on a "
"console command or launch setting (README, \"Profiling builds\"); not with --pgo=train",
)
parser.add_argument(
"--lto",
choices=["full", "thin", "off"],
default="full",
help="link-time optimisation of the native ports (Linux, Windows): full (the default: the whole game "
help="link-time optimisation (Linux, Windows): full (the default: the whole game "
"optimised as one module, the fastest code and the slowest links), thin (parallel and incremental) or off",
)
parser.add_argument(
"--portable",
action="store_true",
help="native x86 ports (Linux, Windows): code for any x86-64 processor (SSE2) rather than for this "
"machine's (-march=native, the default); use it for builds that run on other computers",
help="x86 builds (Linux, Windows): code for any x86-64 processor (SSE2) rather than for this "
"machine's (-march=native, the default); use it for builds that run on other computers. On Linux it also "
"builds against Debian 11's glibc 2.31 rather than this machine's, and brings its own SDL 3 (libSDL3.so.0, "
"beside the executable), so that it starts on SteamOS and older distributions: the first build downloads "
"the Debian packages and SDL's source (build/linux/third_party, tools/linux_sysroot.py), and needs CMake, "
"pkgconf and wayland-scanner, not the 32-bit SDL 3",
)
parser.add_argument(
"--pgo",
@@ -100,7 +68,7 @@ parser.add_argument(
const="train",
default="use",
choices=["use", "train", "off"],
help="profile-guided optimisation of the native ports: use (the default) optimises with the profiles in pgo/, "
help="profile-guided optimisation: use (the default) optimises with the profiles in pgo/, "
"train (or plain --pgo) first records this platform's profile if it is missing, by letting an instrumented "
"build play every campaign level (needs the game data in assets/ and a display; Linux and Windows), off does "
"without",
@@ -109,7 +77,7 @@ parser.add_argument(
"--pgo-profile",
metavar="PROFDATA",
type=Path,
help="native ports: profile-guided optimisation from this profile instead",
help="profile-guided optimisation from this profile instead",
)
parser.add_argument(
"--android-ndk",
@@ -121,114 +89,119 @@ parser.add_argument(
type=str,
help="clang with the arm64_32 target for the Android guest (default: clang)",
)
parser.add_argument(
"--web-emcc",
type=str,
help="Emscripten's emcc for `ninja web` (default: emcc on the PATH, then EMSDK's or ~/emsdk's)",
)
# no need to spam those extra flags when running on Windows
if not is_windows():
parser.add_argument(
"--wrapper",
"--windows-cross",
action="store_true",
help="generate the Windows build (`ninja windows`) on Linux, cross-compiling "
"with clang against an MSVC toolchain (--windows-vctools-dir and --windows-sdk-dir), "
"and make it the default target",
)
parser.add_argument(
"--windows-cc",
metavar="BINARY",
help="compiler for the Windows cross build, `ninja windows` (default: clang). "
"Only clang is supported for now.",
)
parser.add_argument(
"--windows-vctools-dir",
metavar="DIR",
type=Path,
help="path to wibo or wine (optional)",
help="the Visual C++ tools of the MSVC toolchain a --windows-cross build compiles "
"and links against: the versioned folder under VC/Tools/MSVC, the one with `include` "
"and `lib` (default: VCToolsInstallDir)",
)
parser.add_argument(
"--windows-sdk-dir",
metavar="DIR",
type=Path,
help="the Windows SDK a --windows-cross build compiles and links against: the "
"folder with `Include` and `Lib`, each with a versioned folder (default: WindowsSdkDir)",
)
args = parser.parse_args()
try:
check_profile_options(args.profile, args.pgo)
except ValueError as error:
parser.error(str(error))
# path setup
build_dir = Path("build")
config_dir = Path("config")
config_json_path = config_dir / "config.json"
project_dir = Path("projects")
tools_dir = Path("tools")
# build config json
class BuildConfigObject(TypedDict):
name: str
index: int
status: str
options: Dict[str, Any]
class BuildConfigProject(TypedDict):
name: str
guid: str
objects: List[BuildConfigObject]
options: Dict[str, Any]
class BuildConfig(TypedDict):
name: str
guid: str
baserom: str
projects: List[BuildConfigProject]
build_config: BuildConfig = json.load(open(config_json_path, "r", encoding="utf-8"))
# solution configuration
sln = SolutionConfig()
sln.build_dir = build_dir
sln.config_dir = config_dir
sln.project_dir = project_dir
sln.tools_dir = tools_dir
sln.name = build_config["name"]
sln.guid = build_config["guid"]
sln.baserom = build_config["baserom"]
# Apply arguments
sln.objdiff_path = args.objdiff
sln.csplit_path = args.csplit
sln.ninja_path = args.ninja
sln.ml_path = args.ml
sln.linux_cc = args.linux_cc
sln.compiler_launcher = args.compiler_launcher
sln.port_release = args.release
sln.port_lto = args.lto
sln.port_portable = args.portable
sln.port_pgo = args.pgo
sln.port_pgo_profile = args.pgo_profile
sln.android_ndk = args.android_ndk
sln.android_guest_cc = args.android_guest_cc
# the settings the builds read
sln = SimpleNamespace(
build_dir=Path("build"),
linux_cc=args.linux_cc,
compiler_launcher=args.compiler_launcher,
port_release=args.release,
port_profile=args.profile,
port_lto=args.lto,
port_portable=args.portable,
port_pgo=args.pgo,
port_pgo_profile=args.pgo_profile,
android_ndk=args.android_ndk,
android_guest_cc=args.android_guest_cc,
web_emcc=args.web_emcc,
)
if not is_windows():
sln.wrapper = args.wrapper
sln.windows_cc = args.windows_cc
sln.windows_cross = args.windows_cross
sln.windows_vctools_dir = args.windows_vctools_dir
sln.windows_sdk_dir = args.windows_sdk_dir
# Tool versions
sln.objdiff_tag = "v3.6.0"
sln.csplit_tag = "v0.0.2"
# v0.0.2 plus upstream's "Fix uninit phony section"; built from source on Linux
sln.csplit_source_commit = "db510e7ef9f60bb09b13a3df88c7419e9b215cde"
sln.wibo_tag = "1.0.0"
sln.uasm_tag = "v2.57r"
sln.projects = []
for build_project in build_config["projects"]:
project = ProjectConfig(**build_project.get("options", {}))
project.name = build_project["name"]
project.guid = build_project["guid"]
project.objects = []
for build_object in build_project["objects"]:
def get_object_completed(status: str) -> ObjectStatus:
if status == "MISSING":
return ObjectStatus.Missing
elif status == "Matching":
return ObjectStatus.Matching
elif status == "NonMatching":
return ObjectStatus.NonMatching
assert False, f"Invalid object status {status}"
project.objects.append(Object(get_object_completed(build_object["status"]), Path(build_object["name"]), **build_object.get("options", {})))
sln.projects.append(project)
# build.ninja
out = io.StringIO()
n = ninja_syntax.Writer(out)
n.variable("ninja_required_version", "1.3")
n.newline()
# build file generation
configure_script = Path(os.path.relpath(os.path.abspath(sys.argv[0])))
n.comment("The arguments passed to configure.py, for rerunning it.")
n.variable(
"configure_args",
[f'"{arg}"' if any(ch.isspace() for ch in arg) else arg for arg in sys.argv[1:]],
)
n.variable("python", f'"{sys.executable}"')
n.newline()
if args.mode == "configure":
if sln.matching and any(
obj["status"] != "MISSING" and obj["name"].startswith("libs/d3d8/")
for project in build_config["projects"]
for obj in project["objects"]
):
subprocess.run([sys.executable, "libs/d3d8/generate_sdk_overlay.py"], check=True)
# Write build.ninja and objdiff.json
generate_build(sln)
elif args.mode == "progress":
# Print progress information
calculate_progress(sln)
else:
sys.exit("Unknown mode: " + args.mode)
generate_linux_build(n, sln)
generate_android_build(n, sln)
generate_windows_build(n, sln)
generate_web_build(n, sln)
n.comment("Reconfigure on change")
n.rule(
name="configure",
command=f"$python {configure_script} $configure_args",
generator=True,
description=f"RUN {configure_script}",
)
n.build(
outputs="build.ninja",
rule="configure",
implicit=[
configure_script,
Path("tools/ninja_syntax.py"),
*linux_configure_inputs(),
*android_configure_inputs(),
*windows_configure_inputs(),
*web_configure_inputs(),
],
)
n.newline()
# the build for this computer, where it could be generated (the Windows
# build is left out when SDL cannot be fetched, for instance)
default = "windows" if is_windows() or sln.windows_cross else "linux"
if f"\nbuild {default}: " in out.getvalue():
n.comment("Default rule: the build for this computer")
n.default(default)
with open("build.ninja", "w", encoding="utf-8") as f:
f.write(out.getvalue())
out.close()
+3
View File
@@ -0,0 +1,3 @@
*
!healthz.sh
!*.zip
+2
View File
@@ -0,0 +1,2 @@
# a release zip copied here to build the image from (README.md, "Building it")
*.zip
+60
View File
@@ -0,0 +1,60 @@
# syntax=docker/dockerfile:1
#
# The dedicated server (docs/dedicated_server.md) as a container image, made
# from a release's halo-linux-release.zip (the portable build: halo, its
# libSDL3.so.0, brokers.txt, masters.txt):
#
# docker build -t opence-server deploy/docker
# docker build -t opence-server --build-arg RELEASE=https://github.com/OpenCommunityEdition/OpenCE/releases/download/build-206/halo-linux-release.zip deploy/docker
# docker build -t opence-server --build-arg RELEASE=halo-linux-release.zip deploy/docker (a zip in deploy/docker)
#
# The game data is never in the image: mount the folder that holds maps/ at
# /data, read-only. /state is the only folder the server writes (config.toml,
# the save root with its map cache, the logs), so the root can be read-only.
# deploy/docker/README.md says how to run it.
FROM debian:bookworm-slim AS release
RUN apt-get update \
&& apt-get install -y --no-install-recommends unzip \
&& rm -rf /var/lib/apt/lists/*
# a URL, or a file beside this Dockerfile
ARG RELEASE=https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-release.zip
ADD ${RELEASE} /tmp/halo-linux-release.zip
COPY healthz.sh /opt/opence/healthz.sh
RUN unzip -q /tmp/halo-linux-release.zip -d /opt/opence \
&& test -f /opt/opence/halo \
&& chmod 0755 /opt/opence/halo /opt/opence/healthz.sh \
&& if [ -f /opt/opence/halo-server ]; then chmod 0755 /opt/opence/halo-server; fi
FROM debian:bookworm-slim
# The 32-bit C runtime; the release's own libSDL3.so.0 needs nothing more for
# a server, which opens no window or sound device. ca-certificates: the
# master servers and the update check are HTTPS.
RUN dpkg --add-architecture i386 \
&& apt-get update \
&& apt-get install -y --no-install-recommends libc6:i386 libgcc-s1:i386 ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd -g 10001 opence \
&& useradd -u 10001 -g 10001 -d /state -M -s /usr/sbin/nologin opence \
&& mkdir -p /data /state \
&& chown 10001:10001 /state
COPY --from=release /opt/opence /opt/opence
ENV HALO_SERVER=1 \
HALO_CONFIG_FILE=/state/config.toml \
HALO_DATA_ROOT=/data \
HALO_SAVE_ROOT=/state/saves \
HALO_LOG_ROOT=/state/logs \
HALO_NET_TUNNEL_PORT=2302 \
HALO_SERVER_WEB_BIND=0.0.0.0 \
HALO_SERVER_WEB_PORT=8080 \
HOME=/state
VOLUME ["/state"]
# Internet play's tunnel, and the web page, API and RCON (publish it on the
# host's loopback, or behind a proxy with TLS). System link's 5150, 5151 and
# 5153 are for a local network only.
EXPOSE 2302/udp 8080/tcp
USER 10001:10001
WORKDIR /state
HEALTHCHECK --interval=15s --timeout=3s --start-period=60s --retries=3 CMD ["/opt/opence/healthz.sh"]
STOPSIGNAL SIGTERM
ENTRYPOINT ["/opt/opence/halo", "--server"]
+134
View File
@@ -0,0 +1,134 @@
# The dedicated server in a container
`ghcr.io/opencommunityedition/opence-server` is the dedicated server
(`docs/dedicated_server.md`) as a container image: a release's
`halo-linux-release.zip` on Debian 12 with the 32-bit C runtime, run as
`halo --server` by the user 10001. CI builds it from each release
(`.github/workflows/server-image.yml`) and signs it with cosign; the tags are
`latest`, the release's (`build-206`) and `sha-<commit>`.
The game data is never in the image. Mount the folder that holds `maps/`
(from your own copy of the game: port/linux/README.md, "Game data") at
`/data`, read-only. Without it the server exits with code 2 and says which
maps it looked for.
## Running it
```sh
docker run -d --name halo --restart unless-stopped \
--read-only --cap-drop ALL --security-opt no-new-privileges --tmpfs /tmp \
-p 2302:2302/udp -p 127.0.0.1:8080:8080 \
-v /srv/halo/data:/data:ro -v halo-state:/state \
-e HALO_SERVER_NAME="My server" \
-e HALO_SERVER_ROTATION="bloodgulch:ctf, sidewinder:team_slayer" \
-e HALO_SERVER_WEB_PASSWORD=changeme-please \
ghcr.io/opencommunityedition/opence-server:latest
docker logs -f halo
```
Or with Compose: `compose.yaml` beside this file runs the same, with the
settings taken from the shell or a `.env` file
(`HALO_DATA=/srv/halo/data`, `HALO_SERVER_NAME=...`). Its `https` profile
adds Caddy in front of the web port, with a certificate for
`HALO_WEB_DOMAIN`:
```sh
HALO_DATA=/srv/halo/data docker compose -f deploy/docker/compose.yaml up -d
HALO_WEB_DOMAIN=halo.example.org docker compose -f deploy/docker/compose.yaml --profile https up -d
```
The Compose file puts both on a network of its own, `172.30.80.0/24`, with
Caddy at the fixed address `172.30.80.10` and the server's
`HALO_SERVER_WEB_TRUSTED_PROXY` set to it: the server believes Caddy's
`X-Forwarded-For`, `-Host` and `-Proto`, so its rate limits and log name the
visitor rather than Caddy, and the session cookie is `Secure`. If that
subnet overlaps one of the host's, change the subnet, Caddy's
`ipv4_address` and the variable together.
`docker stop` sends SIGTERM: the game in progress goes to its postgame,
the players are told, the listing is withdrawn, and the server exits 0
within the 15 seconds Compose waits (`docker stop -t 15` otherwise).
## Folders
| Path | What |
| --- | --- |
| `/opt/opence` | The release: `halo`, `libSDL3.so.0`, `brokers.txt`, `masters.txt`, `healthz.sh`, the licences (read-only) |
| `/data` | The game data: `maps/`, and if you have them `custom_maps/`, `gametypes/` and `init.txt` (yours, read-only) |
| `/state` | The only folder written: `config.toml` (`HALO_CONFIG_FILE`), `publisher.key` beside it, the save root `saves/` with the map cache (about 115 MB), `logs/` (`debug.txt`, `bans.txt`, `cheaters.txt`) |
The image sets `HALO_CONFIG_FILE=/state/config.toml` rather than linking
`config.toml` beside the executable into the volume: the settings file is
then in the volume from the first start, and nothing in `/opt/opence` is
written. The server writes every setting there at the first start, with
the generated web password if none was given.
## Settings
Every setting is an environment variable (port/linux/README.md, "Dedicated
server", and docs/dedicated_server.md). A variable that is set wins over
`config.toml`, and the web page's settings cannot change it. The image sets:
| Variable | Value |
| --- | --- |
| `HALO_SERVER` | `1` |
| `HALO_CONFIG_FILE` | `/state/config.toml` |
| `HALO_DATA_ROOT` | `/data` |
| `HALO_SAVE_ROOT` | `/state/saves` |
| `HALO_LOG_ROOT` | `/state/logs` |
| `HALO_NET_TUNNEL_PORT` | `2302` |
| `HALO_SERVER_WEB_BIND` | `0.0.0.0` (within the container; publish the port on `127.0.0.1` or behind TLS) |
| `HALO_SERVER_WEB_PORT` | `8080` |
The ones most often given: `HALO_SERVER_NAME`, `HALO_SERVER_ROTATION`,
`HALO_SERVER_MAX_PLAYERS`, `HALO_SERVER_PASSWORD`, `HALO_SERVER_PUBLIC`,
`HALO_SERVER_MOTD`, `HALO_SERVER_WEB_PASSWORD`, `HALO_NET_PUBLIC_ADDRESS`,
`HALO_SERVER_PUBLISHER_KEY` (the master servers' key, as 64 hex
characters; `docker run --rm -v halo-state:/state <image> --keygen` makes one
into the volume instead), `HALO_SERVER_INSTANCE`, `HALO_SERVER_REGION`,
`HALO_SERVER_LOG_FORMAT=json`.
A tunnel port other than 2302 is published as the same number on both sides
(`-e HALO_NET_TUNNEL_PORT=2303 -p 2303:2303/udp`), as the server offers its
own port to joiners. Behind a NAT that STUN cannot see through, give the
public address and port in `HALO_NET_PUBLIC_ADDRESS`.
## Health
`HEALTHCHECK` runs `/opt/opence/healthz.sh`, which asks `GET /healthz` on
the web port (bash's `/dev/tcp`: the image has no curl) and is healthy on a
200: the server is hosting (lobby, loading, playing or postgame) and its
frames run. With `HALO_SERVER_WEB_PORT=0` it always passes.
## Building it
```sh
docker build -t opence-server deploy/docker # the latest release
docker build -t opence-server --build-arg RELEASE=https://github.com/OpenCommunityEdition/OpenCE/releases/download/build-206/halo-linux-release.zip deploy/docker
```
A build of your own (`python tools/ci_build.py linux release` leaves
`dist/halo-linux-release/`) goes in as a zip beside the Dockerfile:
```sh
(cd dist/halo-linux-release && zip -qr ../../deploy/docker/halo-linux-release.zip .)
docker build -t opence-server --build-arg RELEASE=halo-linux-release.zip deploy/docker
```
`RELEASE` is a URL or a file beside the Dockerfile. The image is 32-bit
x86 and runs on amd64 hosts only.
## Networking
Bridge networking works for internet play: the tunnel port is published one
to one, and the server needs no hole punching of its own. System link
(UDP 5150 and 5151, TCP 5150 and 5153) finds other machines only with
`--network host`.
Verify an image's signature:
```sh
cosign verify ghcr.io/opencommunityedition/opence-server:latest \
--certificate-identity-regexp '^https://github\.com/OpenCommunityEdition/OpenCE/\.github/workflows/server-image\.yml@' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
```
+92
View File
@@ -0,0 +1,92 @@
# The dedicated server with Docker Compose (deploy/docker/README.md):
#
# docker compose -f deploy/docker/compose.yaml up -d
# docker compose -f deploy/docker/compose.yaml --profile https up -d (with Caddy in front of the web port)
#
# HALO_DATA is the folder that holds maps/ (and custom_maps/, gametypes/),
# mounted read-only; settings come from the environment below or from a
# .env file beside this one.
name: opence
services:
server:
image: ${OPENCE_IMAGE:-ghcr.io/opencommunityedition/opence-server:latest}
restart: unless-stopped
read_only: true
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
tmpfs:
- /tmp
ports:
- "${HALO_NET_TUNNEL_PORT:-2302}:${HALO_NET_TUNNEL_PORT:-2302}/udp"
# (the web page on the host's loopback only; Caddy reaches it on the
# compose network)
- "127.0.0.1:${HALO_SERVER_WEB_PORT:-8080}:${HALO_SERVER_WEB_PORT:-8080}/tcp"
volumes:
- ${HALO_DATA:-./data}:/data:ro
- state:/state
# A variable named without a value is passed only when it is set (in the
# shell or .env): one that is set wins over config.toml, and sv_set
# cannot change it.
environment:
- HALO_NET_TUNNEL_PORT=${HALO_NET_TUNNEL_PORT:-2302}
- HALO_SERVER_WEB_PORT=${HALO_SERVER_WEB_PORT:-8080}
- HALO_SERVER_NAME
- HALO_SERVER_ROTATION
- HALO_SERVER_MAX_PLAYERS
- HALO_SERVER_PASSWORD
- HALO_SERVER_PUBLIC
- HALO_SERVER_MOTD
# (8 characters or more; unset: one is made at the first start, printed
# in the log and kept in the state volume's config.toml)
- HALO_SERVER_WEB_PASSWORD
- HALO_NET_PUBLIC_ADDRESS
- HALO_SERVER_PUBLISHER_KEY
- HALO_SERVER_INSTANCE
- HALO_SERVER_REGION
- HALO_SERVER_LOG_FORMAT
# Caddy's address on the network below (the https profile): its
# X-Forwarded-For, -Host and -Proto are believed, so the page's rate
# limits and log name the visitor, and its session cookie is Secure.
# Nothing else has this address.
- HALO_SERVER_WEB_TRUSTED_PROXY=172.30.80.10
networks:
- opence
stop_grace_period: 15s
# HTTPS for the web page, with a certificate from Let's Encrypt for
# HALO_WEB_DOMAIN (which must resolve to this host, ports 80 and 443 open).
caddy:
image: caddy:2
profiles: [https]
restart: unless-stopped
depends_on: [server]
ports:
- "80:80/tcp"
- "443:443/tcp"
- "443:443/udp"
command: ["caddy", "reverse-proxy", "--from", "${HALO_WEB_DOMAIN:-halo.example.org}", "--to", "server:${HALO_SERVER_WEB_PORT:-8080}"]
volumes:
- caddy-data:/data
- caddy-config:/config
networks:
opence:
# (server.web_trusted_proxy above; outside the range Docker hands
# out, so no other container takes it)
ipv4_address: 172.30.80.10
# A fixed subnet, so that Caddy's address is known in advance. Containers
# other than Caddy get addresses from ip_range only. Change it (and the two
# addresses above) if it overlaps a network of the host's.
networks:
opence:
ipam:
config:
- subnet: 172.30.80.0/24
ip_range: 172.30.80.128/25
gateway: 172.30.80.1
volumes:
state:
caddy-data:
caddy-config:
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
# The image's health check: GET /healthz on the web port, healthy on a 200
# (the server answers 200 while it hosts and its frames run, 503 otherwise).
# Bash's /dev/tcp, as the image has no curl.
port="${HALO_SERVER_WEB_PORT:-8080}"
# (no web port: nothing to ask)
if [ -z "$port" ] || [ "$port" = 0 ]; then
exit 0
fi
{ exec 3<>"/dev/tcp/127.0.0.1/$port"; } 2>/dev/null || exit 1
printf 'GET /healthz HTTP/1.1\r\nHost: 127.0.0.1:%s\r\nConnection: close\r\n\r\n' "$port" >&3 || exit 1
IFS=' ' read -r -t 3 version status _ <&3 || exit 1
exec 3<&-
case "$version" in
HTTP/1.*) ;;
*) exit 1 ;;
esac
[ "$status" = 200 ]
+29
View File
@@ -0,0 +1,29 @@
# Big team slayer: first team to 50 kills, vehicles as Halo PC places them.
# Play it with server.rotation entries such as bloodgulch:btb_slayer.
base = "team_slayer"
name = "BTB Slayer"
[universal]
teams = true
score_to_win = 50
lives = 0
respawn_time = 5
health = 100
weapon_set = "normal"
vehicle_set = "default"
radar = true
friend_indicators = true
[options]
time_limit = 20
friendly_fire = "shields_only"
friendly_fire_penalty = 5
vehicle_respawn_time = 60
auto_team_balance = true
radar_players = "friends"
team_vehicles = "pc"
[slayer]
no_death_bonus = false
no_kill_penalty = false
kill_in_order = false
+27
View File
@@ -0,0 +1,27 @@
# Capture the flag as players remember it: three captures, the flag must be
# at home to score.
base = "ctf"
name = "Classic CTF"
[universal]
teams = true
score_to_win = 3
respawn_time = 5
weapon_set = "normal"
vehicle_set = "default"
radar = true
[options]
time_limit = 25
friendly_fire = "off"
vehicle_respawn_time = 90
auto_team_balance = true
radar_players = "friends"
team_vehicles = "pc"
[ctf]
assault = false
reset_on_capture = false
flag_must_reset = false
flag_at_home_to_score = true
single_flag_time = 0
+17
View File
@@ -0,0 +1,17 @@
# King of the hill whose hill moves about the map, for teams.
base = "team_king"
name = "Moving Hill"
[universal]
teams = true
score_to_win = 3
respawn_time = 5
radar = true
[options]
time_limit = 15
friendly_fire = "off"
auto_team_balance = true
[king]
moving_hill = true
+23
View File
@@ -0,0 +1,23 @@
# Free for all slayer to 25 kills, starting with a pistol and an assault rifle.
base = "slayer"
name = "Lone Wolves"
[universal]
teams = false
score_to_win = 25
respawn_time = 3
health = 100
radar = false
[options]
time_limit = 15
radar_players = "none"
loadout = "custom"
primary_weapon = "pistol"
secondary_weapon = "assault_rifle"
team_vehicles = "pc"
[slayer]
no_death_bonus = false
no_kill_penalty = false
kill_in_order = false
+2
View File
@@ -0,0 +1,2 @@
#!/bin/sh
exec "$(dirname "$0")/halo" --server "$@"
+3
View File
@@ -0,0 +1,3 @@
# Not part of the packaged chart: unit tests and CI values files.
tests/
ci/
+17
View File
@@ -0,0 +1,17 @@
apiVersion: v2
name: opence-server
description: >-
The OpenCE dedicated server: a headless Halo: Combat Evolved host that plays games one after
another, with no window, sound or player of its own. The game data (maps) is mounted from a
volume you provide.
type: application
version: 0.1.0
kubeVersion: ">=1.29.0-0"
# Informational only: which image runs is set by image.repository, image.tag and image.digest.
appVersion: "latest"
home: https://github.com/OpenCommunityEdition/OpenCE
sources:
- https://github.com/OpenCommunityEdition/OpenCE/tree/main/deploy/helm/opence-server
keywords:
- halo
- game-server
+111
View File
@@ -0,0 +1,111 @@
# opence-server
The OpenCE dedicated server (`docs/dedicated_server.md`) on Kubernetes: one
pod of the image `ghcr.io/opencommunityedition/opence-server`
(`deploy/docker`), its tunnel port behind a UDP Service, its web page behind
a ClusterIP Service, probes on `GET /healthz`, and a NetworkPolicy.
The game data is never in the image or the chart: give a volume that holds
`maps/` from your own copy of the game.
## Quick start
```sh
kubectl create namespace halo
# a claim (or an NFS export) holding maps/, read-only is enough
helm install halo deploy/helm/opence-server -n halo \
--set gameData.existingClaim=halo-maps \
--set server.name="My server" \
--set persistence.enabled=true
kubectl -n halo logs -f deploy/halo-opence-server
```
The admin password of the web page is made at the first install and kept in
the release's Secret (NOTES.txt prints the command that shows it), unless
`web.password` or `web.existingSecret` gives one. Reach the page with
`kubectl -n halo port-forward deploy/halo-opence-server 8080`, or put an
Ingress with TLS in front of the `-web` Service (and allow its namespace
in `networkPolicy.webFrom`).
## Values
`values.yaml` describes each value. Each one under `server`, `web`,
`network` and `publisherKey` is one of the server's `HALO_*` variables:
| Value | Variable |
| --- | --- |
| `server.name` | `HALO_SERVER_NAME` (15 characters) |
| `server.rotation` (a list) | `HALO_SERVER_ROTATION`, joined with commas |
| `server.maxPlayers` | `HALO_SERVER_MAX_PLAYERS` |
| `server.public` | `HALO_SERVER_PUBLIC` |
| `server.lobbySeconds` | `HALO_SERVER_LOBBY_SECONDS` |
| `server.motd` | `HALO_SERVER_MOTD` |
| `server.logFormat` | `HALO_SERVER_LOG_FORMAT` |
| `server.instance`, `server.region` | `HALO_SERVER_INSTANCE`, `HALO_SERVER_REGION` |
| `server.masterPublish` | `HALO_SERVER_MASTER_PUBLISH` |
| `server.password` | `HALO_SERVER_PASSWORD` (from a Secret) |
| `publisherKey` | `HALO_SERVER_PUBLISHER_KEY` (from a Secret) |
| `web.port` | `HALO_SERVER_WEB_PORT` (0: none, and no probes) |
| `web.password`, `web.existingSecret` | `HALO_SERVER_WEB_PASSWORD` (from a Secret) |
| `web.moderatorPassword` | `HALO_SERVER_WEB_MODERATOR_PASSWORD` (from a Secret) |
| `network.tunnelPort` | `HALO_NET_TUNNEL_PORT` |
| `network.publicAddress` | `HALO_NET_PUBLIC_ADDRESS` |
| `customMaps` | `HALO_CUSTOM_MAPS_ROOT=/custom_maps` |
The chart also sets `HALO_SERVER=1`, `HALO_CONFIG_FILE=/state/config.toml`,
`HALO_DATA_ROOT=/data`, `HALO_SAVE_ROOT=/state/saves`,
`HALO_LOG_ROOT=/state/logs`, `HALO_SERVER_CONSOLE=false` (a pod has no
standard input) and, with a web port, `HALO_SERVER_WEB_BIND=0.0.0.0` (the
kubelet's probes reach the pod's address). Any other setting goes in
`extraEnv`, which comes last and so overrides the chart's.
A variable the chart sets wins over `config.toml`, and neither `sv_set` nor
the web page's settings can change it. So the values under `server` are
unset by default (empty, `[]` or `null`): the chart then sets no variable,
and the setting is `config.toml`'s (or the server's default, which
`values.yaml` names), changeable from the web page and kept in the state
volume. Give a value only for a setting you want fixed.
## Web password
With neither `web.password` nor `web.existingSecret`, the chart makes an
admin password at the first install and keeps it in the release's Secret;
on an upgrade it finds that Secret again with `lookup`. `helm template`,
and GitOps tools that render with it (Argo CD, Flux's dry runs,
Kustomize's Helm support), have no cluster to look in: each render makes a
new password, and the Secret changes at every sync. There, create the Secret
yourself and name it:
```sh
kubectl -n halo create secret generic halo-web \
--from-literal=web-password="$(openssl rand -base64 18)" \
--from-literal=web-moderator-password="$(openssl rand -base64 18)"
helm template halo deploy/helm/opence-server -n halo \
--set gameData.existingClaim=halo-maps --set web.existingSecret=halo-web
```
(or a SealedSecret or an External Secrets object of the same name).
`web.key` and `web.moderatorKey` name its keys; the moderator's is
optional.
## Networking
`service.type` is `LoadBalancer` with `externalTrafficPolicy: Local` by
default, so that the server sees the players' own addresses (its bans and
limits are by address). `NodePort` works too; `ClusterIP` is for tests. The
tunnel's Service port and `network.tunnelPort` should be the same number, as
the server offers its own port to joiners; behind a NAT that STUN cannot see
through, give the forwarded address in `network.publicAddress`.
The NetworkPolicy lets anyone reach the tunnel port and only `webFrom` the
web port, and lets the pod reach DNS, TCP 1883 (the MQTT brokers) and 443
(the master servers) and any UDP port on public addresses.
## Tests
```sh
helm lint deploy/helm/opence-server --strict -f deploy/helm/opence-server/ci/full-values.yaml
helm unittest deploy/helm/opence-server
```
`.github/workflows/server-image.yml` runs both.
@@ -0,0 +1,87 @@
# Every option the chart has, for lint and the schema check (not installed: the NFS server is made
# up).
image:
tag: build-206@sha256:0000000000000000000000000000000000000000000000000000000000000000
imagePullSecrets:
- name: registry-credentials
gameData:
nfs:
server: nfs.example.com
path: /exports/halo
subPath: xbox
customMaps:
existingClaim: halo-custom-maps
server:
name: Example server
rotation: [bloodgulch:ctf_classic, sidewinder:team_slayer+score=50, hangemhigh:king_moving]
maxPlayers: 24
public: true
lobbySeconds: 20
motd: Welcome
logFormat: json
instance: eu-1
region: eu
masterPublish: true
password:
existingSecret: halo-join
key: password
publisherKey:
existingSecret: halo-publisher
key: seed
web:
port: 8080
existingSecret: halo-web
key: admin
moderatorKey: moderator
network:
tunnelPort: 2303
publicAddress: halo.example.com:2303
extraEnv:
- name: HALO_SERVER_EMPTY_SECONDS
value: "60"
persistence:
enabled: true
storageClass: standard
annotations:
example.com/keep: "true"
service:
type: LoadBalancer
port: 2303
nodePort: 30703
annotations:
example.com/address: 192.0.2.10
externalTrafficPolicy: Local
loadBalancerSourceRanges: [0.0.0.0/0]
tolerations:
- key: dedicated
operator: Equal
value: games
effect: NoSchedule
podAnnotations:
example.com/scrape: "true"
podLabels:
team: games
probes:
liveness:
periodSeconds: 30
readiness:
enabled: false
networkPolicy:
webFrom:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
extraEgress:
- to:
- ipBlock:
cidr: 203.0.113.0/24
ports:
- protocol: TCP
port: 8443
extraObjects:
- apiVersion: v1
kind: ConfigMap
metadata:
name: '{{ .Release.Name }}-extra'
data:
hello: world
@@ -0,0 +1,6 @@
# The smallest install that renders: no maps (an emptyDir in their place), so the server exits with
# code 2 and the pod goes into CrashLoopBackOff. A real install gives gameData.existingClaim or
# gameData.nfs instead.
gameData:
volume:
emptyDir: {}
@@ -0,0 +1,33 @@
OpenCE dedicated server {{ .Chart.Version }}: {{ include "opence-server.fullname" . }} in namespace {{ .Release.Namespace }}.
Rotation: {{ join ", " .Values.server.rotation | default "the server's default" }}.
{{- if kindIs "invalid" .Values.server.public }}
Listed in everyone's server browser{{ with .Values.server.name }} as "{{ . }}"{{ end }}, unless config.toml or the web page turns server.public off.
{{- else if .Values.server.public }}
Listed in everyone's server browser{{ with .Values.server.name }} as "{{ . }}"{{ end }}.
{{- else }}
Not listed: players join with the invite link from the log, or on the LAN.
{{- end }}
The log (the ready line, players, games):
kubectl -n {{ .Release.Namespace }} logs -f deploy/{{ include "opence-server.fullname" . }}
Players reach UDP port {{ .Values.service.port }} through Service {{ include "opence-server.fullname" . }} ({{ .Values.service.type }}).
{{- if eq .Values.service.type "ClusterIP" }}
A ClusterIP Service is not reachable from outside the cluster: set service.type to LoadBalancer or
NodePort for internet play.
{{- end }}
{{- if gt (int .Values.web.port) 0 }}
The web page, API and remote console:
kubectl -n {{ .Release.Namespace }} port-forward deploy/{{ include "opence-server.fullname" . }} {{ .Values.web.port }}
open http://127.0.0.1:{{ .Values.web.port }}/
{{- if not .Values.web.existingSecret }}
The admin password:
kubectl -n {{ .Release.Namespace }} get secret {{ include "opence-server.secretName" . }} -o jsonpath='{.data.{{ .Values.web.key }}}' | base64 -d
{{- end }}
{{- end }}
@@ -0,0 +1,84 @@
{{/* The objects' name: fullnameOverride, else the release name if it contains the chart's name,
else <release>-<chart>. */}}
{{- define "opence-server.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end }}
{{- define "opence-server.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end }}
{{- define "opence-server.labels" -}}
{{ include "opence-server.selectorLabels" . }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "opence-server.selectorLabels" -}}
app.kubernetes.io/name: {{ include "opence-server.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/* repository:tag, repository:tag@digest, or repository@digest; an empty tag is the appVersion. */}}
{{- define "opence-server.image" -}}
{{- $image := .Values.image -}}
{{- $ref := $image.repository -}}
{{- $tag := toString ($image.tag | default .Chart.AppVersion) -}}
{{- if $image.digest }}{{ $tag = regexReplaceAll "@.*$" $tag "" }}{{ if hasPrefix "sha256:" $tag }}{{ $tag = "" }}{{ end }}{{ end -}}
{{- if hasPrefix "sha256:" $tag }}{{ $ref = printf "%s@%s" $ref $tag }}
{{- else if $tag }}{{ $ref = printf "%s:%s" $ref $tag }}{{ end -}}
{{- if and $image.digest (not (contains "@" $ref)) }}{{ $ref = printf "%s@%s" $ref $image.digest }}{{ end -}}
{{- $ref -}}
{{- end }}
{{- define "opence-server.stateClaim" -}}
{{- .Values.persistence.existingClaim | default (printf "%s-state" (include "opence-server.fullname" .)) -}}
{{- end }}
{{/* The chart's Secret: the web passwords it makes or is given, the join password, the publisher
key. */}}
{{- define "opence-server.secretName" -}}
{{- printf "%s-secrets" (include "opence-server.fullname" .) -}}
{{- end }}
{{/* The Secret holding the web password: web.existingSecret, else the chart's. */}}
{{- define "opence-server.webSecret" -}}
{{- .Values.web.existingSecret | default (include "opence-server.secretName" .) -}}
{{- end }}
{{- define "opence-server.hasGameData" -}}
{{- with .Values.gameData -}}
{{- if or .existingClaim (and .nfs.server .nfs.path) .volume }}true{{ end -}}
{{- end -}}
{{- end }}
{{- define "opence-server.hasCustomMaps" -}}
{{- with .Values.customMaps -}}
{{- if or .existingClaim (and .nfs.server .nfs.path) .volume }}true{{ end -}}
{{- end -}}
{{- end }}
{{/* A volume source of gameData or customMaps (the dict), read-only. */}}
{{- define "opence-server.volumeSource" -}}
{{- if .existingClaim }}
persistentVolumeClaim:
claimName: {{ .existingClaim }}
readOnly: true
{{- else if and .nfs.server .nfs.path }}
nfs:
server: {{ .nfs.server }}
path: {{ .nfs.path }}
readOnly: true
{{- else }}
{{- toYaml .volume }}
{{- end }}
{{- end }}
@@ -0,0 +1,198 @@
{{- if not (include "opence-server.hasGameData" .) }}
{{- fail "gameData: set existingClaim, nfs.server and nfs.path, or volume - the server needs the game's maps, which are never in the image" }}
{{- end }}
{{- $fullname := include "opence-server.fullname" . }}
{{- $secret := include "opence-server.secretName" . }}
{{- $webPort := int .Values.web.port }}
{{- $env := list }}
{{- $env = append $env (dict "name" "HALO_SERVER" "value" "1") }}
{{- $env = append $env (dict "name" "HALO_CONFIG_FILE" "value" "/state/config.toml") }}
{{- $env = append $env (dict "name" "HALO_DATA_ROOT" "value" "/data") }}
{{- $env = append $env (dict "name" "HALO_SAVE_ROOT" "value" "/state/saves") }}
{{- $env = append $env (dict "name" "HALO_LOG_ROOT" "value" "/state/logs") }}
{{- if include "opence-server.hasCustomMaps" . }}
{{- $env = append $env (dict "name" "HALO_CUSTOM_MAPS_ROOT" "value" "/custom_maps") }}
{{- end }}
{{- /* (a pod has no standard input to read commands from) */}}
{{- $env = append $env (dict "name" "HALO_SERVER_CONSOLE" "value" "false") }}
{{- $env = append $env (dict "name" "HALO_NET_TUNNEL_PORT" "value" (toString (int .Values.network.tunnelPort))) }}
{{- with .Values.network.publicAddress }}
{{- $env = append $env (dict "name" "HALO_NET_PUBLIC_ADDRESS" "value" .) }}
{{- end }}
{{- with .Values.server.name }}
{{- if gt (len .) 15 }}
{{- fail "server.name: 15 characters at most" }}
{{- end }}
{{- $env = append $env (dict "name" "HALO_SERVER_NAME" "value" .) }}
{{- end }}
{{- with .Values.server.rotation }}
{{- $env = append $env (dict "name" "HALO_SERVER_ROTATION" "value" (join ", " .)) }}
{{- end }}
{{- /* (a value left null is not set, so that config.toml, sv_set and the web page's settings can
change it) */}}
{{- if not (kindIs "invalid" .Values.server.maxPlayers) }}
{{- $env = append $env (dict "name" "HALO_SERVER_MAX_PLAYERS" "value" (toString (int .Values.server.maxPlayers))) }}
{{- end }}
{{- if not (kindIs "invalid" .Values.server.public) }}
{{- $env = append $env (dict "name" "HALO_SERVER_PUBLIC" "value" (toString .Values.server.public)) }}
{{- end }}
{{- if not (kindIs "invalid" .Values.server.lobbySeconds) }}
{{- $env = append $env (dict "name" "HALO_SERVER_LOBBY_SECONDS" "value" (toString (int .Values.server.lobbySeconds))) }}
{{- end }}
{{- with .Values.server.motd }}
{{- $env = append $env (dict "name" "HALO_SERVER_MOTD" "value" .) }}
{{- end }}
{{- with .Values.server.logFormat }}
{{- $env = append $env (dict "name" "HALO_SERVER_LOG_FORMAT" "value" .) }}
{{- end }}
{{- with .Values.server.instance }}
{{- $env = append $env (dict "name" "HALO_SERVER_INSTANCE" "value" .) }}
{{- end }}
{{- with .Values.server.region }}
{{- $env = append $env (dict "name" "HALO_SERVER_REGION" "value" .) }}
{{- end }}
{{- if not (kindIs "invalid" .Values.server.masterPublish) }}
{{- $env = append $env (dict "name" "HALO_SERVER_MASTER_PUBLISH" "value" (toString .Values.server.masterPublish)) }}
{{- end }}
{{- if .Values.server.password.existingSecret }}
{{- $env = append $env (dict "name" "HALO_SERVER_PASSWORD" "valueFrom" (dict "secretKeyRef" (dict "name" .Values.server.password.existingSecret "key" .Values.server.password.key))) }}
{{- else if .Values.server.password.value }}
{{- $env = append $env (dict "name" "HALO_SERVER_PASSWORD" "valueFrom" (dict "secretKeyRef" (dict "name" $secret "key" .Values.server.password.key))) }}
{{- end }}
{{- if .Values.publisherKey.existingSecret }}
{{- $env = append $env (dict "name" "HALO_SERVER_PUBLISHER_KEY" "valueFrom" (dict "secretKeyRef" (dict "name" .Values.publisherKey.existingSecret "key" .Values.publisherKey.key))) }}
{{- else if .Values.publisherKey.value }}
{{- $env = append $env (dict "name" "HALO_SERVER_PUBLISHER_KEY" "valueFrom" (dict "secretKeyRef" (dict "name" $secret "key" .Values.publisherKey.key))) }}
{{- end }}
{{- $env = append $env (dict "name" "HALO_SERVER_WEB_PORT" "value" (toString $webPort)) }}
{{- if gt $webPort 0 }}
{{- /* (the kubelet's probes reach the pod's own address, not its loopback) */}}
{{- $env = append $env (dict "name" "HALO_SERVER_WEB_BIND" "value" "0.0.0.0") }}
{{- $env = append $env (dict "name" "HALO_SERVER_WEB_PASSWORD" "valueFrom" (dict "secretKeyRef" (dict "name" (include "opence-server.webSecret" .) "key" .Values.web.key))) }}
{{- if .Values.web.existingSecret }}
{{- $env = append $env (dict "name" "HALO_SERVER_WEB_MODERATOR_PASSWORD" "valueFrom" (dict "secretKeyRef" (dict "name" .Values.web.existingSecret "key" .Values.web.moderatorKey "optional" true))) }}
{{- else if .Values.web.moderatorPassword }}
{{- $env = append $env (dict "name" "HALO_SERVER_WEB_MODERATOR_PASSWORD" "valueFrom" (dict "secretKeyRef" (dict "name" $secret "key" .Values.web.moderatorKey))) }}
{{- end }}
{{- end }}
{{- $probe := dict "httpGet" (dict "path" "/healthz" "port" "web") }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ $fullname }}
labels:
{{- include "opence-server.labels" . | nindent 4 }}
spec:
# One server owns the tunnel port and the address players reach it at.
replicas: 1
strategy:
{{- toYaml .Values.strategy | nindent 4 }}
selector:
matchLabels:
{{- include "opence-server.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "opence-server.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
# The server never calls the Kubernetes API.
automountServiceAccountToken: false
enableServiceLinks: false
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.priorityClassName }}
priorityClassName: {{ . }}
{{- end }}
terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: opence-server
image: {{ include "opence-server.image" . | quote }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
{{- toYaml $env | nindent 12 }}
{{- with .Values.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- name: tunnel
containerPort: {{ .Values.network.tunnelPort }}
protocol: UDP
{{- if gt $webPort 0 }}
- name: web
containerPort: {{ $webPort }}
protocol: TCP
{{- end }}
{{- if gt $webPort 0 }}
{{- range $name := list "startup" "readiness" "liveness" }}
{{- $p := index $.Values.probes $name }}
{{- if $p.enabled }}
{{ $name }}Probe:
{{- toYaml (merge (omit $p "enabled") $probe) | nindent 12 }}
{{- end }}
{{- end }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
volumeMounts:
- name: game-data
mountPath: /data
readOnly: true
{{- with .Values.gameData.subPath }}
subPath: {{ . }}
{{- end }}
# config.toml, publisher.key, the save root and its map cache, the logs
- name: state
mountPath: /state
{{- if include "opence-server.hasCustomMaps" . }}
- name: custom-maps
mountPath: /custom_maps
readOnly: true
{{- with .Values.customMaps.subPath }}
subPath: {{ . }}
{{- end }}
{{- end }}
- name: tmp
mountPath: /tmp
volumes:
- name: game-data
{{- include "opence-server.volumeSource" .Values.gameData | trim | nindent 10 }}
- name: state
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "opence-server.stateClaim" . }}
{{- else }}
emptyDir:
sizeLimit: {{ .Values.persistence.emptyDirSizeLimit }}
{{- end }}
- name: tmp
emptyDir:
sizeLimit: 64Mi
{{- if include "opence-server.hasCustomMaps" . }}
- name: custom-maps
{{- include "opence-server.volumeSource" .Values.customMaps | trim | nindent 10 }}
{{- end }}
@@ -0,0 +1,4 @@
{{- range .Values.extraObjects }}
---
{{ tpl (toYaml .) $ }}
{{- end }}
@@ -0,0 +1,66 @@
{{- if .Values.networkPolicy.enabled }}
{{- $np := .Values.networkPolicy }}
# In: the players' UDP on the tunnel port, and the web port from webFrom alone. Out: DNS, TCP to
# the MQTT brokers and the master servers, UDP to the internet (STUN, the players' ports); nothing
# on the private ranges.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "opence-server.fullname" . }}
labels:
{{- include "opence-server.labels" . | nindent 4 }}
spec:
podSelector:
matchLabels:
{{- include "opence-server.selectorLabels" . | nindent 6 }}
policyTypes:
- Ingress
- Egress
ingress:
- ports:
- protocol: UDP
port: {{ .Values.network.tunnelPort }}
{{- with $np.from }}
from:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if and $np.webFrom (gt (int .Values.web.port) 0) }}
- ports:
- protocol: TCP
port: {{ .Values.web.port }}
from:
{{- toYaml $np.webFrom | nindent 8 }}
{{- end }}
{{- with $np.extraIngress }}
{{- toYaml . | nindent 4 }}
{{- end }}
egress:
- to:
{{- toYaml $np.dns.to | nindent 8 }}
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
{{- if or $np.internet.tcpPorts $np.internet.udp }}
- to:
- ipBlock:
cidr: {{ $np.internet.cidr }}
{{- with $np.internet.except }}
except:
{{- toYaml . | nindent 14 }}
{{- end }}
ports:
{{- range $np.internet.tcpPorts }}
- protocol: TCP
port: {{ . }}
{{- end }}
{{- if $np.internet.udp }}
# every UDP port: STUN (19302, 3478) and the players' own
- protocol: UDP
{{- end }}
{{- end }}
{{- with $np.extraEgress }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
@@ -0,0 +1,44 @@
{{- $data := dict }}
{{- if and (gt (int .Values.web.port) 0) (not .Values.web.existingSecret) }}
{{- $password := .Values.web.password }}
{{- if $password }}
{{- if lt (len $password) 8 }}
{{- fail "web.password: 8 characters or more (the server refuses a shorter one beyond loopback)" }}
{{- end }}
{{- else }}
{{- /* made once, then kept from the Secret the release already has */}}
{{- $existing := lookup "v1" "Secret" .Release.Namespace (include "opence-server.secretName" .) }}
{{- if and $existing $existing.data (hasKey $existing.data .Values.web.key) }}
{{- $password = index $existing.data .Values.web.key | b64dec }}
{{- else }}
{{- $password = randAlphaNum 24 }}
{{- end }}
{{- end }}
{{- $_ := set $data .Values.web.key ($password | b64enc) }}
{{- with .Values.web.moderatorPassword }}
{{- if lt (len .) 8 }}
{{- fail "web.moderatorPassword: 8 characters or more" }}
{{- end }}
{{- $_ := set $data $.Values.web.moderatorKey (. | b64enc) }}
{{- end }}
{{- end }}
{{- if and .Values.server.password.value (not .Values.server.password.existingSecret) }}
{{- $_ := set $data .Values.server.password.key (.Values.server.password.value | b64enc) }}
{{- end }}
{{- if and .Values.publisherKey.value (not .Values.publisherKey.existingSecret) }}
{{- if not (regexMatch "^[0-9a-f]{64}$" .Values.publisherKey.value) }}
{{- fail "publisherKey.value: the 64 lower-case hex characters of the seed (halo --server --keygen)" }}
{{- end }}
{{- $_ := set $data .Values.publisherKey.key (.Values.publisherKey.value | b64enc) }}
{{- end }}
{{- if $data }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "opence-server.secretName" . }}
labels:
{{- include "opence-server.labels" . | nindent 4 }}
type: Opaque
data:
{{- toYaml $data | nindent 2 }}
{{- end }}
@@ -0,0 +1,23 @@
{{- if and .Values.web.service.enabled (gt (int .Values.web.port) 0) }}
# The web page, its API and the remote console, inside the cluster only: for an Ingress with TLS,
# or kubectl port-forward.
apiVersion: v1
kind: Service
metadata:
name: {{ include "opence-server.fullname" . }}-web
labels:
{{- include "opence-server.labels" . | nindent 4 }}
{{- with .Values.web.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: ClusterIP
selector:
{{- include "opence-server.selectorLabels" . | nindent 4 }}
ports:
- name: web
port: {{ .Values.web.service.port }}
targetPort: web
protocol: TCP
{{- end }}
@@ -0,0 +1,40 @@
# The tunnel port, UDP: the one port players need.
apiVersion: v1
kind: Service
metadata:
name: {{ include "opence-server.fullname" . }}
labels:
{{- include "opence-server.labels" . | nindent 4 }}
{{- with .Values.service.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
{{- if ne .Values.service.type "ClusterIP" }}
{{- with .Values.service.externalTrafficPolicy }}
externalTrafficPolicy: {{ . }}
{{- end }}
{{- end }}
{{- if eq .Values.service.type "LoadBalancer" }}
{{- with .Values.service.loadBalancerClass }}
loadBalancerClass: {{ . }}
{{- end }}
{{- with .Values.service.loadBalancerSourceRanges }}
loadBalancerSourceRanges:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
selector:
{{- include "opence-server.selectorLabels" . | nindent 4 }}
ports:
- name: tunnel
port: {{ .Values.service.port }}
targetPort: tunnel
protocol: UDP
{{- if and (ne .Values.service.type "ClusterIP") .Values.service.nodePort }}
nodePort: {{ .Values.service.nodePort }}
{{- end }}
@@ -0,0 +1,21 @@
{{- if and .Values.persistence.enabled (not .Values.persistence.existingClaim) }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "opence-server.stateClaim" . }}
labels:
{{- include "opence-server.labels" . | nindent 4 }}
{{- with .Values.persistence.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
{{- toYaml .Values.persistence.accessModes | nindent 4 }}
{{- with .Values.persistence.storageClass }}
storageClassName: {{ . }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.size }}
{{- end }}
@@ -0,0 +1,221 @@
suite: deployment
templates:
- templates/deployment.yaml
set:
gameData.existingClaim: halo-maps
tests:
- it: refuses to render without game data, which is never in the image
set:
gameData.existingClaim: ""
asserts:
- failedTemplate:
errorPattern: "gameData: set existingClaim"
- it: mounts an existing claim read-only at /data
asserts:
- equal:
path: 'spec.template.spec.volumes[0]'
value: { name: game-data, persistentVolumeClaim: { claimName: halo-maps, readOnly: true } }
- contains:
path: 'spec.template.spec.containers[0].volumeMounts'
content: { name: game-data, mountPath: /data, readOnly: true }
- it: mounts an NFS export read-only, with a subPath
set:
gameData: { existingClaim: "", nfs: { server: nfs.example.com, path: /halo }, subPath: xbox }
asserts:
- equal:
path: 'spec.template.spec.volumes[0]'
value: { name: game-data, nfs: { server: nfs.example.com, path: /halo, readOnly: true } }
- contains:
path: 'spec.template.spec.containers[0].volumeMounts'
content: { name: game-data, mountPath: /data, readOnly: true, subPath: xbox }
- it: takes any other volume source
set:
gameData: { existingClaim: "", volume: { emptyDir: {} } }
asserts:
- equal:
path: 'spec.template.spec.volumes[0]'
value: { name: game-data, emptyDir: {} }
- it: runs one replica, replaced not rolled, hardened
asserts:
- equal: { path: spec.replicas, value: 1 }
- equal: { path: spec.strategy.type, value: Recreate }
- equal: { path: spec.template.spec.automountServiceAccountToken, value: false }
- equal: { path: spec.template.spec.securityContext.runAsNonRoot, value: true }
- equal: { path: spec.template.spec.securityContext.runAsUser, value: 10001 }
- equal: { path: spec.template.spec.securityContext.seccompProfile.type, value: RuntimeDefault }
- equal: { path: 'spec.template.spec.containers[0].securityContext.readOnlyRootFilesystem', value: true }
- equal: { path: 'spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation', value: false }
- equal: { path: 'spec.template.spec.containers[0].securityContext.capabilities.drop', value: [ALL] }
- equal: { path: spec.template.spec.nodeSelector, value: { kubernetes.io/os: linux, kubernetes.io/arch: amd64 } }
- equal: { path: spec.template.spec.terminationGracePeriodSeconds, value: 15 }
- exists: { path: 'spec.template.spec.containers[0].resources.limits.memory' }
- it: sets the server's paths and mode
asserts:
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER, value: "1" } }
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_CONFIG_FILE, value: /state/config.toml } }
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_DATA_ROOT, value: /data } }
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SAVE_ROOT, value: /state/saves } }
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_LOG_ROOT, value: /state/logs } }
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER_CONSOLE, value: "false" } }
- it: maps the values to the HALO_ variables
set:
server:
name: Test server
rotation: [bloodgulch:ctf, "sidewinder:team_slayer+score=50"]
maxPlayers: 24
public: false
lobbySeconds: 30
motd: Hello
logFormat: json
instance: eu-1
region: eu
masterPublish: false
network: { tunnelPort: 2303, publicAddress: "halo.example.com:2303" }
asserts:
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER_NAME, value: Test server } }
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER_ROTATION, value: "bloodgulch:ctf, sidewinder:team_slayer+score=50" } }
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER_MAX_PLAYERS, value: "24" } }
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER_PUBLIC, value: "false" } }
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER_LOBBY_SECONDS, value: "30" } }
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER_MOTD, value: Hello } }
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER_LOG_FORMAT, value: json } }
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER_INSTANCE, value: eu-1 } }
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER_REGION, value: eu } }
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER_MASTER_PUBLISH, value: "false" } }
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_NET_TUNNEL_PORT, value: "2303" } }
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_NET_PUBLIC_ADDRESS, value: "halo.example.com:2303" } }
- equal: { path: 'spec.template.spec.containers[0].ports[0]', value: { name: tunnel, containerPort: 2303, protocol: UDP } }
- it: leaves empty values to config.toml
asserts:
- notContains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER_NAME, value: "" } }
- notContains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER_MOTD, value: "" } }
- notContains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_NET_PUBLIC_ADDRESS, value: "" } }
- it: leaves the settings the values do not give to config.toml, sv_set and the web page
asserts:
- notExists: { path: 'spec.template.spec.containers[0].env[?(@.name == "HALO_SERVER_ROTATION")]' }
- notExists: { path: 'spec.template.spec.containers[0].env[?(@.name == "HALO_SERVER_MAX_PLAYERS")]' }
- notExists: { path: 'spec.template.spec.containers[0].env[?(@.name == "HALO_SERVER_PUBLIC")]' }
- notExists: { path: 'spec.template.spec.containers[0].env[?(@.name == "HALO_SERVER_LOBBY_SECONDS")]' }
- notExists: { path: 'spec.template.spec.containers[0].env[?(@.name == "HALO_SERVER_LOG_FORMAT")]' }
- notExists: { path: 'spec.template.spec.containers[0].env[?(@.name == "HALO_SERVER_MASTER_PUBLISH")]' }
- it: sets a false or zero value that is given
set:
server: { public: false, masterPublish: false, lobbySeconds: 0 }
asserts:
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER_PUBLIC, value: "false" } }
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER_MASTER_PUBLISH, value: "false" } }
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER_LOBBY_SECONDS, value: "0" } }
- it: refuses a name longer than the game's 15 characters (the values' schema, or the template)
set:
server.name: "A name too long!"
asserts:
- failedTemplate:
errorPattern: "server.name|/server/name|15 characters"
- it: takes the web password from the chart's Secret, bound beyond loopback for the probes
release: { name: opence-server }
asserts:
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER_WEB_PORT, value: "8080" } }
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER_WEB_BIND, value: 0.0.0.0 } }
- contains:
path: 'spec.template.spec.containers[0].env'
content: { name: HALO_SERVER_WEB_PASSWORD, valueFrom: { secretKeyRef: { name: opence-server-secrets, key: web-password } } }
- contains: { path: 'spec.template.spec.containers[0].ports', content: { name: web, containerPort: 8080, protocol: TCP } }
- it: takes the passwords and the publisher key from existing Secrets
set:
web: { existingSecret: halo-web, key: admin, moderatorKey: moderator }
server.password: { existingSecret: halo-join, key: password }
publisherKey: { existingSecret: halo-publisher, key: seed }
asserts:
- contains:
path: 'spec.template.spec.containers[0].env'
content: { name: HALO_SERVER_WEB_PASSWORD, valueFrom: { secretKeyRef: { name: halo-web, key: admin } } }
- contains:
path: 'spec.template.spec.containers[0].env'
content: { name: HALO_SERVER_WEB_MODERATOR_PASSWORD, valueFrom: { secretKeyRef: { name: halo-web, key: moderator, optional: true } } }
- contains:
path: 'spec.template.spec.containers[0].env'
content: { name: HALO_SERVER_PASSWORD, valueFrom: { secretKeyRef: { name: halo-join, key: password } } }
- contains:
path: 'spec.template.spec.containers[0].env'
content: { name: HALO_SERVER_PUBLISHER_KEY, valueFrom: { secretKeyRef: { name: halo-publisher, key: seed } } }
- it: probes GET /healthz on the web port
asserts:
- equal: { path: 'spec.template.spec.containers[0].startupProbe.httpGet', value: { path: /healthz, port: web } }
- equal: { path: 'spec.template.spec.containers[0].readinessProbe.httpGet', value: { path: /healthz, port: web } }
- equal: { path: 'spec.template.spec.containers[0].livenessProbe.httpGet', value: { path: /healthz, port: web } }
- equal: { path: 'spec.template.spec.containers[0].startupProbe.failureThreshold', value: 60 }
- it: drops a probe that is turned off
set:
probes.liveness.enabled: false
asserts:
- notExists: { path: 'spec.template.spec.containers[0].livenessProbe' }
- exists: { path: 'spec.template.spec.containers[0].readinessProbe' }
- it: has no web port, password or probes with web.port 0
set:
web.port: 0
asserts:
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER_WEB_PORT, value: "0" } }
- notContains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_SERVER_WEB_BIND, value: 0.0.0.0 } }
- lengthEqual: { path: 'spec.template.spec.containers[0].ports', count: 1 }
- notExists: { path: 'spec.template.spec.containers[0].startupProbe' }
- notExists: { path: 'spec.template.spec.containers[0].livenessProbe' }
- it: puts extraEnv after the chart's variables, so it can override them
set:
extraEnv: [{ name: HALO_SERVER_LOBBY_SECONDS, value: "5" }]
asserts:
- equal:
path: 'spec.template.spec.containers[0].env[-1:]'
value: { name: HALO_SERVER_LOBBY_SECONDS, value: "5" }
- it: keeps the state in an emptyDir without persistence
asserts:
- contains: { path: spec.template.spec.volumes, content: { name: state, emptyDir: { sizeLimit: 1Gi } } }
- lengthEqual: { path: spec.template.spec.volumes, count: 3 }
- it: uses the claim with persistence
release: { name: opence-server }
set:
persistence.enabled: true
asserts:
- contains: { path: spec.template.spec.volumes, content: { name: state, persistentVolumeClaim: { claimName: opence-server-state } } }
- it: mounts a custom_maps claim read-only, with its variable
set:
customMaps: { existingClaim: halo-custom-maps, subPath: ce }
asserts:
- contains: { path: 'spec.template.spec.containers[0].env', content: { name: HALO_CUSTOM_MAPS_ROOT, value: /custom_maps } }
- contains:
path: spec.template.spec.volumes
content: { name: custom-maps, persistentVolumeClaim: { claimName: halo-custom-maps, readOnly: true } }
- contains:
path: 'spec.template.spec.containers[0].volumeMounts'
content: { name: custom-maps, mountPath: /custom_maps, readOnly: true, subPath: ce }
- it: takes a tag with a digest, and lets digest replace it
set:
image.tag: build-206@sha256:1111111111111111111111111111111111111111111111111111111111111111
image.digest: sha256:2222222222222222222222222222222222222222222222222222222222222222
asserts:
- equal:
path: 'spec.template.spec.containers[0].image'
value: ghcr.io/opencommunityedition/opence-server:build-206@sha256:2222222222222222222222222222222222222222222222222222222222222222
- it: uses the appVersion as the default tag
asserts:
- equal: { path: 'spec.template.spec.containers[0].image', value: "ghcr.io/opencommunityedition/opence-server:latest" }
@@ -0,0 +1,39 @@
suite: network policy
templates:
- templates/networkpolicy.yaml
tests:
- it: lets anyone reach the tunnel port, and nothing else in
asserts:
- equal: { path: spec.policyTypes, value: [Ingress, Egress] }
- equal: { path: spec.ingress, value: [{ ports: [{ protocol: UDP, port: 2302 }] }] }
- it: allows DNS, TCP 1883 and 443 and all UDP to public addresses only
asserts:
- equal: { path: 'spec.egress[0].ports', value: [{ protocol: UDP, port: 53 }, { protocol: TCP, port: 53 }] }
- equal: { path: 'spec.egress[1].to[0].ipBlock.cidr', value: 0.0.0.0/0 }
- contains: { path: 'spec.egress[1].to[0].ipBlock.except', content: 10.0.0.0/8 }
- contains: { path: 'spec.egress[1].to[0].ipBlock.except', content: 192.168.0.0/16 }
- equal: { path: 'spec.egress[1].ports', value: [{ protocol: TCP, port: 1883 }, { protocol: TCP, port: 443 }, { protocol: UDP }] }
- it: lets webFrom reach the web port
set:
networkPolicy.webFrom: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: ingress-nginx } } }]
asserts:
- equal:
path: 'spec.ingress[1]'
value: { ports: [{ protocol: TCP, port: 8080 }], from: [{ namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: ingress-nginx } } }] }
- it: narrows the players' sources and follows the tunnel port
set:
network.tunnelPort: 2303
networkPolicy.from: [{ ipBlock: { cidr: 198.51.100.0/24 } }]
asserts:
- equal:
path: spec.ingress
value: [{ ports: [{ protocol: UDP, port: 2303 }], from: [{ ipBlock: { cidr: 198.51.100.0/24 } }] }]
- it: renders nothing when off
set:
networkPolicy.enabled: false
asserts:
- hasDocuments: { count: 0 }
@@ -0,0 +1,21 @@
suite: persistence
templates:
- templates/state-pvc.yaml
release:
name: opence-server
tests:
- it: makes no claim without persistence
asserts:
- hasDocuments: { count: 0 }
- it: makes the state claim with persistence
set:
persistence: { enabled: true, size: 3Gi, storageClass: fast }
asserts:
- equal: { path: metadata.name, value: opence-server-state }
- equal: { path: spec.resources.requests.storage, value: 3Gi }
- equal: { path: spec.storageClassName, value: fast }
- it: makes none for an existing claim
set:
persistence: { enabled: true, existingClaim: mine }
asserts:
- hasDocuments: { count: 0 }
@@ -0,0 +1,48 @@
suite: secret
templates:
- templates/secret.yaml
release:
name: opence-server
tests:
- it: makes a web password when none is given
asserts:
- hasDocuments: { count: 1 }
- isKind: { of: Secret }
- equal: { path: metadata.name, value: opence-server-secrets }
- matchRegex: { path: data.web-password, pattern: "^[A-Za-z0-9+/]{32}$" }
- it: keeps a given web password, join password and publisher key
set:
web.password: correct-horse
server.password.value: letmein
publisherKey.value: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
asserts:
- equal: { path: data.web-password, value: Y29ycmVjdC1ob3JzZQ== }
- equal: { path: data.password, value: bGV0bWVpbg== }
- exists: { path: data.publisher-key }
- it: refuses a short web password
set:
web.password: short
asserts:
- failedTemplate:
errorPattern: "8 characters or more"
- it: refuses a publisher key that is not 64 hex characters
set:
publisherKey.value: not-a-key
asserts:
- failedTemplate:
errorPattern: "64 lower-case hex"
- it: makes none when every secret comes from elsewhere
set:
web.existingSecret: halo-web
asserts:
- hasDocuments: { count: 0 }
- it: makes none without a web port or secrets
set:
web.port: 0
asserts:
- hasDocuments: { count: 0 }
@@ -0,0 +1,43 @@
suite: service
templates:
- templates/service.yaml
- templates/service-web.yaml
release:
name: opence-server
tests:
- it: exposes the tunnel port over UDP on a load balancer that keeps the players' addresses
template: templates/service.yaml
asserts:
- equal: { path: metadata.name, value: opence-server }
- equal: { path: spec.type, value: LoadBalancer }
- equal: { path: spec.externalTrafficPolicy, value: Local }
- equal: { path: 'spec.ports[0]', value: { name: tunnel, port: 2302, targetPort: tunnel, protocol: UDP } }
- it: ignores externalTrafficPolicy and nodePort on a ClusterIP Service
template: templates/service.yaml
set:
service: { type: ClusterIP, nodePort: 30702 }
asserts:
- notExists: { path: spec.externalTrafficPolicy }
- notExists: { path: 'spec.ports[0].nodePort' }
- it: sets a fixed node port
template: templates/service.yaml
set:
service: { type: NodePort, nodePort: 30702 }
asserts:
- equal: { path: 'spec.ports[0].nodePort', value: 30702 }
- it: keeps the web port inside the cluster
template: templates/service-web.yaml
asserts:
- equal: { path: metadata.name, value: opence-server-web }
- equal: { path: spec.type, value: ClusterIP }
- equal: { path: 'spec.ports[0]', value: { name: web, port: 8080, targetPort: web, protocol: TCP } }
- it: has no web Service without a web port
template: templates/service-web.yaml
set:
web.port: 0
asserts:
- hasDocuments: { count: 0 }
@@ -0,0 +1,445 @@
{
"$schema": "https://json-schema.org/draft-07/schema#",
"title": "opence-server values",
"description": "Strict where the chart defines the structure; free-form where values pass through to Kubernetes. values.yaml describes each value.",
"type": "object",
"additionalProperties": false,
"definitions": {
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535
},
"volume": {
"type": "object",
"additionalProperties": false,
"properties": {
"existingClaim": {
"type": "string"
},
"nfs": {
"type": "object",
"additionalProperties": false,
"properties": {
"server": {
"type": "string"
},
"path": {
"type": "string"
}
}
},
"volume": {
"type": "object"
},
"subPath": {
"type": "string"
}
}
},
"secretValue": {
"type": "object",
"additionalProperties": false,
"properties": {
"value": {
"type": "string"
},
"existingSecret": {
"type": "string"
},
"key": {
"type": "string",
"minLength": 1
}
}
},
"probe": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"initialDelaySeconds": {
"type": "integer",
"minimum": 0
},
"periodSeconds": {
"type": "integer",
"minimum": 1
},
"timeoutSeconds": {
"type": "integer",
"minimum": 1
},
"failureThreshold": {
"type": "integer",
"minimum": 1
},
"successThreshold": {
"type": "integer",
"minimum": 1
},
"terminationGracePeriodSeconds": {
"type": "integer",
"minimum": 1
}
}
}
},
"properties": {
"global": {
"type": "object"
},
"nameOverride": {
"type": "string"
},
"fullnameOverride": {
"type": "string"
},
"image": {
"type": "object",
"additionalProperties": false,
"required": [
"repository"
],
"properties": {
"repository": {
"type": "string",
"minLength": 1
},
"tag": {
"type": [
"string",
"number"
]
},
"digest": {
"type": "string",
"pattern": "^(sha256:[0-9a-f]{64})?$"
},
"pullPolicy": {
"enum": [
"Always",
"IfNotPresent",
"Never"
]
}
}
},
"imagePullSecrets": {
"type": "array"
},
"gameData": {
"$ref": "#/definitions/volume"
},
"customMaps": {
"$ref": "#/definitions/volume"
},
"server": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"maxLength": 15,
"pattern": "^[ -~]*$"
},
"rotation": {
"type": "array",
"maxItems": 64,
"items": {
"type": "string",
"minLength": 1
}
},
"maxPlayers": {
"type": [
"integer",
"null"
],
"minimum": 2,
"maximum": 127
},
"public": {
"type": [
"boolean",
"null"
]
},
"lobbySeconds": {
"type": [
"integer",
"null"
],
"minimum": 0,
"maximum": 300
},
"motd": {
"type": "string",
"maxLength": 127
},
"logFormat": {
"enum": [
"",
"text",
"json",
null
]
},
"instance": {
"type": "string",
"pattern": "^[A-Za-z0-9_-]{0,32}$"
},
"region": {
"type": "string",
"maxLength": 16
},
"masterPublish": {
"type": [
"boolean",
"null"
]
},
"password": {
"$ref": "#/definitions/secretValue"
}
}
},
"publisherKey": {
"$ref": "#/definitions/secretValue"
},
"web": {
"type": "object",
"additionalProperties": false,
"properties": {
"port": {
"type": "integer",
"minimum": 0,
"maximum": 65535
},
"password": {
"type": "string"
},
"existingSecret": {
"type": "string"
},
"key": {
"type": "string",
"minLength": 1
},
"moderatorPassword": {
"type": "string"
},
"moderatorKey": {
"type": "string",
"minLength": 1
},
"service": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"port": {
"$ref": "#/definitions/port"
},
"annotations": {
"type": "object"
}
}
}
}
},
"network": {
"type": "object",
"additionalProperties": false,
"properties": {
"tunnelPort": {
"$ref": "#/definitions/port"
},
"publicAddress": {
"type": "string"
}
}
},
"extraEnv": {
"type": "array"
},
"persistence": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"existingClaim": {
"type": "string"
},
"size": {
"type": "string"
},
"storageClass": {
"type": "string"
},
"accessModes": {
"type": "array",
"items": {
"type": "string"
}
},
"annotations": {
"type": "object"
},
"emptyDirSizeLimit": {
"type": "string"
}
}
},
"service": {
"type": "object",
"additionalProperties": false,
"properties": {
"type": {
"enum": [
"ClusterIP",
"NodePort",
"LoadBalancer"
]
},
"port": {
"$ref": "#/definitions/port"
},
"nodePort": {
"type": [
"string",
"integer"
]
},
"annotations": {
"type": "object"
},
"labels": {
"type": "object"
},
"externalTrafficPolicy": {
"enum": [
"",
"Cluster",
"Local"
]
},
"loadBalancerClass": {
"type": "string"
},
"loadBalancerSourceRanges": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"strategy": {
"type": "object"
},
"resources": {
"type": "object"
},
"nodeSelector": {
"type": "object"
},
"affinity": {
"type": "object"
},
"tolerations": {
"type": "array"
},
"priorityClassName": {
"type": "string"
},
"podAnnotations": {
"type": "object"
},
"podLabels": {
"type": "object"
},
"terminationGracePeriodSeconds": {
"type": "integer",
"minimum": 1
},
"podSecurityContext": {
"type": "object"
},
"securityContext": {
"type": "object"
},
"probes": {
"type": "object",
"additionalProperties": false,
"properties": {
"startup": {
"$ref": "#/definitions/probe"
},
"readiness": {
"$ref": "#/definitions/probe"
},
"liveness": {
"$ref": "#/definitions/probe"
}
}
},
"networkPolicy": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean"
},
"from": {
"type": "array"
},
"webFrom": {
"type": "array"
},
"dns": {
"type": "object"
},
"internet": {
"type": "object",
"additionalProperties": false,
"properties": {
"cidr": {
"type": "string"
},
"except": {
"type": "array",
"items": {
"type": "string"
}
},
"tcpPorts": {
"type": "array",
"items": {
"$ref": "#/definitions/port"
}
},
"udp": {
"type": "boolean"
}
}
},
"extraIngress": {
"type": "array"
},
"extraEgress": {
"type": "array"
}
}
},
"extraObjects": {
"type": "array"
}
}
}
+239
View File
@@ -0,0 +1,239 @@
# opence-server: the OpenCE dedicated server (docs/dedicated_server.md). One replica: the server
# owns its tunnel port and the address players reach it at.
nameOverride: ""
fullnameOverride: ""
image:
repository: ghcr.io/opencommunityedition/opence-server
# Empty: the chart's appVersion (latest). A release's tag (build-206) or sha-<commit> pins it;
# <tag>@sha256:... as Argo CD Image Updater writes it works too.
tag: ""
# sha256:... ; replaces any digest in tag.
digest: ""
pullPolicy: IfNotPresent
imagePullSecrets: []
# The game data: the folder that holds maps/ (and custom_maps/, gametypes/ and init.txt if you have
# them), from your own copy of the game. It is never in the image. Mounted read-only at /data
# (HALO_DATA_ROOT). One source is required.
gameData:
# An existing PersistentVolumeClaim.
existingClaim: ""
# Or an NFS export.
nfs:
server: ""
path: ""
# Or any other volume source, as in a pod's volumes (without the name), e.g.
# {hostPath: {path: /srv/halo, type: Directory}}. Without maps/ui.map and the rotation's maps the
# server exits with code 2 and says which maps it looked for, and the pod goes into
# CrashLoopBackOff.
volume: {}
# The folder within the volume that holds maps/. Empty: the volume's root.
subPath: ""
# A custom_maps folder of its own (Custom Edition maps with bitmaps.map, sounds.map and loc.map),
# mounted read-only at /custom_maps (HALO_CUSTOM_MAPS_ROOT). Optional: without one the server uses
# gameData's custom_maps/.
customMaps:
existingClaim: ""
nfs:
server: ""
path: ""
volume: {}
subPath: ""
# The server's settings, as its HALO_* environment variables (docs/dedicated_server.md and
# port/linux/README.md, "Dedicated server"). A value given here wins over config.toml, and sv_set
# and the web page's settings cannot change it. Each is unset by default (empty, or null): the
# setting is then config.toml's, or the server's default (named below), and can be changed from
# the web page.
server:
# server.name: 15 characters of printable ASCII (default "OpenCE Server").
name: ""
# server.rotation: map[:gametype[+rule=value...]] entries, played in turn, e.g.
# [bloodgulch:ctf, sidewinder:team_slayer, prisoner:slayer] (the default).
rotation: []
# server.max_players: 2 to 127 (default 16).
maxPlayers: null
# server.public: list the server in everyone's server browser (default true).
public: null
# server.lobby_seconds (default 15).
lobbySeconds: null
# server.motd: a notice each player is shown on joining.
motd: ""
# server.log_format: text or json (default text).
logFormat: ""
# server.instance and server.region, as the master servers list them.
instance: ""
region: ""
# server.master_publish: publish the server to the official master servers (needs a publisher
# key, below; default true).
masterPublish: null
# server.password: the password the server browser asks for. value, or a key of an existing
# Secret; empty for none.
password:
value: ""
existingSecret: ""
key: password
# The master servers' publisher key (HALO_SERVER_PUBLISHER_KEY): the 64 hex characters of the seed
# halo --server --keygen writes. value, or a key of an existing Secret; empty: the state volume's
# publisher.key, if there is one.
publisherKey:
value: ""
existingSecret: ""
key: publisher-key
# The web page, its API and the remote console (server.web_port), on which the probes ask
# GET /healthz. 0 turns them off, and the probes with them.
web:
port: 8080
# The admin password (server.web_password, 8 characters or more), from existingSecret's key, else
# from password, else made at the first install and kept in the chart's Secret. The made one is
# found again with lookup on upgrades, which helm template and GitOps tools (Argo CD, Flux's
# dry runs) cannot do: they make a new one at each render. Give password or existingSecret
# there (README.md, "Web password").
password: ""
existingSecret: ""
key: web-password
# server.web_moderator_password, from the same Secret's moderatorKey when it has one.
moderatorPassword: ""
moderatorKey: web-moderator-password
# A ClusterIP Service for the web port, for an Ingress (with TLS) or kubectl port-forward.
service:
enabled: true
port: 8080
annotations: {}
network:
# network.tunnel_port: the UDP port of internet play, which the Service exposes.
tunnelPort: 2302
# network.public_address: host[:port] players reach the tunnel port at, when the cluster is behind
# a NAT that STUN cannot see through. Empty: what STUN finds.
publicAddress: ""
# More environment variables, as in a pod spec ({name, value} or {name, valueFrom}); they come after
# the chart's, so the same name overrides it.
extraEnv: []
# /state: config.toml (HALO_CONFIG_FILE), publisher.key, the save root with its map cache (about
# 115 MB for the multiplayer maps), the logs. Without persistence an emptyDir: the cache is built
# again at each start, and settings changed from the web page are lost.
persistence:
enabled: false
existingClaim: ""
size: 2Gi
storageClass: ""
accessModes:
- ReadWriteOnce
annotations: {}
emptyDirSizeLimit: 1Gi
# The tunnel port's Service. Players reach it from outside the cluster, so a real deployment needs
# LoadBalancer or NodePort.
service:
type: LoadBalancer
port: 2302
nodePort: ""
annotations: {}
labels: {}
# Local keeps the players' own addresses (no SNAT to a node), which bans and the server's limits
# rely on.
externalTrafficPolicy: Local
loadBalancerClass: ""
loadBalancerSourceRanges: []
strategy:
type: Recreate
resources:
requests:
cpu: 100m
memory: 192Mi
limits:
memory: 600Mi
# The image is 32-bit x86 Linux code, which runs on amd64 nodes only.
nodeSelector:
kubernetes.io/os: linux
kubernetes.io/arch: amd64
affinity: {}
tolerations: []
priorityClassName: ""
podAnnotations: {}
podLabels: {}
# SIGTERM ends the game in progress, tells the players and exits 0; a second signal, or 5 seconds
# on, exits at once.
terminationGracePeriodSeconds: 15
podSecurityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
# GET /healthz on the web port: 200 while the server hosts (lobby, loading, playing, postgame) and
# its frames run. Each probe's fields override these; enabled: false drops it.
probes:
startup:
enabled: true
periodSeconds: 2
failureThreshold: 60
timeoutSeconds: 2
readiness:
enabled: true
periodSeconds: 10
failureThreshold: 3
timeoutSeconds: 2
liveness:
enabled: true
periodSeconds: 20
failureThreshold: 3
timeoutSeconds: 3
networkPolicy:
enabled: true
# Who may reach the tunnel port. Empty: anyone (players are on the internet).
from: []
# Who may reach the web port, as NetworkPolicy peers (an Ingress controller's namespace, say).
# Empty: no pod; kubectl port-forward and the kubelet's probes still reach it.
webFrom: []
dns:
to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
# The internet: TCP to the MQTT brokers (1883) and the master servers (443), and UDP to anywhere
# (STUN, and the players' own ports). The private ranges are excepted.
internet:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- 100.64.0.0/10
- 169.254.0.0/16
tcpPorts:
- 1883
- 443
udp: true
extraIngress: []
extraEgress: []
# More objects to render with the release (templated).
extraObjects: []
+31
View File
@@ -0,0 +1,31 @@
# init.txt: commands a dedicated server runs once at its start, one a line,
# as if typed at its console (docs/dedicated_server.md, "Commands"). Copy
# this file to init.txt in the data folder (server.init_file), and take the
# "# " off the lines you want. Lines starting with # or ; are comments.
#
# Settings are better given in config.toml or as HALO_* variables; these are
# for what changes from one start to the next.
# The rotation, replacing server.rotation (and written to it):
# sv_rotation_set bloodgulch:ctf, sidewinder:team_slayer+score=50, hangemhigh:king_moving
# sv_rotation_add prisoner:slayer+time=10
# sv_rotation_begin 2
# The game's name, players and notice (sv_set of the server.* settings):
# sv_name My server
# sv_maxplayers 12
# sv_motd Welcome. Be nice; admins are watching.
# sv_public 1
# sv_password secret
# Any setting the server lists in sv_settings:
# sv_set server.lobby_seconds 20
# sv_set server.empty_seconds 60
# A first game other than the rotation's, which goes on after it:
# sv_map bloodgulch ctf_classic
# The state of the server, in the log:
# sv_status
# The game's own console commands run too, with the owner's role.
+40
View File
@@ -0,0 +1,40 @@
# The dedicated server on MCSManager
`mcsmanager-config.json` holds two MCSManager quick-install entries for the
dedicated server (`docs/dedicated_server.md`). Each unzips a release's
`halo-linux-release.zip` into the instance's folder and runs
`./halo --server` there, stopped with `quit`.
| Entry | How it runs |
| --- | --- |
| Host | On the node itself, which needs the 32-bit C runtime (Debian and Ubuntu: `dpkg --add-architecture i386 && apt install libc6:i386`) |
| Docker | In a container of `ghcr.io/ptero-eggs/games:source` (Debian 12 with the 32-bit libraries), the instance's folder mounted at `/workspace` |
## Using it
1. Add the entries to the node's quick-install list, and create an
instance from one.
2. Upload the `maps/` folder of your own copy of the game into the
instance's folder. Without `maps/ui.map` and the rotation's maps the
server exits with code 2 and says which maps it looked for.
3. Start it.
## Folders
The Docker entry's container is made again at each start, so it sets
`HALO_SAVE_ROOT=/workspace/saves` and `HALO_LOG_ROOT=/workspace/logs`: the
save root with the map cache (about 115 MB, built at the first start) and
the logs stay in the instance's folder rather than in the container. The
host entry keeps the save root in the home folder of the daemon's user
(`~/.local/share/halo-linux`), which every host instance of that user
shares; give each its own with
`HALO_SAVE_ROOT=saves` (in the instance's environment, or as
`env HALO_SAVE_ROOT=saves ./halo --server` for its start command).
`config.toml` is written beside `halo` at the first start.
## Ports
UDP 2302 is the tunnel port, the one players need; the web page, its API
and the remote console are on 127.0.0.1:8080. A second instance on the node
needs ports of its own: `HALO_NET_TUNNEL_PORT` and `HALO_SERVER_WEB_PORT`,
and for the Docker entry its published ports to match.
+139
View File
@@ -0,0 +1,139 @@
{
"_comment": "MCSManager quick-install entries for the OpenCE dedicated server (docs/dedicated_server.md). Each package unzips the release into the instance's folder; put the maps/ folder of your own copy of the game beside halo before the first start. Settings: config.toml beside halo, written at the first start, or HALO_* variables.",
"languages": [
{
"label": "English",
"value": "en_us"
}
],
"packages": [
{
"language": "en_us",
"platform": "Linux",
"title": "OpenCE Dedicated Server",
"category": "Halo: Combat Evolved",
"runtime": "Linux x86-64 with 32-bit libraries",
"hardware": "1 CPU core, 600 MB of memory",
"size": "45 MB, and the game data you upload",
"author": "OpenCE",
"targetLink": "https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-release.zip",
"description": "The dedicated server run on the node itself, which needs the 32-bit C runtime (Debian and Ubuntu: dpkg --add-architecture i386 && apt install libc6:i386). Open UDP 2302; the web page is on http://127.0.0.1:8080/.",
"remark": "Host",
"setupInfo": {
"nickname": "OpenCE Dedicated Server",
"startCommand": "./halo --server",
"stopCommand": "quit",
"ie": "utf-8",
"oe": "utf-8",
"type": "universal",
"tag": [
"halo",
"opence"
],
"processType": "general",
"updateCommand": "",
"crlf": 1,
"actionCommandList": [
{
"name": "Status",
"command": "sv_status"
},
{
"name": "Players",
"command": "sv_players"
},
{
"name": "Next map",
"command": "sv_map_next"
},
{
"name": "Reload settings",
"command": "sv_reload"
}
],
"eventTask": {
"autoStart": false,
"autoRestart": true,
"ignore": false
},
"terminalOption": {
"haveColor": false,
"pty": false
}
}
},
{
"language": "en_us",
"platform": "Linux",
"title": "OpenCE Dedicated Server",
"category": "Halo: Combat Evolved",
"runtime": "Linux x86-64 with 32-bit libraries",
"hardware": "1 CPU core, 600 MB of memory",
"size": "45 MB, and the game data you upload",
"author": "OpenCE",
"targetLink": "https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-release.zip",
"description": "The dedicated server in a container of ghcr.io/ptero-eggs/games:source (Debian 12 with the 32-bit libraries), the instance's folder mounted as its working folder; the save root (with the map cache) and the logs are kept in it, as the container is made again at each start. Publishes UDP 2302 and the web page on the node's 127.0.0.1:8080 (each instance on a node needs ports of its own).",
"remark": "Docker",
"setupInfo": {
"nickname": "OpenCE Dedicated Server",
"startCommand": "./halo --server",
"stopCommand": "quit",
"ie": "utf-8",
"oe": "utf-8",
"type": "universal",
"tag": [
"halo",
"opence"
],
"processType": "docker",
"updateCommand": "",
"crlf": 1,
"actionCommandList": [
{
"name": "Status",
"command": "sv_status"
},
{
"name": "Players",
"command": "sv_players"
},
{
"name": "Next map",
"command": "sv_map_next"
},
{
"name": "Reload settings",
"command": "sv_reload"
}
],
"eventTask": {
"autoStart": false,
"autoRestart": true,
"ignore": false
},
"terminalOption": {
"haveColor": false,
"pty": false
},
"docker": {
"image": "ghcr.io/ptero-eggs/games:source",
"ports": [
"2302:2302/udp",
"127.0.0.1:8080:8080/tcp"
],
"extraVolumes": [],
"networkMode": "bridge",
"networkAliases": [],
"env": [
"HALO_SERVER_WEB_BIND=0.0.0.0",
"HALO_SAVE_ROOT=/workspace/saves",
"HALO_LOG_ROOT=/workspace/logs"
],
"memory": 600,
"workingDir": "/workspace/",
"changeWorkdir": true
}
}
}
]
}
+94
View File
@@ -0,0 +1,94 @@
# The dedicated server on Pterodactyl and Pelican
`egg-opence-dedicated-server.json` is an egg (`PTDL_v2`) that installs a
release of OpenCE from GitHub and runs it as a dedicated server
(`docs/dedicated_server.md`). Pelican imports the same file.
## Importing it
1. Admin > Nests (Pelican: Eggs) > Import Egg, and choose the file.
2. Create a server with the egg. Its primary allocation is the tunnel port
(`network.tunnel_port`, UDP): the one port players need, from 1025 up
(the container cannot bind lower ports).
3. Upload the game data: the `maps/` folder of your own copy of the game
(port/linux/README.md, "Game data"), into the server's folder over SFTP
(the panel's SFTP details, on the Settings tab). Without `maps/ui.map`
and the rotation's maps the server stops at once with exit code 2 and
says which maps it looked for.
4. Start it. The panel shows it running once it prints its ready line
(`server: ready`, or `"msg":"ready (` with `server.log_format = "json"`).
## Files
| Path (in `/home/container`) | What |
| --- | --- |
| `halo`, `halo-server`, `libSDL3.so.0`, `brokers.txt`, `masters.txt`, the licences | The release (replaced on Reinstall) |
| `maps/` | The game data (yours) |
| `custom_maps/` | Custom Edition maps, with `bitmaps.map`, `sounds.map` and `loc.map` (shared with players who join) |
| `gametypes/*.toml` | Gametypes of your own (the release adds examples, never replacing yours) |
| `init.txt` | Commands run at each start (`init.txt.example` has examples) |
| `config.toml` | Every setting, written at the first start |
| `publisher.key` | The master servers' key: made by `./halo --server --keygen` on any machine with the release and uploaded, or given as the Publisher key variable instead |
| `logs/` | `debug.txt`, `bans.txt`, `cheaters.txt` |
Reinstall (Settings tab) downloads the release again, the one the Release
variable names, and keeps `config.toml`, `init.txt`, `publisher.key`,
`gametypes/`, `custom_maps/`, `maps/` and `logs/`.
## Variables
Every variable is one of the server's settings, by its environment name
(docs/dedicated_server.md and port/linux/README.md, "Dedicated server"). A
variable wins over `config.toml`, and the web page's settings cannot
change it.
| Variable | Setting |
| --- | --- |
| Release (`OPENCE_RELEASE`) | `latest` or a tag (`build-206`); used by Reinstall |
| Server name (`HALO_SERVER_NAME`) | `server.name`, 15 characters |
| Rotation (`HALO_SERVER_ROTATION`) | `server.rotation` |
| Max players (`HALO_SERVER_MAX_PLAYERS`) | `server.max_players` |
| Join password (`HALO_SERVER_PASSWORD`) | `server.password` |
| Public (`HALO_SERVER_PUBLIC`) | `server.public` |
| Message of the day (`HALO_SERVER_MOTD`) | `server.motd` |
| Lobby seconds (`HALO_SERVER_LOBBY_SECONDS`) | `server.lobby_seconds` |
| Web UI port (`HALO_SERVER_WEB_PORT`) | `server.web_port`; 0 for none |
| Web UI password (`HALO_SERVER_WEB_PASSWORD`) | `server.web_password` |
| Public address (`HALO_NET_PUBLIC_ADDRESS`) | `network.public_address` |
| Publisher key (`HALO_SERVER_PUBLISHER_KEY`) | The master servers' key itself, in place of `publisher.key` |
| Instance name (`HALO_SERVER_INSTANCE`) | `server.instance` |
| Region (`HALO_SERVER_REGION`) | `server.region` |
| Log format (`HALO_SERVER_LOG_FORMAT`) | `server.log_format` |
The startup command sets `HALO_NET_TUNNEL_PORT` to the primary allocation's
port and `HALO_LOG_ROOT` to `logs/`. Two variables are the administrator's
only: Web UI bind (`0.0.0.0`, as the allocation's address is the node's,
not the container's) and GitHub repository (`OpenCommunityEdition/OpenCE`).
## The web page
The admin page, its API and the remote console need a second allocation:
the node's administrator adds one to the server (or raises its allocation
limit so the owner can, on the Network tab). Give its port as Web UI port
and a password of 8 characters or more as Web UI password, restart, and
open `http://<allocation address>:<port>/`. The page has no TLS of its own:
put it behind a reverse proxy with HTTPS for anything but a private network.
## Stopping and restarting
The panel's Stop sends SIGINT, which the server takes as SIGTERM: the game
in progress goes to its postgame, the players are told, the listing is
withdrawn, and it exits 0. The console's `quit` command does the same, but
Wings counts an exit it did not ask for as a crash and starts the server
again; use Stop. `sv_restart` exits with code 5, which Wings also takes as a
crash and restarts, which is what it asks for.
Updates: `server.auto_update` is for servers that own their files; on a
panel, change the Release variable (or keep `latest`) and Reinstall, by hand
or as a schedule.
## Checking the egg
`python tools/egg_build.py` copies `install.sh` into the egg and checks it;
`python -m pytest -q tools/test_egg.py` checks the egg and runs `install.sh`
in a `debian:bookworm-slim` container against a release served locally.
@@ -0,0 +1,203 @@
{
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO",
"meta": {
"version": "PTDL_v2",
"update_url": null
},
"exported_at": "2026-10-24T00:00:00+00:00",
"name": "OpenCE Dedicated Server",
"author": "opence@halocombatevolved.com",
"description": "A dedicated server of OpenCE, the port of Halo: Combat Evolved: hosts multiplayer games one after another, with no window, sound or player of its own. Installs a release from GitHub. The game data is yours to upload: put the maps/ folder of your own copy of the game in the server's folder over SFTP; the server does not start without it.",
"features": [],
"docker_images": {
"Debian 12 with 32-bit libraries (Pterodactyl)": "ghcr.io/ptero-eggs/games:source",
"Debian 12 with 32-bit libraries (Pelican)": "ghcr.io/pelican-eggs/games:source"
},
"file_denylist": [],
"startup": "HALO_NET_TUNNEL_PORT={{SERVER_PORT}} HALO_LOG_ROOT=/home/container/logs ./halo --server",
"config": {
"files": "{}",
"startup": "{\r\n \"done\": [\r\n \"server: ready\",\r\n \"\\\"msg\\\":\\\"ready (\"\r\n ]\r\n}",
"logs": "{}",
"stop": "^C"
},
"scripts": {
"installation": {
"script": "#!/bin/bash\n# The OpenCE dedicated server egg's installation script (Pterodactyl and\n# Pelican), run as root in ghcr.io/ptero-eggs/installers:debian with the\n# server's folder at /mnt/server. tools/egg_build.py copies it into\n# egg-opence-dedicated-server.json.\n#\n# OPENCE_RELEASE: \"latest\" or a release tag (build-206).\n# OPENCE_REPOSITORY: the GitHub repository (owner/name) to download from.\n#\n# A reinstall replaces the release's files and keeps the owner's:\n# config.toml, init.txt, publisher.key, gametypes/, custom_maps/, maps/ and\n# logs/. The panel ignores this script's exit code, so each failure says\n# what went wrong.\n\nset -u\n\nSERVER_DIR=\"${SERVER_DIR:-/mnt/server}\"\nRELEASE=\"${OPENCE_RELEASE:-latest}\"\nREPOSITORY=\"${OPENCE_REPOSITORY:-OpenCommunityEdition/OpenCE}\"\nGITHUB_URL=\"${OPENCE_GITHUB_URL:-https://github.com}\"\nGITHUB_API=\"${OPENCE_GITHUB_API:-https://api.github.com}\"\nASSET=halo-linux-release.zip\n\nfail() {\n\techo \"INSTALL FAILED: $*\"\n\texit 1\n}\n\nif ! [[ \"$REPOSITORY\" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]]; then\n\tfail \"the repository \\\"$REPOSITORY\\\" is not owner/name\"\nfi\nif [ -z \"$RELEASE\" ] || [ \"$RELEASE\" = latest ]; then\n\t# (no API call, so no rate limit)\n\turl=\"$GITHUB_URL/$REPOSITORY/releases/latest/download/$ASSET\"\nelse\n\tif ! [[ \"$RELEASE\" =~ ^[A-Za-z0-9._-]+$ ]]; then\n\t\tfail \"the release \\\"$RELEASE\\\" is not a tag name (build-206) or latest\"\n\tfi\n\techo \"Looking up release $RELEASE of $REPOSITORY\"\n\trelease_json=\"$(curl -fsSL --retry 3 -H 'Accept: application/vnd.github+json' \\\n\t\t\"$GITHUB_API/repos/$REPOSITORY/releases/tags/$RELEASE\")\" \\\n\t\t|| fail \"no release $RELEASE in $REPOSITORY (or GitHub's API did not answer)\"\n\turl=\"$(printf '%s' \"$release_json\" | jq -r --arg name \"$ASSET\" \\\n\t\t'[.assets[]? | select(.name == $name) | .browser_download_url][0] // empty')\"\n\t[ -n \"$url\" ] || fail \"release $RELEASE has no $ASSET\"\nfi\n\nmkdir -p \"$SERVER_DIR\" || fail \"cannot make $SERVER_DIR\"\ncd \"$SERVER_DIR\" || fail \"cannot enter $SERVER_DIR\"\n\nwork=\"$(mktemp -d)\" || fail \"cannot make a temporary folder\"\ntrap 'rm -rf \"$work\"' EXIT\n\necho \"Downloading $url\"\ncurl -fL --retry 3 --progress-bar -o \"$work/$ASSET\" \"$url\" || fail \"the download of $url failed\"\nunzip -tq \"$work/$ASSET\" > /dev/null || fail \"$ASSET is not a zip file\"\nmkdir \"$work/release\"\nunzip -q \"$work/$ASSET\" -d \"$work/release\" || fail \"cannot unzip $ASSET\"\n[ -f \"$work/release/halo\" ] || fail \"$ASSET has no halo executable\"\n\n# the release's files, over the old release's; the owner's are never in it,\n# and gametypes/ only adds the examples not already there\nfor entry in \"$work/release\"/* \"$work/release\"/.[!.]*; do\n\t[ -e \"$entry\" ] || continue\n\tname=\"$(basename \"$entry\")\"\n\tcase \"$name\" in\n\tconfig.toml | init.txt | publisher.key | custom_maps | maps | logs)\n\t\tcontinue\n\t\t;;\n\tgametypes)\n\t\tmkdir -p gametypes\n\t\tcp -rn \"$entry\"/. gametypes/\n\t\tcontinue\n\t\t;;\n\tesac\n\trm -rf \"./$name\"\n\tcp -a \"$entry\" \"./$name\" || fail \"cannot copy $name\"\ndone\n\nchmod 0755 halo\nif [ ! -f halo-server ]; then\n\tprintf '#!/bin/sh\\nexec \"$(dirname \"$0\")/halo\" --server \"$@\"\\n' > halo-server\nfi\nchmod 0755 halo-server\nmkdir -p maps custom_maps gametypes logs\n\necho \"Checking that the server runs\"\nif version=\"$(HOME=\"$work\" HALO_SAVE_ROOT=\"$work/saves\" timeout 30 ./halo --version 2>&1)\"; then\n\techo \"$version\"\nelse\n\techo \"WARNING: ./halo --version did not run. The server needs the 32-bit C runtime\"\n\techo \"WARNING: (the egg's ghcr.io/ptero-eggs/games:source image has it):\"\n\techo \"$version\"\nfi\n\nif [ ! -f maps/ui.map ]; then\n\techo \"No game data yet: upload your maps/ folder (ui.map and the multiplayer maps)\"\n\techo \"over SFTP into the server's folder before starting it.\"\nfi\necho \"Installation completed\"\n",
"container": "ghcr.io/ptero-eggs/installers:debian",
"entrypoint": "bash"
}
},
"variables": [
{
"name": "Release",
"description": "A release tag (build-206) or latest. A change takes effect on Reinstall (Settings tab), which keeps config.toml, init.txt, gametypes/, custom_maps/ and maps/.",
"env_variable": "OPENCE_RELEASE",
"default_value": "latest",
"user_viewable": true,
"user_editable": true,
"rules": "required|string|max:40|regex:/^[A-Za-z0-9._-]+$/",
"field_type": "text"
},
{
"name": "Server name",
"description": "The game's name in the lobby and the server browser: 15 characters of printable ASCII.",
"env_variable": "HALO_SERVER_NAME",
"default_value": "OpenCE Server",
"user_viewable": true,
"user_editable": true,
"rules": "required|string|max:15|regex:/^[\\x20-\\x7E]+$/",
"field_type": "text"
},
{
"name": "Rotation",
"description": "The games played one after another, separated by commas: map:gametype, with +rule=value after a gametype (bloodgulch:ctf, sidewinder:team_slayer+score=50). A gametype is a built-in's name or a gametypes/<name>.toml of yours.",
"env_variable": "HALO_SERVER_ROTATION",
"default_value": "bloodgulch:ctf, sidewinder:team_slayer, prisoner:slayer",
"user_viewable": true,
"user_editable": true,
"rules": "required|string|max:4096",
"field_type": "text"
},
{
"name": "Max players",
"description": "2 to 127.",
"env_variable": "HALO_SERVER_MAX_PLAYERS",
"default_value": "16",
"user_viewable": true,
"user_editable": true,
"rules": "required|integer|between:2,127",
"field_type": "text"
},
{
"name": "Join password",
"description": "The password the server browser asks for; empty for none. The invite link joins without it.",
"env_variable": "HALO_SERVER_PASSWORD",
"default_value": "",
"user_viewable": true,
"user_editable": true,
"rules": "nullable|string|max:64",
"field_type": "text"
},
{
"name": "Public",
"description": "List the server in everyone's server browser.",
"env_variable": "HALO_SERVER_PUBLIC",
"default_value": "true",
"user_viewable": true,
"user_editable": true,
"rules": "required|string|in:true,false",
"field_type": "text"
},
{
"name": "Message of the day",
"description": "A notice each player is shown on joining (127 characters); empty for none.",
"env_variable": "HALO_SERVER_MOTD",
"default_value": "",
"user_viewable": true,
"user_editable": true,
"rules": "nullable|string|max:127",
"field_type": "text"
},
{
"name": "Lobby seconds",
"description": "Seconds the lobby counts down before each game (0 to 300).",
"env_variable": "HALO_SERVER_LOBBY_SECONDS",
"default_value": "15",
"user_viewable": true,
"user_editable": true,
"rules": "required|integer|between:0,300",
"field_type": "text"
},
{
"name": "Web UI port",
"description": "0: no web page. Else the port of an additional allocation (Network tab) for the admin page, its API and the remote console, at http://<allocation address>:<port>/; it needs a web UI password.",
"env_variable": "HALO_SERVER_WEB_PORT",
"default_value": "0",
"user_viewable": true,
"user_editable": true,
"rules": "required|integer|between:0,65535",
"field_type": "text"
},
{
"name": "Web UI password",
"description": "The admin password of the web page, 8 characters or more.",
"env_variable": "HALO_SERVER_WEB_PASSWORD",
"default_value": "",
"user_viewable": true,
"user_editable": true,
"rules": "nullable|string|min:8|max:64",
"field_type": "text"
},
{
"name": "Public address",
"description": "host[:port] offered to joiners first, for a node behind a NAT that STUN cannot see through; usually empty.",
"env_variable": "HALO_NET_PUBLIC_ADDRESS",
"default_value": "",
"user_viewable": true,
"user_editable": true,
"rules": "nullable|string|max:253",
"field_type": "text"
},
{
"name": "Publisher key",
"description": "Your approved key for the master servers that list dedicated servers: the 64 hex characters of the seed ./halo --server --keygen writes to publisher.key. Empty: the key file, if there is one; the server is not listed on the masters without one.",
"env_variable": "HALO_SERVER_PUBLISHER_KEY",
"default_value": "",
"user_viewable": true,
"user_editable": true,
"rules": "nullable|string|regex:/^[0-9a-f]{64}$/",
"field_type": "text"
},
{
"name": "Instance name",
"description": "Tells this server from your others on the master servers: letters, digits, dashes.",
"env_variable": "HALO_SERVER_INSTANCE",
"default_value": "main",
"user_viewable": true,
"user_editable": true,
"rules": "required|string|alpha_dash|max:32",
"field_type": "text"
},
{
"name": "Region",
"description": "A label in the server browser (eu, na-east); empty for none.",
"env_variable": "HALO_SERVER_REGION",
"default_value": "",
"user_viewable": true,
"user_editable": true,
"rules": "nullable|string|alpha_dash|max:16",
"field_type": "text"
},
{
"name": "Log format",
"description": "The console's lines: text, or json for a log collector.",
"env_variable": "HALO_SERVER_LOG_FORMAT",
"default_value": "text",
"user_viewable": true,
"user_editable": true,
"rules": "required|string|in:text,json",
"field_type": "text"
},
{
"name": "Web UI bind",
"description": "The container's address the web port listens on: all of them, as the allocation's address is the node's.",
"env_variable": "HALO_SERVER_WEB_BIND",
"default_value": "0.0.0.0",
"user_viewable": false,
"user_editable": false,
"rules": "required|string|in:0.0.0.0",
"field_type": "text"
},
{
"name": "GitHub repository",
"description": "owner/name the installation downloads the release from.",
"env_variable": "OPENCE_REPOSITORY",
"default_value": "OpenCommunityEdition/OpenCE",
"user_viewable": false,
"user_editable": false,
"rules": "required|string|max:100|regex:/^[A-Za-z0-9._-]+\\/[A-Za-z0-9._-]+$/",
"field_type": "text"
}
]
}
+100
View File
@@ -0,0 +1,100 @@
#!/bin/bash
# The OpenCE dedicated server egg's installation script (Pterodactyl and
# Pelican), run as root in ghcr.io/ptero-eggs/installers:debian with the
# server's folder at /mnt/server. tools/egg_build.py copies it into
# egg-opence-dedicated-server.json.
#
# OPENCE_RELEASE: "latest" or a release tag (build-206).
# OPENCE_REPOSITORY: the GitHub repository (owner/name) to download from.
#
# A reinstall replaces the release's files and keeps the owner's:
# config.toml, init.txt, publisher.key, gametypes/, custom_maps/, maps/ and
# logs/. The panel ignores this script's exit code, so each failure says
# what went wrong.
set -u
SERVER_DIR="${SERVER_DIR:-/mnt/server}"
RELEASE="${OPENCE_RELEASE:-latest}"
REPOSITORY="${OPENCE_REPOSITORY:-OpenCommunityEdition/OpenCE}"
GITHUB_URL="${OPENCE_GITHUB_URL:-https://github.com}"
GITHUB_API="${OPENCE_GITHUB_API:-https://api.github.com}"
ASSET=halo-linux-release.zip
fail() {
echo "INSTALL FAILED: $*"
exit 1
}
if ! [[ "$REPOSITORY" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]]; then
fail "the repository \"$REPOSITORY\" is not owner/name"
fi
if [ -z "$RELEASE" ] || [ "$RELEASE" = latest ]; then
# (no API call, so no rate limit)
url="$GITHUB_URL/$REPOSITORY/releases/latest/download/$ASSET"
else
if ! [[ "$RELEASE" =~ ^[A-Za-z0-9._-]+$ ]]; then
fail "the release \"$RELEASE\" is not a tag name (build-206) or latest"
fi
echo "Looking up release $RELEASE of $REPOSITORY"
release_json="$(curl -fsSL --retry 3 -H 'Accept: application/vnd.github+json' \
"$GITHUB_API/repos/$REPOSITORY/releases/tags/$RELEASE")" \
|| fail "no release $RELEASE in $REPOSITORY (or GitHub's API did not answer)"
url="$(printf '%s' "$release_json" | jq -r --arg name "$ASSET" \
'[.assets[]? | select(.name == $name) | .browser_download_url][0] // empty')"
[ -n "$url" ] || fail "release $RELEASE has no $ASSET"
fi
mkdir -p "$SERVER_DIR" || fail "cannot make $SERVER_DIR"
cd "$SERVER_DIR" || fail "cannot enter $SERVER_DIR"
work="$(mktemp -d)" || fail "cannot make a temporary folder"
trap 'rm -rf "$work"' EXIT
echo "Downloading $url"
curl -fL --retry 3 --progress-bar -o "$work/$ASSET" "$url" || fail "the download of $url failed"
unzip -tq "$work/$ASSET" > /dev/null || fail "$ASSET is not a zip file"
mkdir "$work/release"
unzip -q "$work/$ASSET" -d "$work/release" || fail "cannot unzip $ASSET"
[ -f "$work/release/halo" ] || fail "$ASSET has no halo executable"
# the release's files, over the old release's; the owner's are never in it,
# and gametypes/ only adds the examples not already there
for entry in "$work/release"/* "$work/release"/.[!.]*; do
[ -e "$entry" ] || continue
name="$(basename "$entry")"
case "$name" in
config.toml | init.txt | publisher.key | custom_maps | maps | logs)
continue
;;
gametypes)
mkdir -p gametypes
cp -rn "$entry"/. gametypes/
continue
;;
esac
rm -rf "./$name"
cp -a "$entry" "./$name" || fail "cannot copy $name"
done
chmod 0755 halo
if [ ! -f halo-server ]; then
printf '#!/bin/sh\nexec "$(dirname "$0")/halo" --server "$@"\n' > halo-server
fi
chmod 0755 halo-server
mkdir -p maps custom_maps gametypes logs
echo "Checking that the server runs"
if version="$(HOME="$work" HALO_SAVE_ROOT="$work/saves" timeout 30 ./halo --version 2>&1)"; then
echo "$version"
else
echo "WARNING: ./halo --version did not run. The server needs the 32-bit C runtime"
echo "WARNING: (the egg's ghcr.io/ptero-eggs/games:source image has it):"
echo "$version"
fi
if [ ! -f maps/ui.map ]; then
echo "No game data yet: upload your maps/ folder (ui.map and the multiplayer maps)"
echo "over SFTP into the server's folder before starting it."
fi
echo "Installation completed"
+49
View File
@@ -0,0 +1,49 @@
# The dedicated server on PufferPanel
`template.json` is a PufferPanel template for the dedicated server
(`docs/dedicated_server.md`), run in PufferPanel's host environment: it
downloads a release's `halo-linux-release.zip` into the server's folder,
runs `./halo --server` there, and stops it by writing `quit` to its
standard input.
## Using it
1. The node needs the 32-bit C runtime (Debian and Ubuntu:
`dpkg --add-architecture i386 && apt install libc6:i386`). The template
mounts `/lib32` and `/usr/lib32` into PufferPanel's sandbox, which does
not otherwise; without them the 32-bit `halo` cannot start.
2. Add the template (Templates, New, and paste the file), and create a
server from it.
3. Upload the `maps/` folder of your own copy of the game into the
server's folder. Without `maps/ui.map` and the rotation's maps the server
exits with code 2 and says which maps it looked for.
4. Start it.
## Ports
The **Tunnel port** (UDP, 2302 by default) is the one port players need.
The **Web UI port** is the web page, its API and the remote console, on
127.0.0.1 only. Its default is 8090, because PufferPanel itself listens on
8080. Each server on a node needs a tunnel port and a web port of its own
(or a web port of 0, for none).
## Files
Everything the server writes stays in its folder: `config.toml` (every
setting, written at the first start, with the web page's admin password if
none was given), `saves/` (`HALO_SAVE_ROOT`: the profiles and the map cache,
about 115 MB, built at the first start), `logs/` (`HALO_LOG_ROOT`:
`debug.txt`, `bans.txt`, `cheaters.txt`) and `status.json`
(`server.status_file`, the status every 2 seconds).
The template's variables are passed as `HALO_*` variables, which win over
`config.toml`: `sv_set` and the web page cannot change those settings.
## PufferPanel versions
The template is in the format of PufferPanel 2's templates (`run.command`
as a string, `run.stop` as the command written to standard input). We have
not confirmed how PufferPanel 3 imports it, nor the form of its standard
input settings; if a server made from it on PufferPanel 3 does not stop
with `quit`, set its stop to send `quit` to standard input (or SIGTERM,
which the server takes as well) in the server's settings.
+140
View File
@@ -0,0 +1,140 @@
{
"type": "opence",
"display": "OpenCE Dedicated Server",
"install": [
{
"type": "download",
"files": [
"${release_url}"
]
},
{
"type": "command",
"commands": [
"unzip -o halo-linux-release.zip",
"rm -f halo-linux-release.zip",
"chmod 0755 halo halo-server",
"mkdir -p maps custom_maps gametypes saves logs",
"./halo --version"
]
}
],
"run": {
"command": "./halo --server",
"stop": "quit",
"workingDirectory": "",
"pre": [],
"post": [],
"environmentVars": {
"HALO_NET_TUNNEL_PORT": "${port}",
"HALO_SERVER_NAME": "${name}",
"HALO_SERVER_ROTATION": "${rotation}",
"HALO_SERVER_MAX_PLAYERS": "${max_players}",
"HALO_SERVER_PUBLIC": "${public}",
"HALO_SERVER_WEB_PORT": "${web_port}",
"HALO_SERVER_LOG_FORMAT": "${log_format}",
"HALO_SERVER_STATUS_FILE": "status.json",
"HALO_SAVE_ROOT": "saves",
"HALO_LOG_ROOT": "logs"
},
"autostart": false,
"autorecover": true,
"autorestart": false
},
"data": {
"release_url": {
"type": "string",
"display": "Release",
"desc": "The release's halo-linux-release.zip: .../releases/latest/download/halo-linux-release.zip, or .../releases/download/<tag>/halo-linux-release.zip. Used when the server is installed again.",
"required": true,
"value": "https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-release.zip",
"userEdit": true
},
"port": {
"type": "integer",
"display": "Tunnel port",
"desc": "The UDP port of internet play (network.tunnel_port), the one port players need.",
"required": true,
"value": 2302,
"userEdit": true
},
"name": {
"type": "string",
"display": "Server name",
"desc": "The game's name in the server browser: 15 characters of printable ASCII.",
"required": true,
"value": "OpenCE Server",
"userEdit": true
},
"rotation": {
"type": "string",
"display": "Rotation",
"desc": "The games played one after another: map:gametype entries separated by commas.",
"required": true,
"value": "bloodgulch:ctf, sidewinder:team_slayer, prisoner:slayer",
"userEdit": true
},
"max_players": {
"type": "integer",
"display": "Max players",
"desc": "2 to 127.",
"required": true,
"value": 16,
"userEdit": true
},
"public": {
"type": "boolean",
"display": "Public",
"desc": "List the server in everyone's server browser.",
"required": true,
"value": true,
"userEdit": true
},
"web_port": {
"type": "integer",
"display": "Web UI port",
"desc": "The web page, its API and the remote console, on 127.0.0.1 (server.web_port); 0 for none. Each server on a node needs a port of its own, and not PufferPanel's own (8080). The admin password is printed at the first start and kept in config.toml.",
"required": true,
"value": 8090,
"userEdit": true
},
"log_format": {
"type": "option",
"display": "Log format",
"desc": "The console's lines.",
"required": true,
"value": "text",
"userEdit": true,
"options": [
{
"value": "text",
"display": "Text"
},
{
"value": "json",
"display": "JSON"
}
]
}
},
"environment": {
"type": "host",
"mounts": [
"/lib32",
"/usr/lib32"
]
},
"supportedEnvironments": [
{
"type": "host",
"mounts": [
"/lib32",
"/usr/lib32"
]
}
],
"requirements": {
"os": "linux",
"arch": "amd64"
}
}
+1
View File
@@ -0,0 +1 @@
d04e8b101007d1641cace8b95fcca76f7f44709f6885c3d031b52394dbdf9376
+52
View File
@@ -0,0 +1,52 @@
# The dedicated server under systemd
`halo-server@.service` runs the dedicated server (`docs/dedicated_server.md`)
as a templated unit: one instance for each folder
`/opt/opence/servers/<name>`, all sharing one release in `/opt/opence/bin`
and one read-only copy of the game data in `/opt/opence/data`.
| Path | What |
| --- | --- |
| `/opt/opence/bin/` | The release (`halo-linux-release.zip`, unzipped) |
| `/opt/opence/data/` | The game data, only read: `maps/`, `custom_maps/`, `gametypes/` |
| `/opt/opence/servers/<name>/` | The instance's own, the only folder it may write: `config.toml`, `init.txt`, `publisher.key`, `server.env`, `saves/` (the map cache), `logs/` |
## Installing
```sh
sudo useradd --system --home-dir /opt/opence --shell /usr/sbin/nologin halo
sudo install -d -o halo -g halo -m 0750 /opt/opence/servers/main
sudo cp deploy/systemd/halo-server@.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now halo-server@main
journalctl -u halo-server@main -f
```
Open UDP 2302 (the instance's `network.tunnel_port`) in the firewall.
## Settings
`server.env` in the instance's folder holds its settings as `HALO_*`
variables, one a line (port/linux/README.md, "Dedicated server"). A
variable set there wins over `config.toml`, and `sv_set` and the web page
cannot change it; leave out what you want to change from the web page.
A second instance on the machine needs ports of its own, and its own
loopback address for system link:
```sh
HALO_NET_TUNNEL_PORT=2303
HALO_SERVER_WEB_PORT=8081
HALO_NET_ADDRESS=127.0.0.2
HALO_NET_BROADCAST=127.0.0.1
```
The web port stays on 127.0.0.1 unless `server.env` says otherwise (and
then it needs a password, and TLS in front of it).
## Exit codes
Exit code 5 (`sv_restart`, or an update installed) restarts the server; 2
(no usable maps) and 3 (bad configuration) do not, as a restart would fail
the same way. The unit has no console (`StandardInput=null`): commands come
from `init.txt` and the web port.
+96
View File
@@ -0,0 +1,96 @@
# A dedicated server under systemd (docs/dedicated_server.md), one instance
# for each folder /opt/opence/servers/<name>:
#
# /opt/opence/bin/ the release (halo-linux-release.zip, unzipped)
# /opt/opence/data/ the game data, shared by every instance and
# only read: maps/, custom_maps/, gametypes/
# /opt/opence/servers/<name>/ the instance's own: config.toml, init.txt,
# publisher.key, server.env, logs/, saves/
#
# server.env holds the instance's settings as HALO_* variables, one a line.
# A second instance on the machine needs its own ports, and its own loopback
# address for system link:
#
# HALO_NET_TUNNEL_PORT=2303
# HALO_SERVER_WEB_PORT=8081
# HALO_NET_ADDRESS=127.0.0.2
# HALO_NET_BROADCAST=127.0.0.1
#
# Install:
# sudo useradd --system --home-dir /opt/opence --shell /usr/sbin/nologin halo
# sudo install -d -o halo -g halo -m 0750 /opt/opence/servers/main
# sudo cp deploy/systemd/halo-server@.service /etc/systemd/system/
# sudo systemctl daemon-reload
# sudo systemctl enable --now halo-server@main
# journalctl -u halo-server@main -f
#
# Open UDP 2302 (each instance's network.tunnel_port) in the firewall. The
# web port stays on 127.0.0.1 unless server.env says otherwise.
#
# Exit code 5 (sv_restart, or an update installed) restarts the server;
# 2 (no usable maps) and 3 (bad configuration) do not, as a restart would
# fail the same way.
[Unit]
Description=OpenCE dedicated server %i
Documentation=https://github.com/OpenCommunityEdition/OpenCE/blob/main/docs/dedicated_server.md
After=network-online.target
Wants=network-online.target
[Service]
Type=exec
User=halo
Group=halo
WorkingDirectory=/opt/opence/servers/%i
Environment=HOME=/opt/opence/servers/%i
Environment=HALO_CONFIG_FILE=/opt/opence/servers/%i/config.toml
Environment=HALO_DATA_ROOT=/opt/opence/data
Environment=HALO_SAVE_ROOT=/opt/opence/servers/%i/saves
Environment=HALO_LOG_ROOT=/opt/opence/servers/%i/logs
Environment=HALO_SERVER_INIT_FILE=/opt/opence/servers/%i/init.txt
Environment=HALO_NET_TUNNEL_PORT=2302
EnvironmentFile=-/opt/opence/servers/%i/server.env
ExecStartPre=/bin/mkdir -p /opt/opence/servers/%i/logs /opt/opence/servers/%i/saves
ExecStart=/opt/opence/bin/halo --server
# (no console: commands come from init.txt and the web port)
StandardInput=null
StandardOutput=journal
StandardError=journal
KillSignal=SIGTERM
TimeoutStopSec=15
Restart=on-failure
RestartSec=10
RestartForceExitStatus=5
RestartPreventExitStatus=2 3
# (no privileges, nothing writable but the instance's folder)
NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=
ProtectSystem=strict
ReadWritePaths=/opt/opence/servers/%i
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
ProtectProc=invisible
RestrictAddressFamilies=AF_INET AF_UNIX AF_NETLINK
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
# (a 32-bit x86 program)
SystemCallArchitectures=native x86
SystemCallFilter=@system-service
SystemCallFilter=~@privileged
SystemCallErrorNumber=EPERM
UMask=0027
MemoryMax=600M
TasksMax=64
[Install]
WantedBy=multi-user.target
+240
View File
@@ -0,0 +1,240 @@
> This is the README of [OpenCE](https://github.com/OpenCommunityEdition/OpenCE),
> the port Mjolnir VR is built on, kept for its build and
> development notes. Its download links are OpenCE's. For the VR mod, see
> the [main README](../README.md).
# Halo: Combat Evolved for Linux, Windows, Android and the web
[![Join our Discord](https://invidget.switchblade.xyz/9gqcHyr5km)](https://discord.gg/9gqcHyr5km)
This project is a port of the Halo: Combat Evolved decompilation to Linux,
Windows, Android and web browsers. The decompilation is of the Xbox build 2342
(`cachebeta.exe`, SHA-256
`4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`).
<img width="1289" height="995" alt="The game on Linux" src="https://github.com/user-attachments/assets/0d3ad50f-f8b8-46cf-aef8-e3661da2a7d7" />
The port starts from the decompilation of [bnunu/halo-1](https://github.com/bnunu/halo-1).
That project is a fork of [punpckhdq/halo](https://github.com/punpckhdq/halo).
## Download
GitHub Actions builds the game for each commit. These links download the
builds of the latest release:
| Platform | Release | Debug |
| --- | --- | --- |
| Linux | [halo-linux-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-release.zip) | [halo-linux-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-linux-debug.zip) |
| Windows | [halo-windows-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-release.zip) | [halo-windows-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-windows-debug.zip) |
| Android | [halo-android-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-release.zip) | [halo-android-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-android-debug.zip) |
| Web | [halocombatevolved.com](https://halocombatevolved.com/) (or [halo-web-release.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-web-release.zip) for your own server) | [halo-web-debug.zip](https://github.com/OpenCommunityEdition/OpenCE/releases/latest/download/halo-web-debug.zip) |
Use the release build to play. The debug build stops at the first failed
assertion and writes it to the log. Use the debug build to find and report
problems.
The game updates itself. At start-up it looks for a newer release, and asks
if you want to install it. Refer to "Updates" in
[port/linux/README.md](port/linux/README.md#updates). The web site offers
each new build when it is out.
Each build of the `main` branch that passes on all four platforms is a new
release, and the web site then plays it. The [Releases](https://github.com/OpenCommunityEdition/OpenCE/releases)
page keeps the last five releases. If the latest build has a problem, get
an older build from that page.
### Arch-based Linux Distributions
The above Linux binary release is available on the [AUR](https://aur.archlinux.org/packages/opence-bin).
## Game data
The port does not include the game data. Download an Xbox disc image
(`.xiso` or `.iso`) of Halo: Combat Evolved. All versions of the game
operate. The maps of the European (PAL) version were made for a slower
console. The port changes them to play as the North American (NTSC) maps do,
so players of the two versions can play together.
1. Start the game.
2. At the first start, the game asks for the disc image. Select it.
3. The game extracts the `maps/` folder. Then the game starts.
On Linux and Windows, the game puts `maps/` next to the executable. On
Android, copy the disc image to the phone first. The app puts `maps/` in its
data folder. Refer to [port/android/README.md](port/android/README.md). On
the web, the page copies `maps/` out of the disc image into the browser's
storage for the site; nothing is uploaded. Refer to
[port/web/README.md](port/web/README.md).
## Mods
Mods are folders of Halo PC tag files, as the Halo Editing Kit and Invader
write them, in `mods/` next to `maps/`: texture, sound, text and model packs
that play over the maps' on your machine only, in any game. Each has a
`mod.toml`. Every mod in the folder is on; the main menu's Mods turns them
off and on and sets the order they load in.
Refer to [docs/mods.md](docs/mods.md).
## Platforms
Each platform has its own instructions:
| Platform | Instructions |
| --- | --- |
| Linux (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/linux/README.md](port/linux/README.md) |
| Windows (32-bit x86 executable, OpenGL 4.5, SDL3) | [port/windows/README.md](port/windows/README.md) |
| Android (arm64 app, OpenGL ES 3, SDL3) | [port/android/README.md](port/android/README.md) |
| Web (WebAssembly, WebGL 2, SDL3; desktop Chrome, Edge and Firefox) | [port/web/README.md](port/web/README.md) |
The Linux README also gives the controls, the settings and the multiplayer
functions. These are almost the same on all platforms.
## Multiplayer
The game can play system link games on a local network and on the internet:
- A system link game can have up to 128 players on up to 128 machines.
- Linux, Windows and Android machines and browsers can play in the same
game. On a local network, the native builds play system link; browsers
play each other in rooms, over WebRTC
([port/web/README.md](port/web/README.md)).
- An invite link, or the server browser, lets a machine join a game on the
internet, browsers too: a native build takes a browser's WebRTC
([port/linux/README.md](port/linux/README.md), "Browsers"). No server of
this project is necessary.
- The netcode is new. Each machine moves its own player at once,
and the host makes the decisions for the game. Refer to
[port/linux/NETCODE.md](port/linux/NETCODE.md).
## Build the game
You do not need the Xbox SDK. The port supplies the SDK declarations that
the game uses. Refer to [port/include/xdk](port/include/xdk/README.md).
To build the game:
1. Install Python and [ninja](https://ninja-build.org/).
2. Install the tools for your platform. Refer to the README for the
platform.
3. In the root folder of the repository, enter `python configure.py`.
4. Enter `ninja` with the target for the platform:
| Target | Result |
| --- | --- |
| `ninja linux` | `build/linux/halo` |
| `ninja windows` (on Windows) | `build/windows/halo.exe` and `SDL3.dll` |
| `ninja android_apk` | `port/android/app/build/outputs/apk/debug/app-debug.apk` |
| `ninja web` | `build/web/site/`, the web site (`python tools/web_serve.py` serves it) |
If you enter `ninja` without a target, ninja builds the game for the
computer that you use.
`tools/ci_build.py` makes the same builds as GitHub Actions. For example,
enter `python tools/ci_build.py linux release`.
### Build options
Give these options to `configure.py`:
| Option | Result |
| --- | --- |
| (none) | A debug build. A failed assertion stops the game. |
| `--release` | A release build. The game does not examine assertions, as in the retail game. |
| `--portable` | The Linux and Windows builds operate on all x86-64 processors. The Linux build also operates on older distributions and on SteamOS: refer to "Portable build" in [port/linux/README.md](port/linux/README.md#portable-build). Use this option for builds that you give to other persons. |
| `--lto=thin`, `--lto=off` | Less link-time optimization. The link is faster. |
| `--pgo=off` | No profile-guided optimization. |
| `--pgo=train` | Records a new optimization profile. Refer to "Optimization profiles". |
| `--profile` | A profiling build. It records CPU times. Refer to "Profiling builds". |
| `--web-emcc PATH` | The Emscripten `emcc` for `ninja web` (else the one on the PATH, or `~/emsdk`'s). |
Without `--portable`, the Linux and Windows builds use all the instructions
of the processor that builds them (`-march=native`). Such a build does not
always start on a different computer.
### Optimization profiles
The builds use profiles of the game to optimize the code:
- `pgo/halo_linux.profdata` for Linux and Android.
- `pgo/halo_windows.profdata` for Windows.
The profiles need clang 22 or later. With an older clang, the builds do not
use the profiles.
To record a new profile:
1. Delete the profile.
2. Enter `python configure.py --pgo=train`.
3. Enter `ninja linux` or `ninja windows`.
The build then plays the main menu and the first minute of each campaign
level. This procedure continues for approximately 15 minutes. The game
data must be in `assets/`.
### Profiling builds
A profiling build records where the game spends its time.
1. Enter `python configure.py --profile`, then build as usual.
2. Start a recording. Enter `profile_record` in the console (or the telnet
console), or set `debug.profile_record = true` in `config.toml`. To
record each game of a session, also set
`debug.profile_record_when = "game"`.
3. Stop the recording. Enter `profile_stop`, load another map, or quit.
The game writes numbered `.part<n>.json` files under `profiles/` in its data root
and logs their paths. Parts are retained after the recording stops. Logs go to
standard error; a Windows release build without standard error writes them to
`halo.log` next to `halo.exe`.
`tools/net_report.py` is a helper for developers. Enter
`python tools/net_report.py <recording.part1.json>` to read a recording
as tables. The command also writes `<recording>.summary.txt`, a short text
file.
| Command | Result |
| --- | --- |
| `profile_record [seconds]` | Starts a recording. With a number of seconds, the recording stops after that time. |
| `profile_stop` | Stops the recording. |
`debug.profile_memory` sets the memory of a recording in MB (default 256,
from 4 to 1024). A recording has no length limit:
the game writes a part each time the memory is full. Refer to "Settings" in
[port/linux/README.md](port/linux/README.md).
On Android, the files are in `/sdcard/Android/data/com.halo.decomp/files/profiles/`.
Enter `adb pull` to copy the recording parts to a computer.
A profiling build plays with normal builds. It does not use
`--pgo=train`.
## Licence
OpenCE is free software: you can give it to others and change it under the
terms of the GNU Affero General Public License as published by the Free
Software Foundation, version 3 of the License or (at your option) any later
version (`AGPL-3.0-or-later`). It comes with no warranty. The full text is in
[LICENSE](LICENSE).
- **From the change.** The AGPL applies from the commit that made the change,
and to each release built from that commit or a later one. Earlier
releases, and the source at every earlier commit, were released under
[CC0 1.0](LICENSES/CC0-1.0.txt) and stay that way.
- **What OpenCE builds on.** The decompilation of
[punpckhdq/halo](https://github.com/punpckhdq/halo) is under the AGPL 3.0
from 17 October 2026, and under CC0 1.0 before that date.
[bnunu/halo-1](https://github.com/bnunu/halo-1) is under CC0 1.0.
- **Contributions** are under `AGPL-3.0-or-later`. A change taken from a
project under GPL 3.0 keeps that licence and its notices; the GPL 3.0 and
the AGPL 3.0 let the two be combined (section 13 of each).
- **Hosts and servers.** If you change the game and let other persons play
with your version over a network (for example, as the host of an internet
game, or on a web site), the AGPL asks you to offer them the source of
your version (section 13). The game as released has its source here, and
each release's source is its tag (`build-<n>`).
- **Third-party code, fonts and art** keep their own licences, which are
next to them (`port/third_party`, `port/assets/fonts`,
`port/assets/icons`, `port/assets/text_filter`). The release downloads and
the web site's `licenses.txt` carry their notices.
- **Halo is not covered.** The licence covers the work of OpenCE's authors.
It gives no rights in Halo, its maps or anything else that Microsoft owns.
-106
View File
@@ -1,106 +0,0 @@
# Assertions, warnings, and halts: shared API and January matching
The owner is [source/cseries/cseries.h](../source/cseries/cseries.h).
All assertions express a condition that **must be true**. A false condition
reports the diagnostic with `display_assert(..., TRUE)` and then calls
`system_exit(-1)`. Evaluate diagnostic expressions only on the failure path.
Warnings likewise report only when their condition is false, but use
`display_assert(..., FALSE)` and **continue**. Halts are unconditional: they
report with `TRUE` and call the existing `halt_and_catch_fire()` owner.
They do not take an assertion condition.
| Intent | Current supported API | Diagnostic |
| --- | --- | --- |
| Plain assertion | `assert(expression)` | Stringified expression |
| Diagnostic assertion | `dassert(expression, diagnostic)` | Supplied string expression |
| Formatted assertion | `vassert(expression, csprintf(temporary, format, ...))` | Formatted string, produced only on failure |
| Preserve original source location | `match_assert(file, line, expression)` | Stringified expression |
| Preserve location and diagnostic | `match_dassert(file, line, expression, diagnostic)` | Supplied string expression |
| Preserve location and formatted diagnostic | `match_vassert(file, line, expression, csprintf(temporary, format, ...))` | Formatted string |
| Plain warning | `warn(expression)` | Stringified expression; nonfatal |
| Diagnostic warning | `dwarn(expression, diagnostic)` | Supplied string; nonfatal |
| Formatted warning | `vwarn(expression, csprintf(temporary, format, ...))` | Failure-only formatting; nonfatal |
| Plain halt | `halt()` | No message; unconditional fatal report |
| Diagnostic halt | `dhalt(diagnostic)` | Supplied string; unconditional |
| Formatted halt | `vhalt(csprintf(temporary, format, ...))` | Formatted string; unconditional |
Warnings and halts also have `match_warn`, `match_dwarn`, `match_vwarn`,
`match_halt`, `match_dhalt`, and `match_vhalt` counterparts. Their first two
arguments are the original `file` and `line`, followed by the ordinary API's
arguments.
`vassert`/`match_vassert` already accepted a message expression before this
cleanup. The `dassert`/`dwarn`/`dhalt` names and matching counterparts are
statement-safe diagnostic convenience wrappers for the existing behavior,
not a claim that these exact wrappers were in Bungie's January source.
Existing callers remain compatible.
The pinned XDK 3911 compiler is VC7 13.00.9254.1. Its preprocessor rejects
`#define vassert(expression, format, ...)` with C2010, and does not implement
`__VA_ARGS__`. Variadic **functions** such as `csprintf` are supported. Do not
change compiler versions, add a function-call wrapper, or force inline code
merely to obtain the newer comma-separated macro syntax.
The supplied CE reference files themselves use two-argument
`vassert(expression, message)` calls: `random_math.c`'s random-seed lock
checks, server line 1248, and client line 521. These calls corroborate the
older API, but the attachments do not include its defining `cseries.h`.
See [the source reconstruction map](user_source_reconstruction_map_20260906.md).
## Units example: assert the invariant, not the error condition
The invariant is that an inventory weapon being dropped is **not** connected
to the map:
```c
match_vassert(
"c:\\halo\\SOURCE\\units\\units.c",
8505,
!TEST_FLAG(weapon->object.flags, _object_connected_to_map_bit),
csprintf(
temporary,
"a %s tried to drop a %s which was connected to the map.",
tag_get_name(unit->definition_index),
tag_get_name(weapon->definition_index)));
```
Omitting `!` reverses the behavior: it would assert for a disconnected weapon
and accept the connected weapon. Keep the two `tag_get_name` calls and the
formatting inside the assertion message expression, so successful execution
does not evaluate them.
The matching form retains January's original filename and line number. In a
`NON_MATCHING` build, `MATCH_FILE`/`MATCH_LINE` deliberately use the actual
current source location instead. For new code with no original location to
preserve, use the ordinary `assert`/`dassert`/`vassert` form.
## Limits and adoption
- Use braces around surrounding `if`/`else` branches. Some inherited macros
expand to an `if` statement; an unbraced outer `else` can bind incorrectly.
- Keep expressions free of unnecessary side effects. The condition is
evaluated once, and the diagnostic only on failure, but argument evaluation
order inside a formatted call remains the C compiler's responsibility.
- `temporary` is the existing shared 256-byte buffer and `csprintf` uses
unbounded `vsprintf`. Preserve authenticated legacy call sites; this is not
a thread-safe or size-safe general logging facility. Do not pass arbitrary
unbounded input or preformat into this buffer on the success path.
- Do not add `NDEBUG`-based removal: the current project macros do not have
that contract. Both debug and `NDEBUG` fixture builds are tested.
- Migrate one coherent owner at a time and run its strict gate plus consumer
regression checks. Do not bulk-rewrite active lanes or lose original
diagnostic text, argument types, failure polarity, or source locations.
The executable macro tests are
[tools/test_cseries_assertions.py](../tools/test_cseries_assertions.py), using
[C fixtures](../tools/fixtures/cseries_assertions.c) compiled by the actual VC7
toolchain. They check success/failure, lazy formatting, single evaluation,
exact matching/nonmatching locations, fatal flag and `system_exit(-1)` or
`halt_and_catch_fire()` dispatch, nonfatal warning continuation, and
connected-weapon polarity. They use bounded diagnostic API doubles; they do
not validate the termination implementation, `vsprintf` buffer safety, or the
game's complete error-reporting subsystem.
See also [common constants](common_constants.md) and the
[Units cleanup ledger](object_matching_logs/units_obj_assertion_cleanup_20260906.md).
-81
View File
@@ -1,81 +0,0 @@
# Campaign house rules and working cadence
User-confirmed 2026-09-07. Apply these to reconciliation and new reconstruction.
1. Put a no-argument `void` on its own line inside the parameter list.
2. Put each parameter on its own line.
3. Use explicit returns: `return;` for void functions, an appropriate returned
value for non-void functions. Do not add an invalid bare return to non-void C.
4. Use subsystem typed macros wrapping `tag_get`.
5. Use typed object-access macros instead of repeated raw-access casts.
6. Preserve January's inline schedule; do not emit a `point_from_line3d` COMDAT.
7. Give private functions authentic or descriptive project-style names, never
`code` plus an address.
8. Likewise name globals; do not add `bss` plus address identifiers.
9. Put prototypes in their genuine owners, not unrelated consumer C files.
10. Helper/math functions may use inline assembly sparingly, with evidence.
11. If easy exactness fails, retain credible fuzzy code, document it and park.
12. Prioritize small objects in substantially reconstructed families.
13. Avoid manual/forced inlining; retain ordinary helpers and natural scheduling.
14. Use open-source Marathon as a style/semantic reference, not January byte proof.
15. When no dedicated header exists, use the closest genuine associated header.
16. Use correct enum constants in switch tables.
17. Combine declaration and initialization when practical.
18. Prefer suitable `cseries.h` macros over manual bit manipulation.
19. Use project types, e.g. `real`, rather than generic replacements.
20. Reject fake matching and source implausible for the original program.
21. Reject nonsensical logic even when bytes happen to match.
22. Use `/QIfist` where required by the target/compiler evidence. It is a
per-TU option, not a blanket default or license to change unrelated flags.
Check the complete TU and affected callers; conversion instructions and
widths must agree. Existing usage is recorded in `config/config.json`.
Rule 6 has a later, narrow owner-approved exception for folded header-inline
helpers (see `docs/object_matching_logs/claude_lane_b_HANDOFF_20260920.md`,
section 3). A translation unit may emit a shared-header `__inline` COMDAT,
including `_point_from_line3d`, only when the emitted copy is byte-identical
to January's selected copy, its caller is strictly exact, the full ownership
and build sweep has zero regressions, and the selected-provider link passes.
January's per-object folded survivor is not an absolute emission boundary.
Do not hand-expand or hand-copy the helper to evade this check.
## Batch workflow
Reconcile coherent multi-object or owner/family packets, normally several
thousand bytes when available. Review source quality, headers, ABI and emitted
ownership together; run one full build, tools suite and stable regression sweep
per batch. Use existing strict gates for ordinary exact leaves. Reserve new
bespoke runtime harnesses for ambiguous behavior/ABIs, not every tiny function.
Isolate blockers without holding unrelated admissible work. Preserve donor trees.
Publish only verified source/tooling gains to canonical and the authorized
GitHub branches. Donor-relative headline totals are not additive canonical
credit. Keep meaningful exact bytes distinct from padded and fuzzy counts.
User clarification, 2026-09-07: preserve the best available house-rule-compliant
Claude implementations in canonical and GitHub even when non-exact. Document
the donor, fresh measurements, remaining differences and research pointers;
do not leave useful fuzzy work stranded in donor trees. Closeness never
justifies undefined/nonsensical logic or inappropriate Matching credit.
Any accepted loss of exactness must be explicitly recorded and subtracted,
not hidden behind the batch's gross gains or a semantic-match override.
Refresh the canonical treemap after each **10,000 additional verified reconciled
meaningful code bytes**. Starting checkpoint for this cadence is **930,828**;
the first threshold is **940,828**. Record the next threshold in the queue.
Do not start a timer or unrelated recurring automation for this work threshold.
## Quick references
- [Constants, types, flags and representations](common_constants.md)
- [Assertions, warnings and halts](assertions.md): predicates express required
invariants; diagnostics are lazy. The weapon example needs `!TEST_FLAG(...)`.
VC7 uses a formatted message expression, not modern variadic macros.
- [Supplied-source reconstruction map](user_source_reconstruction_map_20260906.md)
- [Matching methodology](matching_methodology.md)
- [Reconciliation queue](reconciliation_queue_20260907.md)
Cross-build symbols/types are evidence, not interchangeable January layouts.
Analyze supplied executables as data; do not execute them. Preserve binaries
and private symbol assets locally, outside published source batches. Inlined
source and absent debug assets cannot be recovered automatically with certainty.
@@ -1,631 +0,0 @@
# CLAUDE LANE A — AI Core Exact-Match Closeout (79,657 Remaining Halo Bytes)
You are continuing the Halo January 14, 2002 Xbox exact-match campaign. Your job is to maximize verified, source-authentic strict matches within one isolated AI-core portfolio while producing house-rule-clean fuzzy reconstructions for genuinely unresolved functions.
## 1. Frozen baseline and isolation
Use exactly this baseline:
- Commit: `6130661c9585fdb2785b3ff6bdb2ad0cfef0520c`
- Branch represented by the baseline: `jonas/exact-pilots`
Create a new isolated Git worktree and branch from that exact commit, for example:
- Worktree: `C:\halo-worktrees\claude-lane-a-ai-core-20260920`
- Branch: `claude/lane-a-ai-core-20260920`
Before doing any work:
1. Verify `git rev-parse HEAD` equals the full baseline hash.
2. Verify `git status --short` is empty.
3. Do not use a newer or older canonical checkout as the source of truth.
4. Do not mutate the canonical checkout.
5. Do not push anything.
6. Do not rebase, merge, pull, or incorporate another active lane.
7. Do not copy complete files from stale worktrees. Historical worktrees and branches are evidence sources only.
8. Keep every experiment in this lane or under `scratch/`.
9. At completion, leave the lane committed and clean.
Expected baseline sanity figures are approximately:
- Halo accepted: `7,291 / 7,574` functions
- Halo accepted meaningful bytes: `1,443,979 / 1,770,166`
- Overall accepted: `7,558` functions / `1,465,211` meaningful bytes
- Parks: `184 active / 0 stale / 0 invalid`
Recompute these figures from the baseline rather than blindly trusting them. Stop and diagnose if the baseline materially differs.
## 2. Exclusive production scope
You may edit only these 11 production C translation units:
1. `source/ai/actor_firing_position.c` — 1,113 remaining meaningful bytes
2. `source/ai/actor_looking.c` — 5,316
3. `source/ai/actor_moving.c` — 10,402
4. `source/ai/actor_perception.c` — 17,272
5. `source/ai/actors.c` — 4,308
6. `source/ai/ai.c` — 2,253
7. `source/ai/ai_communication.c` — 11,377
8. `source/ai/ai_debug.c` — 26,513
9. `source/ai/ai_script.c` — 214
10. `source/ai/encounters.c` — 745
11. `source/ai/props.c` — 144
Frozen portfolio total: **79,657 remaining Halo meaningful bytes**.
This number is the starting residual portfolio, not earned credit. Re-measure it on the frozen baseline and disclose any difference.
Do not edit any other `.c`, `.cpp`, `.h`, `.hpp`, `.inl`, or assembly source file. In particular:
- No Xbox library or vendor-library work.
- No unrelated Halo unit.
- No shared-header edits.
- No generated-source edits.
- No compiler, comparator, split-target, object, or build-tool modification.
Documentation under `docs/object_matching_logs/`, evidence under `scratch/`, and narrowly justified configuration edits described below are allowed.
If a candidate requires a header correction, do not put a surrogate prototype in the consumer C file and do not edit the header in this lane. Record:
- the genuine owner header;
- every consumer;
- the proposed declaration;
- the exact function expected to benefit;
- the baseline controls;
- a minimal deferred patch or edit description;
- the evidence needed for a later header-owner lane.
Then continue elsewhere. A header prerequisite must not stall the portfolio.
## 3. Goal and work order
The goal is not “try a few promising functions.” Work the entire allowed portfolio in descending leverage until every remaining owner has one of these dispositions:
- strict exact and admitted;
- credible improved fuzzy source retained at zero credit;
- unchanged because the current source remains best;
- blocked with a concrete, evidence-specific reopening condition.
Start with the small units to bank inexpensive closures and validate your workflow:
1. `props`
2. `ai_script`
3. `encounters`
4. `actor_firing_position`
5. `ai`
6. `actors`
Then work:
7. `actor_looking`
8. `actor_moving`
9. `ai_communication`
10. `actor_perception`
11. `ai_debug`
You may reorder the last five when primary evidence makes another sequence more efficient. Do not spend an entire campaign fighting one allocator/x87 tie while untouched structural or unwritten work remains.
An old park or rejection is not proof of impossibility. It is binding evidence about hypotheses already tested. Reopen only when you have a genuinely new witness, mechanism, compiler-context fact, source graph, type, owner, ABI, call graph, constant, or cross-build correspondence. Do not repeat rejected spelling searches under new filenames.
## 4. Required reading before editing
Read these completely before production edits:
- `docs/campaign_house_rules.md`
- `docs/common_constants.md`
- `docs/assertions.md`
- `docs/matching_methodology.md`
- `docs/campaign_throughput_lessons.md`
- `docs/user_source_reconstruction_map_20260906.md`
- `tools/campaign/README.md`
- `docs/object_matching_logs/astra_90pct_HANDOFF_20260920.md`
- `docs/object_matching_logs/astra_90pct_admission_manifest_20260920.json`
- `docs/object_matching_logs/astra_90pct_research_atlas_20260920.md`
- `docs/object_matching_logs/astra_90pct_rejected_hypotheses_20260920.md`
- `docs/object_matching_logs/astra_90pct_context_packet_20260920.md`
- `docs/object_matching_logs/astra_90pct_debug_packet_20260920.md`
- `docs/object_matching_logs/astra_90pct_canonical_reconciliation_20260920.md`
Read the relevant unit-specific evidence before assigning each unit:
### Actor looking
- `docs/object_matching_logs/actor_looking_obj_evidence_closeout_20260919.md`
- `docs/object_matching_logs/astra_90pct_actor_looking_packet_20260920.md`
- `docs/object_matching_logs/astra_90pct_research10_actor_looking_20260920.md`
- `docs/object_matching_logs/astra_90pct_research10_actor_looking_independent_20260920.md`
- `docs/object_matching_logs/astra_90pct_research14_aiming_blockage_20260920.md`
- `docs/object_matching_logs/astra_90pct_research14_aiming_blockage_independent_20260920.md`
### Actor moving
- `docs/object_matching_logs/astra_90pct_actor_moving_avoidance_packet_20260920.md`
- `docs/object_matching_logs/astra_90pct_research8_actor_moving_20260920.md`
- `docs/object_matching_logs/astra_90pct_research11_actor_move_update_20260920.md`
- `docs/object_matching_logs/astra_90pct_research11_biped_moving_20260920.md`
### Actor perception
- `docs/object_matching_logs/astra_90pct_actor_perception_packet_20260920.md`
- `docs/object_matching_logs/astra_90pct_research10_perception_refresh_20260920.md`
- `docs/object_matching_logs/astra_90pct_research12_perception_update_20260920.md`
- `docs/object_matching_logs/astra_90pct_wave2_perception_independent_review_20260920.md`
### AI communication
- `docs/object_matching_logs/astra_90pct_research10_communication_event_20260920.md`
- `docs/object_matching_logs/astra_90pct_research10_communication_independent_20260920.md`
- `docs/object_matching_logs/astra_90pct_research18_communication_probe_20260920.md`
- `docs/object_matching_logs/astra_90pct_research18_communication_private_owner_20260920.md`
- `docs/object_matching_logs/astra_90pct_communication_independent_review_20260920.md`
- `docs/object_matching_logs/astra_90pct_wave4_communication_20260920.md`
- `docs/object_matching_logs/astra_90pct_wave4_communication_independent_review_20260920.md`
### AI debug
- `docs/object_matching_logs/astra_90pct_wave2_ai_debug_20260920.md`
- `docs/object_matching_logs/astra_90pct_wave2_ai_debug_independent_review_20260920.md`
- `docs/object_matching_logs/astra_90pct_research12_ai_debug_20260920.md`
- `docs/object_matching_logs/astra_90pct_research12_ai_debug_color_independent_20260920.md`
- `docs/object_matching_logs/astra_90pct_research12_ai_debug_length_independent_20260920.md`
Also search `docs/object_matching_logs/` for every allowed object and read its newest reports plus any report named by their “read first,” “prior work,” or “reopening” sections.
## 5. Evidence sources and authority
Evidence priority is:
1. January split object code, padding, relocations, owner topology, target data, and actual executable instructions.
2. Authenticated January PDB publics, MAP/public names, strings, and image identities.
3. Original Bungie source, symbols, headers, or same-era builds when provenance is established.
4. Ghidra and DIA evidence from authenticated supplied builds.
5. Earlier/later Halo cross-builds, HCEA, the supplied 2020 symbol executable, and open-source Marathon as corroboration.
6. Historical branches and old candidates as hypotheses only.
Useful local assets include:
- `C:\Users\isabe\Downloads\Halo Combat Evolved (Jan 14, 2002 prototype)\cachebeta.xbe`
- `C:\Users\isabe\Downloads\Halo Combat Evolved (Jan 14, 2002 prototype)\cachebeta.exe`
- `C:\Users\isabe\Downloads\Halo Combat Evolved (Jan 14, 2002 prototype)\cachebeta.pdb`
- `C:\Users\isabe\Downloads\haloleak2024\halo1\h1_tags\halo_cache_symbols.exe`
- `C:\Users\isabe\Downloads\Halo - Combat Evolved (Oct 12, 2001 prototype)\Halo 2276`
- `C:\Users\isabe\Documents\Codex\2026-09-20\files-pasted-by-the-user-halo\outputs`
- the local Marathon research tree referenced by the campaign documentation.
Known evidence limits:
- The January PDB has publics/compiland information but no useful Halo locals, lexical blocks, source lines, or original headers.
- The supplied 2020 executable was built with a different compiler and contains confirmed source differences. Its RTC locals, call graphs, assertions, and structural correspondences are corroborating evidence, not automatic January source proof.
- HCEA supplies useful types, names, and line information, but it is a different platform/build.
- IDA Free 9.4 has no callable automation endpoint here. Do not stall or ask again for one.
- Ghidra and DIA are available and authorized.
- No additional local full-symbol January build or original Halo source/header archive exists. Do not waste time repeatedly searching for byte-identical copies or asking for nonexistent artifacts.
- Decompiler output is reconstructed pseudocode, never original source text.
Use multiple independent lenses whenever possible: target disassembly, relocation identity, call graph, PDB/MAP identity, cross-build raw instructions, HCEA types, historical compiler behavior, and exact sibling controls.
## 6. Evidence-backed mechanisms to prioritize
Astra already established useful, bounded source mechanisms. Use them precisely; do not generalize them beyond their evidence.
### Actor looking
The authenticated fuzzy reconstruction establishes:
- `real look_delta_deviations[2]` and the corresponding private parameter type;
- two independently evaluated priority equalities rather than copying the first Boolean result;
- the initial `succeeded = FALSE`;
- clearing free/optional facing states before installing primary facing.
The remaining barrier is a concrete stack/register lifetime cascade involving `facing_locked`, `aiming_cosine`, `primary_priority`, the secondary aim-valid result, and desired-facing pointer lifetime. Research authentic owner types, scopes, call-return use, or natural same-compiler donor patterns for this boundary. Do not repeat the already exhausted generic scope/order, stationary-copy, representation-cast, or spelling permutations.
### Actor moving
The avoidance packet authenticates array widths, typed aggregate identities, ray/fraction/point families, the transform operation, and several real control facts. The unresolved issue is the genuine January helper/include/inlining mechanism and remaining register lifetime, not a license to emit an unauthorized `point_from_line3d` owner. Seek authentic macro/helper definitions, same-compiler donor call sites, or owner/include evidence. Do not repeat the historical helper-storage-class, force-inline, short-local, clear-first, PIN/MAX, cross-product, or generic flag searches without new evidence.
### Actor perception
The authenticated fuzzy patch establishes:
- one `interesting_orphan_index`;
- repeated inner danger predicates that are redundant semantically but supported by original control-flow evidence.
It fixes the timer and tail structural regions while retaining zero credit. Remaining barriers are a precise call-return scheduling boundary and x87 ordering around squared 2D distance. Seek an authentic helper definition, call-site type context, or natural same-compiler donor. Do not repeat declaration splits, generic scope changes, header-COMDAT distance helpers, or scalar spelling searches already shown inert.
### AI communication
The existing packets authenticate much of the possibility-array structure, narrow private types, conversation ownership, and several call relationships. Reconcile the new original private-owner evidence before trying new body shapes. The event residual still has concrete relocation-count differences in actor data, datum access, race lookup, team communication, and low-priority rating paths. Use those relocation multiplicities and January call order as a semantic source-graph diagnostic. Do not count frame equality or later-build store order as exact progress.
### AI debug
The best authenticated fuzzy body restores:
- the fifth activation-marker point;
- the vision-cone zero-vector/accumulation graph;
- January’s target extent and relocation count.
Its remaining differences are allocation/scheduling and possibly still-unrecovered scalar lifetimes. Use the 94 protected-local records and direct January references as lower-bound lifetime evidence only. Do not infer complete lifetimes, lexical scopes, or 72/80-byte buffer extents from RTC records alone. Preserve the inherited genuine `point_from_line3d` ownership exactly; do not add, remove, or duplicate it casually.
### Small AI units
For the six small units, prioritize:
- unwritten or placeholder bodies;
- authentic private names from January/HCEA/MAP evidence;
- wrong scalar widths or enum types;
- missing calls and relocation multiplicity;
- genuine control-flow topology;
- incorrect owner/linkage;
- wrong constants already catalogued in `docs/common_constants.md`;
- historical best source that was never reconciled onto this baseline.
Do not assume “small residual” means an allocator tie. Inspect the raw function and relocations first.
## 7. Complete house rules
Apply all of these to every retained source change:
1. Format a no-argument parameter list with `void` on its own line inside the parentheses.
2. Put every parameter on its own line.
3. End every function, including `void` functions, with an explicit valid `return;`; non-void functions must return an appropriate value.
4. Use subsystem typed macros wrapping `tag_get` for typed tag access.
5. Use typed object-access macros rather than repeated casts after raw `object_get`.
6. Preserve January’s inline schedule. Do not introduce an unauthorized `point_from_line3d` COMDAT or alter its selected owner.
7. Name private functions authentically when evidence exists; otherwise use a credible project-style descriptive name. Never add `code_XXXXXXXX` or address-based names.
8. Name globals authentically or descriptively according to project conventions. Never add `bss_XXXXXXXX` names.
9. Put function prototypes in their genuine owner. Never hide a missing owner declaration in an unrelated consumer C file.
10. Helper and math functions may use inline assembly only sparingly and only with function-specific machine-code evidence that C cannot reproduce naturally.
11. If exact matching is not readily achievable, retain only credible source-authentic fuzzy progress, document it, give it zero exact credit, and park it with a concrete reopening condition.
12. Prefer small objects and high-confidence closures before prolonged allocator fights.
13. Avoid manual or forced inlining. Preserve ordinary helpers and natural compiler scheduling.
14. Use open-source Marathon only as a semantic/style reference, never as January byte proof.
15. If a C file lacks a dedicated header, use the closest genuine associated owner header. Since headers are outside this lane’s write scope, document the needed owner change rather than creating a local facade.
16. Use correct named enum constants in switch tables and comparisons.
17. Declare and initialize variables together when natural and consistent with genuine source lifetime.
18. Prefer established `cseries.h` macros over manual flag and bit manipulation.
19. Use project-defined types such as `real`, `boolean`, `byte`, `short`, and `long`, not generic substitutes such as `float`, `bool`, or fixed-width types unless the project itself requires them.
20. Reject fake matching and source forms that almost certainly were not present in the original.
21. Reject nonsensical logic even if it happens to byte-match.
22. Use `/QIfist` only when target/compiler evidence requires it, only for the relevant TU, and only after checking every function and caller affected by that option.
23. Use the correct `assert`, `dassert`, `vassert`, `warn`, `dwarn`, `vwarn`, `halt`, `dhalt`, and `vhalt` family contracts. Assertion predicates describe the required true invariant. Diagnostics must remain lazy. Do not manually expand one into `display_assert`/`system_exit` merely to shape bytes.
24. Preserve authentic January bugs only when direct evidence proves them. Mark them with a precise `BUG:` comment where project policy requires it. Do not invent undefined behavior, uninitialized reads, overflows, invalid aliasing, or out-of-bounds access to steer code generation.
25. A strict match does not excuse implausible source. Equality is necessary but not sufficient for admission.
26. Do not add dead locals, redundant stores, duplicate calls, empty branches, goto-next-label constructs, self-assignments, volatile reinterpretation, arbitrary union punning, raw-byte emission, linker tricks, or code-generation pragmas unless independent evidence authenticates that exact construct.
27. Do not use manual machine bytes or edit target objects.
28. Do not change the comparator, normalization rules, semantic-credit policy, or acceptance thresholds.
29. Do not trade away an inherited exact function for a larger gross gain.
30. Preserve code, data, BSS/COMMON, relocation, linkage, COMDAT, and selected-owner topology.
31. Correct fuzzy code gets zero exact credit. Never call 100% diagnostic similarity strict exact unless the strict comparator and ownership checks agree.
32. Function exactness and whole-object completion are separate claims. Do not mark an object complete until all target owners, data, selected providers, extra owners, and veto conditions have been audited.
33. Do not count external Xbox/vendor code toward this Halo lane.
34. Do not count already-exact baseline work, embedded labels, alias duplicates, documentation, configuration edits, or repeated historical work as new bytes.
35. Do not fabricate original names. A unique byte match from `namegap.py` is a proposal, not name provenance.
## 8. Parallel worker architecture
Use subagents aggressively, but keep their responsibilities disjoint.
Recommended structure:
- One worker per C file, or one worker for each small-file group with no overlapping file.
- Dedicated research workers may inspect evidence for actor looking, moving, perception, communication, and debug.
- Workers may read anything but may write only scratch candidates and reports.
- Workers must not run Ninja, edit shared configuration, commit, push, or land directly in production.
- Workers compile with `tools/campaign/gate.py` into PID-specific scratch objects.
- The orchestrator independently reviews and lands minimal patches.
- Every landed function receives a separate adversarial review by a worker that did not author it.
- Do not let multiple workers edit the same production file.
- Do not let workers change build outputs used by another worker.
Each worker brief must include:
- exact baseline commit;
- exact owned C file;
- current exact/residual/unwritten census;
- existing parks and prior rejected hypotheses;
- mandatory evidence files;
- house rules;
- required before/after gate;
- owner/data/COMMON/point guard;
- prohibition on production/config/header edits;
- exact/fuzzy accounting rules.
## 9. Baseline workflow
Before production edits:
1. Run the full baseline build:
`ninja -j4 all_source progress semantic_progress`
2. Save a stable snapshot:
`python -m tools.campaign.stable_verdicts snapshot scratch/lane-a-before.json`
3. Save copies of:
- `build/semantic_report.json`
- the progress report;
- parked-function validation output;
- object-admission audit output;
- fake-match scan output;
- per-unit `board.py` and `gate.py --all` results;
- owner/data/COMMON/point inventories for all 11 units.
4. Validate parks:
`python -m tools.parked_functions`
5. Run object-admission audit:
`python tools/audit_object_admission.py`
6. Run tests:
`python -m pytest -q tools`
7. Run a fake-match baseline scan:
`python tools/fake_match_scan.py --format json source/ai`
8. Record the exact compiler flags for all 11 TUs from `build.ninja`.
9. For each unit, inventory:
- January function owners;
- current candidate owners;
- exact/residual/unwritten status;
- meaningful and padded size;
- normalized hashes;
- relocation count and identities;
- linkage/storage class;
- parks and reopening criteria;
- target and candidate data sections;
- COMMON/BSS;
- point-helper definitions;
- target-absent candidate owners.
## 10. Function-level workflow
For each candidate:
1. Read current source and all prior reports first.
2. Confirm it is not already exact or already accepted under another identity.
3. Use `git log --all -- <file>` and `tools/campaign/branch_sweep.py` to census distinct historical source, but do not treat historical code as admitted.
4. Inspect target and candidate using:
- `tools/campaign/gate.py`
- `tools/campaign/alndiff.py`
- `tools/campaign/tinfo.py`
- `tools/campaign/relocdiff.py`
- `tools/bijection_walk.py` where allocation bijection matters
- `tools/campaign/namegap.py` only for identity proposals
- Ghidra/DIA/raw PDB or MAP evidence as relevant.
5. Diagnose semantic structure before editing:
- missing or extra calls;
- relocation multiplicity;
- parameter width;
- return type;
- enum width/value;
- aggregate identity/layout;
- branch topology;
- loop shape;
- expression order;
- source-owned repeated predicates or stores;
- helper ownership and inlining;
- local lifetime and scope;
- declaration initialization;
- frame extent;
- x87 stack order;
- calling convention and linkage.
6. Test bounded evidence-backed shapes with in-memory edits or scratch copies. Do not perform combinatorial permutation searches or random spelling sweeps.
7. Require a strict function gate for exact claims.
8. For changed units, compare the complete unit and preserve every inherited exact sibling.
9. Run the emitted-symbol point guard wherever January does not own the helper. For `ai_debug`, preserve its inherited genuine owner byte-for-byte and prevent any duplicate owner.
10. Run the fake-match scanner on every changed file and manually adjudicate every new lead.
11. Compare all codeowners, data, COMMON, runtime sections, relocations, linkage, COMDAT selection, and target-absent owners.
12. If still fuzzy, compare it against baseline. Retain it only if it is semantically more authentic and materially improves a documented structural defect. Similarity percentage alone is not enough.
13. For every rejected shape, save a concise negative result so later workers do not repeat it.
## 11. Config policy
No header changes are permitted.
Narrow configuration changes are allowed only as follows:
### `config/parked.json`
You may change entries only for functions in the 11 allowed TUs.
- Remove a park only when the function is independently verified strict exact.
- Re-baseline an improved fuzzy function only when the retained source is house-rule compliant, genuinely better supported, and its new measurements and evidence are documented.
- Do not change unrelated parks.
- Do not use a park edit to hide a regression.
- Keep fuzzy bytes at zero credit.
### `config/symbols.json`
You may edit only an entry belonging to an allowed TU and only when an original or independently authenticated name is established through January PDB/MAP, original source, or sufficiently strong private-symbol/call-graph evidence.
- A byte-equal alias or `namegap.py` proposal alone is insufficient.
- Do not rename target symbols merely to make the comparator agree.
- Land the name and corresponding source owner atomically.
- Preserve old/new file offset, evidence source, and identity reasoning in the manifest.
### `config/config.json`
A per-TU `/QIfist` or whole-object status change is allowed only with direct target/compiler evidence and complete unit validation.
- No blanket compiler-option changes.
- No unrelated object changes.
- No “Matching” status until complete owner/data/extra-owner audit passes.
- Do not add a semantic-match exception for a nonexact function.
No other configuration or policy file may change.
## 12. Wave cadence and commits
Work in coherent waves. Do not wait for exactly 10,000 bytes if a clean smaller wave is ready, but avoid one tiny commit per leaf unless isolation is necessary.
Suggested waves:
- Wave A1: small-unit closures.
- Wave A2: actor looking and moving.
- Wave A3: communication and perception.
- Wave A4: AI debug and portfolio closeout.
- Additional waves are allowed when evidence supports them.
For each wave:
1. Land minimal production/config patches.
2. Independently review every changed function.
3. Run all per-function and complete-TU gates.
4. Run full Ninja once for the coherent batch.
5. Take a stable after snapshot and diff against the frozen baseline, not merely the previous wave.
6. Recompute semantic accepted bytes.
7. Validate parks and admissions.
8. Run changed-file fake scans and owner/data/COMMON/point checks.
9. Run `git diff --check`.
10. Commit code, config, and its evidence report together.
At each cumulative 10,000 newly verified meaningful-byte checkpoint, also run the full tools test suite and record the refreshed treemap/progress inputs. Do not claim the checkpoint until the semantic accepted ledger proves it.
Never squash away the evidence boundaries. Keep an ordered, cherry-pickable commit series.
## 13. Verification and accounting
Maintain these totals separately:
1. **New strict meaningful Halo bytes** — authoritative campaign credit from the accepted semantic ledger.
2. **New strict padded bytes** — physical function-section gain from stable comparison.
3. **New strict functions**.
4. **Fuzzy meaningful bytes changed or improved** — always zero exact credit.
5. **Whole-object completions** — separate status, never duplicate function credit.
6. **Regressions** — must remain zero.
Do not report:
- diagnostic similarity as byte credit;
- target-size totals for nonexact bodies as “progress”;
- already exact functions as new;
- config-only identity changes without admission proof;
- external/vendor bytes;
- duplicate aliases or embedded labels;
- gross gains that conceal losses.
Every exact row in the final manifest must include:
- unit;
- function;
- meaningful bytes;
- padded bytes;
- target section/index;
- target and candidate normalized SHA-256;
- relocation count and identity result;
- before/after verdict;
- evidence basis;
- reviewer;
- source commit;
- owner/data/COMMON/point result.
Every fuzzy row must include:
- zero credit explicitly;
- baseline and final measurements;
- what source defect was corrected;
- why the retained code is more authentic;
- remaining differing regions;
- rejected alternatives;
- precise reopening evidence.
## 14. Final verification
On final tip:
1. Confirm `git status --short` is empty.
2. Run:
`ninja -j4 all_source progress semantic_progress`
3. Save:
`python -m tools.campaign.stable_verdicts snapshot scratch/lane-a-after.json`
4. Diff against the frozen baseline snapshot:
`python -m tools.campaign.stable_verdicts diff scratch/lane-a-before.json scratch/lane-a-after.json`
5. Run:
- `python -m tools.parked_functions`
- `python tools/audit_object_admission.py`
- `python tools/fake_match_scan.py --format json source/ai`
- `python -m pytest -q tools`
- `git diff --check 6130661c9585fdb2785b3ff6bdb2ad0cfef0520c..HEAD`
6. Verify all inherited exact functions remain exact.
7. Verify no production C/H file outside the 11-file allowlist changed.
8. Verify no Xbox/vendor source changed.
9. Verify no target object, build tool, compiler, comparator, or acceptance policy changed.
10. Verify all data, COMMON/BSS, owner, linkage, COMDAT, and point-helper inventories.
11. Verify every retained fake-scan lead is inherited or individually evidence-approved.
12. Recompute exact meaningful and padded gains from final production, not worker self-reports.
13. List the ordered commit range from baseline to tip.
14. Do not push and do not touch canonical.
## 15. Required deliverables
Create:
- `docs/object_matching_logs/claude_lane_a_ai_core_HANDOFF_20260920.md`
- `docs/object_matching_logs/claude_lane_a_ai_core_admission_manifest_20260920.json`
- `docs/object_matching_logs/claude_lane_a_ai_core_rejected_hypotheses_20260920.md`
The handoff must contain:
- worktree, branch, baseline, and final tip;
- exact ordered commits;
- complete changed-file list;
- baseline and final accepted totals;
- strict meaningful, padded, and function gains;
- fuzzy extent at zero credit;
- zero-regression proof;
- per-wave results;
- per-function evidence;
- config changes and why;
- all header prerequisites deferred;
- every remaining owner and reopening condition;
- verification commands and actual outputs;
- whether the tree is clean;
- explicit statement: nothing pushed and canonical untouched.
The JSON manifest must be machine-readable and include hashes, measurements, relocations, evidence, reviewer decisions, exact/fuzzy disposition, and credit for every retained change.
Preserve compact reproducible scratch evidence locally, but do not commit huge generated disassemblies, copied binaries, object files, or private symbol assets. Record their paths and hashes in the handoff.
## 16. Completion standard
Do not stop because one large function reaches an allocator tie. Move to the next function and continue the portfolio.
Do not declare the lane “exhausted” unless:
- every residual/unwritten owner in all 11 units was inventoried;
- previous work was checked for duplication;
- all existing high-confidence Astra mechanisms were either integrated, rejected with current evidence, or superseded;
- every new evidence-backed structural hypothesis was tested in bounded form;
- every retained change follows the house rules;
- every exact claim passed strict production verification;
- every nonexact improvement is explicitly zero credit;
- every untouched blocker has a concrete reopening condition rather than a generic “compiler tie” label.
The desired outcome is the largest defensible strict gain possible from these 79,657 remaining Halo-owned bytes, plus a materially better research boundary for everything not yet exact. Source authenticity, semantics, ownership, and zero regressions take precedence over a large but artificial headline number.
@@ -1,444 +0,0 @@
HALO JANUARY EXACT-MATCH CAMPAIGN — CLAUDE LANE B
AI ACTIONS, PATHFINDING, GAMEPLAY, PHYSICS/COLLISION, BIPEDS/UNITS/VEHICLES
MISSION
Work only from canonical commit:
6130661c9585fdb2785b3ff6bdb2ad0cfef0520c
branch: jonas/exact-pilots
Your lane contains exactly 75,377 currently non-exact meaningful Halo-owned code bytes. Reconstruct as much of this entire allocation as possible, aiming for at least +50,000 newly proven strict meaningful bytes and ultimately all 75,377. Do not manufacture matches to meet the number. If evidence is insufficient, do more research; do not declare human-written Halo code inherently unmatchable.
This is one of four concurrent, disjoint Claude lanes. Stay inside the source-unit allowlist below. Do not touch Xbox/XDK libraries, Bink/vendor code, libcmt, or any source unit assigned to another lane.
Administrative “reserved,” “do not spend,” and old lane-scope holds are lifted inside this allowlist. Scientific rejections are not automatically erased: do not repeat an exhausted spelling unless new evidence refutes its premise. Parked functions in this lane may be reopened when new evidence satisfies or invalidates their recorded criteria.
Do not push. Do not modify the canonical checkout or canonical branch. Work in a new local worktree and branch created directly from 6130661c.
REQUIRED WORKTREE
Create a clean worktree such as:
C:\halo-worktrees\claude-lane-b-ai-physics-units-20260920
branch: claude/lane-b-ai-physics-units-20260920
If either name already exists, use a unique suffixed name. Never reset, delete, clean, or repurpose an existing worktree.
Before editing, prove:
- `git rev-parse HEAD` is exactly `6130661c9585fdb2785b3ff6bdb2ad0cfef0520c`.
- `git status --short` is empty.
- The configured full build passes.
- A complete stable-verdict snapshot has been saved.
- Baseline meaningful, padded, function, object, park, and test totals have been recorded.
ALLOWED SOURCE UNITS — NO OTHERS
The values are the current meaningful residual-byte ceilings at the frozen baseline.
AI/action/path family — 20,507 bytes:
- `source/ai/action_charge.c` — 3,246
- `source/ai/action_converse.c` — 248
- `source/ai/action_obey.c` — 984
- `source/ai/action_vehicle.c` — 1,679
- `source/ai/actions.c` — 1,981
- `source/ai/actor_combat.c` — 1,529
- `source/ai/actor_type_flood.c` — 1,295
- `source/ai/actor_type_infection.c` — 3,609
- `source/ai/path.c` — 2,783
- `source/ai/path_obstacle_avoidance.c` — 1,569
- `source/ai/path_obstacles.c` — 160
- `source/ai/path_smoothing.c` — 326
- `source/ai/path_structure_bsp.c` — 1,098
Game family — 5,186 bytes:
- `source/game/aim_assist.c` — 1,173
- `source/game/game.c` — 92
- `source/game/game_engine_king.c` — 444
- `source/game/game_engine_race.c` — 215
- `source/game/player_control.c` — 1,577
- `source/game/players.c` — 1,685
Memory family — 821 bytes:
- `source/memory/stack_memory_pool.c` — 821
Physics/collision family — 25,691 bytes:
- `source/physics/breakable_surfaces.c` — 4,020
- `source/physics/collision_bsp.c` — 3,158
- `source/physics/collision_debug.c` — 4,182
- `source/physics/collisions.c` — 4,744
- `source/physics/physics.c` — 9,587
Scenario family — 380 bytes:
- `source/scenario/wind.c` — 380
Units family — 22,792 bytes:
- `source/units/biped_limp_noodle.c` — 3,443
- `source/units/bipeds.c` — 13,147
- `source/units/unit_dialogue.c` — 1,041
- `source/units/units.c` — 48
- `source/units/vehicles.c` — 5,113
The corresponding genuine owner headers may be changed only when required for one of these units and only under the header protocol below. Do not edit generic shared headers such as `cseries.h`, `real_math.h`, object/tag core headers, bitmap headers, weapon headers, or unrelated subsystem headers. If a correct reconstruction requires an out-of-lane shared-header change, preserve the source candidate in scratch, document the exact required header packet and blast radius, and continue elsewhere.
The only permitted non-source edits are:
- lane-specific entries in `config/parked.json`;
- independently authenticated lane-specific names in `config/symbols.json`;
- lane documentation under `docs/object_matching_logs/`.
Do not edit build flags, tools, `semantic_matches.json`, semantic overrides, object-admission overrides, or unrelated configuration to make a candidate appear accepted.
READ THESE BEFORE EDITING
Read these files completely:
- `docs/campaign_house_rules.md`
- `docs/campaign_throughput_lessons.md`
- `docs/common_constants.md`
- `docs/assertions.md`
- `docs/matching_methodology.md`
- `docs/exact_match_acceleration_playbook.md`
- `docs/object_matching_logs/opus5_next150_compiler_laws_20260916.md`
- `docs/object_matching_logs/opus5_next150_compiler_laws_round2_20260916.md`
- `docs/object_matching_logs/astra_90pct_canonical_reconciliation_20260920.md`
- `docs/object_matching_logs/astra_90pct_HANDOFF_20260920.md`
- `docs/object_matching_logs/astra_90pct_research_atlas_20260920.md`
- `docs/object_matching_logs/astra_90pct_rejected_hypotheses_20260920.md`
- `docs/object_matching_logs/astra_90pct_admission_manifest_20260920.json`
Then read the relevant Astra evidence before touching its corresponding family:
- action charge:
- `astra_90pct_wave13_action_charge_setup_20260920.md`
- `astra_90pct_wave13_action_charge_primary_independent_20260920.md`
- `astra_90pct_wave13_action_charge_independent_20260920.md`
- `astra_90pct_wave13_call_sequence_census_20260920.md`
- `astra_90pct_research8_action_charge_20260920.md`
- `astra_90pct_research8_action_charge_independent_20260920.md`
- aim assist:
- `astra_90pct_research8_aim_assist_20260920.md`
- `astra_90pct_research8_aim_assist_independent_20260920.md`
- infection:
- `astra_90pct_research7_infection_20260920.md`
- collisions:
- `astra_90pct_wave2_collisions_20260920.md`
- physics:
- `astra_90pct_wave4_physics_20260920.md`
- `astra_90pct_wave4_physics_independent_review_20260920.md`
- `astra_90pct_wave4_frame_statistics_20260920.md`
- collision debug:
- `astra_90pct_wave6_collision_debug_20260920.md`
- bipeds:
- `astra_90pct_wave3_bipeds_packet_20260920.md`
- `astra_90pct_wave3_bipeds_independent_review_20260920.md`
- `astra_90pct_research18_bipeds_probe_20260920.md`
- `astra_90pct_research18_biped_private_owner_20260920.md`
- `astra_90pct_research11_biped_moving_20260920.md`
- vehicles:
- `astra_90pct_research7_vehicles_20260920.md`
- `astra_90pct_vehicle_independent_review_20260920.md`
- path/obstacles:
- `astra_90pct_wave17_obstacle_owner_20260920.md`
- players:
- `astra_90pct_wave17_player_probe_20260920.md`
- `astra_90pct_wave17_player_private_owner_20260920.md`
- `astra_90pct_wave17_player_independent_20260920.md`
- `astra_90pct_research16_player_context_20260920.md`
The complete immutable Astra evidence package is here:
`C:\Users\isabe\Documents\Codex\2026-09-20\files-pasted-by-the-user-halo\outputs`
Its `SHA256SUMS.json` and package README define its integrity and contents. The large function atlas is research evidence, not current accounting authority. The current build at 6130661c is authoritative.
IMPORTANT ASTRA BOUNDARY
Astra already made `_action_charge_setup` and `_player_examine_nearby_unit` strict exact. They and every other exact baseline function are immutable. Do not “improve,” restyle, or rewrite an exact function.
Use Astra’s rejected-hypothesis ledger to avoid rerunning identical failed spellings. A negative result is not a permanent ban when you have genuinely new evidence, but state what new evidence invalidates the old premise before spending on it. “Try it again” is not new evidence.
EVIDENCE SOURCES
January is the byte authority:
`C:\Users\isabe\Downloads\Halo Combat Evolved (Jan 14, 2002 prototype)`
This directory contains the January `cachebeta` XBE variants, `cachebeta.exe`, and `cachebeta.pdb`. The PDB has useful public symbols but lacks Halo local-variable and line records. Do not claim those missing records exist.
Additional evidence, never interchangeable byte targets:
- October 2001 prototype:
`C:\Users\isabe\Downloads\Halo - Combat Evolved (Oct 12, 2001 prototype)\Halo 2276`
- supplied later symbol executables and three original Bungie source files:
`C:\Users\isabe\Downloads\haloleak2024\halo1`
- open-source Marathon reference:
`C:\Users\isabe\Documents\Codex\2026-07-13\i-w\_research-marathon-source-agent-20260904`
- locally mirrored public Halo research:
- `C:\Users\isabe\Documents\Codex\2026-07-13\i-w\_research-punpck-halo`
- `C:\Users\isabe\Documents\Codex\2026-07-13\i-w\_research-stian-halo`
- the other `_research-*` trees beside them.
The supplied 2020 executables have confirmed source differences. They may establish names, types, broad control-flow topology, ownership, and semantic intent, but their instructions are not January byte proof.
Use Ghidra and DIA where useful. Use the installed IDA Free interactively for ambiguous high-value functions when available, but it has no working callable scripting/RPC interface; do not waste the campaign repeatedly probing for one or declare the lane blocked because IDA cannot be automated. Analyze supplied binaries as data only—never execute them.
HOUSE RULES — ALL ARE BINDING
1. Format a no-argument function with `void` on its own line inside the parameter list.
2. Put every parameter on its own line.
3. End every function with an explicit return: `return;` for `void`, an appropriate value for non-void functions.
4. Use subsystem typed macros wrapping `tag_get`; do not scatter raw typed casts.
5. Use typed object-access macros instead of repeated casts after raw `object_get`.
6. Preserve January’s inline schedule and do not emit a `point_from_line3d` COMDAT.
7. Give private functions authentic or descriptive project-style names; never use `code_XXXXXXXX` or address names.
8. Give globals authentic or descriptive project-style names; never use `bss_XXXXXXXX` or address names.
9. Put prototypes in their genuine owner locations, not unrelated consumer `.c` files.
10. Inline assembly is exceptional. It may be used sparingly in a genuine helper/math routine only when target evidence proves ordinary VC7 C cannot emit the required instructions. Earlier asm rulings are function-specific and do not create blanket permission.
11. If easy exactness fails, preserve credible, useful fuzzy source, document it, and park it at zero credit.
12. Prioritize small objects and small unwritten functions within these substantially reconstructed families.
13. Avoid manual or forced inlining. Reproduce the natural helper boundary and January schedule.
14. Use Marathon as a style/semantic reference, not as January byte proof.
15. If a `.c` file has no dedicated header, use the closest genuine associated header.
16. Use correct enum constants in switch tables; do not replace missing research with raw magic case numbers.
17. Combine declaration and initialization when practical and source-authentic.
18. Prefer appropriate `cseries.h` macros over manual bitwise logic.
19. Use project-defined types such as `real`, not generic substitutes such as `float`.
20. Reject fake matching and source Bungie almost certainly would not have written.
21. Reject nonsensical logic even when it coincidentally byte-matches.
22. Use `/QIfist` only where target/compiler evidence requires it. It is a per-TU property, not a blanket flag. Audit the complete TU and callers before changing it.
23. Assertions/warnings/halts must follow `docs/assertions.md`. Predicates state required invariants, diagnostics remain lazy, and the weapon-connected example requires `!TEST_FLAG(...)`. Do not hand-expand `display_assert` when the genuine `assert`/`dassert`/`vassert` family is appropriate.
24. Never invent undefined behavior, uninitialized reads, out-of-bounds access, representation punning, volatile steering, dummy locals, redundant assignments, self-assignments, impossible conditions, raw-byte emission, pragmas, compiler barriers, fake globals, or arbitrary struct offsets to force bytes.
25. If January conclusively proves an original bug that cannot be expressed in defined natural C, preserve the evidence and candidate at zero credit and request a narrow owner ruling. Do not silently land UB.
26. Correct semantics, ABI, linkage, symbol ownership, data ownership, and source authenticity are required in addition to byte equality.
STRICT CREDIT
A function receives exact credit only when the current strict comparator proves the January and candidate sections identical, including size, normalized bytes, relocation layout and identities, and the required section/owner metadata.
Do not count:
- fuzzy or parked functions;
- “100%” fuzzy similarity;
- same-size functions;
- matching relocation counts without matching instructions;
- semantic overrides;
- asm-implemented or externally supplied rows;
- padded alignment bytes as meaningful bytes;
- pre-existing exact functions;
- worker estimates;
- renamed functions without independent name provenance;
- whole objects whose data/BSS/linkage/extra-owner state has not been audited.
Keep these totals separate in every report:
1. newly strict exact functions;
2. newly strict meaningful code bytes from the accepted ledger;
3. newly strict padded bytes from stable-verdict diff;
4. credible fuzzy bytes retained at zero credit;
5. object-level completions after complete code/data/ownership audit.
Do not mark an object green/Matching merely because all visible functions look exact.
ANTI-DUPLICATION AND CURRENT-STATE INVENTORY
Before source work:
1. Run the current board on every allowed unit.
2. Record each target function’s target-section identity, baseline status, meaningful/padded size, relocation count, frame size, and current owner.
3. Query the Astra function atlas, handoff, and rejection ledger by function.
4. Run `tools/campaign/branch_sweep.py` for each allowed source file to locate better historical bodies without blindly cherry-picking them.
5. Map promising blobs back to commits and read their ledgers/reviews.
6. Treat historical code only as a donor hypothesis. Recompile against 6130661c and re-audit it under current house rules.
7. Never assign two workers the same `.c` file.
8. Never count a function already exact at baseline as a lane gain.
Use the current source/config trees, not an old patch, old all-lanes treemap union, or a prior worker’s headline total.
THROUGHPUT STRATEGY
Do not spend the campaign fighting one register-allocation tie while unwritten or structurally wrong functions remain.
Start with a closeout wave over the smallest residual objects:
- `units` — 48
- `game` — 92
- `path_obstacles` — 160
- `game_engine_race` — 215
- `action_converse` — 248
- `path_smoothing` — 326
- `wind` — 380
- `game_engine_king` — 444
- `stack_memory_pool` — 821
- `action_obey` — 984
Within every unit, prioritize:
1. unwritten bodies;
2. proven missing calls/helper boundaries/incorrect signatures or types;
3. structural control-flow and frame-layout defects;
4. exact historical donors that pass current authenticity review;
5. only then near-tie scheduling/x87/register residuals.
After a few bounded source-shape attempts without new evidence, move to another function. Do not create a bespoke Python tester for every 200–500 byte function. Use the universal gate, relocation diff, aligned disassembly, and stable sweep. Add a runtime harness only when behavior or ABI is genuinely ambiguous.
Return to hard functions after a research pass across:
- public/private symbol provenance;
- caller and callee graphs;
- strings/assert paths/line-number evidence;
- target relocation identities;
- cross-build topology;
- analogous exact functions in January;
- C2 compiler probes;
- Ghidra/IDA/DIA evidence;
- original Bungie/Marathon coding idioms.
A historical park is a research index, not an excuse to stop. Test its explicit reopen criterion. If the criterion is stale or contradicted, reopen it honestly and update only that lane-specific park entry.
PARALLEL AGENT ARCHITECTURE
Use an orchestrator plus workers and independent adversarial reviewers.
- One worker per `.c` file; files must remain strictly disjoint.
- Workers use `tools/campaign/gate.py` and scratch objects only.
- Workers do not run `ninja`, `configure.py`, full pytest, or shared-build commands.
- Workers do not edit headers, config, tools, or commit.
- Workers report exact baseline counts and only newly gained functions.
- The orchestrator reviews and applies candidates to the real source.
- A different agent independently reviews every proposed landing for semantics, house rules, ownership, and exactness.
- The orchestrator alone performs full builds, config updates, and commits.
`gate.py` is safe for concurrent TU work because it writes PID-isolated scratch objects. Useful forms include:
- `python tools/campaign/gate.py <unit>`
- `python tools/campaign/gate.py <unit> --fn <function>`
- `python tools/campaign/gate.py <unit> --source <scratch-source>`
- `python tools/campaign/gate.py <unit> --edits <json>`
- `python tools/campaign/gate.py <unit> --out <scratch-object>`
- `python tools/campaign/gate.py <unit> --forbid-emitted-symbol _point_from_line3d --all`
Do not rely on `gate.py --disas` after instruction counts diverge; it is index-zipped. Use:
- `python tools/campaign/alndiff.py <unit> <function>`
- `python tools/campaign/relocdiff.py <unit> <function>`
- `python tools/campaign/tinfo.py <unit> --fn <function>`
- `python tools/bijection_walk.py --unit <unit> <function> -v`
Use spelling sweeps only to test evidence-backed hypotheses. A successful brute-force spelling is not authenticity evidence by itself.
HEADER PROTOCOL
Header edits are high risk because VC7 allocation can change in unrelated includers based only on declaration position.
Before touching a header:
1. Enumerate every including `.c` with `rg`.
2. Prove the declaration belongs in that header.
3. Confirm it cannot correctly remain TU-private in the owner `.c`.
4. Save a pristine baseline snapshot.
5. Make the header change as a separate prerequisite commit.
6. Rebuild and compare every dependent TU against pristine.
7. Run the whole-tree stable sweep.
8. Reject the header change if it loses any inherited exact function, even if the lane’s net total rises.
9. Never compensate for a header regression with unrelated source steering.
Private structs, enums, helpers, and prototypes should remain in their genuine owner TU when historically and semantically appropriate. Public prototypes belong in the real subsystem header, never copied into whichever `.c` happens to call them.
CONFIG AND NAMING POLICY
`config/parked.json` may change only for functions in this allowlist:
- remove an entry after current strict proof;
- remeasure it when an admitted source/header change legitimately changes the residual;
- add a credible fuzzy landing with measured remaining differences and a concrete reopen criterion.
`config/symbols.json` may change only when the name has independent evidence from a January public, PDB, source/map artifact, unambiguous cross-build identity plus caller graph, or similarly strong provenance. A unique byte alias is a proposal, not name proof.
Do not retain `code + address` or `bss + address` identifiers in finished source. If the original private name is unrecoverable, use a descriptive project-convention name, label it as descriptive rather than proven, and document the evidence.
FULL VERIFICATION
At baseline:
- full configured `ninja -j4 all_source progress semantic_progress`;
- `python -m tools.campaign.stable_verdicts snapshot scratch/lane_b_before.json`;
- `python -m tools.campaign.board`;
- park validation;
- object-admission audit;
- test suite;
- changed-scope fake-scan baseline.
After each coherent wave—and at least every +10,000 newly proven meaningful bytes—the orchestrator must:
1. run all affected per-function strict gates;
2. run the `_point_from_line3d` emitted-symbol guard on every changed TU;
3. run a full `ninja -j4 all_source progress semantic_progress`;
4. capture a new stable-verdict snapshot;
5. diff it against the frozen lane baseline and prove zero exact regressions;
6. inspect accepted meaningful totals from the semantic report;
7. validate parks with zero stale/invalid entries;
8. run the object-admission audit;
9. run `tools/fake_match_scan.py` over every changed source file and adjudicate every new finding;
10. audit emitted code/data/COMMON owners;
11. run relevant runtime/ABI tests and the full pytest suite at final;
12. run `git diff --check`;
13. review the complete diff against the source allowlist.
No inherited exact regression is permitted. Net gain does not excuse one lost exact function.
COMMITS
Commit in coherent, reviewable waves, preferably family/file packets. Keep prerequisite header changes separate. Keep zero-credit fuzzy preservation separate from strict admissions. Do not create a giant opaque final commit.
Every commit message and wave record must identify:
- affected units/functions;
- new strict meaningful and padded bytes;
- fuzzy bytes, separately;
- stable regression result;
- config/header changes;
- evidence used;
- independent reviewer result.
Do not push. Do not merge into `jonas/exact-pilots`. Leave a clean local lane.
FINAL DELIVERABLES
Create:
- `docs/object_matching_logs/claude_lane_b_HANDOFF_20260920.md`
- `docs/object_matching_logs/claude_lane_b_admission_manifest_20260920.json`
- `docs/object_matching_logs/claude_lane_b_rejected_hypotheses_20260920.md`
The handoff must include:
- worktree, branch, frozen base, and final tip;
- ordered commit list;
- exact changed-file list;
- baseline and final accepted ledgers;
- newly strict functions with unit, target section, meaningful bytes, padded bytes, relocation counts, and commit;
- fuzzy functions retained at zero credit with fresh measurements;
- all header/config edits and their blast-radius proofs;
- exact-regression proof;
- test/build/park/admission/fake-scan results;
- remaining function-by-function inventory;
- concrete next evidence required for every remaining substantial function;
- any candidates held for a narrow owner ruling;
- confirmation that no out-of-lane source, vendor library, canonical checkout, or remote branch was touched.
The JSON manifest must be machine-readable and include baseline/final hashes, per-function evidence, source provenance, strict verdict, semantic-credit status, reviewer decision, and commit hash.
If context or token budget becomes low, stop launching new probes. Finish the current coherent packet, run the complete verification, commit it, write the handoff/manifest, and leave the tree clean. Do not leave valuable work uncommitted or source claims unsupported.
Do not stop merely because the headline +50,000 target looks difficult. Continue broad, evidence-driven work across the full 75,377-byte allocation. The valid stopping conditions are:
- every allocated byte is strict exact; or
- every remaining row has received a fresh evidence pass, all admissible improvements are committed, and each unresolved function has a specific documented blocker/reopen criterion rather than a generic “compiler tie” claim.
@@ -1,564 +0,0 @@
You are Claude Opus 5 running Lane C of the Halo: Combat Evolved January 14, 2002 exact-match campaign.
Your task is to reconstruct and strictly verify the remaining Halo-owned graphics/UI code assigned below. Work for maximum durable progress, not a brief sampling pass.
## 1. Frozen baseline and isolation
The immutable starting point is:
- Branch: `jonas/exact-pilots`
- Commit: `6130661c9585fdb2785b3ff6bdb2ad0cfef0520c`
- Canonical checkout, READ ONLY:
`C:\Users\isabe\Documents\Codex\2026-07-13\i-w\worktrees\astra-reconcile-20260920`
Use a dedicated worktree and branch, for example:
- Worktree: `C:\halo-worktrees\claude-lane-c-graphics-ui-20260920`
- Branch: `claude/lane-c-graphics-ui-20260920`
If that worktree or branch already exists, inspect it instead of blindly recreating it. Before doing any work, prove:
```text
git status --short
git rev-parse HEAD
```
The tree must be clean and HEAD must be exactly `6130661c9585fdb2785b3ff6bdb2ad0cfef0520c`.
Never edit the canonical checkout, never move or reset `jonas/exact-pilots`, and never push. Make local commits only in your dedicated lane. Do not cherry-pick another lane wholesale.
## 2. Objective
This lane begins with approximately **83,898 meaningful non-exact Halo code bytes** in its assigned units.
Campaign target:
- Primary target: at least **50,000 newly proven strict meaningful exact bytes**.
- Stretch target: close the entire **83,898-byte** assigned residual pool.
- Also maximize genuinely complete, whole-object `Matching` closures.
- Preserve credible, house-rule-compliant fuzzy improvements at zero matching credit when exactness is not yet attainable.
Do not stop after one or two low-yield waves. Do not declare the target impossible merely by summing current park entries. Parks are prior evidence and hypotheses, not eternal prohibitions; reopen them only when their recorded criteria or genuinely new evidence justify it.
A lower result is acceptable only after every in-scope residual has received a current-baseline classification and all high-confidence structural opportunities have been pursued. “Register allocation tie” or “compiler noise” requires evidence from the existing classifiers or controlled experiments, not intuition.
Count only gains relative to commit `6130661c`. Pre-existing exact functions, donor-relative gains, fuzzy bodies, helper COMDATs, and already-integrated Astra work are not new credit.
## 3. Exclusive source scope
Only the following Halo translation units are assigned to this lane. The byte figures are starting estimates and must be remeasured at the frozen baseline.
### Bitmaps — 8,790 bytes
```text
source/bitmaps/bitmap_group.c 995
source/bitmaps/bitmap_utilities.c 2,362
source/bitmaps/bitmaps.c 1,287
source/bitmaps/s3tc/s3tc.c 4,146
```
### Interface/UI — 32,470 bytes
```text
source/interface/first_person_weapons.c 1,533
source/interface/hud.c 1,744
source/interface/hud_draw.c 364
source/interface/hud_messaging.c 2,922
source/interface/hud_nav_points.c 1,618
source/interface/hud_weapon.c 7,696
source/interface/interface.c 2,981
source/interface/motion_sensor.c 3,144
source/interface/progress_bar.c 349
source/interface/ui_widget.c 6,425
source/interface/ui_widget_game_data_input_functions.c 1,008
source/interface/virtual_keyboard.c 2,686
```
### Rasterizer — 34,352 bytes
```text
source/rasterizer/rasterizer_frame_statistics.c 4,236
source/rasterizer/rasterizer_geometry.c 1,179
source/rasterizer/rasterizer_lights.c 3,049
source/rasterizer/xbox/rasterizer_xbox_active_camouflage.c 2,014
source/rasterizer/xbox/rasterizer_xbox_environment.c 4,957
source/rasterizer/xbox/rasterizer_xbox_environment_fog.c 4,685
source/rasterizer/xbox/rasterizer_xbox_hardware_bitmaps.c 388
source/rasterizer/xbox/rasterizer_xbox_hardware_geometry.c 337
source/rasterizer/xbox/rasterizer_xbox_lights.c 2,338
source/rasterizer/xbox/rasterizer_xbox_models.c 9,553
source/rasterizer/xbox/rasterizer_xbox_shadows.c 1,616
```
### Render — 8,286 bytes
```text
source/render/render_cameras.c 4,649
source/render/render_particles.c 1,154
source/render/render_sprite.c 2,483
```
The arithmetic total for the complete allowlist is **83,898 meaningful residual bytes**.
The files under `source/rasterizer/xbox/` listed above are Halo-owned Xbox renderer code and are explicitly in scope. Do not classify them as external merely because `xbox` appears in the path.
Everything else is out of scope:
- No Xbox SDK/XDK library objects.
- No Bink, Direct3D, XAPI, libcmt, zlib, libtiff, or other vendor-library campaign work.
- No AI, units, vehicles, networking, physics, effects, sound, game-state, or unrelated renderer files.
- Do not modify another lane’s `.c` file to make one of your objects match.
Related genuine headers and narrowly necessary configuration entries may be changed only under the header/configuration policy below.
## 4. Required reading before assigning work
Read these files completely before editing:
```text
docs/campaign_house_rules.md
docs/matching_methodology.md
docs/exact_match_acceleration_playbook.md
docs/campaign_throughput_lessons.md
docs/common_constants.md
docs/assertions.md
docs/user_source_reconstruction_map_20260906.md
docs/fake_match_review.md
tools/campaign/README.md
docs/object_matching_logs/opus5_next150_compiler_laws_20260916.md
docs/object_matching_logs/opus5_next150_compiler_laws_round2_20260916.md
docs/object_matching_logs/opus5_next150_research_campaign_20260916.md
```
Then search and read every relevant current ledger before touching a function:
```text
docs/object_matching_logs/astra_90pct_*
docs/object_matching_logs/*bitmap*
docs/object_matching_logs/*hud*
docs/object_matching_logs/*interface*
docs/object_matching_logs/*widget*
docs/object_matching_logs/*motion_sensor*
docs/object_matching_logs/*rasterizer*
docs/object_matching_logs/*render_camera*
docs/object_matching_logs/*render_particle*
docs/object_matching_logs/*render_sprite*
config/parked.json
config/symbols.json
```
Use `rg` by exact unit and function name. Do not rely on filenames alone. Newer Astra ledgers supersede older guesses when they contain fresh measurements, but every candidate must still be remeasured against commit `6130661c`.
Before assigning a function to a worker, run the existing unclassified/ledger cross-reference workflow described in the compiler-law documents. Determine:
1. Is it already exact at this baseline?
2. Is the same source already integrated under another name?
3. Was the proposed shape already tried?
4. Is it parked, and what is the explicit reopen criterion?
5. Is there a historical branch with a stronger body?
6. Is the mismatch structural, relocation/ownership, frame/type, helper-boundary, x87 ordinal, or a proven allocator/scheduling tie?
Do not duplicate prior work merely because an old handoff started from a lower byte total.
## 5. Evidence hierarchy and research assets
Use evidence in this order:
1. January target object bytes, relocations, section ownership, public symbols, strings, callers, and callees.
2. Authentic Bungie source or authenticated same-function cross-build source.
3. Earlier/later Halo builds for semantics, names, types, and control-flow hypotheses.
4. Open-source Marathon for Bungie style and likely abstractions.
5. Compiler micro-labs and corpus-wide laws for source-shape discrimination.
6. Pure visual similarity or decompiler output only as a hypothesis.
Relevant local evidence includes:
```text
C:\Users\isabe\Downloads\Halo Combat Evolved (Jan 14, 2002 prototype)
C:\Users\isabe\Downloads\Halo - Combat Evolved (Oct 12, 2001 prototype)\Halo 2276
C:\Users\isabe\Downloads\haloleak2024\halo1
C:\Users\isabe\Downloads\haloleak2024\halo1\h1_tags
C:\Users\isabe\Documents\Codex\2026-07-13\i-w\_research-marathon-source-agent-20260904
C:\Users\isabe\Documents\Codex\2026-07-13\i-w\_research-punpck-halo
C:\Users\isabe\Documents\Codex\2026-07-13\i-w\_research-stian-halo
C:\Users\isabe\Documents\Codex\2026-07-13\i-w\_research-halopc-restored
C:\Users\isabe\Documents\Codex\2026-07-13\i-w\_research-demon
C:\Users\isabe\Documents\Codex\2026-07-13\i-w\halocea-review
C:\Users\isabe\Documents\Codex\2026-07-13\i-w\research
C:\Users\isabe\Documents\Codex\2026-07-13\i-w\research-cache
C:\Users\isabe\Documents\Codex\2026-07-13\i-w\_research_cache
```
Known limitations:
- The January `cachebeta.pdb` has useful public symbols but lacks Halo locals and line records.
- The supplied 2020 symbol executables have confirmed source differences. Use them for names, types, and semantic hypotheses—not January byte proof.
- Use both IDA and Ghidra evidence where available. IDA Free on this machine has no usable headless scripting/RPC interface; do not burn a wave repeatedly trying to automate it. Use an existing interactive IDA database/export if available, and use Ghidra headless plus DIA/public-symbol tooling for repeatable extraction.
- Never execute supplied game/tool binaries merely to inspect them.
- Decompiler pseudocode is not original source.
When existing evidence is insufficient, research the target rather than declaring human-written code impossible. Build caller/callee graphs, inspect data and relocation ownership, compare cross-build implementations, search authentic source idioms, and construct narrowly controlled compiler labs. Validate any new “compiler law” against exact controls before applying it.
## 6. House rules — mandatory
1. Format a no-argument parameter list with `void` on its own line inside the parentheses.
2. Put every parameter on its own line.
3. End every function—including `void` functions—with an explicit `return;`. Non-void functions must return an appropriate value, never a bare `return;`.
4. Use subsystem macros wrapping `tag_get` for typed tag access.
5. Use typed object-access macros rather than repeated casts after raw `object_get` calls.
6. Preserve the January inline schedule without emitting a `point_from_line3d` COMDAT.
7. Name private functions correctly. Never introduce `code_XXXXXXXX`, `sub_XXXXXXXX`, or address-based private names.
8. Name globals correctly. Never introduce `bss_XXXXXXXX` or address-based names. When the original name is unavailable, use a descriptive name consistent with project conventions and label it as inferred.
9. Put function prototypes in their genuine owner headers or genuine TU-private location, never in an unrelated consumer `.c` file.
10. Helper and math functions may use inline assembly only sparingly and with direct evidence. Existing admitted asm is not blanket precedent for new asm.
11. If easy exactness is not achievable, retain the best credible fuzzy reconstruction, document and park it, and grant it zero exact credit.
12. Prioritize small functions and near-complete objects within this assigned family, while still researching the larger structural functions.
13. Avoid forced/manual inlining. Preserve natural helpers and January’s observed inline schedule.
14. Use open-source Marathon as a style and semantic reference, not proof of January bytes.
15. If a `.c` file has no dedicated header, use the closest genuine associated header.
16. Use proper enum constants in switch tables, not unexplained raw case numbers.
17. Declare and initialize variables together when practical and natural.
18. Prefer established `cseries.h` macros over manual bit manipulation.
19. Use project-defined types such as `real`, `boolean`, `byte`, `word`, and `dword` rather than generic substitutes such as `float`, C++ `bool`, or platform-dependent integers.
20. Reject fake matching and source that Bungie almost certainly would not have written.
21. Reject nonsensical function logic even if it coincidentally byte-matches.
22. Use `/QIfist` only where target/compiler evidence requires it. It is a per-TU option, not a blanket setting.
Additional fidelity requirements:
- Use `assert(expression)`, `dassert(expression, diagnostic)`, and `vassert(expression, format, ...)` through the established `cseries.h` owner. Likewise use the established `warn`/`dwarn`/`vwarn` and `halt`/`dhalt`/`vhalt` families. Do not hand-expand them into `display_assert`, formatting buffers, and `system_exit`.
- Assertion predicates state the required invariant. Do not accidentally invert them.
- Preserve evaluation count, evaluation order, and lazy diagnostics.
- No `volatile`, `register`, dummy statements, dead locals, arbitrary buffer inflation, invented globals, compiler barriers, pragmas, raw byte emission, self-assignments, empty branches, goto-next-label tricks, or duplicated equivalent branches merely to steer code generation.
- No undefined behavior, uninitialized reads, out-of-bounds accesses, inactive-union-member tricks, representation-punning casts, raw numeric struct offsets, or stack overruns for a match. If January appears to contain a genuine bug, document the evidence and hold it for an owner ruling; do not silently land it.
- Do not extend function-specific prior rulings to other functions.
- Correct semantics and credible source are admission requirements even when bytes match.
## 7. Strict exactness, fuzzy work, and accounting
A function is strict exact only when the established comparator proves:
- exact function size,
- exact normalized code bytes,
- exact relocation count,
- exact relocation addresses/types,
- exact relocation target identities,
- and the correct target-function owner.
Do not call a size/relocation near-tie exact. Do not use semantic overrides or matching allowlists to manufacture credit.
Maintain three separate totals:
1. Newly strict **meaningful** code bytes from `build/semantic_report.json`.
2. Newly strict padded/function-section bytes from the stable verdict diff.
3. Credible fuzzy bytes retained at **zero credit**.
Also report:
- strict functions gained,
- strict functions lost,
- newly complete function-only objects,
- newly admitted whole `Matching` objects,
- data/BSS/linkage/extra-owner changes,
- parks removed, remeasured, stale, or invalid.
A function-only 100% object is not automatically a whole matching object. Before marking an object `Matching`, prove:
- every required Halo function is strict exact,
- data, rdata, BSS, and COMMON ownership agree,
- linkage/storage classes agree,
- no extra code or data owner is emitted,
- helper/COMDAT ownership agrees with January,
- the object is source-authentic,
- and the object-admission audit accepts it.
A blue “100%” result is not green whole-object proof. Halo-owned Xbox rasterizer objects must not be hidden as `external`.
Never trade away an inherited exact function. Gross gain does not conceal a regression. If an intentional exactness loss is ever proposed, do not land it without an explicit owner ruling and explicit subtraction from the totals.
## 8. Anti-duplication procedure
Before editing each unit:
```text
git log --all -- <unit.c>
python tools/campaign/branch_sweep.py <unit-without-.c>
python tools/campaign/gate.py <unit-without-.c> --all
python -m tools.campaign.unparked <unit-without-.c>
```
Search the function and unit name across all ledgers and handoffs. Map any promising historical blob back to its commits and provenance. A historical exact body still needs current-baseline source, owner, ABI, house-rule, and regression review.
Never count:
- code already exact at `6130661c`,
- code newly exact only because of a symbol alias without authenticated naming evidence,
- a donor’s claimed total,
- a function that another worker landed earlier in this lane,
- helper code not owned by the January object,
- or fuzzy code.
Keep a central assignment ledger so two workers never attack the same TU or function.
Historical leads such as `_bitmap_2d_sharpen`, `_bitmap_group_add_bitmap`, `_EncodeBlockRGBColorKey`, `_first_person_weapon_update`, `_hud_render_unit_interface`, `_hud_update_weapon`, motion-sensor updates, environment/lightmap/fog functions, and render-camera frustum functions must be remeasured. Their old labels are hypotheses, not instructions. Do not repeat a recorded negative shape unless new evidence directly refutes the old premise.
## 9. Parallel campaign architecture
Use agents aggressively, but safely:
- One implementation worker per `.c` file.
- Files must be strictly disjoint.
- No worker edits headers, config, shared docs, or another worker’s file.
- Workers use `tools/campaign/gate.py`, which writes PID-isolated scratch objects.
- Workers do not run `ninja`, `configure.py`, or any command that writes the shared `build/` tree.
- Workers do not commit.
- Workers record the exact current unit census before editing.
- Workers retain only source-authentic improvements and fully revert reductions in strict exactness.
- Each landed file receives an independent adversarial review by an agent other than its author.
- Only the orchestrator edits genuine shared headers/config, runs full builds, updates parks, and commits.
Use waves of disjoint TUs. Start with:
1. Unwritten functions.
2. Small functions capable of closing near-complete objects.
3. Structural mismatches with a named source-level defect.
4. Larger functions where call graph, frame, type width, relocation, or helper-boundary evidence gives a concrete route.
5. Pure allocation/x87/scheduling ties only when a new evidenced compiler law supplies a legitimate source lever.
Eight small closures are preferable to one speculative rewrite. However, do not abandon large objects forever: give their biggest functions dedicated research packets after the easy structural pool is harvested.
If a wave yields little, preserve its durable negative evidence and immediately pivot to a different mechanism. Do not spend multiple waves permuting declarations or random spellings.
## 10. Existing tools — use universal gates
Prefer the committed campaign tools over writing a bespoke Python test for every function:
```text
python tools/campaign/board.py
python tools/campaign/board.py <unit>
python tools/campaign/gate.py <unit> --fn <function>
python tools/campaign/gate.py <unit> --all
python tools/campaign/alndiff.py <unit> <function>
python tools/campaign/relocdiff.py <unit> <function> --count-by-target
python tools/campaign/tinfo.py <unit> --fn <function>
python tools/campaign/tinfo.py <unit> --data
python tools/campaign/branch_sweep.py <unit>
python tools/bijection_walk.py --unit <unit> <function> -v
python tools/fake_match_scan.py <changed paths>
python -m tools.campaign.unparked <unit>
python -m tools.campaign.stable_verdicts snapshot <file>
python -m tools.campaign.stable_verdicts diff <before> <after>
python -m tools.parked_functions
python -m tools.audit_semantic_matches
python tools/audit_object_admission.py
```
Use `alndiff.py`, not `gate.py --disas`, when instruction counts diverge; the latter is an index-aligned zip and hides insert/delete boundaries.
Use the existing frame-slot, x87-mod8, inline-boundary, cross-jump, scheduling-selection, and unclassified-triage detectors described in the compiler-law ledgers. Run their required ordering before inventing source shapes.
Spelling sweeps and compiler probes are diagnostic only. A winning spelling is not source-authenticity evidence by itself.
Do not add general tooling unless it solves a genuinely recurring gap. Any committed tool must be tested and documented; otherwise keep research scripts under untracked scratch evidence.
## 11. Headers and configuration
Header changes are high-risk because VC7 code generation is definition-position sensitive.
To keep the four concurrent lanes mechanically reconcilable, do not commit
changes to broad cross-lane headers such as `cseries.h`, `real_math.h`, core
object/tag headers, or a header genuinely owned by another lane. Preserve any
such requirement as a minimal proposed patch with consumer census and strict
before/after evidence, then continue elsewhere. Lane C may commit only genuine
bitmap, interface, rasterizer, or render owner-header changes whose complete
consumer set has been audited under the protocol below.
Before changing any header:
1. Identify every `.c` includer with `rg`.
2. Prove the declaration belongs in that header.
3. Determine whether a TU-private declaration is more authentic.
4. Compile pristine and patched header trees for every dependent TU.
5. Compare every dependent function’s size, relocations, normalized hash, emitted owners, and data ownership.
6. Run a full stable whole-tree diff.
7. Obtain an independent reviewer’s approval.
8. Commit the header prerequisite in an independently buildable packet.
Do not keep a header change that gains your object by regressing an unrelated object. Do not “repair” a regression with coincidental declaration ordering.
Configuration policy:
- `config/symbols.json`: edit only with authenticated January/PDB/source/caller evidence. Keep a symbol rename and the corresponding source identifiers in the same coherent commit.
- `config/parked.json`: orchestrator only. Remove a park when the function is newly exact. Rebaseline a credible fuzzy function only with fresh measurements and dated evidence. Never rebaseline a park to conceal a regression.
- `config/config.json` or object `Matching` status: change only after whole-object admission proves code, data, BSS, linkage, extra-owner, and source credibility.
- Never add a semantic-match override.
- Never broadly change compiler flags. `/QIfist` requires per-TU proof and full consumer/caller verification.
If a required owner header is shared with an out-of-scope unit, you may make the minimal genuine header change but may not edit the out-of-scope `.c` file. Every consumer must remain exact or unchanged.
## 12. COMDAT, owner, and inline safeguards
For every changed TU:
```text
python tools/campaign/gate.py <unit> --forbid-emitted-symbol _point_from_line3d --all
```
This must explicitly pass. Undefined references do not fail the guard; a candidate code definition does.
Also inspect:
- target and candidate code-symbol owner sets,
- COMDAT selection and storage class,
- helper bodies emitted only by the candidate,
- missing target helper owners,
- data/rdata/BSS/COMMON owners,
- relocation target identity and addends,
- private versus external linkage.
Do not gain a caller by changing the wrong helper owner. Preserve January’s observed inline schedule. Do not make shared `real_math.h` inline bodies `static` or otherwise globally change helper emission to fix one object.
Existing admitted asm such as `fast_ftol` or the already-reviewed color helper is function-specific evidence. It is not permission to hand-write arbitrary renderer bodies in assembly.
## 13. Baseline, checkpoint, and final verification
Before editing:
1. Run the full build.
2. Save a stable verdict snapshot.
3. Save `build/semantic_report.json`.
4. Record board totals and all in-scope per-unit gates.
5. Validate parks and object admission.
6. Record the baseline owner/data census for every assigned object.
At each coherent admission batch:
1. Review the source diff for house-rule compliance.
2. Run each changed unit’s full `gate.py --all`.
3. Run the `_point_from_line3d` emitted-symbol guard.
4. Review every target/candidate owner and relocation change.
5. Run the fake-match scanner over all changed source.
6. Run `ninja`.
7. Take a fresh stable snapshot and diff it from the frozen baseline and the previous checkpoint.
8. Require zero inherited exact regressions.
9. Validate parks.
10. Run semantic-match and whole-object admission audits.
11. Run the repository’s full pytest suite.
12. Run `git diff --check`.
13. Commit only when the tree is fully verified.
At final tip, require:
```text
ninja
python -m tools.campaign.stable_verdicts snapshot scratch/lane-c-after.json
python -m tools.campaign.stable_verdicts diff scratch/lane-c-before.json scratch/lane-c-after.json
python -m tools.parked_functions
python -m tools.audit_semantic_matches
python tools/audit_object_admission.py
python tools/fake_match_scan.py <all changed source/header paths>
python -m pytest -q
git diff --check
git status --short
```
If scratch content interferes with pytest discovery, move evidence outside the worktree rather than excluding real tests.
The final tracked tree must be clean. Keep bulky scratch objects and experimental variants outside Git.
## 14. Fuzzy preservation
If a credible reconstruction is the best current implementation but does not become strict exact:
- Keep it only if semantics, ABI, ownership, and house rules are sound.
- Measure size, normalized divergence, relocations, and first differing regions.
- Compare it with the baseline and every known donor.
- Document why it is better.
- Record remaining blockers and exact reopen criteria.
- Update its park evidence honestly.
- Give it zero strict credit.
- Do not sacrifice an exact sibling or whole-object owner to retain it.
A fuzzy implementation is useful future research, but it must never inflate the strict total.
## 15. Campaign behavior
Work autonomously. Do not pause for ordinary implementation questions. If one function needs an owner ruling, park that question, continue unrelated work, and present rulings in a compact batch.
Do not terminate because:
- the first wave was low yield,
- a previous ledger called something difficult,
- a function is parked,
- a header is shared,
- or the PDB lacks locals.
Instead, determine whether new evidence can satisfy the reopen condition. Conversely, do not repeatedly attack a proven allocator tie without a new source-level lever.
Every one of the 83,898 starting residual bytes must finish in one of these categories:
1. Strict exact and landed.
2. Credible fuzzy improvement landed at zero credit.
3. Evidence-backed structural blocker with a concrete reopen criterion.
4. Demonstrated compiler allocation/scheduling tie with the validating detector.
5. Owner/policy ruling required, with a minimal ready candidate.
6. Previously solved/duplicate at the frozen baseline and therefore excluded from new credit.
“Unexamined” is not an acceptable final category.
## 16. Commit and handoff requirements
Make small, independently buildable commits organized by coherent TU or owner packet. Avoid a tangled final megacommit. Header/config prerequisites and their affected source must remain in a clear, ordered series.
Do not push.
Create:
```text
docs/object_matching_logs/claude_lane_c_graphics_ui_HANDOFF_20260920.md
docs/object_matching_logs/claude_lane_c_graphics_ui_admission_manifest_20260920.json
```
The handoff must include:
- frozen base and final commit IDs,
- exact reverse-order cherry-pick range,
- every commit and changed file,
- baseline and final strict functions,
- baseline and final meaningful exact bytes,
- new meaningful bytes,
- new padded bytes,
- fuzzy bytes at zero credit,
- exact regressions, which must be zero,
- objects moved to function-only 100%,
- objects admitted as whole `Matching`,
- data/BSS/linkage/COMDAT owner changes,
- header/config changes and their full blast-radius evidence,
- parks removed or remeasured,
- tests and audits with exact counts,
- fake-scan findings and disposition,
- per-function sizes, hashes, relocation counts, and source evidence,
- rejected experiments and do-not-repeat findings,
- unresolved candidates and precise reopen criteria,
- confirmation that canonical was untouched and nothing was pushed.
The JSON manifest must be machine-readable and contain per-function before/after evidence, not merely headline totals.
Finish with a clean local branch and report:
```text
branch
base
tip
commit count
strict meaningful gain
strict padded gain
strict function gain
whole-object Matching gain
fuzzy bytes retained at zero credit
regressions
test/audit results
git status
nothing pushed
canonical untouched
```
@@ -1,77 +0,0 @@
# Claude Lane D — refreshed after A/B/C reconciliation
Work on Halo January 2342 **Halo-owned** code, not Xbox/vendor libraries. Start
an isolated local worktree and branch at the **latest fetched tip** of
`jonas/exact-pilots` (`https://github.com/bnunu/halo`). Verify that this tip
contains the audited A/B/C reconciliation commit
`4f4c85021db48dae61252d8e1301c60e1c2cc138`, record its full hash in
your handoff, and confirm a clean tree before editing. Do not start from the
stale `6130661c` base in the old Lane D brief. Never modify canonical or
push your lane.
Read these complete before selecting work:
- `docs/campaign_prompts/CLAUDE_REMAINING_LANE_D_SYSTEMS_EFFECTS_20260920.md`
for the full 49-TU Lane D allowlist, reference paths, workflow and house rules;
**this refresh overrides its baseline, numeric portfolio, cross-lane header
reservations, and absolute COMDAT rule**.
- `docs/campaign_house_rules.md`, `docs/matching_methodology.md`,
`docs/common_constants.md`, `docs/assertions.md`,
`docs/campaign_throughput_lessons.md`.
- `docs/object_matching_logs/claude_lane_a_ai_core_HANDOFF_20260920.md`,
`docs/object_matching_logs/claude_lane_a_HANDOFF_TO_LANE_B_20260921.md`,
`docs/object_matching_logs/claude_lane_b_HANDOFF_20260920.md`,
`docs/object_matching_logs/claude_lane_b_rejected_hypotheses_20260920.md`,
`docs/object_matching_logs/claude_lane_c_canonical_reconciliation_20260922.md`,
and `docs/common_pool_ordering_20260922.md`.
Re-measure the allowlist against **this** baseline. A/B/C already changed some
Lane D files, so no old residual size, park count, donor gain, or archived lane
report is current authority. Freeze a stable-verdict snapshot, per-object
function/data/owner census, parks, admission audit, build and tests before any
edit. Prioritize still-unwritten and structural gaps in projectiles/weapons,
decals/effects, HS compile/runtime, sound, geometry, and the smaller systems
objects; do not redo already exact rows. Aim for 40K new meaningful exact code
bytes while evidence supports it, but report the actual verified delta only.
Lessons that bind this lane:
1. Exact function credit requires a strict stable row, full build, unchanged
inherited exact rows, sound semantics, and an ownership audit. A fuzzy body
is useful source but earns zero exact bytes. Data scoring changed with
objdiff v3.6.0; separate scorer/accounting gains from newly reconstructed
source and keep `linker_common` at zero credit while incomplete.
2. Header definition position can alter unrelated TUs. Put each genuine owner
header change in a prerequisite commit, enumerate all consumers, and run a
board-wide per-row sweep. Do not hide a needed declaration in a consumer
`.c`. The A/B/C work has landed, so prior cross-lane reservations are lifted;
the blast-radius rule is not.
3. COMMON pool adjacency is only a negative constraint, **never** proof of a
global's owning TU. Do not add unreferenced or address-named globals to
complete `.data`/`.bss`. Require an independent original owner/type oracle.
4. A real shared-header inline COMDAT may be emitted under the documented
A/B/C narrow exception only when January's folded copy is byte-identical,
the caller is strict exact, the full owner census is reviewed, and no exact
sibling regresses. Do not hand-expand helpers to evade ownership. In
particular, preserve the January `_point_from_line3d` schedule.
5. Previously held UB, artificial duplicate work, pragmas, two-pass asm, and
source-shape steering do **not** become acceptable merely because they
byte-match. Reopen only on new primary evidence and an explicit owner
ruling. The rejected Lane C COMMON, null-write, HUD asm/pragma, and
lens-flare-overrun packets are examples, not backlog to re-land.
6. Use first-party January bytes/relocations, January MAP/PDB, the supplied
Bungie C files, HCEA/2020, Ghidra/DIA and Marathon with their correct
evidentiary limits. The later builds are not interchangeable with January.
Do not spend a wave on known pure register/x87 ties without a falsifiable
source, type or header hypothesis.
Use one worker per disjoint `.c`; workers use PID-isolated `gate.py` scratch
objects, never shared build/config. Orchestrator alone edits headers/config,
runs Ninja, performs the stable diff, commits, and updates parks. In each
5K–15K wave reject exact losses, invalid parks, invented owners, surplus
COMDATs, fake-scan leads, and nonsensical logic before banking. Keep a
machine-readable per-function manifest and an evidence-backed list of rejected
trials. End with local commits, a clean worktree, final tests/build/sweep and
handoff; **do not push**. Report strict meaningful code, padded code, fuzzy
zero-credit extent, data-only/scorer gains and whole-object completions
separately.
@@ -1,311 +0,0 @@
# Claude prompt — Lane D: effects, items, world data, sound, scripting, and systems
You are running one of four strictly disjoint Halo 1 January 2342 exact-match
lanes. Work aggressively and persistently, but never purchase byte equality
with fake source, undefined behavior, invented ownership, target rewriting, or
nonsensical logic.
## Fixed baseline and isolation
- Repository: https://github.com/bnunu/halo
- Baseline branch: jonas/exact-pilots
- Exact baseline commit: `6130661c9585fdb2785b3ff6bdb2ad0cfef0520c`
(Reconcile final Astra campaign, +4,073 bytes)
- Create an isolated worktree and branch from that exact commit, for example
C:\\halo-worktrees\\claude-remaining-lane-d-20260920 on
claude/remaining-lane-d-20260920.
- Confirm `git rev-parse HEAD` equals the full baseline hash and the tree is
clean.
- Never edit the canonical checkout. Never push. Do not merge or rebase another
campaign. Finish with local commits and a clean worktree.
- Read the whole repository and evidence corpus, but modify only Lane D files,
genuine Lane D owner headers after the header protocol below, uniquely named
Lane D documentation, and a final isolated bookkeeping commit.
The reconciled baseline has 1,443,979 / 1,770,166 accepted Halo meaningful
bytes (81.573084%), 7,291 / 7,574 accepted Halo functions, and 184 valid active
parks. All four lanes together cover 326,187 remaining Halo-owned bytes.
Lane D owns 87,255 meaningful residual bytes across 49 translation units:
32,593 written-unparked, 54,651 parked, and 11 unwritten. Target at least
40,000 new strict meaningful bytes, with a stretch goal of closing the full
portfolio. If 40K is not reached, continue until every unit has a verified
landing or a precise evidence-backed blocker. Do not declare the lane
unreachable from aggregate park counts.
## Exclusive Lane D allowlist
Only these translation units are in scope:
~~~text
source/bink/bink_playback 979
source/bungie_net/common/prime_numbers 340
source/bungie_net/network/transport_endpoint_set_winsock 1344
source/bungie_net/network/transport_endpoint_winsock 294
source/cache/cache_files_decompress_windows 2117
source/cache/cache_files_windows 746
source/cache/xbox_sound_cache 368
source/cache/xbox_texture_cache 209
source/camera/dead_camera 1512
source/camera/editor_flying_camera 2563
source/camera/observer 1561
source/cseries/profile 276
source/cseries/stack_walk_windows 1167
source/cutscene/cinematics 1272
source/effects/contrails 945
source/effects/decals 7930
source/effects/effects 3003
source/effects/particle_systems 2103
source/effects/player_effects 2355
source/effects/weather_particle_systems 2346
source/hs/hs 81
source/hs/hs_compile 2632
source/hs/hs_runtime 3404
source/items/items 2352
source/items/projectiles 7905
source/items/weapons 2224
source/main/main 2542
source/math/geometry 4775
source/math/periodic_functions 244
source/networking/network_connection 588
source/objects/damage 2663
source/objects/widgets/flags 1170
source/objects/widgets/glow 1729
source/objects/widgets/light_volumes 905
source/objects/widgets/lightning 2571
source/saved games/player_profile 1148
source/saved games/saved_game_files 359
source/sound/game_sound 1570
source/sound/ima_adpcm 404
source/sound/sound_dsound_xbox 630
source/sound/sound_manager 3966
source/structures/leaf_map 1950
source/structures/structure_detail_objects 66
source/structures/structure_lens_flares 4331
source/structures/structure_visibility 468
source/structures/structures 1210
source/tag_files/files_windows 209
source/text/draw_string 920
source/tool/connected_geometry 809
~~~
Numbers are current residual bytes, not promised gains. Do not work in libs/**,
xbox/**, vendor Bink objects, libcmt, d3d8, d3dx8, dsound, xapi, xnet, xkbd, or
any translation unit absent from the allowlist.
## Read before editing
Read these files completely:
~~~text
README.md
docs/campaign_house_rules.md
docs/matching_methodology.md
docs/common_constants.md
docs/assertions.md
docs/campaign_throughput_lessons.md
tools/campaign/README.md
docs/object_matching_logs/astra_90pct_HANDOFF_20260920.md
docs/object_matching_logs/astra_90pct_research_atlas_20260920.md
docs/object_matching_logs/astra_90pct_rejected_hypotheses_20260920.md
docs/object_matching_logs/astra_90pct_admission_manifest_20260920.json
docs/object_matching_logs/astra_90pct_research5_shared_context_20260920.md
docs/object_matching_logs/astra_90pct_research5_shared_schedule_20260920.md
docs/object_matching_logs/astra_90pct_research5_projectiles_20260920.md
docs/object_matching_logs/astra_90pct_research5_structure_lens_20260920.md
docs/object_matching_logs/astra_90pct_research5_sound_type_rejected_20260920.md
docs/object_matching_logs/astra_90pct_research8_damage_20260920.md
docs/object_matching_logs/astra_90pct_research8_damage_independent_20260920.md
docs/object_matching_logs/astra_90pct_research8_sound_listener_20260920.md
docs/object_matching_logs/astra_90pct_research10_weapon_projectiles_20260920.md
docs/object_matching_logs/astra_90pct_research10_weapon_projectiles_independent_20260920.md
docs/object_matching_logs/astra_90pct_research12_weapons_tail_20260920.md
docs/object_matching_logs/astra_90pct_research12_weapons_owner_independent_20260920.md
docs/object_matching_logs/astra_90pct_wave4_decals_20260920.md
docs/object_matching_logs/astra_90pct_wave4_decals_independent_review_20260920.md
~~~
Search current per-object logs before reopening any park. Useful read-only
primary and cross-build material:
~~~text
C:\\Users\\isabe\\Downloads\\Halo Combat Evolved (Jan 14, 2002 prototype)
C:\\Users\\isabe\\Downloads\\Halo - Combat Evolved (Oct 12, 2001 prototype)\\Halo 2276
C:\\Users\\isabe\\Downloads\\haloleak2024\\halo1\\network_client_manager.c
C:\\Users\\isabe\\Downloads\\haloleak2024\\halo1\\network_server_manager.c
C:\\Users\\isabe\\Downloads\\haloleak2024\\halo1\\random_math.c
C:\\Users\\isabe\\Downloads\\haloleak2024\\halo1\\h1_tags
C:\\Users\\isabe\\Documents\\Codex\\2026-07-13\\i-w\\_research-marathon-source-agent-20260904
C:\\Users\\isabe\\Documents\\Codex\\2026-09-20\\files-pasted-by-the-user-halo\\outputs\\astra_90pct_function_atlas.csv
~~~
January's PDB has publics but no useful Halo locals or line records. The
2020/debug executables have confirmed source differences. Use them as
typed/control-flow evidence, not interchangeable source. IDA Free has no
callable automation endpoint. Use Ghidra headless, DIA/PDB tools, January raw
bytes and relocations, original MAP names, the supplied Bungie source, and
authenticated cross-build windows.
## Mandatory house rules
1. Put void on its own line in every no-argument parameter list.
2. Put every parameter on its own line.
3. End every function with an explicit valid return.
4. Use subsystem typed macros wrapping tag_get.
5. Use typed object macros instead of raw object_get plus casts.
6. Preserve January's inline schedule; emit no new point_from_line3d COMDAT.
7. Give private functions authentic/descriptive names, never code plus address.
8. Give globals authentic/descriptive names, never bss plus address.
9. Put prototypes in genuine owners, not unrelated consumers.
10. Use inline assembly only for evidenced low-level/helper/math instructions
with no faithful VC7 C spelling.
11. Retain and document credible fuzzy code at zero credit when easy exactness
fails; park it for later.
12. Prefer small closable functions before one giant residual.
13. Do not force-inline or duplicate helper bodies to steer code generation.
14. Marathon is style/semantic reference, not January byte proof.
15. With no dedicated header, use the closest genuine associated header.
16. Use named correct enum constants in switches.
17. Declare and initialize variables together when practical.
18. Prefer cseries.h macros over manual bit operations.
19. Use project types such as real, boolean, byte, short, and long.
20. Reject fake matching and implausible original source.
21. Reject nonsensical logic even when bytes match.
22. Use /QIfist only when target/compiler evidence requires it per TU and all
affected callers/conversions have been audited.
Use the repository's verified assert/dassert/vassert, warn/dwarn/vwarn, and
halt/dhalt/vhalt contracts from docs/assertions.md. Do not manually expand them.
Preserve a target-proven original bug only when primary evidence is decisive,
the semantics are documented with a BUG comment, and independent review agrees.
Never introduce undefined behavior merely to shape bytes.
## Workflow
1. Establish the full baseline:
~~~text
ninja -j4 all_source progress semantic_progress
python -m pytest -q tools
python -m tools.campaign.stable_verdicts snapshot scratch/lane-d-before.json
python -m tools.parked_functions
python tools/audit_object_admission.py --output scratch/lane-d-admission-before.json
~~~
2. Build a Lane D ledger. For every row record meaningful and padded target
size, candidate size, relocation count, normalized hash, objdiff percent,
park class, reopen criterion, and prior experiments. Re-measure locally; the
Astra CSV is routing evidence, not authority.
3. Attack in this order:
- genuinely unwritten functions and missing coherent source graphs;
- written-unparked structural differences involving calls, branches, fields,
locals, types, or ownership;
- small near-complete functions with new primary evidence;
- parks only when evidence satisfies or refutes their reopen criteria.
Do not spend a wave on pure register allocation, independent-load ordering,
or x87 stack ties without a falsifiable source/type/header hypothesis.
4. Highest leverage: projectiles and weapons; decals, structure lens flares,
effects, particles and weather; HS compile/runtime; sound manager/game sound;
geometry/math with natural helpers; then small cache, camera, object-widget,
save, text and tool units. Use the three Bungie C files as style and semantic
maps where applicable, not January byte authority.
5. Use PID-isolated scratch objects:
~~~text
python tools/campaign/gate.py <unit> --fn <function> --out scratch/<unique>.obj
python tools/campaign/alndiff.py <unit> <function> --ours-object scratch/<unique>.obj
python tools/campaign/relocdiff.py <unit> <function> scratch/<unique>.obj --count-by-target
python tools/campaign/tinfo.py --object scratch/<unique>.obj --data
~~~
Use gate --edits or scratch source copies for bounded experiments. A gate hit is
necessary but not sufficient: independently review semantics, source
plausibility, relocations, owners, non-code sections, and siblings.
6. If using subagents, assign exactly one C file per worker. Workers use only
gate.py and unique scratch objects; they do not run Ninja, edit headers or
config, commit, or push. The orchestrator reproduces every hit.
7. Header protocol: edit only a genuine owner; enumerate every C consumer with
rg first; freeze exact controls and owner/data/COMMON/COMDAT state; compile
every consumer; put a header prerequisite in its own commit; reject any
inherited exact loss or unsupported owner. If a required shared header is
owned by another lane, preserve a proposed patch and evidence but do not
edit it.
To minimize four-lane reconciliation, do not commit changes to broad
cross-lane headers such as cseries.h, real_math.h, or core object/tag
headers. Preserve those as separately reviewable proposed patches with a
complete consumer census and strict before/after evidence. Commit only
narrow genuine Lane D subsystem owner headers after the protocol above.
8. Shared config protocol: never change comparators, target objects, build
metadata, global optimizer defaults, or semantic-credit rules. symbols.json
requires original identity evidence. Matching requires full code, data, BSS,
relocation, COMDAT, linkage, surplus-owner and source-authenticity audits.
Keep source commits separate. Put minimal park changes in a final bookkeeping
commit and duplicate the exact delta in the manifest for conflict-free
integration.
9. Run the hard point_from_line3d emitted-symbol guard on every changed TU
unless January genuinely owns it there. For inherited genuine owners, compare
the complete before/after owner census. Reject surplus fast_ftol, pixel,
math, or XDK helper owners unless authentic topology and link selection are
proved. Equal bytes in another object are not ownership proof.
10. Preserve best credible fuzzy bodies only at zero credit. Do not count fuzzy
target extent, objdiff percentage, or padded envelope as exact progress.
## Wave gates and accounting
Use coherent 5K–15K candidate waves. Before and after each landing, run full
build and stable snapshots. At minimum:
~~~text
ninja -j4 all_source progress semantic_progress
python -m tools.campaign.stable_verdicts snapshot scratch/lane-d-after-N.json
python -m tools.campaign.stable_verdicts diff scratch/lane-d-before-N.json scratch/lane-d-after-N.json
python -m tools.parked_functions
python tools/audit_object_admission.py --output scratch/lane-d-admission-N.json
python tools/fake_match_scan.py --format json <changed files>
git diff --check
~~~
At each cumulative 10K meaningful checkpoint and final closeout also run the
full tools tests and global fake scan. Audit exact bytes, ordered relocation
identities, inherited matches, code/data/BSS/COMMON owners, COMDAT copies,
externals, names/linkage, and non-code sections.
Report separately: strict meaningful bytes, strict padded bytes, and fuzzy
target extent at zero credit. Never derive canonical gain from donor headlines.
## Commits and handoff
- Commit only independently verified packets, one coherent source/owner packet
per commit. Header prerequisites precede source; park/config bookkeeping is
last.
- Do not add scratch, build outputs, binaries, PDB/MAP/EXE files, generated
atlases, or large probe reports.
- Use uniquely named Lane D records. End clean and do not push.
Create:
~~~text
docs/object_matching_logs/claude_remaining_lane_d_HANDOFF_20260920.md
docs/object_matching_logs/claude_remaining_lane_d_admission_manifest_20260920.json
~~~
The handoff must state base/tip, ordered commits, changed files, per-function
meaningful/padded bytes and fingerprints, strict gains, zero-credit fuzzy work,
rejected trials, parks/config/header changes, owner/COMDAT/data results, exact
commands/results, and blockers with concrete reopen criteria. The manifest must
contain machine-readable measurements and exact proposed shared-config deltas.
Do not stop after a few easy leaves. Continue through the allowlist while
tokens remain, switching quickly from exhausted scheduling ties to a different
unit with structural evidence. Authenticity and zero regressions remain
absolute under the 40K goal.
-25
View File
@@ -1,25 +0,0 @@
# Four-lane remaining-Halo campaign (2026-09-20)
These four self-contained Claude prompts partition every Halo-owned meaningful
residual byte measured at canonical commit
`6130661c9585fdb2785b3ff6bdb2ad0cfef0520c`. They deliberately exclude Xbox
SDK/XDK and other vendor-library objects.
| Lane | Scope | Units | Residual bytes | Prompt |
| --- | --- | ---: | ---: | --- |
| A | AI core, actors, perception, communication, encounters | 11 | 79,657 | `CLAUDE_REMAINING_LANE_A_AI_CORE_20260920.md` |
| B | AI actions/pathfinding, gameplay, physics/collision, units/vehicles | 31 | 75,377 | `CLAUDE_REMAINING_LANE_B_SIMULATION_PHYSICS_20260920.md` |
| C | Bitmaps, UI/HUD, Halo-owned Xbox rasterizer, rendering | 30 | 83,898 | `CLAUDE_REMAINING_LANE_C_GRAPHICS_UI_20260920.md` |
| D | Effects, items, scripting, sound, structures, systems | 49 | 87,255 | `CLAUDE_REMAINING_LANE_D_SYSTEMS_EFFECTS_20260920.md` |
| **Total** | **All remaining Halo-owned code** | **121** | **326,187** | |
The translation-unit allowlists are mutually exclusive. Each prompt requires a
dedicated worktree at the exact baseline, forbids pushes and canonical edits,
requires current-state anti-duplication checks, applies all house rules, keeps
fuzzy work at zero credit, and demands a clean machine-readable handoff for
later reconciliation.
Run all four lanes in parallel only when each Claude session has its own
worktree. Give each session exactly one prompt; do not combine the prompts or
allow a lane to cross its allowlist. Canonical accounting remains authoritative
over worker headline totals.
-218
View File
@@ -1,218 +0,0 @@
# Campaign throughput lessons (2026-08-30 / 2026-08-31)
Operational lessons from a run that took the board from **669,879 to 714,807
matched bytes (+44,928)** and **4,829 to 4,973 credited functions (+144)** with
**zero regressions**, verified by a full build plus a per-function sweep of all
569 built units.
This document is about *throughput and hazards* - how to close many functions
in parallel without breaking what already works. For per-function matching
technique see `matching_methodology.md` and
`exact_match_acceleration_playbook.md`.
---
## 1. Write unwritten functions before fighting residuals
This is the single highest-leverage finding. Measured across four waves:
| wave | scope | approach | bytes gained | yield |
| --- | --- | --- | --- | --- |
| 1 | 71,112 B residual | close residuals | 11,040 | 16% |
| 3 | 27,920 B "sha-only" residual | close residuals | 3,168 | 11% |
| 4 | ~160,000 B unwritten | write missing functions | **30,720** | far larger scope |
Wave 3 deliberately picked the *closest* residuals on the board - functions
whose padded size **and** relocation count already matched, so only instruction
selection differed - and still returned only 11%. Those residuals are typically
a single register-allocation or x87 stack-discipline decision, and they resist
source-level control.
An **unwritten** function has no wrong prior to undo. You read the target
disassembly and write natural C89. That is a fundamentally easier problem.
The board currently holds roughly **1,000,000 bytes of unwritten functions**
against **100,000 bytes of residual**. Work the unwritten pool first.
Within a unit, start with the **small** functions. Eight 300-byte closures beat
one failed heroic attempt at a 3,000-byte function.
## 2. Parallel fan-out architecture
`tools/campaign/gate.py <unit>` compiles **one** translation unit to a
PID-named object under `scratch/` and never writes to `build/`. That makes it
safe to run from many workers simultaneously.
The architecture that worked:
- **One worker per `.c` file, files strictly disjoint.** Never two workers in
one file.
- Every worker measures **only** with `gate.py`. Useful flags: `--fn <FUNC>`,
`--disas <FUNC>`, `--source <FILE>` (compile a variant without touching the
real file), `--edits <json>`.
- **No worker runs `ninja` or `configure.py`.** A shared build directory cannot
take concurrent writers.
- **No worker commits.** The orchestrator does one full build, one board
measurement, and one commit at the end.
- Workers record `gate.py`'s exact count *before* starting and revert anything
that lowers it.
### `--disas` hides insert/delete holes
`gate.py --disas` is an **index-aligned zip**, not an alignment diff. Once
instruction counts diverge, every later row reads as different and a genuinely
inserted or deleted block is invisible. Use `scratch/alndiff.py` (difflib
SequenceMatcher over normalized capstone streams, branch targets normalized):
python scratch/build1.py <unit>.c scratch/cur.obj
python scratch/alndiff.py <unit> <FUNC> scratch/cur.obj
A 48-instruction hole in `_ai_debug_render_actor` was only visible this way.
Ignore `.rdata` / `__real@...` annotation mismatches in that output - those are
symbol-naming artifacts, not code differences.
## 3. Header edits perturb objects that never name the added types
**"Edit only your own object's header" is not a safe rule.**
`source/items/weapons.h` is included by ten other `.c` files. Adding two
interface-state structs to it silently dropped `units.obj` from **189/189 to
188/189**, breaking `_unit_preprocess_node_orientations` - a function that
names none of those types and calls none of the new functions.
Bisecting the three added blocks:
| added to weapons.h | units.obj |
| --- | --- |
| enum only | 189 ok |
| structs only | **188 BREAKS** |
| protos only | 189 ok |
| enum + structs | 189 ok |
| structs + protos | **188 BREAKS** |
| all three | **188 BREAKS** |
Adding the enum *in front of* the structs repairs it. The effect is not
semantic - it is the C2 allocator's **definition-position** sensitivity. What
perturbs codegen is where declarations sit, not what they mean.
### Rules that follow
1. Before touching any `.h`, run
`grep -rl "<header filename>" source/ --include=*.c`. If anything else
includes it, default to putting the declaration in the `.c`.
2. Definitions private to one TU belong in that TU. The weapons fix: structs
moved into `weapons.c`; the header kept the enum, the prototypes, and a
forward declaration so the prototype's struct tag has file scope. `units`
returned to 189/189 with all 50 of weapons' exact functions intact.
3. **Never** "fix" this by relying on a coincidental ordering that happens to
restore the victim.
4. If a header edit is genuinely required, prove it: build **shadow header
trees** (patched and pristine), compile every dependent unit against both,
and compare per-function size, relocation count and normalized hash. This
was done to drop `__declspec(align(4))` from `struct guard_state_data` in
`actions.h` (24 includers) and it held up under independent re-check.
### Some cases have no local remedy
Adding two trailing fields to `struct _projectile_datum` in `projectiles.h` -
fields January demonstrably uses at composite offsets 0x1FC and 0x200 - drops
`units.obj` to 188/189. The type is public, so internalising it in the `.c`
does not apply. About 1,152 bytes of projectile work is gated on this. That is
an owner decision; do not trade away a complete object for it quietly.
## 4. Verification discipline
**Worker self-reports are systematically optimistic.** One wave's rolled-up
"confirmed bytes" read 18,544 against 3,168 actually closed, because verifiers
counted pre-existing exact functions alongside new ones.
The only numbers that count:
1. A full `ninja` build. Its semantic gate fails closed if any previously exact
function stops matching.
2. `python tools/campaign/board.py` for the byte and function totals.
3. A **per-function regression sweep**: snapshot the exact set of every unit
from `build/split` vs `build/base` before the run, and re-compare after.
Object-level counts hide one object lost against another gained.
`board.py`'s per-object listing is **truncated** - an object appearing in or
disappearing from it is not evidence of a change. Use the sweep.
## 5. Cross-cutting codegen facts confirmed this run
- **Frame size is structural.** A differing `sub esp, N` means a local's scope
or type is wrong. It is never scheduling noise. This is what reopened
`_generate_prime_numbers` after a prior session had declared evidence
exhaustion on it (81.61% -> 91.15%).
- **Local declaration order is inert** for stack-slot assignment - proven
byte-identical across four permutations. Do not spend waves permuting
declarations.
- **The zero-register pin.** January frequently keeps a zero live in a register
and spends it repeatedly (`mov [mem],reg`, `push reg`, `cmp reg,zeroreg`)
where our build rematerialises immediates. Pinning a zero consumes a
callee-saved register, so this surfaces *downstream* as stack-slot
permutations and duplicated or merged blocks rather than as a local byte
difference. It is not a compiler flag: on `_lra_allocate`, `/O1`, `/O2 /Os`
and `/O1 /Ob2` all shrink the function to 319 bytes and `/Ox /Os` explodes it
to 1255, while the campaign flags give the closest result at 368.
- **Shared-header inlines must stay non-`static`.** Converting the 89
`__inline` definitions in `real_math.h` (plus 6 elsewhere) to
`static __inline` costs **21 objects and 119 functions**: `static` lets VC7
dead-code the out-of-line copy and inline more aggressively, changing call
sites. This also kills the theory that our ~20-28 surplus COMDATs per object
come from storage class - non-static is what reproduces January's bytes, so
the surplus-COMDAT question remains open and is a whole-tree property, not
something an individual object's reconstruction introduced.
## 6. Guards against fake matching
A byte match bought with source Bungie would not have written is worth less
than an honest residual. Rejected on sight: `volatile`, `register`, `#pragma`,
barriers, compiler-flag changes, raw numeric struct offsets, type punning or
representation casts, undefined behaviour, byte forcing, and logic that is
nonsensical for the function's stated job and only coincidentally matches.
Three live examples from this run, all correctly refused:
- The only shape that narrowed `_ai_debug_render_actor` required
`real_vector3d *` to `real_point3d *` casts to reach `point_from_line3d`.
That is representation punning, so it was reverted - and re-reading the
numbers, it only reached 24,864 of 24,976 bytes anyway. It was never a
closure, just a 16-byte improvement on a 128-byte gap.
- Writing an early return as `*num_primes = i` to deliberately spend a live
zero is a coincidence generator, not a reconstruction. XDK 3911 folds it to
identical code and it was already on a do-not-repeat list.
- A `vehicle_update` shape that forced an increment into both arms of a clamp
using two invented file statics measured the right size only because the
shortfall was cancelled downstream by alignment padding. The real shape is
the `cseries.h` `MIN` macro with the constant on the left and no local
increment at all.
Inline assembly is allowed **sparingly** and **unit-locally** in small helper
and math functions. The admitted case is `fast_ftol` (`fld` / `fistp`), because
`/QIfist` lowers every C float-to-int conversion through a 64-bit `fistp qword`
and a 32-bit inline `fistp` is never compiler output. Keep it `static __inline`
in the `.c` that needs it - never in a shared header.
## 7. Reusable scratch tooling
Left in `scratch/` by this run (note that `scratch/` is per-worktree and is not
committed, so these exist only in the worktree that produced them):
| file | purpose |
| --- | --- |
| `alndiff.py` | true alignment diff over normalized capstone streams |
| `build1.py` | compile one unit with the real build flags to a chosen `.obj` |
| `cs_build.py`, `cs_gate.py`, `cs_blast2.py` | shadow-header-tree blast-radius harness |
| `pn_score.py` | score source variants by byte divergence and first-diff offset |
| `pn_try.py` | batch-apply source variants through `gate.py` and report |
Use the committed `tools/fake_match_scan.py` for fake-match review; unlike the
scratch helpers above, it is available in every checkout.
A caution learned twice the hard way: **Python heredocs in this environment eat
one level of backslashes.** Build Windows path separators with `chr(92)` rather
than writing them literally, or you will silently emit a single-backslash
`__FILE__` string where the source needs a doubled one and break relocation
identity against the target's string literal.
-355
View File
@@ -1,355 +0,0 @@
# Common constants and representation reference
Verified against canonical on 2026-09-08. Start here before guessing literals.
This is a lookup guide, not a new constants owner. Most rows record high-use
names that already have canonical owners; the supplied-source crosswalk is
explicitly separate. Future reconstructions should include the
owning header and use the existing symbol or macro; they should not duplicate
these declarations in a translation unit merely to obtain a desired code
shape.
`Confirmed` below means the value and spelling are present in the named
canonical owner. Float bits show the rounded binary32 representation; tag
immediates use the pinned MSVC convention. January corroboration is called out
explicitly below. A recognizable bit pattern is a lookup clue, not proof of
which source expression produced it. `Context-specific` means a value belongs
to the stated enum, storage type, or algorithm, not an unrelated global owner.
## Core values and representations (confirmed)
| Purpose | Canonical symbol / type | Value or representation | Owning source | Important boundary |
| --- | --- | --- | --- | --- |
| Boolean true/false | `TRUE`, `FALSE` | `1`, `0` | [source/cseries/cseries.h](../source/cseries/cseries.h) | Use these for the project's one-byte `boolean` APIs. A `boolean` i386 return may define only `AL`; a `void` return promises no value. Do not require unrelated upper `EAX` bits. |
| Invalid signed index/handle | `NONE` | `-1` (`0xFFFFFFFF` as a 32-bit `long`) | [source/cseries/cseries.h](../source/cseries/cseries.h) | This is not `NULL`. Confirm the destination width: a byte or word sentinel has the narrowed all-ones representation, and some APIs use zero as a distinct valid or invalid value. |
| Simulation rate | `TICKS_PER_SECOND`, `ACTUAL_TICKS_PER_SECOND` | `30`, `30` | [source/cseries/cseries.h](../source/cseries/cseries.h) | Express seconds-to-ticks as `seconds * TICKS_PER_SECOND`; express per-tick scale as `1.0f / TICKS_PER_SECOND`. Do not invent a `SECONDS_PER_TICK` owner. The latter folds to float bits `0x3D088889`. |
| Wall-clock unit | `MILLISECONDS_PER_SECOND` | `1000` | [source/cseries/cseries.h](../source/cseries/cseries.h) | Do not mix millisecond tag fields with 30 Hz game ticks. Existing code explicitly converts when needed. |
| Display cadence | `VBLANKS_PER_SECOND` | `60` | [source/cseries/cseries.h](../source/cseries/cseries.h) | Vblanks are not simulation ticks even though both are integer rates. |
| Integer widths | `LONG_BITS`, `SHORT_BITS`, `CHAR_BITS` | `32`, `16`, `8` | [source/cseries/cseries.h](../source/cseries/cseries.h) | These describe the January/Win32 ABI. Preserve signedness as well as width. |
| Integer extrema | `LONG_MAX`, `LONG_MIN`, `UNSIGNED_LONG_MAX`, `SHORT_MAX`, `SHORT_MIN`, `UNSIGNED_SHORT_MAX` | `2147483647`, `-2147483648`, `0xFFFFFFFF`, `32767`, `-32768`, `0xFFFF` | [source/cseries/cseries.h](../source/cseries/cseries.h) | Prefer the named bound. `UNSIGNED_LONG_MAX` is also the canonical all-object mask, but that does not make it a universal sentinel. |
| Scalar real | `real` | `typedef float real` (4-byte IEEE-754 binary32 in target ABI) | [source/cseries/cseries.h](../source/cseries/cseries.h) | Use `f` suffixes for ordinary `real` constants. Use `double` only where the genuine API or default vararg promotion requires it. |
| Engine boolean | `boolean` | `typedef byte boolean`; `byte` is `unsigned char` | [source/cseries/cseries.h](../source/cseries/cseries.h) | It is one byte, not C/C++ `bool` and not a four-byte `long`. This affects fields, parameters, stack snapshots, and return checks. |
| Tag word | `tag` | `typedef unsigned long tag` (32 bits in target ABI) | [source/cseries/cseries.h](../source/cseries/cseries.h) | Preserve the four-character group token and the unsigned 32-bit type. |
| Coordinate axes | `_x`, `_y`, `_z` | `0`, `1`, `2` | [source/cseries/cseries.h](../source/cseries/cseries.h) | Use these only as component/projection indices. They are not vector values or bit masks. |
| Geometric tolerance | `_real_epsilon` | `0.0001f`, binary32 bits `0x38D1B717` | [source/math/real_math.h](../source/math/real_math.h) | This is not a universal replacement for exact-zero tests. January functions use both epsilon and exact `0.0f`; preserve the observed predicate and NaN behavior. |
| Pi | `_pi` | `(real)M_PI`, binary32 bits `0x40490FDB` | [source/math/real_math.h](../source/math/real_math.h) | Use the project cast rather than a hand-rounded decimal when the source works in `real`. |
| Angle conversion | `DEGREES_TO_RADIANS`, `RADIANS_TO_DEGREES` | `((real)angle) * _pi / 180.f` or `((real)angle) * 180.f / _pi` | [source/math/real_math.h](../source/math/real_math.h) | Angles stored in tags are not uniformly degrees or radians. First authenticate the field's unit; then use the owner macro. Expression order can affect exact float bytes. |
| Finite real envelope | `REAL_MIN`, `REAL_MAX` | `-3.4028235e38f`, `3.4028235e38f` | [source/math/real_math.h](../source/math/real_math.h) | `REAL_MIN` here is the most-negative finite bound, not the C-library meaning “smallest positive normal.” |
January examples that corroborate the float rows are the
`__real@38d1b717`, `__real@41f00000`, and `__real@3d088889` relocations in
`build/split/source/objects/damage.obj`. They represent `_real_epsilon`,
`(real)TICKS_PER_SECOND`, and `1.0f / TICKS_PER_SECOND`; they do not justify
replacing the source-level owners with hexadecimal literals.
## Canonical macros whose semantics matter
| Macro | Definition owner | Correct use | Pitfall |
| --- | --- | --- | --- |
| `MIN(a,b)`, `MAX(a,b)` | [source/cseries/cseries.h](../source/cseries/cseries.h) | Ordinary project min/max spelling when January establishes the same comparison and argument order. | Each expression can be evaluated more than once. Argument order controls signed-zero and NaN outcomes: do not normalize or swap operands merely because finite results look equivalent. |
| `FLOOR`, `CEILING`, `PIN` | [source/cseries/cseries.h](../source/cseries/cseries.h) | Named lower/upper/clamp operations. | `PIN` is a macro composition, not an IEEE `fminf`/`fmaxf` substitute. It may evaluate the value repeatedly and lets a NaN value pass through under its current ordering. |
| `FLAG(bit)` | [source/cseries/cseries.h](../source/cseries/cseries.h) | Convert an enum *bit index* to a mask. | Most `_..._bit` enum members are indices, not masks. Do not apply `FLAG` again to an existing `_..._mask_*` value. Preserve the owning integer width, especially near bit 31. |
| `TEST_FLAG`, `SET_FLAG` | [source/cseries/cseries.h](../source/cseries/cseries.h) | Test or mutate a named bit in the authenticated field. | Do not replace an enum bit with a raw hex mask. Avoid side-effecting lvalues/expressions because these are macros. |
| `VALID_FLAGS(flags,bits)` | [source/cseries/cseries.h](../source/cseries/cseries.h) | Validate that no bit at or above the declared count is set. | `bits` is a count, not a mask; signedness and shift width matter. |
| `BIT_VECTOR_SIZE_IN_LONGS`, `BIT_VECTOR_TEST_FLAG`, `BIT_VECTOR_SET_FLAG` | [source/cseries/cseries.h](../source/cseries/cseries.h) | Long-word-packed bit vectors, 32 flags per `long`. | The macros index a `long` array. Do not silently cast an original `byte *` API to this representation. A byte-buffer use needs independent owner/caller evidence, as documented for the collision BSP boundary in [docs/object_matching_logs/collision_bsp_obj_sphere_cluster_20260904.md](object_matching_logs/collision_bsp_obj_sphere_cluster_20260904.md). |
| `NUMBEROF(array)` | [source/cseries/cseries.h](../source/cseries/cseries.h) | Compile-time count of an actual array in the same scope. | On a pointer it returns a pointer-size ratio, not the pointee count. |
| `VALID_INDEX(index,count)` | [source/cseries/cseries.h](../source/cseries/cseries.h) | Signed lower-bound and upper-bound check. | Preserve the signed type. Replacing it with an unsigned comparison can alter code shape and invalid-negative behavior. |
| `DATUM_INDEX_NEW`, `DATUM_INDEX_TO_ABSOLUTE_INDEX`, `DATUM_INDEX_TO_IDENTIFIER` | [source/cseries/cseries.h](../source/cseries/cseries.h) | Construct or split the 16-bit absolute index / 16-bit identifier handle representation. | A datum handle is not a plain array index. Mask before indexing; do not discard the identifier except at a proven boundary. |
| `TAG_BLOCK_GET_ELEMENT` | [source/tag_files/tag_groups.h](../source/tag_files/tag_groups.h) | Typed access through `tag_block_get_element_with_size(..., sizeof(type))`. | The type and element size are part of the runtime contract. Avoid a raw `address + index*stride` replacement. |
## Tag strings and common group IDs (confirmed)
`TAG_STRING_LENGTH` is **31** ([source/tag_files/tag_files.h](../source/tag_files/tag_files.h)). A
canonical fixed tag string buffer that includes its terminator is therefore
usually `char name[TAG_STRING_LENGTH + 1]`, or 32 bytes. The constant is a
payload limit, not permission to omit the final NUL.
MSVC packs the following four-character constants into the shown immediate
value. Always write the named owner; the hex form is provided only to read
disassembly and relocations. On little-endian x86 the bytes in memory appear
least-significant byte first.
| Domain | Symbol | Source token / i386 immediate | Owner |
| --- | --- | --- | --- |
| Game globals | `GAME_GLOBALS_TAG` | `'matg'` / `0x6D617467` | [source/game/game_globals.h](../source/game/game_globals.h) |
| Scenario | `SCENARIO_TAG` | `'scnr'` / `0x73636E72` | [source/scenario/scenario_definitions.h](../source/scenario/scenario_definitions.h) |
| Structure BSP | `STRUCTURE_BSP_TAG` | `'sbsp'` / `0x73627370` | [source/structures/structure_bsp_definitions.h](../source/structures/structure_bsp_definitions.h) |
| Base object definition | `OBJECT_DEFINITION_TAG` | `'obje'` / `0x6F626A65` | [source/objects/object_definitions.h](../source/objects/object_definitions.h) |
| Damage effect | `DAMAGE_EFFECT_DEFINITION_TAG` | `'jpt!'` / `0x6A707421` | [source/objects/damage_effect_definitions.h](../source/objects/damage_effect_definitions.h) |
| Bitmap | `BITMAP_GROUP_TAG` | `'bitm'` / `0x6269746D` | [source/bitmaps/bitmap_group.h](../source/bitmaps/bitmap_group.h) |
| Sound | `SOUND_DEFINITION_TAG` | `'snd!'` / `0x736E6421` | [source/sound/sound_definitions.h](../source/sound/sound_definitions.h) |
Four-character C constants are implementation-defined outside this compiler
context. Do not byte-swap them manually or replace a typed getter such as
`object_definition_get` with a raw `tag_get` call only to expose the numeric
immediate.
## Common engine enums and limits (confirmed, domain-specific)
| Domain | Symbol(s) | Value | Owner and boundary |
| --- | --- | --- | --- |
| Local rendering/input players | `MAXIMUM_LOCAL_PLAYERS` | `4` | [source/game/players.h](../source/game/players.h). This is not the network/player-datum capacity. |
| Network join token | `NETWORK_JOIN_GAME_TOKEN_SIZE` | `16` | [source/networking/network_messages.h](../source/networking/network_messages.h). Protocol byte count, corroborated by the supplied source and January's generator/callers; restored in the canonical protocol owner, not a claim about the absent original header. |
| Network header direction | `_byte_order_host`, `_byte_order_network` | `0`, `1` | [source/bungie_net/common/message_header.h](../source/bungie_net/common/message_header.h). January inbound reads pass 0; outbound connection/key/rejection writes pass 1. The former inferred host/network labels were reversed; the corrected names preserve every caller's immediate and emitted code. |
| Connection creation flags | `_connection_create_server_bit`, `_connection_create_clientside_client_bit`, `_connection_create_serverside_client_bit` | Bit indices `0`, `1`, `2`; masks `1`, `2`, `4` | [source/networking/network_connection.h](../source/networking/network_connection.h). Use `FLAG` at creation calls; these are not the separate closed/stale state bits. |
| Network game ports | `NETWORK_GAME_SERVER_PORT`, `NETWORK_GAME_CLIENT_PORT` | `0x141E`, `0x141F` / `5150`, `5151` | [source/networking/network_game_protocol.h](../source/networking/network_game_protocol.h). Shared only by actual protocol consumers; values are not transport-type ordinals. |
| Server rejection reasons | `_rejection_code_version_too_old` through `_rejection_code_blacklisted_machine`; `NUMBER_OF_SERVER_REJECTION_CODES` | Reasons `0..6`, count `7`; game full `4` | [source/networking/network_game_protocol.h](../source/networking/network_game_protocol.h). Serialized rejection field is a `word`; count is an exclusive bound, not a valid reason. |
| Connection timeout disabled | `_connection_dont_timeout` | `0` | [source/networking/network_connection.h](../source/networking/network_connection.h). The idle API tests a nonzero timeout before enforcing it. This literal is not the separate `global_connection_dont_timeout` debug boolean. Name from supplied source, value from January, header placement reconstructed. |
| Public-endpoint datagram capacity | `DATAGRAM_MAXIMUM_SIZE` | `400` / `0x190` bytes | [source/networking/network_connection.h](../source/networking/network_connection.h). Shared connection/provider limit, distinct from the reliable-message receive capacity below. `network_connection_read` takes a `word *` size; a DWORD stack-slot initialization does not make this a 32-bit API. |
| IPv4 loopback address | `IPV4_LOOPBACK_ADDRESS` | `0x7F000001` | [source/bungie_net/network/transport_address_constants.h](../source/bungie_net/network/transport_address_constants.h). Source-attested spelling and January compare immediate. Use in the transport address's host-order IPv4 field, not as four already serialized network bytes. This narrow shared header is included by the actual client/server consumers. |
| Client/server receive buffer | `MAXIMUM_NETWORK_MESSAGE_SIZE` | `0x800` / `2048` bytes | Local manager enums in [server](../source/networking/network_server_manager.c) and [client](../source/networking/network_client_manager.c). January proves the buffer and word-sized capacity reset. This is not `MAXIMUM_MESSAGE_SIZE=0xFFF` in the separate Bungie-net message header. A word-aligned array preserves the receiving handler's genuine word-pointer contract. |
| Difficulty | `_game_difficulty_level_easy`, `_game_difficulty_level_normal`, `_game_difficulty_level_hard`, `_game_difficulty_level_impossible` | `0`, `1`, `2`, `3` | [source/game/game.h](../source/game/game.h). Use the enum; a raw ordinal is ambiguous at a call site. |
| Campaign team | `_game_team_default`, `_game_team_player`, `_game_team_human`, `_game_team_covenant`, `_game_team_flood`, `_game_team_sentinel` | `0` through `5` | [source/game/game.h](../source/game/game.h). Multiplayer `_team_red=0`, `_team_blue=1` intentionally overlaps numerically; the field/API determines the namespace. |
| Object types | `_object_type_biped` through `_object_type_sound_scenery` | `0` through `11`; `NUMBER_OF_OBJECT_TYPES=12` | [source/objects/object_types.h](../source/objects/object_types.h). Values are ordinals used by object headers and switch statements. |
| Object masks | `_object_mask_unit`, `_object_mask_item`, `_object_mask_device`, `_object_mask_all` | `0x3`, `0x1C`, `0x380`, `0xFFFFFFFF` | [source/objects/object_types.h](../source/objects/object_types.h). These are already masks; test them against an object type with the established `TEST_FLAG(mask, type)` convention. |
| Object capacity | `MAXIMUM_OBJECTS_PER_MAP` | `2048` | [source/objects/objects.h](../source/objects/objects.h). Capacity is not the current datum count. |
| Rendered-object capacity | `MAXIMUM_RENDERED_OBJECTS` | `256` | [source/objects/objects.h](../source/objects/objects.h). Do not use it to size the master object datum array. |
| UI deletion input-inhibit mask | `WIDGET_DELETED_PLAYER_CONTROL_INHIBIT_FLAGS` | `0x0FFF` | TU-local owner in [source/interface/ui_widget.c](../source/interface/ui_widget.c). Passed as a `word` to `player_control_inhibit_buttons` when a root widget with a non-`NONE` local-player index is deleted. It fences the evidenced low twelve discrete action buttons until each held button is released; it does not cover analog movement/look, and it is not `UNSIGNED_SHORT_MAX`, a universal all-buttons mask, or a bit index for `FLAG`. |
| Markers/regions/attachments | `MAXIMUM_MARKERS_PER_OBJECT`, `MAXIMUM_REGIONS_PER_OBJECT`, `MAXIMUM_NUMBER_OF_ATTACHMENTS_PER_OBJECT` | `64`, `8`, `8` | [source/objects/objects.h](../source/objects/objects.h) and [source/objects/object_definitions.h](../source/objects/object_definitions.h). These belong to different arrays despite sharing an object definition. |
| Damage material modifier capacity | `MAXIMUM_NUMBER_OF_MATERIAL_TYPES` | `40` | [source/game/game_globals.h](../source/game/game_globals.h); used by `damage_definition.material_modifiers` at [source/objects/damage_effect_definitions.h](../source/objects/damage_effect_definitions.h). It is storage capacity, not the current named-material count. |
## Xbox shader encodings (confirmed SDK owners, context-specific)
The existing [Xbox D3D8 types header](../xbox/include/D3D8Types.h) owns the
register-combiner macros and enums. Prefer them to packed hexadecimal shader
words; this documents the actual inputs and outputs without creating new
engine constants. They are hardware encodings, not Bungie identifier recovery.
| Encoding | Existing owner / interpretation | Example |
| --- | --- | --- |
| Four combiner inputs | `PS_COMBINERINPUTS(a,b,c,d)` packs four input bytes | `PS_COMBINERINPUTS(PS_REGISTER_T0, PS_REGISTER_T0, PS_REGISTER_ZERO, PS_REGISTER_ZERO)` is `0x08080000`. |
| Input register/mapping/channel | `PS_REGISTER_*`, `PS_INPUTMAPPING_*`, `PS_CHANNEL_*` | `PS_REGISTER_V0 \| PS_INPUTMAPPING_UNSIGNED_INVERT` is `0x24`; `PS_REGISTER_V1 \| PS_CHANNEL_ALPHA` is `0x15`. These are already encoded masks, not bit indices for `FLAG`. |
| Constant one input | `PS_REGISTER_ONE` | `0x20`, zero register with unsigned-invert mapping; not a literal register number 32. |
| Combiner destinations | `PS_COMBINEROUTPUTS(ab,cd,mux_sum,flags)` | R0/discard/discard/identity is `0xC0`; R1/discard/discard/identity is `0xD0`. |
| Stage count | `PS_COMBINERCOUNT(count,flags)` | Counts 1 and 3 with zero flags encode as 1 and 3. Flags zero select the SDK defaults; count is not a bit index. |
| Texture addressing modes | `PS_TEXTUREMODES(t0,t1,t2,t3)` with `PS_TEXTUREMODES_*` | PROJECT2D/NONE/NONE/NONE encodes as 1. |
The Widgets reconciliation replaces these packed words using the existing
macros; all generated code and non-debug sections remain unchanged. Do not
assign a fixed-function `D3DVSDE_*` semantic name to an arbitrary custom vertex
shader register just because its number agrees: Widgets sends color through
register 9 and texture coordinates through register 4. The custom shader's
input contract, not the fixed-function spelling, determines their meaning.
## Damage flags relevant to the current caller/helper packet
These are confirmed but context-specific. The enum values are **bit indices**;
use `TEST_FLAG`/`SET_FLAG` rather than the numeric masks shown for audit
convenience.
| Storage | Symbol | Bit / mask | Owner |
| --- | --- | --- | --- |
| `being_damaged_flags` | `_object_being_damaged_body_depleted_bit` | `0` / `0x01` | [source/objects/damage.h](../source/objects/damage.h) |
| `being_damaged_flags` | `_object_being_damaged_shield_depleted_bit` | `3` / `0x08` | [source/objects/damage.h](../source/objects/damage.h) |
| `being_damaged_flags` | `_object_being_damaged_by_friendly_bit` | `4` / `0x10` | [source/objects/damage.h](../source/objects/damage.h) |
| `being_damaged_flags` | `_object_being_damaged_multiplied_by_difficulty_bit` | `5` / `0x20` | [source/objects/damage.h](../source/objects/damage.h) |
| `damage_data.flags` | `_damage_no_statistics_bit` | `7` / `0x80` | [source/objects/damage.h](../source/objects/damage.h) |
| `object.damage_flags` | `_object_passed_shield_damage_threshold_bit` | `1` / `0x0002` | [source/objects/objects.h](../source/objects/objects.h) |
| `object.damage_flags` | `_object_shield_depleted_bit` | `3` / `0x0008` | [source/objects/objects.h](../source/objects/objects.h) |
| `object.damage_flags` | `_object_shield_over_charging_bit` | `4` / `0x0010` | [source/objects/objects.h](../source/objects/objects.h) |
| `object.damage_flags` | `_object_cannot_take_damage_bit` | `11` / `0x0800` | [source/objects/objects.h](../source/objects/objects.h) |
Do not merge the two “shield depleted” names: one is an event/result bit in a
32-bit accumulator and the other is persistent object state in a 16-bit
field. Their current numeric bit happens to be the same.
January-specific category, side-effect, material-response, and private-helper
values should remain in the owning Damage enum unless a genuine shared owner
is found. A comparison against `1`, `2`, or `3` in disassembly is not enough
to infer which enum namespace it belongs to. Likewise, the Damage aftermath
upward bias and similar algorithm coefficients are not general math
constants; retain a semantic local expression and document its function-level
provenance instead of exporting a global name.
## Assertions and object-access macros
See [the shared assertion API and Units example](assertions.md). Assertions test
an invariant that must be true; a diagnostic error condition usually needs
negating. `_object_connected_to_map_bit` is bit 11 in `object.flags`, whereas
`_object_header_connected_to_map_bit` is bit 5 in a separate header flags byte.
Do not substitute one for the other.
Use typed accessors such as `unit_get`, `weapon_get`, `object_definition_get`,
`bitmap_group_get`, and `sound_definition_get` from their existing subsystem
headers. Keep raw `tag_get` and object-access casts encapsulated there rather
than repeating casts at each call site.
## Supplied-source crosswalk (evidence, not yet canonical declarations)
The user supplied three CE source files on 2026-09-06. Their hashes, local
locations, target fingerprints, and integration caveats are recorded in the
[source reconstruction map](user_source_reconstruction_map_20260906.md).
The following names occur there; do not assume they are already available
from canonical headers or export TU-private coefficients globally.
| Supplied name / expression | Value | Canonical correspondence and scope |
| --- | --- | --- |
| `RANDOM_A` | `1664525L` / `0x0019660D` | Literal in [source/math/random_math.c](../source/math/random_math.c); LCG multiplier, not a general math constant. |
| `RANDOM_C` | `1013904223L` / `0x3C6EF35F` | Same owner; LCG increment. Seed arithmetic is unsigned 32-bit modulo arithmetic. |
| `RANDOM(seed)` | Update seed, then take its upper 16 bits | Source macro uses a 16-bit right shift; do not substitute low bits or mutate the seed twice. |
| `REAL_RANDOM(seed)` | `(1.f / (real)0xffff) * RANDOM(seed)` | Divisor is `65535`, not `65536`; the source real result can reach both 0 and 1. Integer range scaling instead uses a right shift by 16 and is upper-exclusive for its valid range. Preserve expression order and promotions. |
| `RANDOM_DIRECTION_TABLE_GEOSPHERE_SEGMENT_COUNT` | `16` | RandomMath's geosphere construction argument, not a player capacity or direction-table length. |
| `COUNTDOWN_TIMER_CANNOT_ACCLERATE_AMOUNT` | `MILLISECONDS_PER_SECOND - 1` = `999` | Source spelling includes `ACCLERATE`; canonical server calls it `NETWORK_GAME_MINIMUM_COUNTDOWN_TIME`. |
| `CLIENT_COUNTDOWN_TIMER_MODIFICATION_AMOUNT` | `5 * MILLISECONDS_PER_SECOND` = `5000` | Canonical server `NETWORK_GAME_COUNTDOWN_ADJUSTMENT`. |
| `LAN_INITIAL_SERVER_GAME_COUNTDOWN` | `31 * MILLISECONDS_PER_SECOND - 1` = `30999` | Canonical server `NETWORK_GAME_COUNTDOWN_TIME`. |
| `LOCAL_INITIAL_SERVER_GAME_COUNTDOWN` | `11 * MILLISECONDS_PER_SECOND - 1` = `10999` | Canonical server `NETWORK_GAME_SPLITSCREEN_COUNTDOWN_TIME`. |
| `MAXIMUM_SERVER_GAME_COUNTDOWN` | `31 * MILLISECONDS_PER_SECOND - 1` = `30999` | Separate semantic role from the LAN initial time, despite equal values. |
| `NETWORK_GAME_CLIENT_CONNECTION_PROCESS_TIMEOUT_MILLISECONDS` | Debug `120000`; release `15000` | Supplied client enum selects `120` or `15` times `MILLISECONDS_PER_SECOND` under `DEBUG`; do not use one build's timeout in the other. |
| `NETWORK_GAME_CLIENT_GAME_SEARCH_INTERVAL_MILLISECONDS` | `2000` | Supplied client search cadence, `2 * MILLISECONDS_PER_SECOND`. |
| `NETWORK_GAME_CLIENT_GAME_ADVERTISED_GAME_TIMEOUT` | `6000` | Supplied client: twice the search interval plus `2 * MILLISECONDS_PER_SECOND`; not the connection timeout. |
| `NETWORK_GAME_CLIENT_GAME_PING_INTERVAL_MILLISECONDS` | `1000` | Supplied client ping cadence, `1 * MILLISECONDS_PER_SECOND`. |
| `NETWORK_GAME_CLIENT_INITIAL_PACKET_SEQUENCE_NUMBER` | `1` | Supplied client protocol sequence origin; not a universal first array index. |
Countdown correspondences are in
[source/networking/network_server_manager.c](../source/networking/network_server_manager.c).
The supplied server's client-machine flag indices are connected `0`,
validated `1`, level-loaded `2`, and precached `3`. Canonical currently uses
joined/loaded names for indices 1/2; numeric agreement alone does not authorize
a global semantic rename. These flags are not object-datum flags.
The supplied RandomMath also references `_full_circle`, `_half_circle`, and
`_quarter_circle`, but their defining header was not supplied and they are not
present in the current canonical math headers. Do not invent a declaration
or infer exact expression ordering solely from those names.
The supplied client names its two flag indices
`_network_game_client_connected_to_server_bit = 0` and
`_network_game_client_sent_join_request_to_server_bit = 1`. Canonical uses
shorter inferred names and literal timing values; restore names only with
ordinary strict-object and consumer checks, not by unreviewed bulk replacement.
## AI profile meter layout
January's shared AI profiling state is declared in
`source/ai/ai_profile.h`. Include that owner when incrementing a meter; do not
declare an opaque `extern byte ai_profile[]`, a partial local structure, or a
consumer-specific view of the same global.
- `ai_profile.meters` begins at offset `0x0C`.
- There are 28 meter records, each `0x88` bytes.
- `accumulator` is at record offset `0`; `current_value` is at offset `2`.
- A meter accumulator is therefore at `0x0C + index * 0x88`.
Frequently used indices and accumulator offsets:
| meter enum | index | offset |
| --- | ---: | ---: |
| `_ai_meter_encounters_updated` | 1 | `0x094` |
| `_ai_meter_actors_updated` | 3 | `0x1A4` |
| `_ai_meter_actors_active` | 4 | `0x22C` |
| `_ai_meter_units_updated` | 6 | `0x33C` |
| `_ai_meter_units_active` | 7 | `0x3C4` |
| `_ai_meter_dead_props_acknowledged` | 9 | `0x4D4` |
| `_ai_meter_dead_props_orphaned` | 10 | `0x55C` |
| `_ai_meter_dead_props_unacknowledged` | 11 | `0x5E4` |
| `_ai_meter_enemy_props_acknowledged` | 12 | `0x66C` |
| `_ai_meter_enemy_props_orphaned` | 13 | `0x6F4` |
| `_ai_meter_enemy_props_unacknowledged` | 14 | `0x77C` |
| `_ai_meter_friendly_props_acknowledged` | 15 | `0x804` |
| `_ai_meter_friendly_props_orphaned` | 16 | `0x88C` |
| `_ai_meter_friendly_props_unacknowledged` | 17 | `0x914` |
| `_ai_meter_collisions` | 21 | `0xB34` |
| `_ai_meter_line_of_sight` | 22 | `0xBBC` |
| `_ai_meter_line_of_fire` | 23 | `0xC44` |
| `_ai_meter_action_change` | 26 | `0xDDC` |
The leading booleans are independently exposed through the Halo Script
global table, which authenticates their names and offsets: `disabled` at
`0x00`, `move_actors_randomly` at `0x01`, `show` through
`show_sound_distance` at `0x04` through `0x0B`. The complete global is
`0xEEC` bytes.
## Reconstruction checklist
1. Search for an existing owner before adding a constant, enum, macro, or
typedef. Include that owner instead of redeclaring it locally when header
placement is safe.
2. Establish the storage type and namespace before interpreting `-1`, `0`, or
a bit number. `NONE`, `NULL`, a tag index, and a byte terminator are not
interchangeable merely because some bytes match.
3. Compare exact float bytes and x87 predicates before replacing a literal
with `_real_epsilon`, `MIN`, `MAX`, or `PIN`. Finite agreement does not
prove NaN, infinity, tangent, or signed-zero agreement.
4. Treat enum `_..._bit` members as bit positions and `_..._mask...` members
as masks. Keep the owning field width; do not widen a byte/word flag field
to `long *` for macro convenience.
5. Treat every maximum as a capacity for its named owner, not a current count
or a substitute capacity for a nearby array.
6. Preserve public type widths at ABI boundaries. In particular, `real` is
float and `boolean` is byte even when i386 register or stack mechanics make
a mismatched declaration appear to work.
## Leaf Map construction constants (context-specific)
These values are authenticated by January's Leaf Map strings, data, control
flow, and the reconciled private caller graph. They belong to
`source/structures/leaf_map.c`; they are not general geometry constants.
| Role | Source spelling / value | Boundary |
| --- | --- | --- |
| BSP traversal stack capacity | `MAXIMUM_NODE_STACK_COUNT = 256` | Assertion text names the constant and aggregate field. Stack entries are signed node designators whose sign bit records the child side. |
| Initial face polygon capacity | `MAXIMUM_LEAF_FACE_VERTICES = 64` | Bounds the local clipped polygon and the correctly owned Geometry clip call. |
| Portal intersection capacity | `MAXIMUM_PORTAL_VERTICES = 64` | Bounds the temporary intersection polygon; it is not a generic tag-block capacity. |
| Initial projected world square | coordinates `+/-1536.f` | The four-point polygon is clipped by every ancestor BSP plane; do not export 1536 as a world-wide coordinate bound. |
| Leaf-face clip epsilon | `0.000244140625f` (`1/4096`) | Algorithm-specific clip tolerance, distinct from `_real_epsilon`. |
| Portal intersection epsilon | `0.00048828125f` (`1/2048`) | Algorithm-specific intersection tolerance. |
| Portal closure thresholds | area `< 0.0025f` or `sqrt(area)/perimeter < 0.01f` | Preserve expression order and the perimeter call; these are not generic degenerate-polygon rules. |
See [the Leaf Map reconciliation ledger](object_matching_logs/leaf_map_obj_private_graph_reconciliation_20260909.md)
for symbol provenance, strict-credit boundaries, and the remaining data/BSS
gap.
## Rasterizer shared-state lookup
Use `source/rasterizer/rasterizer_frame_statistics.h` for the complete January
**0x170-byte** frame-statistics record and its external declaration. Do not
introduce another consumer-specific prefix. Dynamic buffer counts are not
maximum capacities; model skinning/lighting accumulators count uploaded
constant bytes, not objects. See the
[shared counter owner ledger](object_matching_logs/rasterizer_frame_statistics_obj_shared_counter_owner_20260906.md)
for the current writer-backed fields and intentionally unreviewed interiors.
Use `source/rasterizer/rasterizer_debug_options.h` for the genuine January
debug-options record; do not add another TU-local prefix. The size is **0x8C**
(not later HCEA's 0x90). Active Camouflage flags are at 0x41/0x42; January
`zbias` at 0x54 is `long`, not `real`; `pad3_scale` is at 0x6C and the six
debug reals start at 0x70. The pixel-counter active/enabled pair is 0x88/0x89.
Unknown bytes must not inherit later-build names merely by offset.
The complete 0xF0 Xbox `pixel_shader_definition` is in
`source/rasterizer/xbox/rasterizer_xbox_pixel_shader.h`. Use this associated
header when a complete type is needed; the broad Xbox header retains its
forward declaration to avoid colliding with legacy local definitions.
See the [shared-owner/cache admission](object_matching_logs/rasterizer_xbox_active_camouflage_obj_shared_owner_cache_20260906.md)
for provenance, the typed initializer and pinned-compiler layout tests.
### PlayerProfile and S3TC constants
January; 2026-09-07 owner reconciliation:
- `source/saved games/player_profile.h` owns the 48-byte profile: name is
12 wchar_t at +0, color short at +24, flags word at +26, ten byte map flags
at +28, last level short at +38 and eight-byte controller settings at +40.
These are not the larger later-PC/PDB profile layout.
- `SAVED_GAME_FILE_BLOCK_SIZE` is 512 in saved_game_files.h. PlayerProfile's
typed file block has 48 profile bytes, a 20-byte XDK signature and 444 bytes
of zeroed padding. Do not serialize uninitialized padding for matching.
- `S3TC_BLOCK_PIXELS` is 16 (4 by 4); single-pixel coordinates are 0..3.
RGB565 is blue/green/red 5/6/5 in the project's byte-channel order.
S3TC's weights are 0.082f, 0.6094f, 0.3086f in that order; maps are
RGB4 {0,2,3,1}, RGB3 {0,2,1,3}. These are codec operations, not flag bits.
- See [the coherent batch ledger](object_matching_logs/player_profile_s3tc_batch_reconciliation_20260907.md)
for evidence limits, safe repairs, retained fuzzy work and exact debits.
Useful corroborating ledgers:
- [docs/object_matching_logs/damage_obj_object_damage_update_20260904.md](object_matching_logs/damage_obj_object_damage_update_20260904.md)
documents project `real`, enum, flag, tick, and clamp use in a verified
Damage owner.
- [docs/object_matching_logs/bipeds_obj_jonas_disconnect_bsp_20260827.md](object_matching_logs/bipeds_obj_jonas_disconnect_bsp_20260827.md)
records the January all-bits-set `NONE` representation in real object
fields.
- [docs/object_matching_logs/collision_bsp_obj_sphere_cluster_20260904.md](object_matching_logs/collision_bsp_obj_sphere_cluster_20260904.md)
records a specifically authenticated byte-buffer/bit-vector boundary; it is
an exception requiring evidence, not a blanket license for representation
casts.
-35
View File
@@ -1,35 +0,0 @@
# January COMMON pool: ordering hypothesis and ownership limits
Lane C research, 2026-09-22; corrected during canonical reconciliation.
`linker_common.obj` is a synthetic split of tentative COMMON definitions.
Several independently attributed records appear in contiguous clusters whose
positions correlate with module order. This is useful as a **negative
constraint**, but it does not prove the owner of an individual symbol.
Every pooled record in `config/contribs.json` resolves to synthetic module 847.
The HCEX PDB lists these globals without a compiland. Declaration order and
first-reference order do not reliably reproduce the pool order, either.
Consequently, do not create a tentative definition merely because the symbol
falls between two apparently owned records.
The original Lane C packet applied that inference too strongly. Canonical
reconciliation excluded its 116-definition COMMON wave. An existing owner test
reads January's split `rasterizer_frame_statistics` object, where its eponymous
global appears only as an UNDEF external. That does **not** prove an import:
csplit renders a pooled COMMON record as UNDEF (value 0) in a defining TU and in
an importing TU alike, so the test cannot tell them apart and the owner of
`_rasterizer_frame_statistics` remains **undetermined** (correction recorded
2026-09-26; the test itself and its ownership checks are unchanged). The pool-order
hypothesis does not settle it either. The same lack of
independent proof affects `hs_syntax_data`, `ai_globals`, and
`rasterizer_model_cortana_hack`; their Lane C moves were also not integrated.
These are unresolved ownership questions, not credited reconstructions.
The useful, narrower rule remains: a genuine tentative definition must be
uninitialised in source (`struct foo bar;`), while `= 0` or `= NULL` gives the
symbol different COFF ownership. Compare both the January object and the
rebuilt object's symbol class before accepting a declaration. The read-only
`tools/campaign/linker_common_base.py` can inspect the known pool records, but
its partial base earns **zero** matching credit under the all-or-nothing data
metric. Its output must not be used as an ownership oracle.
-171
View File
@@ -1,171 +0,0 @@
# H1 tags cross-build intake (2026-09-06)
## Status and authority
The user supplied `C:/Users/isabe/Downloads/haloleak2024.rar`, specifically
`halo1/h1_tags`. Five explicitly selected executables were extracted for local
analysis. No executable was run and no tag assets were extracted. These inputs
are **CROSSBUILD_ONLY** evidence, not the January 2002 Xbox target.
The user subsequently supplied the extracted folder
`C:/Users/isabe/Downloads/haloleak2024/halo1/h1_tags`. All five symbol-executable
SHA-256 hashes match the archive copies below exactly, so the saved analysis is
reusable. The extracted `halo1` subtree also contains no PDB/DBG files at this
checkpoint. The source folder remains read-only.
All five PE timestamps report 2020-11-23; their CodeView paths identify a
`build/v140/Win32` build. A timestamp is a claim in the file, not independent
build provenance. Each image has zero COFF symbols and zero exports. The
CodeView records are **RSDS references to external PDBs**, not embedded PDB
contents. The full archive listing contained 211,474 entries, no `.pdb` or
`.dbg` files, and three `.map` files that are Halo 2 game assets rather than
linker maps. Do not describe the `_symbols.exe` suffix as proof of a complete
debug-symbol payload.
## Exact input identities
| Executable | Bytes | SHA-256 |
| --- | ---: | --- |
| `guerilla_symbols.exe` | 9,764,864 | `44538c3effe5a740f6995e7f6867995e81ec7d32ee8bb0bb74f04c2b9c203b83` |
| `halo_cache_symbols.exe` | 6,672,896 | `740869688354defd295e28adf94bd4ea41385e9d4a98dc08764515285cf05b55` |
| `halo_tag_symbols.exe` | 7,450,624 | `c5f58fe29cdde668d0e30f6848ad50b8f49a28e99c30de2b1bb75cf7bbfb0712` |
| `sapien_symbols.exe` | 11,303,424 | `b1f0b207f5f49cb0ce90f5be7957197d3be612593ff52e7a50fc17e2b88a4828` |
| `tool_symbols.exe` | 10,631,680 | `079d20d7792e9565b24ab2cff8115f76a6e48749a8e8ffd190f57954495f0a97` |
The missing PDBs must match **both GUID and age**, not just basename:
| PDB basename | GUID | Age (decimal) |
| --- | --- | ---: |
| `guerilla_symbols.pdb` | `9b751e93-d090-4fbf-9a7c-d8e3cbcb1866` | 125 |
| `halo_cache_symbols.pdb` | `7f03f998-b7b0-4ea6-b18f-a6506e3442e5` | 14 |
| `halo_tag_symbols.pdb` | `eac025b6-d3f8-4ae9-a47e-9882d7c58906` | 107 |
| `sapien_symbols.pdb` | `82b677b3-b983-4c49-912e-ae5641ce5f76` | 87 |
| `tool_symbols.pdb` | `3e2e627d-3e3b-4396-afd6-8ded91e9d954` | 80 |
## Local evidence and tools
Research root:
`C:/Users/isabe/Documents/Codex/2026-07-13/i-w/research/symbol-build-h1-tags-20260906`
- `manifest.json`: PE sections, debug-directory records, exact PDB paths,
identities, and explicit limits.
- `inspect_builds.py`: regenerates the manifest using `pefile`, without
executing or modifying the five input images.
- `ghidra-scripts/CrossBuildInventory.java`: exports observed function names,
their Ghidra source classification, and body ranges. It performs no renames.
- `ghidra-projects/h1-tags-crossbuild.gpr`: new, separate cross-build project.
- `ghidra-import.log`: successful automatic analysis and save of
`halo_cache_symbols.exe` (187 seconds, no timeout).
- `ghidra-import-remaining-retry.log`: the other four executables were imported
and saved with `-noanalysis`. The already-imported cache executable was
rejected as a duplicate and was **not overwritten**.
- `ghidra-export-explicit-bundle.log` and
`ghidra-export-cache-verified.log`: successful read-only inventory exports.
Earlier export attempts failed independently of the saved imports. The
explicit `ghidra-scripts/META-INF/MANIFEST.MF` and a subsequent run resolved
script-bundle loading. The installed headless README names an unavailable
timeout method; the installed Java source calls it `analysisTimeoutOccurred`.
The inventory script now uses plain `GhidraScript`; analysis completion is
established by the analysis log, not by a no-analysis export run.
Verified original-project status at the 2026-09-06 handoff (see the separate
2026-09-07 analysis update below):
| Program | Analysis status | Observed function records |
| --- | --- | ---: |
| `halo_cache_symbols.exe` | Automatic analysis completed | 17,858 |
| `halo_tag_symbols.exe` | Imported only; analysis pending | 18 |
| `guerilla_symbols.exe` | Imported only; analysis pending | 5 |
| `sapien_symbols.exe` | Imported only; analysis pending | 11 |
| `tool_symbols.exe` | Imported only; analysis pending | 32 |
Function inventories are `<executable>.functions.tsv`. These counts include
analysis-created functions/thunks and imports, **not recovered original Halo
function counts**. Preserve each row's source classification and do not count
names lacking a `FUN_` prefix as original symbols.
For the analyzed cache executable, 17,525 records have Ghidra source `DEFAULT`,
324 have `ANALYSIS`, and nine have `IMPORTED`; none establishes missing PDB
contents. The `.gpr` file is a zero-byte project marker; its sibling `.rep`
directory holds the databases and must stay with it.
The existing Ghidra installation was found by following its saved application
log, not by assuming it was on PATH:
`C:/Users/isabe/Documents/Codex/halo-tools/apps/ghidra_12.1.2_PUBLIC`.
Its configured JDK is the sibling `jdk-21.0.11+10` directory. The live RPC
endpoint at `127.0.0.1:18081` was offline at intake; the headless launcher is
usable independently. Only Ghidra's local settings/cache directories needed
additional write permission; the new project and outputs stay in the workspace.
IDA has not been located in the available tools, PATH, ordinary installation
directories, registry installation entries, or the local Halo tool bundle.
**The user's requirement to use both IDA and Ghidra remains outstanding.** Do
not claim that Ghidra, Capstone, or a PE parser satisfies the IDA half. A usable
installation/license or further location evidence is needed. Do not upload
these inputs to a cloud decompiler or symbol service without authorization.
Keep the executables, databases, and generated bulk inventories local. This
ledger records pointers and hashes only; it is not authorization to publish
the supplied binary corpus.
## Reconstruction plan and limits
1. Identify the source build and validate any future PDB against the recorded
GUID/age before loading its names/types into its own donor database.
2. Preserve evidence categories: imported API names, disassembler inference,
diagnostic literals, genuine debug records, and independently January-proven
facts are different things. A generated `FUN_...` name is not an original
name and must not become a production `code_<address>` substitute.
3. Use diagnostic strings, callers, data references, and instruction/CFG
comparisons to propose January correspondences. Verify each mapping locally;
never add one global address delta across builds.
4. "Un-inline" means recover and annotate candidate helper operations and their
call-site relationships where evidence permits. It cannot recreate all lost
source, local variables, or original inline boundaries. Do not rewrite the
January target, force new calls, or emit a `point_from_line3d` COMDAT to make
the donor's decompilation look tidier.
5. Names and types can accelerate source reconstruction, but exact credit still
requires the January COFF byte/relocation, source-authenticity, whole-TU,
whole-tree regression, and object-admission gates. No canonical code bytes
are earned by this intake or by automatic analysis alone.
## 2026-09-07: seven further programs analyzed and saved
The original project/cache analysis above remains preserved. A separate project
now holds the four remaining Symbols programs and the three supplied Play
programs:
`C:/Users/isabe/Documents/Codex/2026-07-13/i-w/research/hs7/remaining-projects/h1-tags-remaining-seven.gpr`.
Keep its sibling `.rep` directory with it. No supplied executable was run.
All seven Programs were saved and 21 detailed exports were produced. Input
hashes and exported PDB GUID/ages were rechecked. The bounded analysis phases
finished with warnings for Halo Tag Symbols (19,228 function records), Sapien
Symbols (54,820), Guerilla Play (3,770), Sapien Play (9,045) and Tool Play
(7,893). Guerilla Symbols (41,854) and Tool Symbols (46,630) reached their
300-second limits and remain explicitly **partial**, not complete analyses.
The local guide is `research/hs7/ANALYSIS_GUIDE.md` under the outer workspace;
the detailed functions/types/metadata exports are in `research/hs7/analyze-detailed`.
`research/hs7/remaining-analysis-summary.json` has SHA-256
`dbe1283d88ae4b8839b42888e7f80d70be4a87233824a99f7c6a77540bae5aaa`.
The guide records source classifications, timeout evidence, API-type archive
provenance and analyzer warnings, including WindowsResourceReference's OSGi
warning. PDB analyzers were disabled because matching PDBs are absent; no
external symbol download was performed. These generated records are not
restored original names, inline boundaries or debug assets, and earn no exact
code credit. IDA remains unlocated and that part of the request is outstanding.
## Related networking evidence
[Exploring The Halo 1 System Link Protocol](https://hllmn.net/blog/2023-09-18_h1x-net/)
provides retail PAL packet/debugger evidence. Its decoded advertisement retains
the suffix `age in a bot`. This corroborates, but does not independently prove
for January, our source-attested join-token implementation: the first four
bytes are transformed while the remaining twelve survive. The article's
two-second search cadence is another comparison lead. Its PAL timing and
absolute addresses must not replace January constants or addresses.
Use the article's emulator-boundary inspection method as a future local
runtime-validation aid. No live interception or traffic manipulation was
performed for this intake. The canonical token implementation and runtime tests
are documented in
`docs/object_matching_logs/network_server_manager_obj_source_attested_packet_20260906.md`.
+656
View File
@@ -0,0 +1,656 @@
# Halo Custom Edition maps
The native builds (Linux, Windows, Android) load and run Halo Custom Edition
caches (`.map`, cache version 609), with the Custom Edition resource maps
`bitmaps.map`, `sounds.map` and `loc.map`. A cache that needs OpenSauce
(its OpenSauce header asks for memory upgrades, mod data files or the like)
is refused, and `.yelo` files are never looked for; a `.map` that only
carries OpenSauce's header and its `project_yellow` tags (SPV3's
backwards-compatible releases) is run as stock Custom Edition runs it,
neither read. The loader and the conversions come from DamnationCE
(xshxdex98/DamnationCE), whose documentation this file grew from; in this
build a Custom Edition map's tags are also checked by the tag validator, as
the Xbox maps' are, before anything reads them
(`port/linux/game/tag_validate.c`, [Checks](#checks)).
## Running a map
Custom Edition maps run when the `game.custom_edition` setting is on, as it
is by default (`custom_edition = true` under `[game]` in `config.toml`, or
`HALO_CUSTOM_EDITION`). Put the maps and Custom Edition's `bitmaps.map`,
`sounds.map` and `loc.map` in the data root's `custom_maps` folder, beside
its `maps` folder, which holds the game's own maps only; or set
`paths.custom_edition` to a Halo Custom Edition install, whose `maps` folder
(the Xbox drive `h:\`) is looked in after `custom_maps`.
A Custom Edition map's level name is `custom_maps\<name>`
(`custom_edition_cache.h`, `CUSTOM_EDITION_LEVEL_NAME_PREFIX`), which none
of the game's own levels has: only such a name is loaded from the Custom
Edition folders, and never from `maps`. So a map named as one of the game's
own levels (CMT's `a30.map`, Custom Edition's `bloodgulch.map`) is played as
itself, apart from the level of its name, in the menus, the loader and
network games, and is never taken for one of the campaign's levels (saved
games, progress, the next level). The game engine keeps 63 characters of a
level name, so a map whose file name is longer than 51 is not listed.
Maps keep their names. A network game's level name is the map's own
(`custom_maps\hugeass`), and its game record's map version is the first 32
bits of the map's hash (`port/linux/game/map_share_files.c`), so a client
plays its file of that name and version, whatever else it has of the name
(`custom_edition_map_find_version`); a player without it downloads it
(`port/linux/NETCODE.md`, "Map sharing"). A download beside another version
of its name is named `hugeass_0e2b3ee4.map`, the first 8 hexadecimal digits
of its hash after `_`, and the menus show it as "Hugeass (2)". A level name
without the hash (a saved last map, a test's) finds the one map of that
name in `custom_maps`, and one with it (an earlier build's, which named
every map so) the map given its name back. Maps in a Custom Edition
install's folder are found and shared as `custom_maps`' are.
A map needs only the resource maps that hold its tags: its tag index says
which (`cache_file_resource_maps_needed`, `cache_file_formats.c`: a tag
whose entry's word at 0x18 is set is held whole by `bitmaps.map` for a
bitmap, `sounds.map` for a sound, `loc.map` for a font, string list or HUD
message text). A map without such tags of a kind plays without that
resource map; one that lacks a resource map it needs is refused, saying
which.
### In the menus
The map lists (New Game's, and the Map screen of Create Game's INTERNET and
LAN and of split screen) have four kinds in their first row's chooser:
SINGLEPLAYER (the campaign's levels), MULTIPLAYER (the Xbox's maps), and
CUSTOM SINGLEPLAYER and CUSTOM MULTIPLAYER, the Custom Edition maps of the
custom maps folders by the scenario type their files give (solo or
multiplayer). A map is played as its kind says
(`port/linux/game/menu_functions.c`):
- **New Game**: a CUSTOM SINGLEPLAYER map is played as the campaign's
levels are, at the difficulty chosen next (alone, or with split screen
co-op's two players); a CUSTOM MULTIPLAYER map is walked around alone, as
MULTIPLAYER's are.
- **The Map screen**: a CUSTOM SINGLEPLAYER map lists the difficulties and
is hosted as a network co-op game (Server Setup, then the lobby), as the
campaign's levels are; a CUSTOM MULTIPLAYER map goes on to the gametypes,
as MULTIPLAYER's do. Split screen hosts only the multiplayer kinds.
The Xbox menus' multiplayer level list offers the Custom Edition multiplayer
maps after the thirteen Xbox levels. The pregame lobby shows a Custom
Edition map by its name and picture, multiplayer or campaign, and the game
lists by its name (`port/linux/game/custom_edition_maps.c`). The folders are
looked in once, and again each time a map list opens, which then shows maps
added since. A map is listed under its file's name, `the_bay_of_pigs.map` as
"The Bay Of Pigs". Two optional files beside the map give it what the Xbox
levels have:
- `<name>.bmp`, its picture: an uncompressed 24-bit or 32-bit Windows
bitmap, up to 8192 pixels a side; the middle of it is shown with the
shape of the level pictures, 140 by 114 (`port/linux/game/bmp_files.c`,
which reads it as untrusted input).
- `<name>.txt`, its description: plain text, its lines shown as written.
Without one, the map is described as "Halo Custom Edition map".
A machine joining a network game on a map it has not (a Custom Edition map
not in its `custom_maps`, or one of the game's own not in `maps`) leaves it
with an error that names the map and the folder to copy it into, rather
than the damaged disc error precaching a missing map gives
(`cache_files_map_present`, called as the host's settings arrive): also when
the map cannot run there (Custom Edition maps turned off, the file not a
Custom Edition cache, or `bitmaps.map`, `sounds.map` and `loc.map` missing),
when its copy of a Custom Edition map is another version than the host's
(the host sends its map's hash's first 32 bits as the game record's map
version, which the Xbox left 0: `cache_files_map_version`; a host that sends
0 is not checked), and when the host names a Custom Edition map as other
versions of this port do (`levels\test\<name>\<name>`) and the file is in
`custom_maps`. The
error's text is wrapped to its dialog, in the menus' smaller font when it
would not fit; `debug.txt` has the details.
A Custom Edition campaign map has no next level: winning it ends the game
as the campaign's last level does (alone) or plays it again (network
co-op), and it is not the campaign's saved game.
### From the console
`map_name` takes the level name: `custom_maps\<name>` for a Custom Edition
map (`custom_maps\hugeass` plays `custom_maps\hugeass.map`). A multiplayer
scenario needs a game variant, or no starting location qualifies and no
player spawns:
```
game_variant slayer
map_name custom_maps\hugeass
```
## What the native builds do
- **With `game.custom_edition` on**, the platform reserves the address
window Custom Edition tag data is linked to, `0x40440000`-`0x41B40000`
(`port/linux/src/xbox_memory.c`, at start-up; 23 MB of address space,
backed as it is touched), and a Custom Edition map is loaded into it,
converted as below, checked, and run. It is read in place, never copied
to the cache partition: every read the game makes of it (structure BSPs,
bitmap pixels, sound samples) is served from the map, `bitmaps.map`,
`sounds.map` or the sounds decoded at load, according to where its offset
falls in their combined offset space (`custom_edition_cache_read`). A map
that fails any check is logged in `debug.txt` and not loaded: the game
goes back to its menus.
- **With it off**, a Custom Edition cache is named and refused, instead of
being rejected as "an old version" of this build's caches; so is one put
in `maps`, which is told to go in `custom_maps`.
- **Xbox caches** take the path they took before.
The desktop builds' Xbox memory window is 512 MB (Android's 128 MB), in
which the texture cache is 256 MB: Custom Edition maps draw more texture
than the Xbox's 22 MB cache holds, and their geometry is made into
Direct3D buffers in the same memory (`port/linux/include/halo_port_capacity.h`).
## Loading and conversion
### The loader (`cache_file_formats.c`)
`port/linux/game/cache_file_formats.c` (`cache_file_formats.h`) holds the
format code: standalone C with fixed-width types and no game dependencies,
compiled into the native builds (as every `port/linux/game` unit is) and
into the report tool. Map files are untrusted input: every field is read
little-endian from a bounds-checked buffer, never through a structure laid
over the file, and every offset, count and size is checked before use, with
overflow-safe arithmetic. `port/tools/cache_file_report.c` reports what can
be done with each file:
```sh
clang --target=i686-pc-windows-msvc -fuse-ld=lld -Iport/linux/game \
port/linux/game/cache_file_formats.c port/tools/cache_file_report.c -o cache_file_report.exe
cache_file_report [--maps DIRECTORY] [--dump-tags FILE] FILE...
```
Resource maps are looked for next to the cache (or in `--maps`):
`bitmaps.map`, `sounds.map` and `loc.map`. `--dump-tags` writes the converted
tags to a file, as they would sit at `0x40440000`.
Loading, step by step (`custom_edition_cache_load`):
1. The header: signatures, version 609, terminated strings, file length (0,
as Invader leaves it, for the whole file) within the file and the size
limit (`0x30000000`: Halo PC's was `0x18000000`, and Invader builds
larger maps, which Chimera runs), no compression, tag data within the
file and the tag cache (23 MB). A cache with OpenSauce's header (its
`yelo` signature at offset `0x70`, which Custom Edition leaves as
padding) that sets any of its flags (memory upgrades, mod data files and
the rest) is refused; one that sets none is a Custom Edition cache.
2. The tag data is read to the start of the tag cache, which stands for
`0x40440000`.
3. The tag index (`tags` signature), every tag instance (its handle must
match its position, its name must lie in the tag data and be terminated,
its address must lie in the tag data; only structure BSPs may have none;
only bitmaps, sounds, fonts, unicode string lists and HUD message text may
be held by resource maps; OpenSauce's `project_yellow` and
`project_yellow_globals`, which nothing reads, are left as they are), the
model data range, and the scenario tag.
4. Every structure BSP: its tag, its range in the file, its place in the tag
cache (above the tag data, within the tag cache), its header (`sbsp`
signature, no Xbox vertex buffers, its structure pointer inside it), and
its lightmaps and their materials: every block within the BSP and aligned,
every material's uncompressed environment vertices (56 bytes) and
lightmap vertices (20 bytes) within the BSP and of the size their counts
give, at most 64000 of each (the game's limit, to which the validator
cuts a material's counts), lightmap vertices exactly when the lightmap
has a bitmap, and every normal, binormal, tangent and incident radiosity
within ±1.005 (the game's vertex compressor asserts on anything further
out).
5. The header checksum is computed as OpenSauce computes it, before anything
changes the tag data; a mismatch is reported, not refused.
6. The tags held by resource maps are read into the tag cache after the tag
data, below the lowest structure BSP, and their pointers relocated. A
sound's header in the map lacks its sample rate, encoding, compression
and longest permutation length; they are taken from the header's copy in
`sounds.map` ([Evidence](#observed-not-stated-by-the-sources-established-on-the-sample-maps)).
7. Every bitmap, sound, font, unicode string list, HUD message text, model
(`mod2`) and transparent chicago shader (`schi`, `scex`) is walked through
its blocks: every block and data pointer must lie within the loaded tags,
every bitmap's pixels and sound's samples within their file, and every
model part's strip and vertices within the model data, of the kinds Custom
Edition writes (a precompiled strip, uncompressed 68-byte vertices), with
at most 65535 vertices and at most 22 local nodes.
Then `custom_edition_cache_convert` changes what only needs its bytes
changed:
- **Shader types.** Custom Edition inserted *transparent chicago extended*
as shader type 7, so its water, glass, meter and plasma shaders are types
8 to 11 where this build has 7 to 10 (and would draw a Custom Edition glass
shader as meter, and so on, unnoticed). Every shader gets this build's
number for its group; a type field saying otherwise (as map protection
leaves them) is counted and given the group's.
- **Transparent chicago extended shaders** (`scex`) become transparent
chicago shaders (`schi`): the two layouts agree up to the maps, of which
`scex` has two sets, for four and for two texture stages; the four-stage
maps are kept (this build draws four stages), or the two-stage ones when
there are no others, and the extra flags move to where `schi` has them.
- **Bitmaps and sound permutations** get the state of ones not yet drawn or
played, and name their own tags: `bitmaps.map` and `sounds.map` hold the
tag handles of whatever map they were built with.
- **Sounds this build cannot decode** (Custom Edition's Ogg Vorbis) are made
unplayable, by emptying their pitch ranges, which the game skips.
- **Animation overlays naming animations that do not exist** are made to
name none, which the game skips. Maps built with the editing kit can have
them; Custom Edition reads past the graph's animations there, and this
build's debug builds stop on it.
- **HUD elements drawn from double-resolution bitmaps.** Halo PC added a
third scaling flag to HUD placements, *use high resolution scale*
(OpenSauce `hud_definitions.hpp`), and draws a flagged element at half the
size of its bitmap. Custom Edition's HUD bitmaps are made for that: in
`bloodgulch.map` every flagged element draws a bitmap exactly twice the
size of the one its Xbox counterpart draws (256×64 for 128×32, 512×512 for
256×256), with the same scale, and every unflagged one a bitmap of the same
size ([Evidence](#observed-not-stated-by-the-sources-established-on-the-sample-maps)).
This build ignores the flag, so it drew them twice too large; the
placements of the unit, weapon and grenade HUD interfaces and the HUD
globals' messages that have it get half their scale, and lose the flag (32
in `bloodgulch.map`). Halo PC reads the flag only on statics, meters and
numbers (Chimera's `hud_bitmap_scale.cpp`): crosshair and overlay items
keep their scale. A number's scale is not its
digits', so a flagged number keeps the flag and `hud_draw_numbers` draws
its digits at half their size, spaced as the digits tag says. A bitmap may ask the
same of every element that draws it, with Halo PC's bitmap flags *half hud
scale* and *force hud use highres scale* (Invader's `bitmap.json`): an
element drawing such a bitmap (a static or meter, its bitmap after its
placement; a crosshair's or overlay's items, their crosshair's or
overlay's) gets half its scale too. None of the 18 Custom Edition maps on
hand, nor `bitmaps.map`, sets either.
- **Maps made around Halo PC's own behaviour.** Chimera fixes Halo PC to draw
as the Xbox does, and keeps a list of the maps made around Halo PC's way
instead, by map name (in lower case) and tag data checksum, with the
behaviours each relies on (`map_hacks_config.json`, by SnowyMouse; here
`port/linux/game/custom_edition_behaviours.inc`, generated from it). This
build draws as the Xbox does, so for a listed map it follows these where it
can, and logs each: HUD multitexture overlays' blend functions in Halo PC's
order (`gearbox_multitexture_blend_modes`: Halo PC picks its shader by the
Xbox's value from shaders in alphabetical order), overlays not drawn
(`block_multitexture_overlays`), the HUD digits' metrics halved and every
number's digits drawn at half size (`hud_number_scale`), bitmaps' HUD scale
flags cleared (`disable_bitmap_hud_scale_flags`), and model shaders'
detail after reflection flag flipped (`invert_detail_after_reflection`).
Not yet: Halo PC's fixed-function meters (`gearbox_meters`), its
transparent chicago multiply, bump attenuation and environment shader
types, the old widescreen HUD and embedded Lua.
- **The score hint.** String 100 of `ui\multiplayer_game_text` is Halo PC's
`Hold "%s" for score`, which Halo PC fills in with its score key; this
build copies it as it is (`game_engine.c`, the press-back-for-score
message), so `"%s"` becomes `BACK`, the button this build reads for the
score, in the same four characters.
### In the game (`custom_edition_cache.c`, `custom_edition_geometry.c`, `custom_edition_bitmaps.c`)
- **Models.** Every `mod2` part is given this build's part layout (the first
0x68 bytes of the 0x84-byte Custom Edition part are the same fields; the
parts are repacked in place), its vertices are compressed from the map's
model data by the game's own `rasterizer_geometry_compress_vertices`, its
node indices made the model's when its model's parts have local nodes, and
its strip copied unchanged; the game's own `rasterizer_vertex_buffer_new`
and `rasterizer_triangle_buffer_new` make its buffers, and the tags become
`mode` tags. Before any of that, every index a part holds is checked
against what it names (shaders, nodes, parts, vertices). This build's
renderer skins at most 43 nodes at once, so a model of more is drawn a
part's own nodes at a time: a model of more whose parts have no local
nodes is given them, each part the nodes its vertices name, and is refused
only when a part's vertices name more than a part holds.
- **Structure BSPs.** When the game loads a BSP, every material's vertices
are compressed the same way (environment and lightmap vertices), and the
material gets buffers and the compressed vertices the game reads for
visibility, collision and lights (`scenario_structure_bsp_load` in
`cache_files.c` calls `custom_edition_structure_bsp_load`).
- **Bitmaps.** Every bitmap must pass the game's own `bitmap_verify` and be
of a kind the texture cache and the swizzling code handle (a format with a
hardware texture, a compressed flag that agrees with the format,
power-of-two sizes unless linear, square cube maps, all its pixels
present). Halo PC draws a 2D bitmap of any size, so an uncompressed one
whose sides are not powers of two is made linear as the map loads, and
drawn from its first level (birdcage-plus's 3840 by 64 needler plasma).
Its pixels are laid out for Halo PC, levels as the tag stores them,
unswizzled; as they arrive, the game's own
`rasterizer_xbox_bitmap_rebuild_hardware_format` lays them out as an Xbox
cache would (swizzled, cube maps face by face, padded).
- **Chicago extra layers.** January's transparent chicago shader draws its
extra layers in a loop that never advances
(`rasterizer_xbox_transparent_geometry.c`, marked as a preserved bug). No
Xbox map has such layers, so January never hung on it, but Custom
Edition's editing kit can make them: the native builds advance the loop.
- **Channel orders.** Halo PC keeps two kinds of texture in other channels
than this build reads them from (`enum custom_edition_channel_order`,
[Evidence](#observed-not-stated-by-the-sources-established-on-the-sample-maps)):
a model shader's *multipurpose map*, whose auxiliary (detail) mask,
self-illumination, specular and color change masks Halo PC keeps in red,
green, blue and alpha, where this build's model shaders read specular from
red, self-illumination from green, color change from blue and the
auxiliary mask from alpha; and a *HUD meter*, whose shape Halo PC keeps in
color and fill order in alpha, where this build's meter shader
(`rasterizer_xbox_dynavobgeom.c`) compares the color with the meter's value
and discards what has no alpha. Reordering the pixels would mean
decompressing them, which the texture cache has no room for, so the game
finds the bitmaps drawn as multipurpose maps (by model shaders) and as
meters (by unit and weapon HUD interfaces) when the map loads, and tells
the renderer where each bitmap's pixels arrive and in which order their
channels are; the renderer samples them with each channel taken from where
Halo PC keeps it (`port/linux/src/xbox_textures.c`, a texture swizzle,
which leaves them compressed). A bitmap also drawn another way (a
multipurpose map that is also a base map, say) keeps its channels, since
the renderer has one order for each texture, and is logged: 3 in
`bloodgulch.map`, all multipurpose maps.
- **Reads.** The renderer write-protects the memory it has made textures of
and learns of changes from the faults writes take; the kernel fails a read
into such memory instead. Every read of the map is therefore made into a
64 KB staging buffer and copied, as the platform's file layer does.
- **Texture memory.** A frame of `Elite_Alpha_Siege.map` draws more than the
22 MB of textures the Xbox texture cache holds, and textures that did not
fit were drawn as the default one. The desktop builds' texture cache is
256 MB, in a 512 MB memory window (`halo_port_capacity.h`), for every map;
Android's is the Xbox's.
- **Scripts.** A compiled script names each function it calls, and each
engine global it uses, by its index in the engine's tables, and Halo PC's
tables have entries this build's do not, in among the ones both have: from
some point on, an index names another entry here
([Evidence](#observed-not-stated-by-the-sources-established-on-the-sample-maps)).
`hugeass.map`'s day and night script would call `playback` instead of
`switch_bsp`. A compiled script also keeps every name, in its string data,
so every call and every engine global is found again by name with the
game's own `hs_find_function_by_name` and `hs_find_global_by_name`
(`custom_edition_scripts.c`); one this build does not have does nothing
and is logged by name (Limits, below). The value types are numbered
alike in both builds.
- **Multiplayer vehicle placement.** This build places a multiplayer game's
vehicles by type: only the first three of the globals' multiplayer
vehicles are created, and of those the ones the variant's vehicle set
names (`game_engine_remap_vehicle`; the built-in variants name the first).
Custom Edition maps are made for retail Halo's rules: a vehicle placement's
multiplayer spawn flags name the game types it is placed in by default,
and a script may create any vehicle. `hugeass.map` depends on that (above).
For a Custom Edition map in slayer, capture the flag, king of the hill or
oddball, the native builds place the vehicles whose spawn flags name the
game type by default and let any vehicle be created
(`custom_edition_objects.c`, called from `object_types_place_all` and
`game_engine_remap_vehicle`); a variant without
vehicles still has none, and race, which has no spawn flag, keeps this
build's rule.
- **Multiplayer vehicles.** `game_engine_predict_resources` takes the three
multiplayer vehicles Xbox globals always have; a Custom Edition map's
globals can list fewer, and oddball stopped on such a map. With fewer than
three, the native builds predict none (`game_engine.c`).
## Checks
The loader (`cache_file_formats.c`) checks the container: the header, the
tag index, every structure BSP, every resource-held tag and its relocated
pointers, every bitmap pixel and sound sample range, every model part's
geometry in the model data. Then, once the loader and the conversion of
bytes have made the tags this build's layout, the tag validator checks
every tag as it checks an Xbox map's (`tag_validate_custom_edition_tags`,
called by `custom_edition_cache_tags_load`): every block and data within
the loaded tags and overlapping no other's, counts cut to the game's
capacities, indices, enums and tag references corrected, runtime values
reset, then the graph checks (collision BSPs, node trees, scripts' syntax),
and a tag name that lies in any tag's block or data is emptied (what the
game writes there could take away its end). A map whose pointers cannot be
trusted is refused; what can be corrected is, and logged. What differs
from an Xbox map:
- the tag header has no vertex or index buffers, and the tag cache is the
Custom Edition one (23 MB);
- bitmap pixels and sound samples are in several files: their offsets must
lie in the map, the sounds decoded at load, `bitmaps.map` or `sounds.map`;
- models are gbxmodels (`mod2`), whose parts keep their geometry in the
map's model data and a table of local nodes, which are checked against
the model's (`tag_schema_custom_edition_groups`, in `tag_schema_models.c`);
the game takes them as models;
- a structure BSP's material vertices are uncompressed, and are checked
against its uncompressed vertex data.
Only after the validator has passed the tags are the scripts given this
build's function and global indices, the bitmaps checked for the texture
cache, and the models and each structure BSP (checked as it loads, as an
Xbox map's) given compressed vertices and buffers. The script function
allowlist (`hs.c`) applies to Custom Edition maps as to every map.
`build/linux/map_validate` runs the loader, the conversion of bytes and the
validator on maps outside the game, with the resource maps beside each map
or in `--maps`; `--fuzz` changes words of a map's file at random.
## Tested
### Automated tests
- `python -m pytest tools/test_cache_file_formats.py` (130 tests; needs
clang): synthetic Custom Edition caches and resource maps, caches that
need OpenSauce refused and those that only carry its data loaded, every
check of the loader and the conversion, and seeded random corruptions,
through `port/tools/cache_file_report.c`. With `HALO_CUSTOM_EDITION_MAPS`
naming a folder of maps, four more check real maps: against DamnationCE's
recorded results (which are of its copies of the stock maps), and that any
`.yelo` map among them is either refused or a Custom Edition cache.
- `python -m pytest tools/test_bmp_files.py` (41 tests): the map pictures'
reader.
- `tools/test_linux_port.py`: with `HALO_CUSTOM_EDITION_MAPS` (or the
gitignored `assets/custom_edition`), every map is loaded and checked and
none refused, and three are fuzzed (100 runs each); the retail Xbox maps
still need no correction.
### Maps
Two folders were checked with `map_validate`: the 20 stock Custom Edition
maps (the halopc-restored r220 copies) and 19 community maps (`area53`,
`bigass`, `deltaruins`, `Elite_Alpha_Siege`, `extinction`, `Fission_Point`,
`Floodgulch5.1`, `hugeass`, `immure`, `tactics`, `the_bay_of_pigs`,
`the_rise_of_asis`, `WW_battlefield` and copies of stock levels). None is
refused. The stock maps need one correction each, a character of
`loc.map`'s gamespy font 5 pixels narrower than nothing; the community maps
some more, their own data's mistakes (an enum past its values, a region
index past its block, a particle state of a sequence with no sprites).
Fuzzing, 200 runs of each of those 39 maps (7,800 runs): no crash, no hang,
and every map let through clean when checked again.
Run in the game (Linux release build, 40 to 50 seconds each, slayer):
`hugeass`, `bigass`, `extinction`, `deltaruins`, `Elite_Alpha_Siege`,
`the_rise_of_asis`, `area53`, `tactics`, `Floodgulch5.1`, `immure`,
`WW_battlefield`, `the_bay_of_pigs` and `Fission_Point` load, are checked,
converted and played to the end of the run without a crash; `hugeass`'s
hangar, HUD and first-person weapon were seen drawn.
Two campaign maps were checked with `map_validate`: CMT's `a30.map` (2006,
"Halo"; 13 corrections) and CMT SPV3's backwards-compatible `a50.map` (2012,
The Truth and Reconciliation, with OpenSauce's header setting no flags and
its two `project_yellow` tags; 43 corrections); `a30` was also run. From the
menus (on a virtual display): New Game's CUSTOM SINGLEPLAYER `a30` played as
a campaign level at the difficulty chosen; the Map screen's (LAN) listed its
difficulties, set up co-op in Server Setup and showed it in the lobby as
co-op. With `debug.network_test`, two machines on loopback played
`custom_maps\a30` as network co-op and `custom_maps\hugeass` as slayer; a
joining machine with a maps folder missing the host's Xbox map was shown the
error and left the game. A joining machine without `hugeass` (or without
`sounds.map` and `loc.map` too) downloaded them and played (map sharing,
`port/linux/NETCODE.md`): in the lobby, into a game in progress, from the
host and another player at once with a second machine downloading beside it
(on loopback, and through internet play's tunnels with a local broker), each
file the same as the host's; with `network.map_download` off, or a host with
`network.map_share` off, it was shown the error and left.
### Not tested
- **Playing**: nobody played the maps through.
- **Android** builds, and was not run.
## Evidence
### Sources
- **OpenSauce** (GPL-3.0, Kornner Studios): the `OpenSauce-master.zip`
archive of the upstream repository (SHA-256
`f1097c9cf5975a208506c78c3af47d6b893f998a9f47dddf89c9bbcc1f4b7c73`; newest
entry dated 2016-01-15; `yelo_version.hpp` names OpenSauce 4.0.0).
- **Reclaimer** (GPL-3.0, Gravemind2401): the `Reclaimer-master.zip` archive
(SHA-256 `72fd5270abbdf662ff07a54e9cce80a6158e479c51ecfdcd979f7f9fbd6c2d03`;
newest entry dated 2026-09-20).
- **The Xbox maps of retail build 01.10.12.2276** (`bloodgulch.map`,
decompressed): not the 01.01.14.2342 build this repository reconstructs,
but their model parts have its 0x68-byte layout and their shader types its
numbering, which makes them a check on what the conversion produces.
No code from OpenSauce or Reclaimer (both GPL-3.0) was copied: they were
read as documentation of the file formats, and every layout used was then
checked against the sample maps.
### Documented facts (source, checked against the sample maps)
| Fact | Source |
| --- | --- |
| Cache header layout, version 609, `head`/`foot` signatures | OpenSauce `blamlib/Halo1/cache/cache_files_structures.hpp` (`s_cache_header`) |
| Tag index (0x28 bytes, instances at 0x28, model data as file offsets) and tag instance (0x20 bytes, resource-map flag at 0x18) | same file (`s_cache_tag_header`, `s_cache_tag_instance`) |
| OpenSauce's header at 0x70 begins with the `yelo` signature, where Custom Edition's header has padding, and has its flags (memory upgrades, mod data files, protected, game state upgrades, compression parameters) at 0x76 | OpenSauce `YeloLib/Halo1/cache/cache_files_structures_yelo.hpp` (`s_cache_header_yelo`) |
| Tag cache at `0x40440000`, 23 MB; file size limits | OpenSauce `cache_constants.hpp`, `saved_game_constants.hpp` |
| Resource map header and entries | OpenSauce `data_file_structures.hpp` |
| Groups held by resource maps: bitmaps, sounds, fonts, unicode string lists, HUD message text | OpenSauce `blamlib/Halo1/cache/cache_files.cpp` (`cache_file_data_load`) |
| Structure BSP reference (0x20), header (0x18), structure, lightmap and material layouts | OpenSauce `scenario_definitions.hpp`, `structure_bsp_definitions.hpp`; this build's `structure_bsp_definitions.h` agrees |
| Bitmap, sequence, sprite, bitmap data layouts; pixels in `bitmaps.map` flag | OpenSauce `bitmaps/bitmap_group.hpp` |
| Sound definition, pitch range, permutation layouts; samples-in-`sounds.map` flag | OpenSauce `sound/sound_definitions.hpp` |
| Gbxmodel layout: the part (0x84 bytes) is this build's 0x68-byte part followed by a local node count and a 22-entry node table | OpenSauce `models/model_definitions.hpp` (`gbxmodel_geometry_part`) |
| Where a part's strip and vertices lie in the model data (triangle count, strip offset, vertex count and offset in the buffer fields; 68-byte vertices) | Reclaimer `Blam/Halo1/GbxmodelTag.cs` (`ReadPCMeshes`) |
| Shader types: *transparent chicago extended* inserted at 7, before water, glass, meter and plasma; its two map sets | OpenSauce `shaders/shader_definitions.hpp` |
| Script syntax nodes: 19001 in the stock engine | OpenSauce `hs_constants.hpp` |
| HUD message text layout | OpenSauce `interface/hud_messaging_definitions.hpp` |
| Font (156 bytes) and unicode string list layouts | BlamLib `Blam/Halo1/Tags/Definitions/Misc.cs`, `Resources.cs` |
| Bitmap tags in `bitmaps.map` by index, pixels by absolute offset | Reclaimer `Blam/Halo1/CacheFile.cs`, `BitmapTag.cs`, `BitmapsAddressTranslator.cs` |
| The header checksum: CRC-32 of the structure BSPs (packed from 0x800), the model data and the tag data, not inverted | OpenSauce `cache_files_yelo.cpp` (`CalculateChecksumFromMemoryMap`), `memory_interface_base.cpp` (`CRC`); reproduces the stored checksum of 23 of the 24 sample caches |
### Observed (not stated by the sources; established on the sample maps)
| Fact | Evidence |
| --- | --- |
| Structure BSPs load at the top of the tag cache (`address + size` = `0x41B40000`) | all 24 caches (those with OpenSauce's memory upgrades at the top of its larger tag cache) |
| Bitmaps, fonts, unicode string lists and HUD text held by resource maps: the tag's address field is the entry index, the entry's name is the tag's path, the entry is the whole tag with addresses counting from its start | 9,550 bitmaps, 68 fonts, 2,248 string lists, 23 HUD texts across the sample |
| Sounds held by `sounds.map`: the map keeps the 0xA4-byte header, whose pitch range block has a count and no address; the entry named by the tag path holds the header again, then the pitch ranges and permutations, whose addresses count from the first pitch range | 7,397 sounds, 21,870 sample ranges |
| The map's copy of a resource-held sound's header has its compression and longest permutation length zero, and its encoding and sample rate zero too; the entry's copy has them as the Xbox has them | in `bloodgulch.map` and `beavercreek_halo3.yelo`, the two copies of all 527 resource-held sounds differ only in those four fields, in pointers, and in the map's own promotion sound reference; for the 315 sounds the Xbox `bloodgulch.map` shares, the entry's sample rate, encoding and the runtime fields after the longest permutation length equal the Xbox's; its compression does except for the 38 that Custom Edition has as Ogg Vorbis, and its longest permutation length for 273 (the others differ by a few milliseconds or more, among them the Ogg Vorbis ones); its pitch ranges' runtime fields all do |
| Sound permutations and resource-held bitmaps name tags of another map (the one the resource map was built with) | 632 of 676 bitmaps of `bloodgulch.map`; every resource-held permutation differs from the Xbox's own sound handle |
| Model parts: the strip and vertex data are in the model data, never in the part's blocks; the triangle buffer is a precompiled strip and the vertices uncompressed; the part flag 2 is set on exactly the parts that have a node table | all 24 maps (10,934 parts) |
| Converting a part's vertices with the game's compressor reproduces the Xbox's | for the 335 parts of the 60 models the Custom Edition and the Xbox `bloodgulch.map` share: identical strips; positions equal to within float rounding; texture coordinates and node weights exactly what the compressor makes of the Custom Edition values (clamped to ±1, as the Xbox also has them: `plant_broadleaf_short` has coordinates up to 3.0 in both); node indices the Custom Edition ones times 3, except that the Xbox writes an unweighted second node as -1 where the compressor writes the node Custom Edition names; normals within 0.002 |
| Every node index of a vertex names a node of its part's table (in models whose parts have local nodes) or of its model, every table entry a node of the model, and every vector is within ±1.0001 | all 24 maps |
| Structure BSP materials: environment vertices uncompressed (type 0), with lightmap vertices (as many as the environment ones) exactly in lightmaps that have a bitmap, and no compressed vertices | all 24 maps (the lightmap vertex type field is 0 or 2 regardless) |
| Custom Edition shader type values are 7 to 11 for `scex`, `swat`, `sgla`, `smet`, `spla`; the Xbox's are 8, 9, 10 for `sgla`, `smet`, `spla` | every shader of the 24 maps, and of the Xbox `bloodgulch.map` |
| Chicago shaders with extra layers: none in the stock maps or the Xbox `bloodgulch.map`; 2 in `beavercreek_halo3.yelo`, 2 in `celer_exile_odst_v2.yelo`, 4 in `extinctionrevanepic2.map`, 1 in `fy_killzone.yelo` | every `schi` and `scex` of the sample |
| Compressed color plates are left out of caches: size kept, address 0 | every bitmap in `bitmaps.map` |
| Font style references in `loc.map` are all `NONE` | all 3 fonts |
| Sound compression value 3 is Ogg Vorbis, value 1 is Xbox ADPCM | in `sounds.map`, all 106 value-3 permutations start with `OggS`; all 1,365 value-1 permutations are whole 36-byte blocks |
| `bitmaps.map` bitmaps are not swizzled, and cube maps hold each level's six faces together | all 1,467; 38 of 40 cube maps match that order when checked against their next level (one is uniform, one does not match) |
| Multipurpose maps: Custom Edition's red, green, blue and alpha hold what the Xbox's alpha, green, red and blue do | the multipurpose maps the Custom Edition and the Xbox `bloodgulch.map` share, decoded and compared channel by channel (the cyborg's, the warthog's, the boulders') |
| HUD meters: Custom Edition's alpha holds the Xbox's color (the fill order), and its color the Xbox's alpha (the shape) | `hud_ammo_meters`: Custom Edition's 512×512 A8R8G8B8 averaged down to the Xbox's 256×256 A8Y8 differs from the Xbox's luminance by 6.6 on average in alpha and 23 in color, and from its alpha by 12.3 in color and 23.4 in alpha; `hud_unit_meters` has its sprites rearranged, and shows the same swap when viewed. January's meter shader reads the fill order from color and discards texels without alpha |
| HUD elements flagged *use high resolution scale* draw bitmaps twice the Xbox's size; others the same size | every unit and weapon HUD placement of `bloodgulch.map` whose tag the Xbox map also has (the motion sensor's foreground, unflagged, uses a 128×128 bitmap in both) |
| Vehicle placements: the byte at 0x58 is the multiplayer team and the word at 0x5A the multiplayer spawn flags, default bits 0 to 3 (slayer, capture the flag, king of the hill, oddball) and allowed bits 8 to 11 | `hugeass.map`'s 78 vehicle placements: the byte is 1 on exactly the blue team's; the word is 0x0303 on warthogs, tachikomas and pelicans, 0x0F0F on ghosts, 0x0404 on 16 unnamed jets, 0x0808 on the night jet and 0 on vehicles its scripts create; OpenSauce leaves these bytes unnamed |
| Script function and engine global tables: Halo PC's have entries this build's lacks, so a compiled index names another entry from some point on; value types are numbered alike | the compiled scripts of the sample, each call's function index against the name its first child keeps, and each engine global's index against its name: `timberland.map` 23 calls shifted (`player_effect_start` by 29), `ui.map` 11 (`camera_set` by 1), `hugeass.map` 3 calls and 1 global (`rider_ejection`, 158 against 147), `extinctionrevanepic2.map` 9 calls, and 4 of a function this build lacks; no other stock map has scripts; each call's value type is the type of its function here (1,485 of 1,485 in `hugeass.map`), or one the call site casts to |
| Bitmaps the Xbox keeps in monochrome formats and Custom Edition as 32-bit color at the same size keep their channels (A8Y8 as A8R8G8B8 with red the luminance and alpha the alpha) | 24 of the 26 such bitmaps of `bloodgulch.map` decode to exactly the Xbox's values; the other 2 have other content |
### Assumptions (not verified)
- **Placement of resource-held tags.** They are placed after the tag data,
aligned to 4 bytes, as OpenSauce's unimplemented loader outlines
(`s_cache_file_data_load_state`); how Custom Edition itself places them is
not known, and nothing found depends on it.
- **Which chicago extended maps.** The four-stage maps are kept, as the
renderer draws four stages; that Custom Edition's two-stage maps are a
fallback for older hardware is an inference (BlamLib's exporter makes the
same choice).
- **Centroid nodes of local-node parts** are taken to be the model's nodes:
they are within the model's node count in every map, and they are only
used to sort transparent parts.
- **Editing-kit pointers are cleared.** The definition pointers of relocated
blocks and data are set to 0: they refer to nothing in the game process.
- **Name comparison** is exact (case-sensitive), which matched every sample
sound.
## Limits and remaining work
- **What the validator corrects is lost.** A community map's data that the
validator corrects (an index past its block, an enum past its values) is
what the game then runs; Halo PC read past such data unchecked.
- **Ogg Vorbis sounds** are decoded when the map loads (stb_vorbis, with
added bounds checks: `port/third_party/stb_vorbis`) and encoded again as
Xbox ADPCM in memory (`port/linux/game/custom_edition_sounds.c`); one that
cannot be decoded is silenced, and still logs when played.
- **Models of 44 nodes or more** are drawn a part's nodes at a time, given
local nodes when they have none; one whose part's vertices name more
nodes than a part holds is refused.
- **Linear bitmaps whose rows are not a multiple of 64 bytes**, and
textures larger than 4096 pixels a side, are not drawn.
- **Halo PC's HUD meters' minimum alpha** (where the Xbox's meters have an
overlays block) is dropped, and **its weapon functions "primary firing on"
and "secondary firing on"** are the Xbox's primary and secondary firing.
- **Button prompts on replaced icon sheets** show what the map's icon bitmap
has where the Xbox's buttons are.
- **Channel orders are per texture**: multipurpose maps and meters also drawn
another way keep Halo PC's channels.
- **Scripts that use what this build does not have**: a call of a function
this build lacks, or a read of an engine global it lacks, does nothing
(a constant of its type's harmless value, logged by name:
`custom_edition_scripts.c`); only one that gives a script's index refuses
the map. Maps that need OpenSauce's runtime features are refused; a map
that only carries its tags (`project_yellow`) runs without them, as on
stock Custom Edition.
- **Halo PC behaviours** some maps were made around (Chimera's map list,
`custom_edition_behaviours.inc`) are followed where this build can, and
each is logged.
- **The conversions are one-way and in memory**: nothing is written to the
map files.
### How far Custom Edition tags are from the Xbox tags
`tools/custom_edition_tag_footprints.py` compares the tags a Custom Edition
cache and the Xbox cache of the same level both keep (same group, same name),
without any tag definitions. A tag's *footprint* is the distance from its
address to the next tag's (its structure and the block data after it); its
*structure size* is estimated as the distance to the lowest address in the
footprint that the footprint points at (its first child block). Equal
footprints, level after level, suggest a group's layout is unchanged; they do
not prove it.
The Xbox caches available are the retail ones, build 01.10.12.2276, not the
01.01.14.2342 build this repository reconstructs: they stand in for it, and
differences between the two Xbox builds are not measured. Run on the 14
levels both sets have (13 multiplayer levels and `ui`):
```
python tools/custom_edition_tag_footprints.py --blocks assets/custom_edition "<Xbox 2276>/maps"
```
- **48 groups have the same footprint for every shared tag in every level**:
`Soul actv ant! bipd colo cont deca eqip flag fog font foot grhi hmt hud#
hudg itmc jpt! lens lifi ligh lsnd mach metr mgs2 mply part pctl phys pphy
scen schi senv sgla sky smet snde soso spla ssce str# swat trak udlg unhi
vcky vehi wind`. The shader groups among them have the same layout, but
not the same type values (above).
- **Groups whose footprints differ, but whose structures match**: UI widgets
(`DeLa`, 464 of 657 footprints equal, 423 of 423 structures), animations
(`antr`, 213/239), collision models (`coll`, 331/345), effects (`effe`,
4140/4166), projectiles (`proj`, 197/210), weapon HUDs (`wphi`, 196/222),
weapons (`weap`, 39/182, structures 182/182), globals (`matg`, 1/14,
structures 14/14) and scenarios (`scnr`, 0/14, structures 13/14), plus, in
`ui` only, bitmaps, sounds and string lists. These differ in their block
data. The flamethrower's tags account for every `effe`, `proj` and `wphi`
difference and all but one `coll` difference; 11 of the 14 weapons (every
player weapon and both vehicle guns) differ in every level.
- **What differs in those blocks** (`--blocks`: the top-level blocks of each
differing tag, found without definitions and compared): almost always
the number of elements, or whether a block has any, which is content:
weapons 143 count and 26 presence differences, UI widgets 158 and 63,
scenarios 36 and 13, projectiles 13, weapon HUDs 78 presence, collision
models 26 count. Every weapon difference checked in Blood
Gulch lies in a predicted-resources list (8-byte elements, weapon
offset `0x4E4`; OpenSauce `weapon_definitions.hpp`), or, for the
flamethrower, also in its attachments; the globals differ in their
cheat powerups (16 against 14). The rest (flamethrower effects 26,
globals 13, animations 39, one collision model, and 3 in `ui`) keep their element counts but
span a different amount of data up to the next block, which this method
cannot split into element size and nested content: for the globals, the
block in question (`player_info`, 0xF4-byte elements without blocks of
their own) spans more than its elements, so other data sits in the span.
No difference found has to be a layout change, and the two maps run did
not show one.
- **Groups only Custom Edition has**: `mod2` (models), `scex` (a transparent
shader type), `devc` and `tagc`, in all 14 levels; **only the Xbox has**:
`mode` (models) and `sotr` (a transparent shader type), besides the groups
Custom Edition keeps in resource maps.
+761
View File
@@ -0,0 +1,761 @@
# The dedicated server: reference
`halo --server` runs the game as a host with no window, sound or player of
its own: other machines join it on the LAN, by its invite link or from the
server browser, as they join any host. `port/linux/README.md`, "Dedicated
server", says how to run one; this document is the reference for what it
takes and answers: the command line, the exit codes, the commands, the
HTTP API, the remote console (RCON) over WebSocket, the events, the log and
the files. `docs/master_protocol.md` is the master servers' protocol, which
the server publishes to and the server browser reads.
The code is `port/linux/src/server_*.c` (the platform side: the mode, the
standard input, the log, the rotation's grammar, the HTTP and WebSocket
server, the master servers' publisher) and `port/linux/game/server_*.c`
(the game side: the director that plays one game after another, the
commands, the gametypes, the status). The Android and web builds have no
dedicated server: they compile those files to nothing, but for
`server_json.c` (the JSON reader, which the server browser's list of master
servers uses in every build) and `server_rotation.c` (the rotation's
grammar and the gametype rules), which every build compiles.
## Panels and fleets
A game panel (Pterodactyl, PufferPanel, MCSManager: `deploy/`) runs one
server and reads its standard output; `deploy/systemd` and `deploy/docker`
run servers on one machine. For many servers on many machines, `opence-fleet`
([github.com/OpenCommunityEdition/opence-fleet](https://github.com/OpenCommunityEdition/opence-fleet))
is one control plane over them all: an agent on each machine runs the
servers and drives each through this document's API, as the owner from
the machine itself (`server.web_owner_password`, below). What it relies on
is named in the status's and the RCON's `capabilities`: `owner_web` (the
owner's role over the web, from loopback), `ban_address` (`sv_ban_address`,
`POST /api/bans`), `stop` (`sv_stop`, `POST /api/stop`, the `drain` event),
`event_addresses` (addresses in the player events) and `identity` (the
status's `pid`, `instance`...).
## Command line
| Argument | What it does |
| --- | --- |
| `--server` | Dedicated server mode (as `HALO_SERVER=1` or `server.enabled = true`) |
| `--config <path>` | The settings file; default `config.toml` beside the executable. `HALO_CONFIG_FILE`, when it is set, wins over `--config` |
| `--version` | Prints `OpenCE build-<n> (network version <v>)` and exits 0 |
| `--keygen` | Writes a new publisher key (`server.publisher_key_file`) unless one exists, prints its public key and key id, exits 0; exits 3 when the key cannot be made, read or written |
| `--sign <text>` | Prints the publisher key's Ed25519 signature of the text, in hex (an application's proof of key), exits 0; exits 3 without a text or a usable key |
| `--probe [<invite>]` | A look at a server, for a list site that shows its state: joins nothing and never starts a server (whatever `--server`, `HALO_SERVER` or `server.enabled` say). It finds the invite's game over its tunnel (without an invite, the LAN's first) and prints it as one line of JSON, the game of the invite's host alone (never another game that answers), and exits 0; when nothing answers in 30 seconds it prints `{"ok":false,"error":"no game answered in 30 seconds"}` and exits 1. Internet play is as set (`network.online`, on by default), which an invite needs |
| `--help` | Prints these, exits 0 |
`--version`, `--help`, `--keygen`, `--sign` and `--probe` write no settings: the
settings file is read, never completed or made. `--probe` wins over
`--server`, `HALO_SERVER` and `server.enabled`: a probe never writes to
`config.toml`.
A probe's line:
```json
{"ok":true,"name":"OpenCE Server","map":"bloodgulch","gametype":"ctf","players":5,"max_players":16,
"teams":true,"in_progress":true,"open":true,"dedicated":true,"network_version":38,"kind":0}
```
`gametype` is the game engine's name (`ctf`, `slayer`, `oddball`, `king`,
`race`, or `none`), `kind` the OpenCE gametype's kind the host sends (0 for
none), `open` whether it takes players now.
The release has a `halo-server` script beside `halo`, which runs
`halo --server` with its arguments.
## Exit codes and signals
| Code | Meaning |
| --- | --- |
| 0 | Stopped: `quit`, `sv_stop` (now, or at the end of its drain), SIGTERM or SIGINT |
| 1 | A fatal error in the game (a halt, a map that failed to load); the last error lines were logged. The process exits within 5 seconds of the error, even when what runs at the exit (a hook, a listing's withdrawal) does not return |
| 2 | No usable game data: `maps/ui.map`, or every rotation map, missing or not a cache file; nothing was bound |
| 3 | Bad server configuration: no rotation entry that can be played (and `server.rotation_fallback` off), `server.web_port` past 65535, a web port the server may not bind (a port below 1024 without the right to it) or whose `server.web_bind` is an address the machine does not have, or a web password (admin, moderator or viewer) shorter than 8 characters with the web port bound beyond loopback (on loopback, a warning), or the owner's (`server.web_owner_password`) shorter than 8 wherever it is bound; `--keygen` or `--sign` failing. An entry that cannot be played (a bad gametype file among them) is left out with a warning: the server stops only when none is left |
| 4 | A port in use: the tunnel's UDP port, or the web port (another program listens on it) |
| 5 | Restart asked for (`sv_restart`, now or at the end of its drain, or an update installed: `server.auto_update = "install"` installs a new release once the server has been empty for 10 minutes, on a thread of its own: the server goes on meanwhile, and stops with 5 once it is empty again (a player who joined during the install plays on). An install that fails is tried again at the next check, 6 hours on) |
| 128 + n | Stopped by signal n: a second SIGTERM or SIGINT, or the shutdown not done 5 seconds after the first |
SIGTERM and SIGINT stop the server: a game in progress goes to its
postgame, every machine is told, the listings are withdrawn, and it exits 0.
SIGHUP reloads the server's settings, the rotation and the gametype files
(`sv_reload`). SIGUSR1 prints the status on standard output. SIGPIPE and
SIGTTIN are ignored.
## Standard output
One line per event, for the operator and the panel:
```
2026-10-19T18:03:11Z INFO server: ready (tunnel 2302/udp, web 127.0.0.1:8080, listing public)
2026-10-19T18:03:40Z INFO player: Milenko joined (slot 0, machine 1)
2026-10-19T18:04:02Z INFO game: started: CTF on bloodgulch, 2 players
```
The fields are the time (UTC), the level (`INFO`, `WARN`, `ERROR`), the
category (`server`, `lobby`, `game`, `player`, `chat`, `moderation`, `net`,
`master`, `console`, `health`) and the text. `server: ready` is printed once,
when the lobby is open and, for a public server, its listing has been
published (or 10 seconds on, as the brokers are other people's): it is the
line a panel waits for. With `server.log_format = "json"` each line is an
object instead, and the line a panel waits for holds `"msg":"ready (`:
```json
{"time":"2026-10-19T18:03:11Z","level":"info","cat":"server","msg":"ready (tunnel 2302/udp, ...)"}
```
Every line is also kept in the log the web's viewers read (`/api/log`,
`sv_log`, the RCON's `log` events), but for these, which go to standard
output alone: the web password a first start makes, the invite (a
private server's lets anyone in; `/api/status` gives it to admins), and
the owner's console, the commands typed on standard input or read from the
init file and what they answered (a password's command is shown as `(a
password's command)` there too).
A health line comes once a minute: `health: players=3 frame_ms_max=4.1
frame_ms_avg=1.20 cpu=2.1% rss=96MB` (the longest and the mean frame of
the last minute, in milliseconds). Standard error keeps the platform
layer's `halo-linux:` lines, and `debug.txt` everything, as for a player's
game.
## Commands
The same commands come from standard input, `server.init_file` (run once at
the start, a command a line, `#`, `;` and `//` starting comments), the telnet
console (`debug.telnet_console`, loopback), the RCON, `POST /api/command`
and the web page. Each runs on the game's thread between frames. Every
command has a role; a caller may run it if their role is that one or above:
**viewer** < **moderator** < **admin** < **owner** (standard input, the
init file and the telnet console are the owner's: whoever holds the
process; so is a sign-in to the web or the RCON with
`server.web_owner_password` from the machine itself: "Signing in"). When
standard input ends and `server.console_exit_on_eof` is on, its last line
runs, whether or not a line's end ended it, and then the server stops.
A `<player>` is a slot number (`sv_players` lists them) or the start of a
name; a name that begins more than one player's is refused. A `<duration>`
is a number and a unit (`45s`, `30m`, `2h`, `7d`, `1w`), a bare number of
minutes, or `forever`; ten years at most. A word in quotes (`"two words"`)
is one word.
A command's text (a notice, a reason, `sv_set`'s value, a name) is kept to
printable ASCII: control characters (a line's end, a tab) and other bytes
outside it are dropped, as are the quotes around the whole text, and
standard input's and the init file's lines are cleaned so before they run.
The web is stricter where it takes a value as it is: `PUT /api/settings`
refuses a value with a control character (`schema`), and nothing is changed
for that setting. A command's reply is sent whole, however many lines.
Commands that are not the server's (the game's own console commands, the
scripts' globals and functions) need the owner role: from the web or the
RCON they are refused (`not a command of the server's`) but to the owner's
session. The server's
commands come first, so `kick` and `ban` are always the server's below, not
the game's console commands of those names.
| Command | Role | What it does |
| --- | --- | --- |
| `help [command]` | viewer | The commands, or one command's use |
| `sv_status [--json]` | viewer | The server's state, map, gametype, players, uptime, games played, network version, listing and next game |
| `sv_players [--json]` | viewer | Each player's slot, name, team, score, kills, deaths, assists, ping and machine; with the owner role or `server.log_addresses`, their address and hardware id |
| `sv_rotation` (`sv_mapcycle`), `sv_maps`, `sv_gametypes`, `sv_version`, `sv_log [n]` | viewer | Lists: the rotation, the maps there are, the gametypes, the build, the log's last n lines |
| `sv_bans` (`sv_banlist`), `sv_settings` | admin | Lists: the bans of `bans.txt`, the settings `sv_set` changes |
| `sv_masters`, `sv_publisher` | viewer | The master servers and how each answered; the publisher key's id and public key (never its seed) |
| `say <text>` (`sv_say`) | moderator | A notice on every machine's screen |
| `tell <player> <text>` | moderator | A notice on one player's machine |
| `kick <player>` (`sv_kick`) | moderator | Drops the player's machine; it may join again |
| `ban <player> [<duration>] [<reason>]` (`sv_ban`) | moderator (a day at most), admin | Drops the player's machine and bans its address and hardware id, for the duration (else for ever). A moderator must give a duration of a day or less: a ban for ever, or longer, is refused |
| `unban <number\|address\|hardware id>` (`sv_unban`) | admin | Takes a ban out of `bans.txt`; the machine may join again at once, by the LAN or the internet |
| `sv_ban_address <address\|hardware id> [<duration>] [<reason>]` | admin | Bans an IPv4 address (`a.b.c.d`) or a hardware id (up to 32 hex digits), its machine here or not: its line in `bans.txt` as `ban` writes it (the other part `none`), so that `sv_bans`, `unban` and `GET /api/bans` see it, and a machine present of that address or hardware id is dropped as `ban` drops it (`player_left`'s reason `banned`). Answers `ban <n> added` (`n`: its number in `sv_bans`) |
| `mute <player> [<minutes>]`, `unmute <player>` | moderator | The host's mute of a player's chat and voice for everyone |
| `sv_map <map> [<gametype>]`, `sv_map <entry>` | moderator | Plays that game (or a rotation entry, `map:gametype+rule=value`; a co-op one too), below |
| `sv_map_next` (`sv_next`) | moderator | Ends the game without its scores and plays the rotation's next |
| `sv_end_game` | moderator | Ends the game: its scores, then the next |
| `sv_rotation_set <entries>`, `sv_rotation_add <entry>`, `sv_rotation_del <n>`, `sv_rotation_begin [<n>]` (`sv_mapcycle_add`, `_del`, `_begin`) | admin | Changes the rotation (written to `server.rotation`, 4096 characters at most) |
| `sv_set <setting> <value>` | admin | Changes one of the server's settings, now where it can be, and in `config.toml` (below); a setting its environment variable sets is refused. A value is 256 characters at most |
| `sv_name <name>`, `sv_maxplayers <n>`, `sv_password [<password>]`, `sv_public <0\|1>`, `sv_motd <text>` | admin | `sv_set` of `server.name`, `server.max_players`, `server.password`, `server.public`, `server.motd` |
| `sv_reload` | admin | Reads `config.toml`'s server settings, the rotation and the gametype files again (as SIGHUP) |
| `sv_stop [now\|game\|empty] [<notice>]`, `sv_stop cancel` | owner | Stops the server (exit code 0): `now` (the default) as `quit` does; `game` once the game being played is over, `empty` once nobody plays (a drain, below); `cancel` the drain waiting. The notice is said to every machine first |
| `sv_restart [now\|game\|empty] [<notice>]`, `sv_restart cancel` | owner | As `sv_stop`, with exit code 5 (restart me) |
| `quit` (`exit`) | owner | Stops the server (exit code 0) |
`sv_map`'s game is played as soon as it can be, and its answer says when:
`next: <entry> (now)` in the lobby before its start was sent (the lobby
takes the entry, and the rotation's entry it replaced is played after it),
`(now: this game ends)` while a game is played (it ends at once, without
its scores), and `(once this game is over)` once the lobby's start was sent
or while a map loads (it is the next game). `sv_map` of the game being
played plays it again. The rotation goes on after it.
A drain (`sv_stop` or `sv_restart` with `game` or `empty`, `POST
/api/stop`) is a stop that waits: the listings are withdrawn as `sv_public
0` withdraws them (a new invite, the masters told), without the setting
changing; new machines are refused as while a map loads (the game is not
open: the LAN's search shows it closed, a join by the invite is told the
game is not open); then the server stops. With `game` it stops once no
game is played nor about to be: at once in the lobby with nobody in it, or
with no countdown running (fewer players than `server.min_players`);
otherwise once the game played (or about to start) is over and its
postgame shown. With `empty` it stops once nobody is on the server (at once
if nobody is now). `sv_stop cancel` (or `sv_restart cancel`) ends the
drain: the server is listed again as `server.public` says and takes new
players. A `drain` event says each (`after`: `now`, `game`, `empty` or
`cancel`), and the stop that ends a drain has the `shutdown` reason
`drained`. `/api/status`'s `drain` is the drain waiting, or `null`.
A co-op entry (`<level>:coop`) is ended as a game is: by `sv_end_game`,
`sv_map_next`, `sv_map`, `server.empty_seconds` and `server.idle_minutes`.
It has no scores: the server goes back to the lobby at once (no postgame),
and a level won ends it with `game_ended`'s reason `score`.
`sv_set` (and the settings of the web page, `PUT /api/settings`) changes
these settings, which `sv_settings` and `GET /api/settings` list:
`server.name`, `server.max_players`, `server.min_players`,
`server.password`, `server.public`, `server.rotation_fallback`,
`server.lobby_seconds`, `server.postgame_seconds`, `server.empty_seconds`,
`server.idle_minutes`, `server.team_start`, `server.motd`,
`server.frame_rate`, `server.web_password`,
`server.web_moderator_password`, `server.log_format`,
`server.log_addresses`, `server.status_file`, `server.auto_update`,
`server.coop_difficulty`, `server.region`, `server.master_publish`,
`server.chat_filter`, `server.chat_filter_slurs`, `server.chat_filter_swearing`,
`server.chat_filter_insults`,
`server.master_publish_address`, `server.metrics`, and `network.text_chat`,
`network.voice_lobby`, `network.voice_mode`, `network.voice_kbps`,
`network.votekick`, `network.votekick_minutes`,
`network.votekick_ban_minutes`, `network.map_share`,
`network.map_share_upload`, `network.public_address`,
`network.coop_friendly_fire`, `network.coop_player_collisions`,
`network.coop_ally_friendly_fire` and `network.shield_effects`. Of them,
`server.status_file`, `server.auto_update` and `server.log_addresses` need
the owner role: from the web's admin, `sv_set` of one of them (`POST
/api/command`) answers `403 forbidden`, and `PUT /api/settings` leaves it
out, in its `errors` with the code `forbidden` and the message `<setting>
needs the owner role (the server's console)`; the owner's session changes
them both ways. A web password is empty or
8 characters or more. A value outside a setting's choices or range is
refused (`400 failed` from `sv_set`, `schema` in `PUT /api/settings`'s
`errors`), with `<setting> takes a whole number from <min> to <max>` (or
`from <min>`) or `<setting> is one of <a>|<b>|<c>`; a choice may be given
in either case, and is kept as the table spells it:
| Setting | Choices or range |
| --- | --- |
| `server.max_players` | 2 to 127 |
| `server.min_players` | 1 to 16 |
| `server.lobby_seconds`, `server.postgame_seconds` | 0 to 300 |
| `server.empty_seconds`, `server.idle_minutes` | 0 or more |
| `server.frame_rate` | 30 to 120 |
| `server.team_start` | `one`, `both`, `skip` |
| `server.log_format` | `text`, `json` |
| `server.auto_update` | `off`, `check`, `install` |
| `server.coop_difficulty` | `easy`, `normal`, `hard`, `impossible` |
| `network.text_chat` | `on`, `links`, `off` |
| `server.chat_filter` | `off`, `censor`, `block` |
| `server.chat_filter_slurs`, `server.chat_filter_swearing`, `server.chat_filter_insults` | `off`, `severe`, `strong`, `mild` |
| `network.voice_mode` | `off`, `team_proximity`, `team_enemy_proximity`, `team_global`, `team_global_enemy_proximity` |
| `network.voice_kbps` | 8 to 64 |
| `network.votekick_minutes` | 0 to 60 |
| `network.votekick_ban_minutes` | 1 to 1440 |
| `network.map_share_upload` | 16 to 20480 |
| `network.coop_friendly_fire` | `off`, `on`, `shields_only`, `explosives_only` |
| `network.shield_effects` | `stock`, `glow`, `light_show`, `overshield`, `glow_overshield`, `light_show_overshield`, `players` |
A dedicated server may also keep words out of its chat and its players'
names (`server.chat_filter`, off by default): the words of streamer mode's
list, by group, each group from the least severity its setting names
(`server.chat_filter_slurs`, `mild` by default: every slur;
`server.chat_filter_swearing` and `server.chat_filter_insults`, `off`).
`censor` shows a line's words as stars to everyone; `block` refuses the
line (its sender is told "Your line was not sent: it has words this server
does not allow") and refuses at the door a machine whose player's name has
them, which an updated game shows as "The host refused: your player name"
(an older one says the game is closed). Each is logged (`chat filter: …`,
`refusing a machine … its name has words the server's filter keeps out`).
Links are `network.text_chat`'s, refused by default. A list of words is
not a moderator: spaced letters and look-alikes get past it.
A dedicated server's text chat is its `network.text_chat`'s, listed or
not: `on` by default (lines with links refused), `links`, or `off`, live
(`sv_set network.text_chat off`). A game a player hosts has no text chat
while it is listed in the server browser; a dedicated server is the
exception, and the players who join it from the browser's dedicated servers
have the chat as it says, as long as a master server lists it (the
masters' word that it is an approved operator's; a listing's own flag is
not enough, so a dedicated server listed by the brokers alone, before its
operator is approved, has none for them).
The settings `sv_set` does not change (`server.web_viewer_password`,
`server.web_owner_password`, `server.web_port`, `server.web_bind`, `server.web_trusted_proxy`, the
paths, the master servers' files...) are changed in `config.toml` or their
environment variables: `sv_reload` reads the file's `server.*` settings
again, and the web port and its address are bound once, at the start. The
rotation is changed by the `sv_rotation_*` commands.
## HTTP
The server listens on `server.web_port` (8080; 0 for none) at
`server.web_bind` (`127.0.0.1`, an IPv4 address). Bound beyond loopback,
every web password that is set (admin, moderator, viewer) must have 8
characters or more, else the server exits with code 3; the owner's
(`server.web_owner_password`) must have 8 or more wherever the port is
bound. The page (`/`), the API
(`/api/...`), the RCON (`/ws`), `/healthz` and `/metrics` share the port.
Requests are HTTP/1.1 (or 1.0), `GET`, `HEAD`, `POST`, `PUT` or `DELETE`
(`HEAD` is answered as `GET` is, without the body, and counts as a read,
as a `GET`; `/api/login` takes `POST` alone), a head of 8 KB at most whose
lines end with CRLF (a bare LF, a CR inside a line, or more after the empty
line that ends it: `400 bad_request`) and a body of 64 KB at most, sent within 10 seconds of the
connection's opening or of its last answer (a connection that sends
nothing is closed then too). `Transfer-Encoding` and `Expect` are refused.
24 connections are taken at once; when they are all taken, the oldest that
waits for a request (no RCON, no request being answered) is closed for a
new one. A client that does not read its answers is read no further once
256 KB of them wait, and is closed past 2 MB.
Every request but `/healthz` counts against its address's allowance, the
page's files, `/metrics`, a sign-in and the RCON's upgrades (`/ws`) too: 60
a minute, or 600 with a session (429 `rate_limited` past it, with
`Retry-After: 60`). Behind `server.web_trusted_proxy`, a request's address
is the last entry of its `X-Forwarded-For` (the one the proxy appended: an
IPv4 address, `a.b.c.d` or `a.b.c.d:port`, or an IPv6 one, `v6`, `[v6]` or
`[v6]:port`), and its `X-Forwarded-Host` and `X-Forwarded-Proto` are
believed, whether or not it sends `X-Forwarded-For`; from any other
address they are ignored. IPv6 clients are counted by their /64: one
host's addresses share its allowance and its lockouts. Every answer is JSON
(`Content-Type: application/json`) unless said, with
`Cache-Control: no-store`, `X-Content-Type-Options: nosniff`,
`Referrer-Policy: same-origin` (under `no-referrer` a browser would send
the page's own changes with `Origin: null`) and
`Content-Security-Policy: default-src 'self'; frame-ancestors 'none'`.
### Signing in
`POST /api/login` with `{"password":"…"}` answers
`{"ok":true,"token":"<64 hex>","role":"admin","csrf":"<32 hex>","expires":<unix seconds>}`
and sets the cookie `opence_session=<token>; HttpOnly; SameSite=Strict;
Path=/`, with `Secure` when the request came through
`server.web_trusted_proxy` with `X-Forwarded-Proto: https` (never from
another address, whatever it says). The admin password
(`server.web_password`) gives the admin role, the moderator password
(`server.web_moderator_password`) the moderator role, the viewer password
(`server.web_viewer_password`) the viewer role; `{"by":"…"}` beside the
password names who signed in, in the log. A session lasts 12 hours, or 30
minutes without a request (the RCON's messages do not count: a page left
open asks `GET /api/session` now and then); 16 are kept at most, of every
role.
The owner's password (`server.web_owner_password`, empty by default: none)
gives the owner role, for a supervisor on the server's own machine (a
fleet's agent, a panel's daemon): what standard input may do, over the
API and the RCON (`quit`, `sv_stop`, `sv_restart`, `POST /api/stop`, the
owner's settings by `sv_set` and `PUT /api/settings`, the game's own
console commands by `POST /api/command`, the players' addresses and
hardware ids in `/api/players` and the events). It signs in only when the
connection's own peer address is in `127.0.0.0/8` and is not the trusted
proxy's: never by `X-Forwarded-For`, and never through
`server.web_trusted_proxy`, even one on the machine itself. From any other
address the right owner's password is refused with `403 forbidden` (the
RCON's `auth`: `{"code":"forbidden"}`, and the connection closes 1008),
and counts toward the address's lockout as a wrong password does. An
owner's session is used from the machine itself alone, as it was begun
(from elsewhere its token is no session: `401`, the RCON's `auth` with it
`denied`). A password that is also another role's gives the owner role
from the machine itself and the other role from elsewhere: give the owner
a password of its own.
A script passes `Authorization: Bearer <token>`. The page passes the
cookie, and on every request that changes something the header
`X-CSRF-Token: <csrf>`; the `Origin` of such a request (and of the
WebSocket's) must be the server's own. An `Origin: null` is taken only
when the browser's `Sec-Fetch-Site` says `same-origin`.
`POST /api/logout` ends the session (with the `X-CSRF-Token` by the
cookie; `GET` answers 405). `GET /api/session` answers
`{"ok":true,"role":…,"csrf":…,"expires":…}`, or 401.
When `server.web_password`, `server.web_moderator_password`,
`server.web_viewer_password` or `server.web_owner_password` changes
(`sv_set` and the settings change the first two; `sv_reload` reads any of
them from `config.toml`), the sessions of its role end and that role's RCON
connections close (1008, `the password changed`).
Five wrong passwords from an address (an IPv6 one's /64) in 10 minutes
lock it out for 10 minutes (429), and no more than 30 passwords a minute
are checked in all. `/api/login` takes `POST` alone (any other method:
405).
### Errors
```json
{"ok":false,"error":{"code":"forbidden","message":"needs the admin role"}}
```
| Status | Codes |
| --- | --- |
| 400 | `bad_request`, `schema` (a field missing or out of range), `failed` (the command ran and failed: its lines in `lines`), `rejected` (rotation entries: `rejected[]`), `invalid` (a gametype file: `line`) |
| 401 | `denied` (no session, or a wrong password) |
| 403 | `forbidden` (the role; the owner's password from beyond the machine), `csrf`, `origin` |
| 404 | `not_found` (also a command that is not one, a player or a ban that is not there) |
| 405 | `method` |
| 409 | `pinned` (a setting its environment variable sets), `changed` (a player's action whose slot holds another player now: the current name in `name`, nothing done) |
| 413 | `too_large` |
| 426 | `bad_request` (the WebSocket's version is not 13) |
| 429 | `rate_limited`, `locked` (with `Retry-After`) |
| 500 | `internal` (out of memory, a file that cannot be written) |
| 503 | `loading` (the game did not answer in 20 seconds, as while a map loads; with `Retry-After: 5`), `starting`, `busy` (8 RCON connections already; with `Retry-After: 5`) |
### Endpoints
| Method, path | Role | Body → answer |
| --- | --- | --- |
| `GET /healthz` | none | `200 {"state":"lobby"}` (or `loading`, `playing`, `postgame`) while the server hosts and its frames run; `503` with `{"state":"starting"}`, `{"state":"stalled"}` (no frame for a minute) or the other state otherwise |
| `GET /metrics` | none (`server.metrics`) | Prometheus text |
| `GET /api/status` | viewer | The status (below) |
| `GET /api/players` | viewer | `{"ok":true,"players":[<player>…]}` (with `address` and `hwid` for the owner, or every role with `server.log_addresses`) |
| `POST /api/players/{slot}/kick` | moderator | `{"name":"<the player's name, checked>"}` → `{"ok":true,"lines":[…]}`, or `409 changed` when the slot holds another player |
| `POST /api/players/{slot}/ban` | moderator | `{"name":…,"duration":"1d","reason":"…"}` (no duration: for ever, which a moderator's ban cannot be: `400 failed`) |
| `POST /api/players/{slot}/mute` | moderator | `{"name":…,"minutes":10}` |
| `POST /api/players/{slot}/unmute` | moderator | `{"name":…}` |
| `POST /api/players/{slot}/tell` | moderator | `{"name":…,"text":"…"}` |
| `POST /api/say` | moderator | `{"text":"…"}` |
| `GET /api/rotation` | viewer | `{"ok":true,"index":0,"entries":[<entry>…],"next":<entry>}` |
| `PUT /api/rotation` | admin | `{"entries":["bloodgulch:ctf",…]}` → the rotation, or `400 rejected` with `"error":{…,"rejected":[{"index":1,"entry":"…","reason":"…"}]}` and nothing changed |
| `POST /api/rotation/next` | moderator | `{}`: `sv_map_next` |
| `POST /api/rotation/play` | moderator | `{"map":"…","gametype":"…"}`: `sv_map` (a map or gametype that cannot be played: `400 failed`) |
| `GET /api/gametypes` | viewer | `{"ok":true,"builtin":["slayer",…],"schema":{"fields":[<field>…]},"files":[{"name":…,"base":…,"description":…,"error":null}]}` (`builtin`: the Xbox's gametypes, then OpenCE's; a file that cannot be read has `base` and `description` empty and `error` saying why) |
| `GET /api/gametypes/{name}` | viewer | `{"ok":true,"name":…,"text":"<the TOML>"}` |
| `PUT /api/gametypes/{name}` | admin | `{"text":"<TOML>"}` → `{"ok":true}`, or `400 invalid` with `line` and `message` (a name is `a`–`z`, `0`–`9`, `_` and `-`, 32 at most) |
| `DELETE /api/gametypes/{name}` | admin | `{"ok":true}` |
| `GET /api/maps` | viewer | `{"ok":true,"maps":[{"name":"bloodgulch","level":"levels\\test\\bloodgulch\\bloodgulch","kind":"xbox","title":"Blood Gulch"}…]}` (`kind`: `xbox` or `custom`; `title` is the map's display name, else its `name`) |
| `GET /api/bans` | admin | `{"ok":true,"bans":[{"n":0,"when":"2026-10-19 18:00:00","ip":"…","hwid":"…","players":"…","reason":"…","until":<unix>\|null}…]}` |
| `POST /api/bans` | admin | `{"ip":"203.0.113.5","hwid":"…","names":"…","duration":"1d","reason":"…"}` (`ip` or `hwid`, or both; the rest may be left out: no duration is for ever) → `{"ok":true,"n":<the new ban's number in GET /api/bans>}`: `sv_ban_address`, a machine present of the address or the hardware id dropped; `400 schema` for an address that is not IPv4, a hardware id that is not hex, a duration that is not one |
| `DELETE /api/bans/{n}` | admin | `{"ok":true}` |
| `GET /api/settings` | admin | `{"ok":true,"settings":[<setting>…]}` |
| `PUT /api/settings` | admin | `{"server.name":"…","server.max_players":16,"server.public":true,…}` (a value as JSON, a boolean, a number or a string, or its text: `"true"`, `"16"`; `null` empties it) → `{"ok":true,"applied":["server.name"],"errors":[{"name":…,"code":…,"message":…}]}`; each setting is changed or refused on its own |
| `POST /api/command` | the command's | `{"line":"sv_status"}` (511 characters at most) → `{"ok":true,"lines":["…"]}`, or a refusal (`failed`, `forbidden`, `not_found`, `pinned`) with the command's `lines` |
| `POST /api/stop` | owner | `{"after":"now\|game\|empty","notice":"…","restart":false}` (`notice` and `restart` may be left out) or `{"cancel":true}` → `{"ok":true,"lines":["…"]}`: `sv_stop` (`sv_restart` with `restart`) `after notice`, or `sv_stop cancel` (`400 failed` with no drain waiting); `400 schema` for another `after` |
| `GET /api/log?after=<n>&limit=<m>` | viewer | `{"ok":true,"lines":[{"n":12,"time":<ms>,"level":"info","cat":"game","text":"…"}…],"next":12,"missed":0}` (`limit` 1 to 1024, 256 by default; the chat's lines for admins only) |
| `GET /api/events?after=<seq>` | viewer | `{"ok":true,"events":[<event>…],"next":<seq>,"missed":0}` (`player_joined` and `player_left` with `address` and `hwid` for the owner, or every role with `server.log_addresses`) |
| `GET /ws` | | The RCON (below) |
| `GET /`, `/app.js`, `/app.css`, `/icon.svg` | none | The page (`server.web_ui`) |
`next` is the `after` of the next request: the last line's `n` (or event's
`seq`) read. An `after` past the latest (as after a restart) reads from the
start. `missed` counts lines or events that left the ring (2048 lines, 512
events) before they were asked for.
### The status
```json
{"ok":true,"time":1760900000123,"name":"OpenCE Server","state":"playing",
"build":"build-206","network_version":38,"uptime":3600,"games_hosted":4,
"map":"bloodgulch","level":"levels\\test\\bloodgulch\\bloodgulch","gametype":"ctf","variant":"CTF","teams":true,
"players":5,"max_players":16,"machines":5,"countdown":-1,"game_seconds":312,"team_scores":[2,1],
"listing":{"online":true,"public":true,"password":false,"invite":"halo://join/…"},
"rotation":{"index":0,"count":3,"next":{"text":"sidewinder:team_slayer","map":"sidewinder","gametype":"team_slayer"}},
"frame_ms_max":4.1,"frame_ms_avg":1.2,"tick_ms_avg":1.2,
"masters":[{"url":"https://master1.example.org","listed":true,"probe":"ok","code":"","message":"","updated":1760899990}],
"update":{"available":false,"build":""},
"drain":null,
"pid":4021,"started":1760896400,"instance":"eu-1","region":"eu","tunnel_port":2302,
"publisher_key_id":"3f9a2c7d1e5b8a04","web_port":8080,
"capabilities":["events","status","rotation","settings","bans","owner_web","ban_address","stop","event_addresses","identity"]}
```
`state` is `waiting` (the menus settling), `hosting` (making the game),
`lobby`, `loading`, `playing`, `postgame` or `stopping`. `countdown` is the
lobby's seconds left, `-1` when it is not counting. `team_scores` is `null`
in a game without teams. `frame_ms_max` and `frame_ms_avg` are the longest
and the mean frame of the last minute (`tick_ms_avg` is the mean again, for
older readers). `invite` is given to admins only (empty for others). An entry of the rotation is
`{"text":"sidewinder:team_slayer+score=50","map":"sidewinder","gametype":"team_slayer","rules":{"score":50},"rejected":null}`
(`rejected` is why it cannot be played, else `null`). `drain` is the
drain waiting (`sv_stop game`...), `{"after":"game","restart":false,"since":<unix
seconds>}`, else `null`. `pid` is the server's process, `started` when it
started (seconds since 1970), `instance` the name the master servers know
it by (`server.instance`, else the slug of `server.name` they are given),
`region` `server.region`, `tunnel_port` the tunnel's UDP port
(`network.tunnel_port`; 0 when `network.online` is off), `publisher_key_id`
the publisher key's id (16 hex digits; empty without a key: `sv_publisher`)
and `web_port` `server.web_port`. `capabilities` is the RCON's hello's. The rotation's
`index` (in the status and `GET /api/rotation`) is the entry being played,
`-1` while an `sv_map` game is played or before the first lobby; `next` is
the entry after the one played last (the first while none has been).
A player is
`{"slot":0,"name":"Milenko","team":0,"score":3,"kills":3,"deaths":1,"assists":0,"ping":42,"machine":1,"joined":1760899000,"muted":false}`;
with the owner role, or `server.log_addresses`, also
`"address":"203.0.113.5","hwid":"…"`. `team` is 0 (red), 1 (blue) or -1
(no teams); `ping` is in milliseconds, `null` when it is not known yet.
`client` is the player's machine's build as its game tells it (shortly
after it joins), for every role:
`{"build":282,"flavor":"release","version":"OpenCE Linux | build 282 (release)","repository":"OpenCommunityEdition/OpenCE","mods":["trollface 1.0"]}`
(`build` 0 for a build CI did not number, `repository` empty when the build
does not know it, `mods` the mods it plays in their load order, each its
folder and its version), or `null` from a game that does not tell it. It is
what the player's game says, which a changed build can make anything.
A setting is
`{"name":"server.name","type":"string","value":"OpenCE Server","default":"OpenCE Server","comment":"…","env":"HALO_SERVER_NAME","pinned":false,"secret":false,"choices":null,"min":null,"max":null}`;
`type` is `boolean`, `integer`, `real` or `string`, and `value` and
`default` are of that type in JSON (`true`, `16`, `"OpenCE Server"`); a
reader should also take their text (`"true"`, `"16"`), which older servers
sent. `choices` lists a word setting's words, `min` and `max` bound a
number's, each `null` where there is none (the table above). `env` is
`null` for a setting no environment variable sets. A secret one (the
passwords) has `value` and `default` empty and `"set":true` when it is
set. `pinned` is a setting
its environment variable (`env`) sets: it cannot be changed.
A gametype schema field is
`{"key":"score_to_win","alias":"score","table":"universal","engine":"all","type":"integer","min":0,"max":999,"choices":null,"default":null,"description":"…"}`
(`table`: the gametype file's table it goes in, `universal`, `options`,
`ctf`, `slayer`, `oddball`, `king` or `race`; `engine`: `all`, or the game
engine whose table it is; `type`: `integer`, `boolean` or `choice`).
## The RCON
`GET /ws` with `Upgrade: websocket` and the subprotocol `opence-rcon-1`.
Every frame is text holding one JSON object. The server speaks first:
```
→ {"type":"hello","protocol":1,"server":"OpenCE Server","build":"build-206","network_version":38,
"capabilities":["events","status","rotation","settings","bans","owner_web","ban_address","stop","event_addresses","identity"]}
← {"type":"auth","password":"…","by":"paul"} or {"type":"auth","token":"<session token>"}
→ {"type":"auth","ok":true,"role":"admin"}
→ {"type":"auth","ok":false,"error":{"code":"denied"}} and the connection closes (1008)
← {"type":"command","id":1,"line":"kick Milenko"} or {"type":"command","id":1,"command":"say","args":["two words"]}
→ {"type":"reply","id":1,"ok":true,"lines":["Milenko kicked by the host"]}
→ {"type":"reply","id":2,"ok":false,"lines":["…why…"]} (the command ran and failed)
→ {"type":"reply","id":3,"ok":false,"lines":[],"error":{"code":"busy","message":"…"}} (not run)
← {"type":"subscribe","events":["player","chat","game","moderation","status","log","master"],"after":1830}
→ {"type":"event","seq":1831,"time":1760900000123,"event":"player_joined","slot":3,"name":"Milenko",…,"replay":true}
→ {"type":"event","seq":1831,"time":…,"event":"status","status":{…the status…}} (at the subscribe, then every 2 s)
→ {"type":"event","time":…,"event":"log","n":5120,"level":"info","cat":"game","text":"…"} (no seq: n is its sequence)
← {"type":"ping"}
→ {"type":"pong"}
→ {"type":"overflow"} and the connection closes (1013): the client read too slowly
→ {"type":"error","id":…,"error":{"code":"…","message":"…"}} (an unknown type, a malformed message)
```
A connection signed in with the page's cookie (on the same origin) needs no
`auth` message: its `hello` is followed by `{"type":"auth","ok":true,…}`.
The `capabilities` say what the server has beyond the first release's
(`events`, `status`, `rotation`, `settings`, `bans`): `owner_web`,
`ban_address`, `stop`, `event_addresses` and `identity` ("Panels and
fleets"); a client checks for one before it uses it. The owner's password
signs in from the server's own machine alone ("Signing in").
A command is a `line`, or a `command` and its `args`, which the server
joins with spaces, quoting an argument that holds a space or is empty (511
characters in all at most). Ids are the client's (a number or a string),
echoed in the reply; each command gets exactly one reply. A connection
runs one command at a time: a command sent before the last one's reply is
not run. A reply whose command ran has `ok` and the command's `lines`, and
no `error`, whether it succeeded or failed; one that was not run has `ok`
false, no lines and an `error`: `denied` (not signed in), `rate_limited`
(32 commands a second, in bursts of 64), `bad_request` (no command, or too
long), `busy` (another command's reply is awaited) or `loading` (the game
did not answer in 20 seconds; the command may still have run, and is never
sent again by the server). `after` replays the events still in the ring
since that sequence number, marked `"replay":true`. A connection holds 1024
messages for the client at most; 8 connections are taken at once (a ninth
upgrade answers `503 busy`), and each upgrade counts against its address's
allowance (above). The
event groups are `player` (`player_joined`, `player_left`, `map_share`), `chat`
(`chat`), `game` (`lobby_opened`, `countdown`, `game_started`,
`game_ended`, `postgame`, `score`, `kill`), `moderation` (`kick`, `ban`,
`unban`, `mute`, `unmute`, `speed_hack`), `status` (`status`), `log`
(`log`, each log line as `{"n","level","cat","text"}`), `master`
(`master`), and `server` (`server_started`, `reloaded`, `warning`,
`drain`, `shutdown`), which every subscriber gets.
The log's lines of the `chat` category go to the `chat` group, the others
to `log`.
`status` and `log` events go to their subscribers as they come and are
not kept in the ring: `after` does not replay them (`/api/log` reads the
log's own ring). A `status` event's `seq` is the latest event's (no new
number: the same one comes again until another event), and a `log` event
has no `seq`: its `n`, the log line's number and `/api/log`'s cursor, is
its sequence. A client keeps one cursor for the events (`seq`, of the
events of the ring) and another for the log's lines (`n`); a client that
reconnects with `after` may get events it had, and drops those whose `seq`
it has seen (a `status` event is never dropped so).
## Events
Every event has `seq` (rising, never reused while the server runs; a
`status` event has the latest one's, a `log` event none: above), `time`
(milliseconds since the epoch) and `event`:
| Event | Fields |
| --- | --- |
| `server_started` | `name, build, network_version, pid, started, instance, region, tunnel_port, publisher_key_id, web_port` (as the status's), `rotation[]` (the entries' texts) |
| `lobby_opened` | `map, gametype, variant` (the gametype's name as the game shows it), `teams, index` (the rotation's entry) |
| `countdown` | `seconds` (-1: paused) |
| `game_started` | `map, gametype, variant, teams, team_scores[]` (team games), `players[]` |
| `game_ended` | `map, gametype, variant, teams, reason` (`score`, `time`, `idle`, `empty`, `command`, `next`), `duration` (seconds), `team_scores[]` (team games), `players[]` |
| `postgame` | `seconds` |
| `player_joined` | `slot, name, team, machine`; `address, hwid` for the owner, or every role with `server.log_addresses` |
| `player_left` | `slot, name, reason` (`left`; `kicked` and `banned` when the server's `kick`, `ban` or `sv_ban_address` dropped the machine, `banned` for a speed hack's drop too); `address, hwid` (as it joined) as `player_joined`'s |
| `chat` | `channel` (`everyone`, `team`, `vehicle`), `slot, name, text` (admins only; the lines the host sends on) |
| `kill` | `killer, victim` (slots; -1 for none), `weapon` (the killer's weapon's or the killing object's tag name, or null), `friendly_fire` |
| `score` | `map, gametype, variant, teams, team_scores[]` (team games), `players[]` (once a second at most, on a change) |
| `kick`, `ban`, `unban`, `mute`, `unmute` | `name, by, reason?, until?` |
| `speed_hack` | `machine, names, rate, ahead_ticks, action` (`kicked`, or `unverified`: its datagrams alone said so, and it stays) |
| `map_share` | `machine, map, state` (`asked`, `downloading` at each tenth, `downloaded` or `failed` once for each request), `progress` (0 to 100: a percentage already) |
| `warning` | `text` (a newer release, with `server.auto_update`) |
| `reloaded` | `rotation_changed, rejected[]` (`{"entry","reason"}` of each entry left out) |
| `master` | `url, listed, probe, code, message` |
| `drain` | `after` (`now`, `game`, `empty`, `cancel`), `restart` (exit code 5), `notice` (empty for none) |
| `shutdown` | `reason` (`quit`, `sv_stop`, `sv_restart`, `drained`: a drain's end, `signal <n>`, `update installed`, `nothing to play`, `debug.exit_after`), `code` (the exit code) |
Players in events are as in `/api/players` for the viewer role (no
addresses), but for `player_joined` and `player_left`, which carry the
player's address and hardware id (`""` when not known) to the owner, and to
every role when `server.log_addresses` is on (`/api/players`' rule), in the
RCON's events, their replay and `GET /api/events` alike. `variant` is the gametype's name as the game shows it (`Co-op`
in a co-op entry).
## Bans
A ban is a line of `bans.txt` (in `paths.logs`, else the data root). A
timed ban is kept out by its line's `until=` alone, and ends then. A ban
for ever, and a machine dropped for cheating, also keep the machine out
for the rest of the run by its address, its real address or its hardware
id. `sv_ban_address` and `POST /api/bans` write such a line for an address
or a hardware id with no player of it needed (the part not given is
`none`). `unban` takes the line out and readmits the machine by
the line's address or hardware id, whether it joins on the LAN or by
internet play.
## The rotation
`server.rotation` (and `sv_rotation_set`, `PUT /api/rotation`) is a list of
entries, `map[:gametype[+rule=value...]]`, separated by commas, semicolons
or new lines; a line that begins with `#` is a comment (to its line's end,
commas and all). The text is 4096 characters at most (the entries past it
are left out, with a warning), an entry 255 characters and the list 64
entries. `config.toml` keeps a string setting of up to 4096 characters, so
a rotation that long is saved whole and read back after a restart.
- The map is a multiplayer map's name (`bloodgulch`: the Xbox's
`maps/<name>.map`, else a Custom Edition map of `custom_maps`),
`custom_maps\<name>` (a Custom Edition map; `/` is taken for `\`), or a
level's path (`levels\test\bloodgulch\bloodgulch`). A campaign level is
played as co-op only: `<level>:coop`, at `server.coop_difficulty`.
- The gametype (`slayer` when there is none) is a built-in's name
(`slayer`, `team_slayer`, `slayer_pro`, `elimination`, `phantoms`,
`endurance`, `rockets`, `snipers`, `stalker`, `ctf`, `ironctf`,
`ctf_pro`, `invasion`, `oddball`, `team_oddball`, `reverse_tag`,
`accumulation`, `juggernaut`, `king`, `team_king`, `king_pro`,
`crazy_king`, `race`, `team_race`, `rally`, `team_rally`, and Halo PC's
names `ffa`, `ffa_slayer`, `teamslayer`, `tslayer`, `koth`, `team_koth`,
`iron_ctf`, `teamrace`, `teamoddball`), an OpenCE gametype's (`swat`,
`infection`, `gun_game`, `hardcore`, `hardcore_ctf`, `hardcore_oddball`,
`hardcore_king`), a gametype file's name (`gametypes/<name>.toml`), or
`file=<name>` (a gametype a player's game saved, below). A gametype's
name is letters, digits, `_`, `-` and `.`, in any case.
- A rule is a gametype rule's name or its short name (`score`, `time`,
`weapons`, `ff`...; `GET /api/gametypes`'s schema lists them), with a
value it takes; a rule of another game engine than the gametype's is
refused. 48 rules at most; a rule given twice is the last.
An entry that cannot be played (a map that is not there, a bad gametype
file, a rule out of range) is left out with a warning, at the start and at
`sv_reload`; `sv_rotation_set` and `PUT /api/rotation` change nothing when
one of their entries cannot be played.
A change of the rotation (`sv_rotation_*`, `PUT /api/rotation`,
`sv_reload`) keeps its place: the next game is the entry after the one
played last, found again in the new list (the first entry only when none
has been played yet). `sv_reload` takes the rotation again when its text
changed, and also when only its entries' rejections changed (a map or a
gametype file added or taken out since).
`server.team_start` decides how a team game starts. `one` (the default)
starts it with the players of one team. `both` waits, saying nothing,
for a player on each team. `skip`, when one player waits for a team game
that `sv_map` did not ask for, plays the rotation's next entry without
teams instead (if it has one); it is checked when the lobby opens and
again when its countdown would begin. With no entry left, the server
stops with exit code 3 (2 when no rotation map is there), or plays
`bloodgulch:slayer` with `server.rotation_fallback`.
## Gametype files
A gametype file, `gametypes/<name>.toml` (the name `a`–`z`, `0`–`9`, `_`
and `-`), is a built-in or OpenCE gametype (`base`) and rules over it:
```toml
base = "team_king" # the gametype it starts from
name = "Moving Hill" # the name the game shows: 11 characters of printable ASCII
[universal]
score_to_win = 3
[options]
time_limit = 15
friendly_fire = "off"
[king]
moving_hill = true
```
Each rule goes in its table (the schema's `table`): `[universal]`,
`[options]`, and one game engine's, `[ctf]`, `[slayer]`, `[oddball]`,
`[king]` or `[race]`. A file gives each rule its full name
(`vehicle_set`, `team_vehicles`, `score_to_win`): the short names
(`vehicles`, `score`) are the rotation's alone. A file is 64 KB at most.
`deploy/gametypes` has examples. The server reads the `gametypes` folder
beside `config.toml` if there is one, else the data root's, and writes the
web page's files to it.
A gametype a player's game saved is played with `file=<name>`: copy the
saved gametype, as `<name>.gametype`, into the same `gametypes` folder
(beside `config.toml` if there is one, else the data root's).
## The listing
A public server (`server.public`, with `network.online` and
`network.public_lobby`) is published to each master server of
`masters.txt` (`docs/master_protocol.md`). A change that a list shows (the
listing made public or private, a player joining or leaving, the name, the
map) is published within 5 seconds. Made private, the server makes a new
invite and sends the masters a closed publish, which takes it off their
lists;
`network.public_lobby = false` stops the publishing too. A master's
`Retry-After` is kept (a day at most). `key_revoked` and
`publisher_revoked` stop the publishing to that master for the run;
`unknown_key`, `publisher_pending`, `application_denied` and
`listing_taken` are logged once a minute while they last.
## Files
| File | What |
| --- | --- |
| `config.toml` | Every setting (beside the executable; `HALO_CONFIG_FILE`, else `--config`, names another) |
| `init.txt` (`server.init_file`) | Commands run at the start, in the data root unless a full path |
| `gametypes/<name>.toml` | Gametype files: the `gametypes` folder beside `config.toml` if there is one, else the data root's |
| `gametypes/<name>.gametype` | Gametypes saved by a player's game (`file=<name>` in the rotation): the same `gametypes` folder |
| `publisher.key` | The publisher key for the master servers (beside `config.toml`). On Windows, a server's `config.toml` and `publisher.key` are given an access list of their owner alone (the entries inherited for Administrators and SYSTEM taken out) |
| `masters.txt` | The master servers (`server.masters_file`: beside `config.toml`, else beside the executable); one that names none leaves the official master, built in |
| `bans.txt`, `cheaters.txt`, `debug.txt` | In `paths.logs`, else the data root |
| `status.json` (`server.status_file`) | The status, written every 2 seconds |
| A password's file (`HALO_SERVER_WEB_PASSWORD_FILE`, `HALO_SERVER_WEB_OWNER_PASSWORD_FILE`, `HALO_SERVER_WEB_MODERATOR_PASSWORD_FILE`, `HALO_SERVER_WEB_VIEWER_PASSWORD_FILE`, `HALO_SERVER_PASSWORD_FILE`) | A container's secret: when the plain variable is not set, the file's first line (its CR and LF off) is the setting's value, which is then pinned as the plain variable's would be (`sv_set` and `PUT /api/settings` refuse it, `409 pinned`). Read once, at the start; a file that cannot be read is logged, and the setting is `config.toml`'s |
## Releases
Each release has `SHA256SUMS` (the SHA-256 of each of its files, as
`sha256sum` writes them: `<hex> <name>`, written by `tools/ci_build.py
sums`) and `SHA256SUMS.sig`: the Ed25519 signature of that file's exact
bytes by the project's release key, in 128 lowercase hex digits and a
newline (the form `opence-master-server`'s `cmd/release-sign` writes). The
public key is `deploy/release-key.pub` (64 hex digits:
`d04e8b101007d1641cace8b95fcca76f7f44709f6885c3d031b52394dbdf9376`).
`tools/release_sign.sh verify SHA256SUMS` checks the signature (OpenSSL 3),
and `sha256sum -c --ignore-missing SHA256SUMS` the files downloaded. The
build workflow signs with the `RELEASE_SIGNING_KEY` secret (the key's seed,
64 hex digits) and checks the signature against `deploy/release-key.pub`
before it publishes the release.
-374
View File
@@ -1,374 +0,0 @@
# Exact-match acceleration playbook
This document records techniques with broader application to the January 2002
Halo Xbox decompilation. Object-specific measurements remain in
`docs/object_matching_logs/`; this file contains only controls, triage rules,
and campaign practices that have survived strict comparison.
The objective is faster reconstruction without lowering the bar. A candidate
is not exact because it is correct, similar, the same size, or 99% fuzzy. The
admission evidence remains XDK 3911 output, exact normalized bytes, exact
relocation shape and resolved destinations, exact owned data/layout, and a
whole-translation-unit regression audit.
## Campaign strategy
1. **Finish objects, not isolated functions.** Prefer a unit with a bounded
residual and known data ownership over a larger number of unrelated leaves.
Once an object is opened, carry it through code, data, ownership, regression,
documentation, and `Matching` admission unless a measured compiler tie
satisfies the parking standard.
2. **Prioritize reusable foundations.** Scenario/BSP accessors, real-math
leaves, player/unit layouts, tag/object accessors, datum iterators, compiler
wrapper families, and complete donor translation units reduce later work.
3. **Separate reconstruction from tuning.** First recover the January
semantics, types, call graph, private ABI, and owned data. Tune source shape
only after target size/relocation evidence says the structure is close.
4. **Work atomic private-ABI clusters together.** Static helpers and their
callers can form a register-allocation fixed point. Splitting them across
lanes produces false blockers.
5. **Use a bounded experiment budget.** Test one source factor at a time,
record the exact result, and stop repeating compiler-equivalent spellings.
A parked result needs explicit size, relocation, first-divergence, and
exhausted-control evidence.
6. **Automate candidate generation, never acceptance.** Shape-transfer and
donor tools may propose source, but the unchanged strict comparator and
regression gates decide admission.
### Select closeouts from strict evidence, not the ordinary percentage
Run `python -m tools.rank_object_closeouts` after regenerating the ordinary
and semantic reports. The ranking deliberately combines:
- the complete function inventory from the ordinary report;
- the reviewed strict `accepted_ledger`;
- object `Matching` status; and
- the evidence-backed parked-function catalog.
This prevents three recurring campaign mistakes:
1. treating a differently numbered local jump-table label as unfinished code;
2. repeatedly dispatching a one-function object whose only residual is already
a proven compiler tie; and
3. overlooking a data-only closeout after every function is exact.
The tool is a triage aid, not an admission oracle. `NEAR_CLOSEOUT` means the
remaining work is unparked and bounded; it does not predict that source tuning
will succeed. `LEDGER_REVIEW` means earlier object work exists and its
do-not-repeat/reopen rules must be read before dispatch. `PROVEN_BLOCKED` means
every remaining function is in the
reviewed parked ledger and must not be reopened without satisfying its stated
reopen criterion. Always read the object's Markdown ledger before editing.
## Evidence hierarchy
From strongest to weakest:
1. January target bytes, relocation metadata, linked addresses, and section
ownership.
2. January sibling functions and exact donor translation units.
3. January assert strings, source paths, help tables, tag definitions, and call
sites.
4. XDK 3911 headers and synthetic compiler experiments under the exact flags.
5. October Xbox, Halo PC/CEA/HCEX PDBs, community forks, and later-engine types.
Levels 4 and 5 generate hypotheses. They do not override January evidence.
Every imported field, enum, prototype, or name must be corroborated locally.
## High-yield source controls
### Ownership and alias modeling
VC7 code generation depends on what it can prove about globals:
- Aggregate storage can force address common-subexpression elimination or
destroy an index because sibling fields may alias.
- Separate globals can preserve a live SIB index and eliminate reloads.
- A one-field structure can retain field-oriented optimizer behavior while a
separate array supplies non-aliasing proof. This closed
`game_engine_multiplayer_sounds.obj`: it preserved `dec; mov; je` flags in
the update while producing SIB stores in the queue helper.
- Static versus external linkage affects alias knowledge, section order,
symbol storage class, and whether zero-initialized data becomes `.bss` or a
tentative/common symbol.
Always verify the full object. A linkage change can leave every function exact
while silently deleting or reordering `.bss`.
### Statement and branch order
- VC7 often reflects the original failure-first versus success-first topology.
Reversing an `if` can fix multiple relocation-address regions at once.
- Tail-merging is sensitive to deliberately asymmetric source duplication.
If two switch cases must share a forward machine-code tail but an explicit
shared label sinks a callee-save or changes the prologue, keep the simple
early path's tiny terminal test locally and route the longer path forward to
the other case's copy. In `shaders.obj`, VC7 cross-jumped that readable
duplicate into one shared target tail while preserving the required ESI
lifetime. Verify relocation identities: this is a measured block-placement
control, not permission to duplicate arbitrary logic.
- Common stores in two branches may be tail-merged, but a constant-valued
branch can instead materialize an immediate store and grow the function.
- Comparison polarity matters even when predicates are logically equivalent;
it controls `cmp` operand order, signed branches, and register lifetimes.
- Use a single return when the target structure permits it. When the exact
source requires multiple source returns to reproduce a common compiler
epilogue, record that as an evidence-backed house-style exception.
### Scalar stores versus aggregate operations
- Field-wise stores give the scheduler independent operations and can reproduce
interleavings that `{0}`, aggregate assignment, `memset`, or `rep stos`
cannot.
- Conversely, aggregate assignment is useful when the target uses `rep movs`
or a single calculated element address.
- VC7 groups local-struct zero initialization by brace level. If member zero
is itself an aggregate, `= {0}` can produce one whole-struct zero live range,
while `= {{0}}` can split the first member's fill from the implicit trailing
fill. A target whose zero stores split exactly at that member boundary is
evidence for testing the extra brace; this closed
`telnet_console_initialize` without changing behavior.
- Preserve target load and store order before trying declaration-order noise.
### Local lifetime is more important than local spelling
Naming an expression does not necessarily change VC7 output; the optimizer can
fold a short-lived alias immediately. What matters is where the value becomes
live and how long it must survive. In `data_packets.obj`, naming
`decoded_data + sizeof(short)` beside its final memory call did nothing across
several experiments. Computing the same readable payload pointer *before* the
range assert and keeping it live across validation plus an intervening integer
encode changed no instruction by itself, but rotated three later scratch
registers and closed a 768-byte function exactly.
In plain English: do not keep renaming a value and expect different code. Move
the point where the program genuinely needs to remember it, then measure the
whole function. This is especially useful around asserts and calls, where an
additional live pointer changes the compiler's interference graph without
inventing operations.
### Integer width and signedness
- `movsx`/`movzx`, partial-register operations, and signed branch opcodes reveal
the real promotion boundaries.
- Function parameter widths matter even when the x86 ABI still occupies a
four-byte stack slot. In `rasterizer_xbox_motion_sensor.obj`, changing a
render-target parameter from `long` to the proven `word` transformed
`movzx` into the target's `xor reg, reg; mov reg16, [memory]` sequence and
closed the entire remaining half of a 1,920-byte function.
- Change signedness only where the target proves it. A `jle` versus `jbe`
difference is often a type error; an ESI/EDI mirror is not.
- Recover narrow accumulators instruction by instruction rather than relying
on C's default integer promotions.
### x87 controls
- A value live after a store selects `fst`; a dead value selects `fstp`.
- Source operand order controls non-commutative subtraction/division memory
forms. VC7 canonicalizes commutative addition/multiplication; spelling order
is not a lever there.
- Passing, returning, or globally storing a floating result produces distinct
stack-staging patterns.
- Never substitute mathematically equivalent expressions that change signed
zero, NaN payload/sign, intermediate precision, or rounding.
- When a shared inline vector helper matches standalone but changes scheduling
at one caller, preserve the donor's **expression surface**, including
apparently redundant parentheses. In `collision_features.obj`, changing
`(v->i*t) + p->x` to the equally readable `v->i*t + p->x` altered inline IR
without changing the helper's standalone meaning and restored a January
`fstp`/address-formation boundary. Treat this as a measured inline-context
lever, not a license for blind parenthesis permutations.
- VC7 reassociates a one-expression commutative dot product, so merely reversing
its written terms may do nothing. If January proves a specific x87 load/add
order, sequential accumulation can preserve it. The exact
`collision_prism_test_vector` form initializes the `k` product, then adds
`j`, then `i`; every intermediate remains a normal `real` value.
### XDK inline wrappers
Stock XDK `D3DINLINE` under this compiler emits the January out-of-line D3D
wrapper families when called normally. Address-taking forces a different
standard-ABI body, and redefining `D3DINLINE` as plain `static` can emit a
thunk. Prefer the stock header form and verify every emitted wrapper in the TU.
One motion-sensor translation unit instantiated ten such wrappers exactly in a
single build, including both 432-byte and 544-byte dispatchers. Treat the
header recipe as a high-yield candidate generator, but still compare every
wrapper and its relocation destinations independently.
### Diagnose the first true divergence
- Ignore downstream branch displacements and relocation-address drift until
the earliest semantic or instruction-length difference is closed.
- Decode cdecl pushes right-to-left before blaming register allocation. A wrong
argument order can make a large suffix look like a scheduler problem.
- If a small type correction makes a long suffix exact automatically, the
suffix was displacement drift, not a collection of independent problems.
- Zero-byte intrinsics such as `_ReadWriteBarrier()` are measured controls, not
universal fixes. They can prevent desirable argument hoisting and make an
otherwise close function worse.
- A one-byte `push eax` where the reconstruction emits the two-byte `push 0`
after testing a result is evidence that the original passes the tested
result, not a literal. Preserving that value into the error arm can change a
large register-allocation suffix even when alignment leaves the padded size
unchanged. For size-exact functions containing a switch, inspect padding
and jump-table tails as well as the nominal body.
### Recover the type before tuning the schedule
A value that stays live in a register longer than expected is often evidence
of a wrong prototype or field width, not a mysterious compiler decision.
Before trying declaration orders, barriers, or control-flow rewrites:
- Check what the target returns in `EAX` or `AL`. If the target preserves a
meaningful value until `ret`, a function currently declared `void` may
actually return that value.
- Check whether the target consistently reads, masks, or writes only the low
byte or low word. A `char`, `boolean`, `short`, or `word` may be the real
type even when a 32-bit placeholder produces correct gameplay behavior.
- Prefer the smallest type that is supported by all call sites and target
instructions. Do not narrow a type merely because it happens to improve the
bytes.
In `texture_page.obj`, recovering `spacing_mask` as a 16-bit `word` made a
736-byte packing function exact. Recovering
`texture_page_textures_end` as a `boolean` function explained why January kept
the packing result in `AL` across the loop and made its remaining 112 bytes
exact. In plain English: the compiler was not being stubborn; we had told it
the wrong facts about the program.
### Private ABI controls
A source-static, non-address-taken, frameless helper can receive arguments in
ECX/EDX or another caller-derived convention. External linkage, address-taking,
or a stack frame can destroy that convention. Work from a forced-register
caller outward; do not tune a private helper in isolation.
### Compiler provenance before compiler hunting
Do not attribute a stubborn register or scheduling residual to an unknown
compiler patch until the target's own debug records have been checked.
PDB 2.00 module streams can contain `S_COMPILE2_ST` records with independent
front-end and back-end version triples. For the January `players.obj`, the
record proves `13.0.9254` for both halves, matching the campaign compiler's
reported build.
Use this order:
1. Decode the target compiland's compiler record.
2. Compile the unchanged TU once with each genuinely distinct local compiler
binary.
3. Compare normalized hashes for every residual, not just the first
instruction.
4. If all variants are byte-identical, stop the compiler hunt and return to
source provenance or a demonstrated legal-C control.
For Players, VC7 `13.00.9210` and `13.00.9254.1` emitted byte-identical
candidate hashes for all four residuals. That falsifies the available
patch-level lever and prevents repeated SDK downloads or speculative flag
changes.
## Transfer and donor rules
- A donor is usable only when its source text is resolved and its target
machine shape is exact.
- Derive every recipient callee/global substitution from the recipient target
relocations, never by name similarity.
- Equal relocation-masked bytes can hide a wrong-callee wrapper. Destination
identity and addends remain strict.
- Preserve target statement order; relocation sequence is often its strongest
surviving proof.
- Reject stubs, unresolved donors, inconsistent substitution maps, prologue
mismatches, and data-ownership changes automatically.
### Candidate-only helper COMDATs
An inlined helper can occasionally leave an unreferenced out-of-line COMDAT
copy in the candidate object. Never dismiss it just because the target
function compares equal. It is acceptable only after all of the following are
proved:
- the extra section is a discardable COMDAT with the expected selection rule;
- no relocation in the object refers to it;
- every target-owned function and data section remains strict-exact;
- the ordinary link actually discards it; and
- the consolidated link, semantic audit, and progress report remain clean.
This is a linker-level representation difference, not permission to ignore
extra ordinary code or data.
## Parking standard
Park only when:
- target and candidate padded size are exact;
- relocation count, types, and destinations are exact, or a reviewed
representation-only difference has independently resolved destinations;
- the remaining divergence is classified precisely;
- legal-C controls appropriate to that class have been exhausted;
- the object ledger records a reproducible first divergence and reopen
criterion.
A 92% function with different size or relocations is structural work, not a
compiler tie. A one-instruction scheduler swap can be parked only after the
dependency and source-control search are documented.
## Admission checklist
Before marking an object `Matching`:
1. Build with XDK 3911 and the repository flags.
2. Compare every function with the hardened COFF comparator.
3. Verify every owned `.data`, `.rdata`, and `.bss` span, including symbol
offsets, storage classes, and relocation destinations.
4. Confirm the ordinary report does not hide a strict false positive.
5. Run the whole-TU regression gate against a pre-edit snapshot. Review every
intentional ownership transition; never suppress unrelated failures.
6. Run the consolidated build, semantic audit, and progress report.
7. Update the object ledger with retained and rejected experiments.
8. Mark `Matching` only after all evidence passes.
### Greenfield regression gates
An empty skeleton has no previously exact code or owned data for a pre-edit
manifest to protect. A correct reconstruction will therefore appear to the
regression gate as a set of new functions, symbols, COMDATs, and data. Do not
force that report green and do not waive its findings:
- confirm that it reports no changed previously exact function or section;
- classify every addition against the January target;
- admit the object only after strict function, relocation, ownership,
consolidated-build, and semantic-progress checks pass;
- immediately capture a post-admission manifest and verify it is stable, so
later edits are checked against the newly completed object.
## Original bugs
Exact reconstruction preserves proven original behavior. Mark a defect
`BUG (original)` only when January evidence proves it. In the matching source,
retain the original behavior and add a concise comment describing a safe
corrected-build alternative. Do not silently repair it and do not label our
own reconstruction mistake as an original bug.
An original uninitialized read is an exceptional case, not a general codegen
lever. Preserve one only when the January control flow proves the read, an
independent build or source lineage corroborates it, and a bounded search shows
that defined readable-C alternatives do not match. Record the safe initialized
form and its measured result in the object log. Never introduce a new
uninitialized value merely because it improves register allocation.
Do not infer a fallback assignment merely because target code reloads a value
after a fatal assertion or `system_exit`. In `data_decode_memory`, that
seemingly sensible repair was the sole cause of a whole-function register
allocation mismatch; the January/October machine code plus an exact-unique
HCEA PDB local record and HCEA PPC body showed that the original impossible
default arm left its sole size local unassigned. Remove such a repair only
with the full original-bug evidence gate above, then state the safe corrected
alternative in source and measure it in the object log.
## Prohibited shortcuts
No inline assembly, volatile byte forcing, undefined aliasing, object-byte
patches, compiler pragmas used only to force bytes, flag changes, fuzzy-only
credit, or invented symbols/types. Compiler experiments belong in disposable
worktrees; production admission must remain readable evidence-backed C.
-50
View File
@@ -1,50 +0,0 @@
# Fake-match source review
`tools/fake_match_scan.py` finds unusual C/C++ constructs that deserve a
credibility or semantics review. It is a heuristic lead generator, not an
object comparator, admission gate, or proof that source is fake. A byte match
does not make nonsensical source credible, and a scanner hit does not make
ordinary source wrong.
These examples run from the repository root (the script also derives the
repository root correctly when invoked by an absolute path from elsewhere):
```text
python tools/fake_match_scan.py
python tools/fake_match_scan.py source/ai/actions.c source/game
python tools/fake_match_scan.py --format json --fail-on-findings source/ai
python tools/fake_match_scan.py --list-rules
```
The default input is the repository's `source/` directory. Explicit file and
directory inputs are de-duplicated and emitted in deterministic path/line/rule
order. Known vendored libtiff, zlib, `third_party`, and `vendor` trees are
skipped unless `--include-vendored` is supplied. Comments and string/character
literals are masked before scanning so examples and assert text do not become
findings.
Exit status is deterministic:
- `0`: the scan completed; findings may be present.
- `1`: findings are present and `--fail-on-findings` was requested.
- `2`: an input is invalid or a source file cannot be read.
Every finding should prompt a source review, not an automatic rewrite. Check
whether the construct expresses plausible original program intent, whether the
logic is meaningful for all inputs, and whether first-party source, names,
target CFG/relocations, or a credible contemporary codebase supports it. Re-run
the strict object and regression gates after any resulting edit. Exact bytes
alone are not provenance.
Semantic validity is an admission requirement even when every byte matches.
Reject code whose behavior is nonsensical, whose conditions or data flow are
meaningless, or whose source shape almost certainly would not have existed in
the original program. A coincidental exact result must be reverted or replaced
with the best honest fuzzy reconstruction and parked with its evidence.
The rule set intentionally omits several noisy shortcuts. It does not condemn
ordinary `volatile` declarations, generic representation casts, `!!value`, or
`value != value`; those can express hardware synchronization, serialization,
boolean normalization, or a NaN test. It also performs no data-flow proof, so a
clean scan cannot establish that variables are initialized or that logic is
sound.
@@ -1,63 +0,0 @@
# source/linker_common: what a comparison base can and cannot earn
*2026-09-22, Lane C. Built at the owner's direction, from January's verified
COMMON records only, and scored separately from the board.*
## What the unit is
`source/linker_common` is not a translation unit. It is csplit's name for the
pooled COMMON block of January's image: 242 records totalling 1,272,664 bytes,
gathered by the linker from tentative definitions across the whole program
(`_render` 643,732, `_ai_debug` 547,628, `_dsound_globals` 30,924, and ~200
smaller ones, down to single-byte debug toggles). `objdiff.json` gives it a
`target_path` and **no `base_path`**, because our build produces no such object -
our COMMON symbols live in their owning objects' symbol tables with section 0.
So it reports 0.000% structurally, not because anything is wrong.
## The base, and its score
`tools/campaign/linker_common_base.py` builds a comparison base containing ONLY
verified records: a symbol January pools that our tree independently emits as a
COMMON symbol of the identical size. Nothing is invented, and nothing
initialised is copied - a `.bss` record carries no bytes, only a name and a
size, and the size used is the one our own objects request. Records we do not
emit, emit at a different size, or emit in a real section are dropped.
kept 58 records 1,214,903 bytes
dropped 184 records 56,787 bytes
Scored on its own (`objdiff-cli report generate` over a one-unit config):
.bss 1,272,568 95.541%
.rdata 96 0.000%
credited 0
**It earns nothing.** Data is credited per section only at 100%, so 95.541% is
worth exactly as much as 0%. The number's value is diagnostic: it says 95.5% of
the pooled block is already correct in our tree, and names precisely what is not.
## What the remaining 4.5% is
181 records, 56,187 bytes, absent from our tree entirely. Only 27 of them
(1,125 bytes) are CRT/XDK/dsound runtime symbols. The rest are genuine Halo
globals no TU of ours declares: `_dsound_globals` 30,924, `_rasterizer_lights`
7,172, `_sound_channels` 6,144, `_wind_globals` 3,340, `_error_globals` 2,056,
`_global_window_parameters` 600, and so on. Plus two pooled `.rdata` records
(96 bytes) that are not COMMON at all and so are outside this base by
construction, and one size mismatch, `_file_location_volume_names`, which is the
out-of-bounds array bug recorded separately.
Closing the section therefore needs those 181 globals declared as tentative
definitions in the right units - spread across many TUs, several of which are
not reconstructed - and it pays nothing until every one of them is present.
## Rules this establishes
* A symbol in `linker_common` was a TENTATIVE DEFINITION in January: declared
with no initialiser. **Never add `= {0}` to such a global** - that moves it out
of the pool. Check `linker_common` before touching any global's initialiser.
This is how the `actors.c` `= NULL` defect was found and fixed.
* Conversely, a global January keeps in its OWN object's `.bss` (director,
render_objects) was initialised in source, and must keep its initialiser.
* Do not wire this base into `objdiff.json`. It would fold a structurally
uncreditable section into the board number without closing anything.
+650
View File
@@ -0,0 +1,650 @@
# The master servers' protocol, version 1
A master server lists dedicated servers and vouches for who publishes them.
An operator applies to a master once with the public half of an Ed25519
key, the *publisher key*, through the master's Discord bot; once an
approver approves the application, the
operator's servers publish to the master with requests signed by that key,
and the master lists them after probing their UDP port. The game's server
browser fetches each master's list, checks the master's signature with the
key pinned in `masters.txt`, and joins a listed game by the invite in its
listing, as it joins a game heard on the brokers.
A master carries no game traffic: each list entry holds the host-signed
listing the game publishes to the signalling brokers (`p2p_lobby.c`), so
the join path is unchanged and nobody needs the master to be honest about a
game's details.
This document is the contract between the game (the publisher in
`port/linux/src/server_master.c`, the probe's answer, the list client in
`port/linux/src/master_list.c`) and the masters' implementations. The
project's implementation is `opence-master`
(github.com/OpenCommunityEdition/opence-master-server, Go, AGPL-3.0-or-later),
whose `testdata/vectors.json` holds fixed keys and every value this
document derives from them, and whose `tests/conformance` is the test a
master must pass to be an official one. `docs/dedicated_server.md` is the
server's side: its settings, `--keygen` and `--sign`.
## Conventions
| Thing | Form |
| --- | --- |
| Hexadecimal | Lowercase, two digits a byte, no prefix |
| Base64 | The standard alphabet (RFC 4648 section 4) with `=` padding |
| Times | Unix seconds, decimal, UTC |
| JSON | UTF-8; every string the protocol defines is printable ASCII (0x20 to 0x7E) |
| Signatures | Ed25519 as RFC 8032 defines it (with SHA-512): Monocypher's `crypto_ed25519_sign` and `crypto_ed25519_check`, Go's `crypto/ed25519` |
| Transport | HTTPS. A master refuses publishes, key changes and peers' requests over plain HTTP except from loopback |
| Unknown JSON fields | Ignored by both sides, so later versions can add fields |
## Keys and identifiers
| Thing | Definition |
| --- | --- |
| Key file (`publisher.key`, `master.key`) | One line of 64 hexadecimal digits: the Ed25519 seed (32 bytes). Anything after `#` is a comment; blank lines are skipped. Mode 0600 |
| Public key, as text | `ed25519:` and the 32-byte public key in hexadecimal (64 digits) |
| Key id | The first 8 bytes of SHA-256 of the 32-byte public key. Written as 16 hexadecimal digits (`keyid=`, `key_id`, `Signature-Key`); in the probe's packets it is the 8 raw bytes |
| Instance | A name the operator gives each server process (`server.instance`): 1 to 32 characters of `a-z`, `0-9`, `-` and `_`, the first a letter or a digit |
| Server id | The first 16 hexadecimal digits (8 bytes) of SHA-256 of the ASCII text of the key id's 16 hexadecimal digits followed by the instance's text, with nothing between them |
The server id hashes the key id **as its 16 characters of text**, not its 8
bytes. With the vectors' publisher key (RFC 8032's first test seed):
```
seed 9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60
public key ed25519:d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a
key id 21fe31dfa154a261 (SHA-256 of the 32 key bytes: 21fe31dfa154a261...)
instance main
hashed "21fe31dfa154a261main" (20 ASCII bytes)
server id 4c5f15237c866f6f (SHA-256 of those 20 bytes: 4c5f15237c866f6f...)
```
The same instance under another key is another server id, so a server
keeps its id across restarts as long as it keeps its key and its instance.
A master refuses a public key whose point has a small order (its X25519
form multiplied by a clamped scalar is zero, as `p2p_ed25519_verify`
checks), since anyone can make signatures that verify under one.
`masters.txt` (the one beside `config.toml`, else the one beside the
executable) lists the masters, one per line:
`https://master1.example.org ed25519:<64 hex>`; `#` starts a comment; the
first 4 are used. A file that names none (missing, empty, or comments alone,
as the release's) leaves the official master, built into the game
(`master_list_read_defaults` in `port/linux/src/master_list.c`) and the web
page (`servers.js`):
`https://master.opencommunityedition.com ed25519:89f4eda71229f11f3538679f49ef7fb0c2b407b380093f7f4fe02105b92225aa`.
The web build reads its site's `masters.json` before the official master. The URL is an origin with no path, or a path
the master is served under as it is (a proxy must not rewrite paths, since
the path is signed).
## Signed requests
A publisher's requests carry a detached signature in one header:
```
Authorization: OpenCE-Ed25519 keyid=<16 hex>, ts=<unix seconds>, nonce=<32 hex>, sig=<base64 of 64 bytes>
```
The signature is the key's over the bytes of this string, its parts joined
by single line feeds (0x0A) and no line feed at the end:
```
OpenCE-Master-v1
<the receiving master's key id>
<METHOD>
<PATH>
<ts>
<nonce>
<SHA-256 of the body, in hexadecimal>
```
| Part | Rule |
| --- | --- |
| Master key id | The key id of the master the request is sent to: the first 8 bytes of SHA-256 of the master's list-signing public key (the key pinned for it in `masters.txt`), as 16 lowercase hexadecimal digits. It binds the request to that master: a request signed for one master does not verify at another, even within the replay window. A master checks the signature with its own key id |
| `METHOD` | The request's method in capitals: `PUT`, `DELETE`, `GET`, `POST` |
| `PATH` | The path exactly as the request line sends it, without the query string: `/v1/servers/4c5f15237c866f6f`, `/v1/me`, `/v1/publishers` |
| `ts` | The same decimal text as in the header: no sign, no leading zeros |
| `nonce` | 16 random bytes in hexadecimal (32 digits), new for each request |
| Body hash | Of the exact body bytes sent; an empty body's is SHA-256 of no bytes, `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` |
The four parameters come once each, in any order, separated by commas with
optional spaces; a publisher sends them in the order above. The master
checks, in this order, and answers the first failure:
| Check | Failure |
| --- | --- |
| The header parses (scheme, the four parameters and their forms) | `401 bad_signature` |
| `ts` is within 300 seconds of the master's clock | `401 clock` (the body's `master_time` is the master's clock, for the server to log) |
| The key id is a publisher's, or an application's | `403 unknown_key` |
| The signature verifies, for this master's key id | `401 bad_signature` |
| The nonce was not seen with this key in the last 600 seconds | `401 replay` |
Since `unknown_key` is answered before the signature is checked, anyone can
learn whether a key id is known to a master (a publisher's or an
application's) by sending it with any signature. This is a known property
of version 1: key ids are not secret (each is a hash of a public key, which
an operator hands out when applying), and the answer says nothing more of
the key than that the master has seen it.
The vectors' `GET /v1/me` to the vectors' master (key id
`39f713d0a644253f`) at 1760900001 with the nonce
`f0e0d0c0b0a090807060504030201000` signs
```
OpenCE-Master-v1\n39f713d0a644253f\nGET\n/v1/me\n1760900001\nf0e0d0c0b0a090807060504030201000\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
```
and sends
```
Authorization: OpenCE-Ed25519 keyid=21fe31dfa154a261, ts=1760900001, nonce=f0e0d0c0b0a090807060504030201000, sig=duRZ+WKAI8hbvidtC63U1XHxOSoeRN5mcrZvw4qihhlilhZjbVBCLYjsQLr4+F2C1gy6lN9/ixqevFX7dfNGDA==
```
## Publishing
`PUT /v1/servers/{server_id}`, signed, with a JSON body of 16 KB at most.
A server publishes to every master in `masters.txt` every 30 seconds, and
within 5 seconds of a change (the lobby opened, a game started or ended, a
player joined or left, the name or map changed). On shutdown it publishes
`closed: true` (or `DELETE`s) and waits up to 2 seconds.
```json
{"protocol":1,"instance":"main","name":"OpenCE Dedicated","map":"bloodgulch","gametype":"CTF","variant":"CTF",
"players":5,"max_players":16,"teams":true,"in_progress":false,"password":false,"dedicated":true,
"network_version":38,"build":"build-206","region":"eu","tunnel_port":2302,
"listing":"<base64 of the host-signed listing>","closed":false,"serial":412}
```
(The vectors' publish, made at network version 38: "The listing" says
why the examples keep it.) The game's publisher sends `protocol`,
`instance`, `listing` (but in a tombstone), `tunnel_port`, `dedicated`,
`build`, `region` and `variant` when they are set, `address` with
`server.master_publish_address`, `serial` and `closed`; the listing holds
the rest. A server that goes private, or off the internet, sends one more
publish, `closed: true`, to each master that listed it.
| Field | Type | Required | Meaning |
| --- | --- | --- | --- |
| `protocol` | number | yes | 1 |
| `instance` | string | yes | The instance; the path's server id must be the server id of the signing key's id and this instance, else `400 server_id` |
| `listing` | string | yes, but with `closed: true` | Base64 of the exact bytes `listing_make` makes, the bytes the game publishes to the brokers |
| `tunnel_port` | number | yes, unless `address` is given | The UDP port the game's tunnel socket is bound to (`network.tunnel_port`), 1 to 65535 |
| `address` | string | no | `"203.0.113.5:2302"`: an IP address (IPv4, or IPv6 in brackets) and a port to probe instead of the request's source address. Sent only with `server.master_publish_address`; a master serves it in its list only if its own policy allows (`MASTER_SERVE_ADDRESSES`). It must be globally routable (below); a private or loopback address is taken only from a private or loopback source |
| `dedicated` | boolean | no | A dedicated server. Also read from the listing's flag 32, which a dedicated server sets; either makes the entry dedicated. A player's public game, published with the player's publisher key, sends `false` |
| `variant` | string | no | The gametype's name as the server shows it; printable ASCII, cut to 24 |
| `build` | string | no | The game's build (`build-206`); cut to 32 |
| `region` | string | no | Up to 16 of `a-z`, `0-9` and `-` (`server.region`) |
| `serial` | number | no | A whole number the server raises at each change; kept and shown to admins |
| `closed` | boolean | no | `true`: unlist the server now |
| `name`, `map`, `gametype`, `players`, `max_players`, `teams`, `in_progress`, `password`, `network_version` | | no | Informational only: the listing is authoritative. A value that differs from the listing's is ignored, and the answer's `warnings` says so |
The master parses the listing and checks the host's signature (below), and
takes from it the name, the map, the gametype, the players and most
players, the gametype's engine, the game's kind, the open, in-progress,
teams and password flags, and the network version, so a publish cannot
claim more than the host signed. A listing that does not parse or verify
is `400 listing`. So is one whose own time is more than 3600 seconds from
the master's clock. A listing with the same host key and a lower sequence
than the one the master holds is not taken (the newer stays; a warning
says so). A listing with the closed flag unlists the server, as
`closed: true` does.
The master records the request's source address (behind a reverse proxy,
the address its trusted proxy forwards) to probe the server. It keeps the
addresses 7 days at most after a server's last publish, for probes and
abuse handling, and never serves them.
The addresses a master probes and serves are an allowlist of globally
routable unicast addresses: an IPv4 address outside IANA's special-purpose
ranges (`0/8`, `10/8`, `100.64/10`, `127/8`, `169.254/16`, `172.16/12`,
`192.0.0/24`, `192.0.2/24`, `192.88.99/24`, `192.168/16`, `198.18/15`,
`198.51.100/24`, `203.0.113/24`, `224/4`, `240/4`); an IPv6 address in
`2000::/3` outside `2001::/23`, `2001:db8::/32`, `3fff::/20` and
`5f00::/16`; a 6to4 (`2002::/16`) or well-known NAT64 (`64:ff9b::/96`)
address whose embedded IPv4 address is global. An IPv4-mapped address is
judged as its IPv4 address. Everything else (link-local, site-local
`fec0::/10`, unique local, multicast, `64:ff9b:1::/48`, zones) is refused
(`400 schema`, `field: address`), except a private or loopback address
from a private or loopback source (a master on the same network). A
publish from a source address outside the allowlist without `address` is
refused the same way when the master probes.
A listing's host key is published by one publisher at a time: a listing
whose host key is that of a live server of another publisher is refused,
`403 listing_taken`, so a listing's bytes copied from a list cannot be
published by someone else. The same publisher may carry it under another
server id (another key or instance). A host makes a new listing (a higher
sequence) at each of its publishes, so a live server that has taken no newer
listing of its host key for 90 seconds (3 intervals) is replaying a copy:
a publish of a newer listing of that host key takes the host key from it,
and its server is unlisted. A server an admin hid, or whose publisher or key
is not approved, holds no host key. A master shows its admins the publishes
it refused with `listing_taken`, to settle (see "Known limitations").
### The answer
`200` once the publish is taken and the master's decision for now is made;
`202` while the first probe of the server's address is unanswered:
```json
{"ok":true,"server_id":"4c5f15237c866f6f","listed":true,"interval":30,"ttl":90,"probe":"ok","reachable":true,
"observed_address":"203.0.113.5","master_time":1760900000}
{"ok":true,"server_id":"4c5f15237c866f6f","listed":false,"interval":30,"ttl":90,"probe":"pending","reachable":false,
"observed_address":"203.0.113.5","probe_address":"203.0.113.5:2302","master_time":1760900000}
```
| Field | Meaning |
| --- | --- |
| `listed` | Whether the list shows the server now |
| `interval` | Seconds between publishes the master expects (30) |
| `ttl` | Seconds after its last publish that a server is unlisted (90): one lost publish does not unlist it, three do |
| `probe` | `pending` (no answer yet at this address), `ok`, `failed` (three attempts unanswered), `skipped` (an admin marked the server trusted), `off` (the master does not probe) |
| `reachable` | The probe was answered |
| `observed_address` | The address the master saw the request come from |
| `probe_address` | With `pending` and `failed`: the address and port the master probes |
| `hidden`, `reason` | An admin hid the server: it is listed to nobody, and `reason` says why. With `probe: failed`, `reason` is the hint to log: `let UDP port <n> through your firewall or NAT, or set network.public_address` |
| `closed` | `true` after `closed: true`, a tombstone or `DELETE` |
| `warnings` | Body fields the listing contradicts, which were ignored |
| `master_time` | The master's clock |
A server with a failed probe is unlisted unless the master's policy lists
unreachable servers (`MASTER_LIST_UNREACHABLE`), as `reachable: false`.
The server logs each change of outcome once (not every 30 seconds), and
the failed probe's hint once a minute until the probe passes.
`DELETE /v1/servers/{server_id}`, signed by any key of the server's
publisher with an empty body, unlists the server at once and answers
`200 {"ok":true,"server_id":…,"listed":false,"closed":true,…}`; `404
not_found` for a server that is not the publisher's.
### Limits
| Limit | Answer |
| --- | --- |
| A server publishes at most once each 5 seconds, with a burst of 2 | `429 rate_limited` with `Retry-After` |
| 120 publishes a minute per source address, and per key | `429 rate_limited` |
| A body is 16 KB at most | `413 too_large` |
| A publisher has at most its quota of live servers at once (default 4; live: published within `ttl` and not closed) | `403 quota_exceeded` |
| An instance is published by one key of a publisher at a time | `403 instance_taken` (another key's server of that instance is live) |
| A publisher holds at most its quota and 16 server records (one per server id); for a new server id past that, its oldest records that are not live are dropped, and if none can be | `403 too_many_instances` |
## The listing
The listing is `p2p_lobby.c`'s (`listing_make`, `listing_read`), in network
byte order. The master reads it as `listing_read` does and checks its
signature as `listing_signed` does.
| Offset | Size | Field |
| --- | --- | --- |
| 0 | 2 | `"HL"` |
| 2 | 1 | Format: 1 |
| 3 | 2 | Network version (`HALO_PORT_NETWORK_VERSION`, 39 now) |
| 5 | 1 | Flags (below) |
| 6 | 4 | Sequence: the host's count of its listings (the newest wins) |
| 10 | 4 | The host's Unix time when it made the listing |
| 14 | 32 | The host's Ed25519 public key (one per run of the game) |
| 46 | 16, or 56 with the password flag | The invite's token (zero in a tombstone); with the password flag the token sealed with the password's key: nonce (24), MAC (16), the token encrypted (16), XChaCha20-Poly1305 with the host's key as additional data (`p2p_seal_token`) |
| then | 1 | Players (at most 255) |
| | 1 | Most players |
| | 1 | The gametype's engine: 0 none (co-op), 1 CTF, 2 Slayer, 3 Oddball, 4 King of the Hill, 5 Race |
| | 1 + up to 32 | The game's name: a length byte, then that many bytes |
| | 1 + up to 32 | The map's name |
| | 1 + up to 24 | The gametype's name |
| | 8 | The stamp: the game's kind (`game_modes_kind`: the OpenCE mode, 1 SWAT, 2 Infection, 3 Gun Game, or 0; with 0x80 for Hardcore), then 7 reserved bytes, zero |
| | 64 | The Ed25519 signature, by the listing's key, of the 12 ASCII bytes `hceu-lobby-1` followed by every byte before the signature |
| Flag | Meaning |
| --- | --- |
| 1 | Open: it takes players now |
| 2 | In progress: a game is under way (or loading) |
| 4 | Teams |
| 8 | Closed: a tombstone, the host stopped |
| 16 | Password: the token is sealed |
| 32 | Dedicated: a dedicated server (`halo --server` sets it, since network version 39) |
A listing is 140 bytes at least (all names empty) and 268 at most (a
password and every name at its length). A reader refuses one outside those
sizes, without `"HL"` and format 1, with a name's length byte past its
field's maximum or past the end, or whose stamp and signature do not end it
exactly. The names are sanitised as the game's `sanitize` does: a byte
outside 0x20 to 0x7E becomes `?`, and a zero ends the name. A master also
refuses a listing whose key has a small order.
The vectors' first listing (an open team game of a dedicated server,
sequence 412, 5 of 16 players, CTF on bloodgulch, named `OpenCE Dedicated`)
is these 169 bytes, of which the first 105 are signed. The vectors were made
at network version 38 and are kept as they are (the bytes and signatures
are fixed): their listings carry version 38 with flag 32, which the game
sets from version 39 on, so a game of version 39 reads them, checks their
signatures and hides them from its browser as another version's games, as
it does any. The examples' `network_version` of 38 in this document are the
vectors' too.
| Field | Bytes |
| --- | --- |
| Magic, format | `484c` `01` |
| Network version 38, flags 37 (open, teams, dedicated) | `0026` `25` |
| Sequence 412, time 1760900000 | `0000019c` `68f533a0` |
| Host key | `fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025` |
| Token | `a0a1a2a3a4a5a6a7a8a9aaabacadaeaf` |
| Players 5, most 16, engine 1 (CTF) | `05` `10` `01` |
| Name, map, gametype | `10` `4f70656e434520446564696361746564`, `0a` `626c6f6f6467756c6368`, `03` `435446` |
| Stamp (kind 0) | `0000000000000000` |
| Signature | `633dc63eb27828d6e589e34dc4336dbb2d514b2018e4bf4a3adf5aeb774b6a523fe662c3d29241c6f91c8714a8ca2b5e493aacb32bcaeb9bce715f15b76d230f` |
(`testdata/vectors.json` has it whole, with a password game's, a
tombstone's and an altered one that must be refused.)
## The probe
Before it lists a server, the master proves that the server's UDP port is
reachable from the internet and that the publisher key's holder answers on
it. It sends a datagram to the probe address (the published `address`, or
the request's source address with `tunnel_port`) from its probe socket:
| Offset | Size | Probe (35 bytes) |
| --- | --- | --- |
| 0 | 10 | `"OPCE-PROBE"` |
| 10 | 1 | 1 |
| 11 | 8 | The master's key id, raw |
| 19 | 16 | A nonce |
The game's tunnel socket, which dispatches what it receives by its first
bytes, recognises the magic and answers from the same socket to the
address the probe came from:
| Offset | Size | Answer (103 bytes) |
| --- | --- | --- |
| 0 | 14 | `"OPCE-PROBE-ACK"` |
| 14 | 1 | 1 |
| 15 | 8 | The master's key id, as the probe had it |
| 23 | 16 | The nonce, as the probe had it |
| 39 | 64 | The publisher key's Ed25519 signature of the 29 bytes `"probe"`, the master's key id, the nonce |
The server answers only a probe of exactly 35 bytes whose key id is that of
a master in its `masters.txt`, only to the probe's source, a given nonce at
most once per second, at most 2 answers a second to one source address and
at most 10 answers a second in all, so the probe cannot be used to reflect
traffic (a 103-byte answer to a 35-byte probe is the whole amplification),
and probes forged from one address cannot use up the answers a master's
probe needs. A probe whose nonce was answered in the same second counts
against neither limit.
The master takes only an answer from the probed address and port, with its
own key id, a nonce it made for that address and key, and a signature that
verifies with the key that signed the publish.
| Rule | Value |
| --- | --- |
| Attempts | 3, 2 seconds apart; failed 2 seconds after the third |
| Again | Every 5 minutes while the server is reachable (it stays listed meanwhile); a failed server at its first publish a minute or more after the failure; at once when its probe address or key changes |
| Nonce | Opaque to the server. A master uses a fresh nonce for each probe round (the round's attempts share it), and never the same one for two rounds of one address and key. `opence-master`'s is stateless: the first 16 bytes of HMAC-SHA-256(seed, `"opence-probe-1"` 0x00 address 0x00 key id 0x00 step), the step the decimal Unix time of the round's start divided by 30; it accepts the current and the previous hour's random seed and the current and the previous step, and starts rounds of one address and key at least a step apart |
| Duplicates | A server answers a given nonce at most once per second: it remembers the nonces it answered (the last 64) with the Unix second they came in, and ignores a probe whose nonce it answered in the same second, whatever came between (A, B, A in one second: the second A is ignored). A master's retries, 2 seconds apart, are answered again; a nonce resent within the second is not |
With the vectors' master key (id `39f713d0a644253f`) and the nonce
`101112131415161718191a1b1c1d1e1f`, the probe is
`4f5043452d50524f42450139f713d0a644253f101112131415161718191a1b1c1d1e1f`, the
signed message `70726f626539f713d0a644253f101112131415161718191a1b1c1d1e1f`,
and the publisher's answer
`4f5043452d50524f42452d41434b0139f713d0a644253f101112131415161718191a1b1c1d1e1f`
followed by the signature
`a63af355db038dff90e6c82abc0e252eddbd1ed3562580d7becaf3e4edbdd39cef7ef3163b91849ba57f6e83240b92df1efbce15dfa42f4a12981590a0234109`.
## The list
`GET /v1/servers`, with the optional query `network_version=<n>`,
`dedicated=1` (or `0`), `region=<region>`, `q=<text>` (a case-insensitive
substring of the name, map, gametype or variant; 32 characters at most).
No signature is needed and plain HTTP is allowed: the body is signed.
```json
{"protocol":1,"master":{"name":"OpenCE Master 1","key_id":"39f713d0a644253f","time":1760900000},
"servers":[{"id":"4c5f15237c866f6f","publisher":{"id":"p_kz3c5lb2xvq7mdpa","name":"Milenko's servers","since":"2026-10"},
"name":"OpenCE Dedicated","map":"bloodgulch","gametype":"CTF","variant":"CTF","players":5,"max_players":16,
"teams":true,"in_progress":false,"password":false,"open":true,"dedicated":true,"kind":0,"engine":1,
"network_version":38,"build":"build-206","region":"eu","reachable":true,"updated":1760899990,
"listing":"<base64>"}]}
```
| Entry field | Meaning |
| --- | --- |
| `id` | The server id |
| `publisher` | The publisher's id (`p_` and up to 32 of `a-z0-9`), display name, and the year and month it was approved |
| `name`, `map`, `gametype`, `players`, `max_players`, `teams`, `in_progress`, `password`, `open`, `kind`, `engine`, `network_version` | From the listing |
| `variant`, `build`, `region` | From the publish |
| `dedicated` | The listing's flag 32 or the publish's `dedicated` |
| `reachable` | The last probe was answered (`false` for a trusted server or under `MASTER_LIST_UNREACHABLE`) |
| `updated` | The time of the server's last publish |
| `address` | Only with the operator's and the master's consent (above); absent otherwise |
| `listing` | Base64 of the listing's exact bytes, as the server published them |
The list holds at most 512 entries: dedicated servers first, then by
players. Its headers:
| Header | Value |
| --- | --- |
| `Signature` | `ed25519=` and the base64 of the master key's signature of the body's exact bytes (before any `Content-Encoding`) |
| `Signature-Key` | The master's key id, 16 hexadecimal digits |
| `Signature-Time` | The body's `master.time` |
| `ETag` | `"` and the first 16 hexadecimal digits of SHA-256 of the body, and `"`; `If-None-Match` with it answers `304` |
| `Cache-Control` | `public, max-age=5` (the master serves the same body for 5 seconds) |
| `Content-Encoding` | `gzip` when the request's `Accept-Encoding` asks for it |
| `Access-Control-Allow-Origin` | `*` (and `Access-Control-Expose-Headers` names the four above), for the web build |
A client:
1. checks `Signature` over the decompressed body bytes with the key in
`masters.txt`, and that `Signature-Key` is that key's id, and refuses
the whole body otherwise (a missing `Signature-Key` too; a client that
cannot check Ed25519, such as an old browser, refuses every list rather
than show one unchecked);
2. parses the body, and refuses it unless `protocol` is 1, `master.key_id`
is the key's id, and `master.time` is no older than 300 seconds and no
more than 300 seconds ahead of the client's clock (a list dated ahead
would be taken for longer when it is replayed later, and a clock that
far off is wrong). The time is in the signed body: `Signature-Time` only
repeats it, since a header is not signed, and a client need not read
it;
3. type-checks each entry, decodes its `listing`, runs `listing_read` and
the host signature's check on it, and skips an entry that fails rather
than the list; it shows the name, map, gametype and players of the
signed listing (an entry's own fields are the master's word, and may
differ); it filters control characters from every name, and takes at
most 512 entries (a 512-entry list is some 26,000 JSON tokens: the game
reads a list of 65,536 tokens at most);
4. merges the masters' lists: entries with the same `id` are one game (the
newest `updated` wins), and an entry whose listing key is also heard on
the brokers is one game, shown as master-listed;
5. never pings listed servers (that would tell every server the player's
address): it shows the `region`, and the ping once a game is joined.
The vectors' signed body is
`{"protocol":1,"master":{"name":"OpenCE Master 1","key_id":"39f713d0a644253f","time":1760900000},"servers":[]}`
with `Signature: ed25519=18dmQnIfmRnC+wUwRJ/loxAH1VfASRBVnZgCfRuHrAw/2uo4thTibWnNQFScgRju+5lQtt709blK86BLT0fYCw==`
and `ETag: "cd755a50c5cfc457"`.
`GET /v1/servers/{server_id}` answers one listed server the same way:
`{"protocol":1,"master":{…},"server":{<entry>}}` with the same headers, or
`404 not_found`.
## Other endpoints
| Request | Who | Answer |
| --- | --- | --- |
| `GET /v1/publishers/{id}` | anyone (the list's limit) | `{"ok":true,"id","name","since":"2026-10","status","servers":<listed now>}`, or `404` |
| `POST /v1/applications`, `/v1/applications/…` | anyone | `410 applications_by_discord`: applications are made with the master's Discord bot (below), not over HTTP |
| `POST /v1/keys` | signed, an approved publisher's active key | Body `{"public_key":"ed25519:…"}`: adds a key (8 active at most). `201 {"ok":true,"key_id","status":"active"}`; `409 key_exists`; `403 too_many_keys` |
| `GET /v1/me` | signed, any key the master knows | The publisher's record: `{"ok":true,"key_id","status","publisher":{"id","name","since","status","quota","origin","keys":[{"key_id","public_key","status","updated","last_used"}],"servers":[{"id","instance","key_id","listed","probe","reachable","hidden","trusted","updated","name","players","max_players"}]}}`; for a key still in an application, `"publisher":null` and `"application":{"id","status","reason","created"}`; `403 key_revoked` |
| `GET /v1/master` | anyone | The master's name, key id, public key, contact, data policy and limits (informational: clients use the key in `masters.txt`) |
| `GET /healthz` | anyone | `200 {"ok":true,…}` while the database answers |
| `GET /metrics` | anyone (the list's limit) | Prometheus text, read at most once each 5 seconds: `master_servers{reachable}`, `master_publishers{status}`, `master_publishes_total{result}`, `master_probes_total{result}`, `master_list_requests_total`, `master_peer_requests_total`, `master_build_info{version,protocol}` |
| `GET /`, `GET /apply`, `/admin/*` | people | The list page (the list's limit), how to apply (the Discord bot, and an invite to its server), the admin pages |
## Applications
An operator applies with the master's Discord bot, in the master's
Discord server (the `/apply` page links to it): the bot's `/apply` command
takes the region (a menu of the master's choices) and the number of servers
at once as its options, and answers the applicant alone with a nonce for
the proof of key,
`opence-apply-<unix>-<16 hex>`, good for 24 hours and bound to that Discord
user (a nonce given to another user, or to none, does not prove this one's
key), and a button that opens the application's form: a display name, the
public key, the proof, and a note. The application's name is 3 to 32 printable ASCII characters,
`servers` is 1 to 64, and the region up to 16 of `a-z`, `0-9` and `-`. The
proof of key is required unless the master's policy says otherwise
(`MASTER_REQUIRE_PROOF`, true by default): it is the publisher key's
Ed25519 signature of the nonce's ASCII text, in hexadecimal (128 digits,
what `halo --server --sign <nonce>` prints) or base64. An application
without one is shown to the approvers as unproven. A key already a
publisher's is refused, and so is a key with an application pending. The
applicant is answered on Discord, and told of the decision by a direct
message; the applicant's identity is their Discord user id and username.
Each application is posted to the master's log channel with Approve and
Deny buttons, which only the members of the master's approver roles may
press; the admin pages decide too. Once decided, anywhere, the message
shows the outcome and who decided. The master's operator can also add an
application on its command line (`opence-master challenge` for the nonce,
then `add-application`), as the conformance test does.
A master limits applications, since each one is posted to its approvers:
`opence-master` takes one pending application at a time and 3 a day per
Discord user, 3 a day per key, and, of applications without a proof of key,
100 an hour in all; one with a proof is not refused for the hour's number,
but past 100 applications in an hour the approvers are alerted once and the
next ones are not posted one by one (its settings change these numbers). It
deletes a denied application 30 days after its denial.
The earlier HTTP application (`POST /v1/applications` with an e-mail
address verified by a mailed link, `GET /v1/applications/challenge`, and
the application's status by its mailed token) is gone: its endpoints
answer `410 applications_by_discord`. A master keeps the applications made
that way readable to its admins.
## Approval sharing between masters
The official masters share approvals, so an operator applies once. Each
master fetches each peer's `GET /v1/publishers?since=<unix>` (every minute
in `opence-master`), signed with its own master key in the request scheme
above (`keyid` is its master key id, and the signed string's master key id
is the peer's, the one it fetches from; a master answers only the peers it
is configured with, else `403 not_a_peer`). The answer is signed as the list
is (`Signature`, `Signature-Key`, `Signature-Time`, the body's
`master.time`):
```json
{"protocol":1,"master":{"name","key_id","time"},
"publishers":[{"id","name","status","since","updated","origin","quota",
"keys":[{"key_id","public_key","status","updated"}]}]}
```
A master shares only what it holds a word of its own on: the publishers it
approved or moderated (`status` is its own word: `approved`, `suspended` or
`revoked`; `null` when it only vouches for keys), and the keys it added or
revoked (`active` or `revoked`). `origin` is the key id of the master that
approved the publisher, as the sharing master knows it (a master records a
publisher new to it as come from the peer it fetched the record from,
whatever origin the record claims, and only that peer's records rename
it); `since` its approval time; `updated` the time of
the newest word in the record; `since=` returns the records updated after
it. The next fetch's `since` is the answer's `master.time` less 120 seconds,
on the peer's own clock.
The importing master keeps each master's word apart and takes the most
restrictive of the trusted masters' words as the publisher's status
(`revoked` over `suspended` over `approved`) and the smallest quota given;
a newer word from a master replaces that master's older one. So a
suspension or revocation anywhere wins everywhere, and only the master that
suspended a publisher can lift its suspension. A key revoked by any trusted
master is revoked, for good; a key two publishers claim is refused. A
master may refuse a peer's words (`MASTER_PEERS_TRUST`); they then count
for nothing.
## Errors
Every error is `{"ok":false,"error":{"code":"…","message":"…","field":"…"?},"master_time":<unix>}`.
The message is for people and may change; programs read the code.
| Status | Code | Meaning | A server's publisher should |
| --- | --- | --- | --- |
| 400 | `schema` | A field is missing, of the wrong type or out of range (`field` names it), or the body is not one JSON object | Log it once; it is a bug |
| 400 | `server_id` | The path's id is not the key's and the instance's | Log it once |
| 400 | `listing` | The listing does not parse, its signature fails, its key has a small order, or its time is an hour off | Log it once |
| 401 | `bad_signature` | The Authorization header is malformed or its signature fails | Log it once |
| 401 | `clock` | `ts` is more than 300 seconds from `master_time` | Log the difference (from the answer's `master_time`): the machine's clock is wrong |
| 401 | `replay` | The nonce was used in the last 600 seconds | Send a new nonce |
| 403 | `unknown_key` | No publisher or application has the key | Log `master: apply with key <id>` once a minute |
| 403 | `publisher_pending` | The key's application waits for review | Log `master: publisher pending` once a minute; keep publishing every 30 s |
| 403 | `application_denied` | The key's application was denied (`message` says why) | Log it once a minute |
| 403 | `publisher_suspended` | The publisher is suspended | Log it; keep publishing slowly |
| 403 | `publisher_revoked`, `key_revoked` | Revoked for good | Log it; stop publishing to that master |
| 403 | `quota_exceeded` | The publisher has its quota of servers live | Log it |
| 403 | `instance_taken` | Another key of the publisher has a live server of this instance | Log it; the old server expires after `ttl` |
| 403 | `listing_taken` | The listing's host key is that of another publisher's live server, which has renewed it within 90 seconds | Log it once a minute; it is someone else's listing, or the host key is shared; keep publishing |
| 403 | `too_many_instances` | The publisher holds its quota and 16 server records, none of which can be dropped | Log it; use fewer instance names |
| 403 | `tls_required` | Plain HTTP from beyond loopback | Use the `https://` URL |
| 403 | `not_a_peer` | A peer's request from a master not configured as a peer | |
| 404 | `not_found` | No such server, publisher or endpoint | |
| 409 | `key_exists` | The key added (`POST /v1/keys`) is known already | |
| 410 | `applications_by_discord` | An application sent over HTTP: applications are made with the master's Discord bot | Log it: apply with the bot's `/apply` |
| 413 | `too_large` | The body is over 16 KB | |
| 429 | `rate_limited` | Too many requests; `Retry-After` gives the seconds to wait | Wait that long (the header's name read in any case; the game waits a day at most, and 30 seconds without one) |
| 500 | `internal` | The master failed | Try again at the next interval |
## Privacy and limits
| What | Rule |
| --- | --- |
| Servers' addresses | Kept 7 days after the server's last publish, for probes and abuse handling; never served unless the operator and the master both chose to; not in the master's log unless its operator asks (`MASTER_LOG_ADDRESSES`) |
| Applicants | Their Discord user id and username, seen by the master's admins and approvers (the log channel) only; no address is kept |
| Applications | A denied one: deleted 30 days after the denial |
| Servers' records | A closed server's: deleted an hour after it closed; one not published again: after 24 hours (unless an admin flagged it) |
| Contacts | Discord names (and the earlier HTTP applications' e-mail addresses) are seen by the master's admins and approvers only, and are not shared with peers |
| Strings | Printable ASCII, cut to the listing's lengths: name 32, map 32, gametype and variant 24, build 32 |
| Requests | 60 list requests a minute per address (the list, one server, a publisher's page, the list page and the metrics together); the application limits above; bodies 16 KB at most; the publish limits above. Every limit by address counts an IPv6 address by its /64 |
## Known limitations
A listing's host key is bound to the first publisher whose live server
publishes it, not to the host's own publisher: the listing does not say
which publisher key the host chose. A publisher that copies a listing from a
list while the host's publisher is not publishing (more than `ttl`) holds
the host key, and the host's publisher is refused `listing_taken`, until the
copy goes 90 seconds without a newer listing; one that keeps copying the
host's newest listings from other lists keeps it until an admin hides its
server or suspends it. The planned fix is in the listing: the host signs the
key id of the publisher key it publishes with into the listing (a new
listing format, with a new network version), and a master takes a listing
only from that key.
## Versions
This is protocol 1: `protocol` in bodies, `/v1/` in paths and
`OpenCE-Master-v1` in signed strings (followed by the receiving master's
key id). A change that an older client or
server would misread is a new version, with new paths; fields may be added
to version 1, and both sides ignore what they do not know. The listing's
own format follows the game's network version: a client hides entries
whose `network_version` is not its own, as it does for the brokers'
listings.
## Testing an implementation
`testdata/vectors.json` in the master's repository has, from fixed seeds:
three key pairs with their key ids (the publisher's, the master's, a
host's), a server id, a signed publish and a signed `GET /v1/me` to the
vectors' master (its key id, the canonical strings, the signatures, the
headers), a probe and its answer,
four listings (a dedicated team game, a password game in progress with its
sealed token, a tombstone, and an altered listing that must be refused), a
signed list body with its headers and ETag, a small-order key that must be
refused, and a proof of key. Monocypher's `crypto_ed25519_sign`,
`crypto_ed25519_check` and `crypto_aead_lock` give the same bytes.
`tests/conformance/opence_publisher.py` is a reference publisher in Python
(keys, signatures, listings, publishing, the probe's answer), and
`tests/conformance/test_master_protocol.py` runs a master through the whole
flow: an application (on the master's command line, since the Discord bot
is beyond a test's reach), the HTTP application's `410`, a pending
publish, approval, the probe, the listed
server's exact bytes in a signed list, closing, every refusal, and two
masters sharing an approval, a suspension and a revocation.
-178
View File
@@ -1,178 +0,0 @@
# Exact-matching methodology
This document records recurring January 2002/XDK 3911 matching failures and
the source-level changes that have actually moved them. It is an operational
checklist, not permission to accept approximate code. Exact status still
requires padded bytes, normalized bytes, relocation count/type/address/target/
addend, ABI, build, and regression evidence.
## Triage decision tree
Before guessing literals or reconstructing diagnostics, consult the
[common constants reference](common_constants.md) and
[shared assertion API](assertions.md). They identify existing owners and
target-specific representation pitfalls; they do not authorize numeric or
source-shape substitutions just because a value looks familiar.
1. Establish the untouched baseline: padded size, meaningful size, normalized
hash, relocation table, first differing instruction, and sibling status.
2. If relocation targets or addresses differ, recover source structure before
tuning registers: statement order, branch fall-through, linkage, field
ownership, and eager versus lazy evaluation.
3. If the target uses 16-bit arithmetic, map every promotion and truncation
boundary. Treat this separately from register allocation.
4. If instruction structure is identical modulo registers, locate a
hardware-forced anchor before changing declarations:
- `rep movs`/`rep stos` force ESI, EDI, and ECX;
- variable shifts force CL/ECX;
- multiply and divide force EAX/EDX;
- ABI/API boundaries may force ECX/EDX or preserve callee-saved registers.
5. For private same-translation-unit helpers, make callers exact first. Start
from the hardware-forced caller and propagate the convention toward the
helper; do not tune the helper in isolation.
6. Stop after five evidence-based source shapes if only an interchangeable
register or dependency-free instruction ordering remains. Record and park
the case instead of perturbing unrelated exact code.
7. Run the strict semantic ledger before assigning work. A function already
listed in `build/semantic_report.json` must not be reimplemented merely
because objdiff displays a fuzzy percentage.
8. When a target macro or expression is known from an exact donor, preserve
its **lvalue topology** as well as its arithmetic. VC7 can emit different
code for mathematically equivalent forms such as transforming a temporary
and transforming the destination member in place. Prefer the donor's
assignment destination, member access, and read-back pattern before
permuting commutative terms or adding artificial temporaries.
## Source-credibility gate
An exact object is necessary evidence, but exact bytes alone do not make a
reconstruction admissible. Retained source must express coherent program logic
and remain plausible for the original January 2002 codebase. Reject inert,
contradictory, undefined, or artificial source whose only credible purpose is
to steer VC7 into the target byte sequence. This includes unexplained empty
branches, self-assignments, fixed conditions, volatile or aliasing tricks,
synthetic lifetime anchors, raw instruction emission, and compiler-control
directives. A target-proven original bug is the narrow exception and must meet
the original-bug policy below.
Review semantics across all reachable inputs, not only the path that resembles
the disassembly. Corroborate unusual topology with January code/data/relocation
evidence, authenticated names or type records, first-party source, or a
credible contemporary analogue such as Marathon. When ordinary, meaningful C
does not reach exactness after the bounded methodology, retain the best honest
fuzzy reconstruction, record the residual, and park it.
`python tools/fake_match_scan.py` provides conservative lexical review leads
for several common smells. Its findings are neither proof of fake source nor a
substitute for manual semantic and provenance review, and a clean scan is not
proof of credibility.
## Blocker classes
### A. Recoverable source-shape mismatch
The emitted differences indicate that the reconstructed C is not yet shaped
like the original source. Productive levers include statement order, branch
polarity, local lifetime, explicit versus inlined expressions, linkage, and
structure ownership.
### B. Integer promotion/truncation mismatch
The target contains operand-size prefixes, partial-register operations, or
`movsx`/`movzx` boundaries that ordinary C promotions erase. Recover the width
of the accumulator and place signed/unsigned casts only at disassembly-proven
boundaries. Do not file this as a register mirror.
### C. Private-ABI/register fixed point
The instruction graph, sizes, and relocations agree, but a translation-unit
cluster assigns interchangeable registers differently. Find a forced-register
caller and solve outward. If the complete cluster remains a pure permutation
after the checklist, park it as a compiler allocation tie.
### D. Scheduler/lifetime tie
Two dependency-free instructions are transposed, or local lifetimes alter
spills without changing semantics. Try only source changes justified by the
surrounding target ordering. Do not add undefined behavior, inline assembly,
compiler pragmas, or flag changes to force the result.
### E. Comparator/reporting mismatch
The machine code is exact but COFF producers encode internal labels or
relocation placeholders differently. Credit only through the hardened
semantic comparator and the reviewed `config/semantic_matches.json` allowlist;
each entry is reverified against current objects during `ninja progress`.
### F. Credible but unclassified residual
The retained source can be semantically and historically grounded even when
the fail-closed classifier cannot distinguish a remaining source-width,
optimizer-context, register, or scheduling cause. Park such a case as
`unclassified` instead of inventing a specific mechanism. An unclassified
park carries no code-generation recipe and grants no exact credit; its ledger
must state the positive source evidence, the unresolved machine differences,
and the evidence required to reopen it.
## Case catalog
| Case | Class | Evidence and source lever | Outcome / next action |
|---|---|---|---|
| `lra_new` (`f0e5a48`) | A | Reordering field initialization to the target's emitted store order aligned the failure block and lifecycle setup. | Exact; make store-order recovery a standard first pass. |
| `object_new` (`claude/object-new-16`, then the isolated XDK scheduler-control lane) | A then D | Writing the cleanup as failure-first eliminated 10 relocation differences and two divergence regions. Only `fmuls` versus deferred `add esp, 0x18` ordering remained. A production-context reproducer then tested 20 one-factor legal-C controls; all forms that moved `fmuls` first also changed x87 dataflow or other bytes. | Preserve failure-first shape; park the dependency-free scheduler tie unless new source provenance identifies a distinct original expression. |
| `hashtable_remove` (`f2d30b9`) | A | Source-oriented comparison order, reload points, and the shared deletion path aligned 496 padded bytes and 20 relocations. | Exact; use comparison polarity and reload timing before register tuning. |
| `csstrcasecmp` (`e5852f8`, `claude/csstrcasecmp-15`) | A then C | `if (c1 != c2)` reproduces the target operand order; `if (c2 != c1)` does not. The remainder is an ESI/EDI parameter/local permutation. | Land only evidence-backed logic changes; handle the remaining mirror through a forced-register caller or park. |
| terminal tab stops (`a917110`) | A | Identical six-byte data still failed because `static` produced storage class 3 while the target symbol was external, class 2. | Exact after external linkage/name recovery; audit symbol class, not just bytes. |
| `generate_private_key` (`public_key_crypt.obj`) | A | Applying the exact `SWAP4` donor arithmetic to a local temporary selected the wrong accumulator form across hundreds of equivalent expression trees. Assigning the helper result to `private_key->dwords[i]` and applying `SWAP4` directly to that same member preserved frontend lvalue provenance and reproduced January exactly. | Exact without assembly, volatility, undefined behavior, or flag changes; preserve donor lvalue topology before classifying a commutative residual as a register tie. |
| `objects.obj` BSS ownership | A/E | Explicit zero initialization plus declaration order recovered the linked 13-byte layout (`object_name_list@0`, `object_memory_pool@4`, `object_globals@8`, debug byte `@12`) without changing any exact function. January PDB/public-symbol and cross-TU-reference evidence proves the first three globals are source-private; csplit had defaulted their manually split labels to external because `symbols.json` omitted `"static": true`. | Keep the globals source-static and mark exactly those three csplit symbols static. Regenerate the split target and require exact size, offsets, storage classes, and whole-TU regression; do not paper over the difference with a semantic allowlist. |
| `tif_packbits.obj` (`7c9c092`) | A | Target linkage, RCSID ownership, `_csmemcpy`, and disassembly-proven TIFF field offsets were required. Repository header layout differed from the target object. | Five functions and data exact; raw offsets are acceptable only with explicit target-layout evidence. |
| random-math globals (`aef5a7c`) | A | Replacing an anonymous address placeholder with the real externally linked 20-byte globals structure fixed BSS ownership and relocation addends. | Exact functions retained; model globals explicitly before function tuning. |
| `default_hash_function` (`f2d30b9`) | B | Target uses 16-bit product/partial-register code while natural C promotes the polynomial and key byte to 32 bits (80 target versus 64 candidate bytes). | Reopen only with a promotion/truncation lattice derived instruction by instruction. |
| LRA verify/free/lock cluster (`f0e5a48`) | C | Seven functions share cache-in-EBX versus cache-in-EDI and private helper conventions. Isolated helper shaping did not move the joint allocation. | Anchor on callers with forced registers, then propagate; otherwise park the whole permutation together. |
| hashtable probe helper `code_0010b270` (`073e794`) | C | Size, relocations, control flow, and operations agree; two return blocks mirror ECX and EDX. | Try return-materialization/single-exit shape once under the checklist, then park if unchanged. |
| `reference_list_copy` lane | C | Target and candidate reduce to the same operations with a callee-saved register mirror after source-shape attempts. | Revisit only from its caller cluster and a hardware-forced anchor. |
| `object_get_marker_by_name` lane | A | Recovering the direct object-header block access removed the artificial wrapper lifetime and reproduced the original call/dataflow structure. | Exact: 272 padded bytes, 11 relocations, and normalized SHA-256 `302db0f36677f304c6d076e0f16f3bb4e9603519b34d650a4311c296bd21c8cf`. |
| `object_compute_node_matrices` (`claude/object-compute-node-matrices` plus the E2 closeout) | D | The corrected C now has the exact 6,560-byte padded size, `0xA44` frame, and 318 relocation identities. The first address drift is relocation 245: January homes `tag_get_name`'s EAX result to `[ebp-8]`, while VC7 keeps EAX live. Independent source, PDB, tail-diff, and legal-C shape audits found no controllable semantic or structural difference. | Park as an EAX-liveness/register-allocation tie. Reopen only if new local-variable provenance or a demonstrated legal-C compiler control appears. |
| `data_decode_array` (`d0a735d`) | E | Padded bytes and 31 semantic relocations are exact; objdiff reports 85.19% because one target symbol plus addends corresponds to several rebuilt local labels. | Credited through the dynamic semantic ledger; no source work required. |
| 37-function switch/local-label set (`24c998a`) | E | Full dual sweep found 37 real functions and 21,051 meaningful bytes exact under conservative COFF normalization but fuzzy under objdiff. | The allowlist is reverified on every progress run; uncertain or changed entries fail closed. |
## Complexity gate
For large aggregate objects such as `objects.obj`, queue a remaining function
only when all of the following hold:
- padded size is below approximately `0x60`;
- local state is small and unambiguous;
- relocation count is low;
- no large inlined helper or private-ABI cluster dominates the function.
Larger functions require a specific leverage reason (new structure proof,
forced-register anchor, or reusable subsystem insight) before consuming an
implementation lane.
## External type-information gate
For the supplied `h1_tags` executables, start with the
[cross-build intake and missing-PDB identities](crossbuild_h1_tags_intake_20260906.md).
The `_symbols.exe` images contain external PDB references, not complete
embedded debug records. That ledger also records the IDA/Ghidra requirement
and separates analysis-generated names from original-symbol evidence.
HCEX, Halo PC, October 2001, and community sources are hypothesis generators.
Every imported field or enum value must carry a January-Xbox disassembly proof.
Unproven spans remain named `reserved`/`unknown` and documented as such. No CEA
offset enters a trusted header merely because it compiles or looks plausible.
## Original-bug policy
Exact builds preserve confirmed original behavior. Mark only defects supported
by binary and layout evidence:
```c
/* BUG (preserved for exact matching): <target behavior and evidence>.
* A corrected build should <safe replacement>.
*/
```
Keep the safe fix disabled in the exact build. Do not label suspicious code as
a bug without target-machine and structure-layout proof.
+329
View File
@@ -0,0 +1,329 @@
# Mods
A mod is a folder of Halo PC tag files that the game plays over a map's own
tags on your machine, as a resource pack does: sounds (a high-res audio
pack), bitmaps (a texture pack, a HUD redraw), text (a translation) and
models (higher-detail weapons, vehicles and scenery). Only the machine that
has a mod plays it. Nothing of a mod is sent to another machine, and a mod
changes nothing that the machines of a network game must agree on, so you
can play with mods in any game, with players who have other mods or none. (A host's mod sounds of other lengths change how long
the host's scripts and AI wait for dialogue to end; the host decides those
for every machine, so they still agree.)
The game's code is `port/linux/game/mods.c` (with `mod_sounds.c`,
`mod_bitmaps.c`, `mod_strings.c` and `mod_models.c`, one for each kind of
tag) and `port/linux/src/port_mods.c` (the mods folder and its files).
## A mod's folder
The mods are in the `mods` folder of the data root (the folder with
`maps/`; on Android the app's data folder, on the web the browser's storage
for the site). Each mod is a folder in it, with a `mod.toml` and a `tags`
folder:
```
mods/
hd_audio/
mod.toml
tags/
sound/sfx/weapons/assault rifle/fire.sound
sound/dialog/...
german/
mod.toml
tags/
ui/hud/hud_item_messages.unicode_string_list
```
The files in `tags` have the paths of the tags they replace: a tag's name,
then its group's extension, as the Halo Editing Kit and Invader keep them.
`tags/sound/sfx/weapons/assault rifle/fire.sound` replaces the sound tag
`sound\sfx\weapons\assault rifle\fire` of every map that has one. Names are
not case-sensitive. The game only looks up the names of the map's tags among
a mod's files: a mod cannot add tags, and no tag name of a map makes a path
outside the mod.
`mod.toml` says what the mod is. Every key is optional:
```toml
name = "High-res audio" # what the Mods menu shows (else the folder's name)
id = "hd_audio" # what other mods name it by (else the folder's name)
version = "1.0"
authors = ["Someone", "Someone else"] # or one string
description = "The game's sounds at 44 kHz"
maps = ["a10", "bloodgulch"] # the maps it is for, by their files' names (else every map)
load_after = ["hd_textures"] # mods it plays over: they load before it
load_before = ["fixes"] # mods that play over it: they load after it
incompatible = ["other_audio"] # mods it cannot be played with
format = 1 # the mods' format it is made for
```
`load_after`, `load_before` and `incompatible` name other mods by their `id`
or their folder's name, not case-sensitive. A mod they name that is not in
the mods folder is passed over. `maps = []` is every map.
A folder without `mod.toml` is not a mod. A mod made for a later `format`
than the build's, or whose `maps` names no map, is not played. The game
logs anything else wrong in `mod.toml` (an unknown key, a value of the
wrong type) and uses the rest; a `mod.toml` that is not TOML at all is
logged, and the mod played without what it says.
The game's log is its output: the terminal on Linux, `halo.log` beside
`halo.exe` on Windows, logcat on Android, and the page's log on the web.
Limits: 256 mods, folder names of 63 bytes at most, 262144 files in all, 24
folders deep under `tags`, and 64 maps in a mod's `maps`.
## The load order
Every mod in the mods folder is on until you turn it off. The mods load in
an order, and where two mods have a file for the same tag, the one loaded
later plays.
- The default order is the folders' names in alphabetical order (not
case-sensitive), except that the mods a mod must load after (those it
asks to load after, and those that ask to load before it) are taken to
just before it. Mods that ask in a ring (a after b, b after a) load
alphabetically, and the game's log says so.
- The main menu's **Mods** (in the PC version's menus, `display.menus =
"pc"`, the default) is laid out as the game lobby is: the mods on the
left as a resource pack list has them, the one at the top playing over
all those below it (the load order upside down: the mod loaded last is at
the top), and the chosen mod's details in the frame on the right (its
name, what is wrong with it, whether it is on, its priority, 1 the top,
and its version, authors, folder, maps and description). Choosing a mod
(A, or a click on the chosen one: the first click chooses it) turns it on
or off (ON, OFF); left and right, or **Move up** and **Move down**, move
the chosen mod (up: over one more mod). **OK** keeps the order and plays
it at once (on a map that is loaded, too); **Cancel** leaves it as it was.
- A warning sign beside a mod means it is on the wrong side of a mod it
asks to load before or after (both on): a mod that loads after another is
above it in the menu. A red cross means a mod that is on says it cannot be
played with this one, or that it cannot be played at all (made for a later
build, or its `maps` names none): it stays off. The details say which.
The menu keeps the order, and the mods turned off, in `mods/load_order.toml`,
by the mods' folders' names:
```toml
order = ["german", "hd_audio", "hd_textures"]
disabled = ["hd_textures"]
```
A mod added to the folder later is put where the default order puts it,
after the mod before it there (but on the side it asks for of the mods it
names), and is on. A mod gone from the folder is left out. When the file
cannot be written, the game says so in its log and keeps the order until it
quits.
### Mods that cannot be played together
When a mod that is on names another that is on as `incompatible`, the game
asks as it starts, before the window opens:
- **Play anyway** plays them all (closing the box does too);
- **Turn off ...** turns off the mods named as incompatible (if mod A says
it cannot be played with mod B, B is turned off), and keeps that in
`load_order.toml`;
- **Quit** closes the game.
The Mods menu turns them on again. On the web, **Quit** starts the page
again. A run nobody watches (`debug.hidden_window`, `debug.null_renderer`,
`debug.exit_after` or `debug.network_test`) is not asked: it plays them all,
and the game's log says which conflict.
At the console:
| Command | What it does |
| --- | --- |
| `mods_list` | Lists the mods in the mods folder in their load order, and which are on, then how many of the map's string lists, bitmaps, sounds and models the mods play. |
| `mods_reload` | Finds the mods again and reads their files again: a changed file is read again (one that now fails a check leaves what was played before), a new one is played, and the map's tag comes back for a file that is gone. If a sound changed, all sounds stop. It does nothing while `mods_enable false`. |
| `mods_enable false` | Plays the map's own tags until `mods_enable true` or the next map. |
These commands work in any game, a client's too: they change nothing of the
game.
### Getting a mod onto each platform
- **Linux, Windows**: put the mod's folder in `mods/` next to `maps/`.
- **Android**: put it in `mods/` in the app's data folder,
`/sdcard/Android/data/com.halo.decomp/files/mods`, for example with
`adb push hd_audio /sdcard/Android/data/com.halo.decomp/files/mods/`.
- **Web**: on the page, **Mods**, **Add a mod folder…** copies a mod's
`mod.toml` and `tags` folder into the browser's storage for the site.
**Remove** deletes it again.
## What a mod can replace
The game plays these groups from a mod. It does not read the files of any
other group (a `.weapon`, `.vehicle`, `.model_collision_geometry` or
`.scenario`, for example): they would change what the machines of a network
game must agree on.
| Group | Extension | What it is |
| --- | --- | --- |
| Sound | `.sound` | Sounds, music and dialogue |
| Bitmap | `.bitmap` | Textures, sprites, the HUD's pictures |
| Unicode string list | `.unicode_string_list` | The text the game shows |
| String list | `.string_list` | Other text |
| Model | `.gbxmodel` (Halo PC's), `.model` (the Xbox's) | What objects look like: weapons, first-person weapons and arms, vehicles, scenery, characters |
A file must be a Halo PC tag file of its group, as the Halo Editing Kit's
tools and Invader write them (a model also as `invader-extract` writes an
Xbox map's: `.model`). The game checks every count, size and offset
in it against the file, and every value against what the game reads, before
it uses the file. A file that fails a check is left out (the map's tag
plays), and `debug.txt` and the console say why.
### Sounds
- The samples can be 16-bit PCM, Xbox ADPCM or Ogg Vorbis, mono or stereo,
at 22 or 44 kHz. The game converts them to the Xbox ADPCM that it plays,
as it does for Custom Edition maps: mono at 22 kHz, stereo at its own rate.
- Empty values are filled in as a map's build fills them in: the defaults
of the sound's class, the playback rates and the bend.
- A sound must have permutations: a file with an empty pitch range (some
Halo PC translations have them) is left out.
- A permutation must fit in a block of the sound cache (one sound's samples:
`MAXIMUM_SOUND_DATA_SIZE`).
### Bitmaps
- A bitmap's types and formats are those the game draws: 2D, 3D and cube
maps; A8, Y8, AY8, A8Y8, R5G6B5, A1R5G5B5, A4R4G4B4, X8R8G8B8, A8R8G8B8,
DXT1, DXT3, DXT5 and P8 bump. Sides are 4096 at most (512 for a 3D
bitmap). A bitmap that is not a power of two on each side is drawn
linear, as a Custom Edition map's is: it must be an uncompressed 2D
bitmap.
- One bitmap's pixels may take a quarter of the texture cache: 64 MB on
Linux and Windows, 5.5 MB on Android and the web (a 2048 x 2048 DXT5 with
its mipmaps fits on all of them).
- A bitmap replaces the map's bitmap of its name, and the map's other tags
use it as the map's: it must have at least as many bitmaps, of the same
types, and as many sequences, each with at least as many sprites. It can
be larger: a redraw at twice the size is drawn at the map's size, sharper.
- The HUD draws a bitmap at its own size. So for each HUD element that draws
a mod's bitmap, the game scales the element by the map's size of what it
draws (its sprite, or with no sprites its bitmap) to the mod's, when that
is the same ratio for all of them. A bitmap the HUD draws that is not the
map's redrawn so (Halo PC's HUD bitmaps lay their sprites out to other
sizes) is left out. The HUD's digits can be the map's size or twice it.
- The menus draw a bitmap in their widgets' bounds, whatever its size.
- Halo PC keeps the channels of a model shader's multipurpose maps and of
the HUD's meters in another order than the Xbox. The game finds those
bitmaps among the mod's and draws them in the Xbox's order, as it does for
Custom Edition maps (`custom_edition_bitmaps.c`).
- The pixels are read from the file when the texture cache loads them, not
kept in memory.
### Text
- A list keeps the map's number of strings: a mod's strings past it are
left out, and the map's own strings stay where the mod's list is shorter.
- The game uses many strings as the format of what it prints
(`%s was killed by %s`). A mod's string replaces the map's only if it has
the same conversions in the same order (`%s`, `%d`, ...); otherwise the
map's string stays. A translation keeps the conversions of the original.
- Unicode strings are UTF-16, as the tool writes them.
- The main menu is the port's own (the PC version's menus, `port/assets/menus`),
whose text is in tags named `pc\...`. A mod's `ui\shell` lists change the
Xbox's menus (`display.menus = "xbox"`) and the text shown while playing
(HUD messages, the multiplayer messages, the pause menu).
### Models
A model is more than what is drawn: its nodes (its skeleton) are what its
animations move, its markers are where projectiles leave a weapon, where a
unit sits and where things attach, and its regions and permutations are what
damage and an object's variant choose among. The machines of a network game
agree on those, so a mod's model must have the map's:
- the same nodes: their names, hierarchy and default positions;
- the same markers: their names, regions, permutations, nodes, positions
and rotations (on the model's permutations, as the Halo Editing Kit keeps
them, or on the model, as a map does);
- the same regions and permutations: their names and flags.
Positions and rotations may differ from the map's by 0.001 at most (a model
exported again is not always bit for bit the map's). Only what is drawn is
the mod's: the geometries, their parts and vertices, which geometry each
permutation draws at each detail level, the detail levels' cutoffs, the
base map's scale, and which shaders each part is drawn with. A detail level
the map draws must draw one of the mod's geometries, and one the map draws
nothing at stays so. Every shader must be one the map has (a mod cannot add
tags); a mod's bitmaps can redraw them.
The model is checked against each map's own model of its name as that map
loads. On a map whose model of that name has other nodes, markers or
regions (a custom map's own assault rifle, say), or lacks a shader the mod
names, the mod's model is left out and the map's drawn; the mod's other
files still play there. `maps` in `mod.toml` keeps a mod to the maps it is
made for.
- Where a mod has both, its `.gbxmodel` plays; otherwise the file of the mod
loaded later, whichever its format.
- What the tool works out as it builds a map is worked out as it does: a
part's strip is its triangles block, its centroid's nodes are its most
weighted, its links of 0 to the parts drawn before and after it are none,
a base map scale of 0 is 1, and a model whose tag asks for it has the
normals of its vertices that share a position blended. The vertices are
compressed as the game draws them.
- A part's links to the parts drawn before and after it (for transparency)
must not go round.
- A model of 44 nodes or more (the renderer skins 43 at once; only Custom
Edition maps' have so many) is drawn a part's own nodes at a time, as a
Custom Edition map's is: a part's vertices may name 22 nodes at most.
- The collision geometry, the animations and the object's tag are the map's:
a larger weapon still hits and is hit where the map's does, and an object
is still left undrawn by the map's bounds of it (a much larger model may
vanish at the screen's edges).
- Every model made from a file is kept until the map changes, a file edited
and read again by `mods_reload` too, and counts against the mods' memory.
### Shaders
A model's parts can be drawn with a shader of every group Halo PC has but
one: `shader_transparent_chicago_extended`, which Gearbox added for Halo PC.
The Xbox game does not have it, so do not use it: use a
`shader_transparent_chicago` shader instead. (A Custom Edition map's
chicago extended shaders become chicago shaders as the map loads, keeping
only their four-stage maps: see
[custom_edition_caches.md](custom_edition_caches.md).) As for every
shader, it must be one the map has.
## Making a mod
1. Get the tags you want to change as tag files. `invader-extract` extracts
them from a map, and the Halo Editing Kit's tags are Halo PC's own.
2. Change them with the Halo Editing Kit or Invader (`invader-bitmap`,
`invader-sound` and `invader-model` build bitmaps, sounds and models from
images, audio files and JMS files). Keep each tag's name.
3. Put them in `tags/` of a folder in `mods/`, with a `mod.toml`.
4. Load a map (a new mod is on). `mods_reload` at the console reads the
files again as you change them, without loading the map again.
`debug.txt` has a line for every file the game leaves out, and why, and a
summary of what each kind of tag plays from the mods:
```
mods: 9 string lists from their files (9 new), 0 given back, 0 left out
mods: 66 HUD placements scaled to draw larger bitmaps at the map's size
mods: 354 bitmaps from their files (354 new), 0 given back, 6 left out
mods: 313 sounds from their files (313 new), 0 given back, 2 left out
mods: 4 models from their files (4 new), 0 given back, 1 left out
```
## How it operates
- When a map's tags load, at `mods_reload` and at the Mods menu's **OK**,
the game lists the files of the mods that are on, once, in a table of
their tag paths. Each of the map's sound, bitmap, string list and model
tags is looked up in it.
- A tag replaced by a mod takes the file's values in place, so everything
that holds the tag keeps it, and the map's own values come back when the
file goes. A sound playing or a texture in the cache is never changed
under the game: before a reload changes anything, the sounds stop and the
caches let go of what they hold.
- The mods' tags keep at most 512 MB in memory (192 MB on Android and the
web): their files, their sounds' samples, their bitmaps' tags and their
models' geometry.
- A Custom Edition map takes mods as an Xbox map does.
@@ -1,113 +0,0 @@
# objdiff v3.3.1 mis-scores MSVC data relocations
*2026-09-22, Lane C. Established independently three ways: by reading objdiff's
source at tag v3.3.1, by black-box perturbation of January's own object files,
and by auditing this project's own adjudication ledger. All three agree.*
## The defect
A data section's objdiff score is decided by its **relocations**. Two
relocations are compared by `reloc_eq` in `objdiff-core/src/diff/data.rs`, which
first tries `symbol_name_matches()`. That helper assumes a `$` in a symbol name
is the Metrowerks `symbol$1234` unique-ID separator:
```rust
if let Some((prefix, suffix)) = left_name.split_once('$') {
if !suffix.chars().all(char::is_numeric) { return false; }
...
}
```
Every MSVC mangled string literal contains `?$AA` — the encoded NUL. For example
`??_C@_0P@LFOALBOL@objects_update?$AA@`. The suffix after the first `$` is
`AA@`, which is not numeric, so **the helper returns false for two
character-for-character identical names**.
`reloc_eq` then falls through to `address_eq`, which requires the target literal
to sit at the same offset inside the *synthetic combined* `.rdata` that
`report generate` builds (it forces `combine_data_sections: true`, and
`do_combine_sections` concatenates every same-named section in COFF
section-table order, padding each to `max(align, 4)`). csplit lists January's
`.rdata` COMDATs in image order; CL emits ours in its own order. So the address
test fails too, and a byte-perfect relocation is scored as a mismatch.
That costs the section twice. Relocated bytes are counted twice in
`diff_data_symbol`
match_ratio = (bytes_ratio*len + relocs_ratio*reloc_bytes) / (len + reloc_bytes)
and, worse, the whole-section byte fallback in `diff_data_section` is gated
behind `if all_left_relocs_match`, so it never runs. Since `report.rs` credits
data all-or-nothing per section, one mis-compared relocation forfeits the
entire section.
The code path is immune: `diff/code.rs::reloc_eq` compares names with plain `==`.
Fixed upstream in **objdiff v3.6.0** (2026-01-25), which added
`get_normalized_symbol_name` in `obj/read.rs`; it returns `None` for
`??_C@...?$AA@` so `symbol_name_matches` falls back to exact equality.
## Reproduction
Copy January's `objects.obj`, swap only the two `.rdata` COMDAT section headers
for `_object_memory_release_procs` (24 B) and the `objects_update` literal
(15 B), fixing symbol section numbers; change nothing else. January against
itself scores `.data` 100.0; January against that header-swapped copy scores
`.data` 99.739235 — the exact production number. Swapping two COFF *symbol
table* entries instead leaves it at 100.0, so symbol-table order is inert.
## Scale, measured at HEAD
40 data sections / **32,262 bytes** have identical size, identical raw bytes and
an identical relocation multiset, and would score exactly 100.000000 under an
exact-name rule. Of those:
* **37 sections / 29,582 bytes are already credited** by
`config/semantic_data_matches.json`. No progress bytes are being lost there —
but they are credited as *adjudications*, and the standing rule is that
adjudicated does not count as closed.
* **3 sections / 2,680 bytes are genuinely uncredited**:
`source/ai/actions .rdata` (2,388), `source/ai/ai_communication .data` (276),
`source/ai/actions .data` (16).
## Recommendation
Upgrading the differ to >= v3.6.0 is the root-cause fix. It would recover the
2,680 uncredited bytes and let roughly 37 adjudications be retired, converting
29,582 bytes from adjudicated to genuinely exact. That is a shared build-tool
change and needs an owner decision, and it must be measured board-wide first,
because a differ upgrade re-scores everything.
Do **not** chase these sections with source edits. The only source-side lever is
which `.rdata` COMDATs a TU emits and in what order, so that a literal lands at
January's combined offset — and the target order is csplit's, i.e. the linked
image's layout, which is a link-time property. Reordering or inventing literals
to shift an offset fails the strip test.
Do **not** add new adjudications for them either; that papers over the
instrument. Two entries were retired on 2026-09-22 for the opposite reason —
`cheats .bss` and `tif_version .data` became genuine matches, so their
adjudications were deleted at no cost to credited progress.
## Triage rule this establishes
Run the January-against-January self-diff **before** spending any wave on data,
and **regenerate it whenever the differ changes**. Rewrite `objdiff.json` with
`base_path = target_path` and absolute paths, drop `custom_make`/`build_target`,
then `objdiff-cli report generate`; it takes 0.12 s over all 833 units.
Under v3.3.1 it capped 47 sections / 87,889 bytes. **Under v3.6.0 it caps
nothing at all** - 0 sections, 0 bytes. Every one of those "permanent" ceilings
was this same defect: a January-side string COMDAT comparing unequal to itself,
so even a byte-identical copy could not reach 100. The campaign treated 87,889
bytes as unreachable on the strength of a number produced by the broken
comparator. Do not quote the old figure, and do not kill a target with a stale
ceiling report.
## Resolution
Landed 2026-09-22: `sln.objdiff_tag` pinned to v3.6.0, and the 40 adjudications
the fix makes redundant retired in the same commit. Board effect, measured on
identical objects: raw data +34,886, raw code and functions unchanged, zero
units losing anything, credited data +5,296, adjudications 43 -> 3. v3.8.1 was
measured and rejected - same data gain, but it loses 6 bytes of code.
-91
View File
@@ -1,91 +0,0 @@
# Object exact-match strategy logs
For the completed/interrupted Opus 2 and new Fable 5 lanes, start with the
[current reconciliation queue](../reconciliation_queue_20260907.md). It separates
published progress, donor claims, dirty work, owner holds and tooling defects.
Every object that receives implementation or matching work must maintain one
Markdown ledger in this directory. The filename is the object name with
`_obj.md`, for example `players_obj.md`.
General techniques that apply across translation units belong in
`docs/exact_match_acceleration_playbook.md`; object-specific measurements stay
in the individual ledger.
Start constant and diagnostic recovery with the
[common constants reference](../common_constants.md) and
[shared assertion API](../assertions.md), before testing guessed literals.
The purpose is to make matching work cumulative. A new lane must be able to
distinguish an untried evidence-backed source shape from a spelling that has
already compiled to the same bytes.
## Required contents
Each ledger must include:
1. **Scope and provenance**
- source translation unit and target object;
- exact compiler identity and flags;
- target-build provenance used for names, layouts, or source topology.
2. **Validated baseline**
- exact/total function count and padded code bytes;
- exact/total data sections and bytes;
- for every residual: target/base size, relocation count, normalized hash,
and first meaningful divergence.
3. **Accepted controls**
- the source shapes retained in production;
- why each shape is supported by binary or provenance evidence.
4. **Experiment matrix**
- every meaningful source shape tested;
- resulting size, relocations, normalized hash, first divergence, and
sibling-regression result;
- whether it was retained or reverted.
5. **Do-not-repeat list**
- equivalent spellings or compiler-control attempts already exhausted;
- unsafe or prohibited techniques.
6. **Residual classification**
- source/topology, ABI/register, scheduling, ownership/relocation, data, or
unresolved;
- clearly separate measured fact from inference.
7. **Reopen criteria**
- the exact new evidence that would justify spending another lane.
8. **Disposition**
- strict exact and eligible for `Matching`, still active, or rigorously
parked;
- never grant credit from a fuzzy percentage, equal size alone, or semantic
plausibility.
## Lane rules
- Update the ledger after every bounded experiment batch, not only at handoff.
- Rebuild with XDK 3911 and the repository flags after each retained change.
- Gate every function and owned data section with the hardened comparator.
- Run the whole-translation-unit regression sweep before committing.
- Revert failed source shapes immediately.
- Treat exact bytes as necessary but not sufficient: reject semantically
nonsensical, historically implausible, logically meaningless, or inert
compiler-steering source, even when it happens to match. Coincidental byte
equality never authenticates the source. Park the best honest fuzzy
reconstruction when a natural exact spelling is not available.
- Do not use inline assembly outside a sparse, provenance-backed helper/math
exception. Never use volatile byte forcing, undefined behavior, object-byte
patches, or compiler-flag changes.
- A parked residual must preserve enough evidence for another agent to
reproduce the classification without redoing the full search.
- Label a defect `BUG (original)` only when January binary/source evidence
proves the defective behavior belongs to the original game. A mistake or
omission in our reconstruction is not an original bug. Preserve proven
original behavior in the matching build and document the corrected-build
alternative in a comment without changing the matching token/line shape.
- When retroactively documenting an old lane, state which experiment details
were not preserved. Never manufacture source variants, hashes, or outcomes
to make a historical matrix look complete.
## Minimal experiment row
```markdown
| ID | Source shape | Size T/B | Relocs T/B | Base hash | First divergence | Siblings | Decision |
|---|---|---:|---:|---|---|---|---|
| E01 | ... | `0x.../0x...` | `N/N` | `...` | `+0x...: ...` | `K/K exact` | reverted |
```
@@ -1,113 +0,0 @@
# action_alert.obj complete reconstruction and fuzzy closeout (2026-09-01)
## Result
`source/ai/action_alert.obj` advanced from five exact functions and seven
unwritten functions to eleven exact functions and one complete, documented
fuzzy residual. There are no unwritten functions left in the object.
The newly exact functions are:
- `action_alert_setup`
- `action_alert_flush_position_indices`
- `action_alert_flush_structure_indices`
- `distance_squared3d`
- `action_alert_update`
- `action_alert_perform`
The remaining private function was recovered as
`action_alert_next_position`; the address-derived symbol was removed from
`config/symbols.json` and the symbol remains translation-unit private.
## Evidence and donor audit
The reconstruction was checked against all Action Alert ledgers, repository
history, registered branches and worktrees, the January target disassembly and
relocations, and the later HCEA semantic sources under:
`research-cache/halocea-full-blobs-20260830/src/blam/ai/action_alert_*.c`
Pastudan's setup/update reconstruction was also inspected. Marathon contains
no direct Action Alert donor. No registered worktree contains a complete
definition of either `action_alert_perform` or `action_alert_next_position`,
and no farther candidate than the retained source was found.
The HCEA source and AI definitions corroborate the complete behavior:
- disabled/passenger, no-order, and no-encounter rejection;
- encounter, squad, and move-position traversal;
- current-position, actor-distance, noncombat-sequence, and nearby-prop
availability tests;
- cseries bit-vector tracking of unavailable positions;
- weighted random selection and forward, ping-pong, and per-tick-random
traversal;
- dwell-time selection, animation startup, and move-position state updates.
Action Alert prototypes now live in the closest owner header, `actions.h`,
rather than as ad hoc declarations in `actions.c`. Accesses use the project's
typed actor, unit, definition, tag-block, and scenario helpers. The source
uses semantic cseries types and enum constants and keeps explicit returns.
## Residual measurement
Focused gate after reconstruction:
```text
residual 784 _action_alert_next_position [size 768!=784, sha]
== exact 11 residual 1 unwritten 0 (of 12 listed)
```
Fresh full campaign-report similarity for the residual is `99.46479%`.
This value, rather than a separate aligned-disassembly score, is recorded in
`config/parked.json`.
- Target: 784 padded bytes, 18 relocations, normalized SHA-256
`677fd6cc1a9e66de32123c4614de5a0cc1188501baee69ea20fd5bf74eac3fe4`.
- Candidate: 768 padded bytes, 18 relocations, normalized SHA-256
`4e16c85b544ded3352df48ee7baa0e19b5a34a45e421a975cf11cd771184f371`.
Aligned instruction comparison is retained only as diagnostic evidence. It
shows that the geometry, prop-iterator, bit-vector, random-selection, and
ordered-traversal body is byte-identical. The remaining decoded differences
are limited to January preloading the `NONE` result in EAX, the resulting
AL/CL and EAX/ECX allocation choices in the entry guard, two equivalent
full-EAX versus AX `NONE` returns, and target-envelope padding.
## Bounded source-shape audit
Ordinary-C probes covered combined, separate, and enclosing entry guards;
short and long default-result lifetimes; declaration order; direct and
assigned random returns; natural `for` and `while` traversal; and switch
default/case arrangements. The retained `while` traversal reproduces
January's initialization schedule, and initializing the traversal direction
before a semantic enum switch with no redundant default reproduces January's
entire switch body. The remaining entry/result variants either returned to
the same compiler fixed point or disturbed larger exact regions.
No volatile or register forcing, compiler barrier, pragma, inline assembly,
fake dependency, raw structure offset, undefined behavior, forced inlining,
or nonsensical control flow is retained. The function is parked as
`register-allocation` and should reopen only for authoritative January source
or local-variable records, or a natural compatible-compiler donor that
explains the default-result lifetime.
## Validation
The admission run passed:
- full `ninja`, including report, semantic report, and progress validation;
- an independent strict per-function sweep against a clean build of base
`f73b19d30`: 5,026 exact to 5,032 exact, exactly the six Action Alert gains
above, with zero lost functions and zero unit-read errors;
- focused gates: Action Alert `11/12` exact with one parked residual, Actions
`58/63` exact with its five pre-existing parks, Units `189/189`, objects
`121/121`, and object types `31/31`;
- `python -m tools.parked_functions`: 78 active, zero stale, zero invalid;
- `python -m pytest -q`: 255 passed;
- semantic audit: 5,032 strict exact functions, 134 hidden exact
functions, and zero unit errors;
- fake-match scan of all changed C/header files: zero findings;
- object-admission audit: no contradictions or revocations (one unrelated
pre-existing `player_rumble` ownership-review candidate);
- all-unit `point_from_line3d` owner comparison against the base: no new or
lost COMDAT owner.
@@ -1,152 +0,0 @@
# `action_alert.obj`: `distance_squared3d` wrapper fixed point (2026-08-26)
## Outcome
No production source change and no matching credit. One evidence-backed
candidate disproved the locally renamed inline-wrapper topology for
`_distance_squared3d`: the candidate emitted 48 padded bytes, while January
and every exact donor emit 64. The source was immediately reverted without a
tuning compile, the committed five-function baseline was rebuilt, and its
whole-TU manifest is green.
`action_alert.obj` therefore remains 5/12 exact functions and 177/2,120
meaningful code bytes. The retained production commits remain:
- `020449c3b3518fd6bbc1dfbe739ab3a15762befa`
(`Match action alert geometry wrappers`);
- `5196b8e4b409c571abc6be5634d0d09bc90ac9e5`
(`Record action alert geometry validation`).
## Frozen target and baseline
The clean committed translation unit was frozen at `5196b8e4` in:
- `build/audit/action_alert_distance_squared_baseline_20260826.json`;
- manifest SHA-256
`f1ceaca1e7bd5c97bbac65192214909d4068cb9532af1f0cd29973938a3ec30c`.
The January target is:
- 49 meaningful / 64 padded bytes;
- zero relocations;
- normalized SHA-256
`f42fa39f289bfa50174aa61e77b345b092df8329d46f43f88ca127227ad1e9c8`;
- split-object SHA-256
`bb7a70aae881cdf5672ae48c3b9c5b7d0cdcc9a30a90730672f04b71fa1cab5f`.
The baseline source SHA-256 is
`0682c4b9fbf896d8434a1a8f6f78b355c2fabf20669601be3d25f64541fee93b`
and its Git blob is `8de8debb9ac7108acc7352c53e36e305f75d06f7`.
## Same-name donor census
The canonical nested body is owned by `source/math/real_math.h`:
```c
__inline real distance_squared3d(
real_point3d const *a,
real_point3d const *b)
{
real_vector3d v;
return magnitude_squared3d(vector_from_points3d(a, b, &v));
}
```
The read-only census script
`build/audit/find_action_alert_distance_donors_20260826.py` has SHA-256
`c8989f86e767ca76965766eace71c8f8a9d326d96b32545df14e219fd3a32e72`.
It found twelve rebuilt objects defining the same-name section, and hardened
comparison reports every one `all_equal: true` against the action-alert
target:
- `source/ai/action_fight.obj`;
- `source/ai/action_guard.obj`;
- `source/ai/action_search.obj`;
- `source/ai/action_uncover.obj`;
- `source/ai/actor_perception.obj`;
- `source/effects/material_effects.obj`;
- `source/game/game_engine.obj`;
- `source/objects/objects.obj`;
- `source/physics/breakable_surfaces.obj`;
- `source/physics/collisions.obj`;
- `source/physics/physics.obj`;
- `source/units/units.obj`.
Direct hardened comparisons against both `action_fight.obj` and `units.obj`
independently prove the 64 padded bytes and zero-relocation inventory. The
donors obtain the COMDAT from a real call to the canonical header inline;
none supplies an ordinary out-of-line source definition that can be
transplanted verbatim.
## Sole candidate and rejection
The candidate locally renamed `distance_squared3d` across the header include
and added the ordinary wrapper:
```c
real distance_squared3d(
real_point3d const *a,
real_point3d const *b)
{
return distance_squared3d_inline(a, b);
}
```
This is ordinary C89 and obeys all formatting and return rules. Exactly one
normal VC7 compile was spent. The preserved first candidate is:
- `build/audit/action_alert_distance_squared_first_current_tu_20260826.obj`;
- raw SHA-256
`c20dfa1ae881e1fae04d85b0a1f8e8911c837ece78b17bc33f64bcac6d5734c6`.
Its `_distance_squared3d` section is:
- 48 padded bytes;
- zero relocations;
- normalized SHA-256
`3e3811d46757035eda49d4b9374d60a716646e589c018726d37a8e0ae2e3564a`.
The wrapper therefore changes the compilation context enough for VC7 to
collapse the canonical nested operation into a shorter form. It is not the
same emission topology as the donor COMDAT. The candidate whole-TU gate also
fails closed on the additive symbol/debug inventory and records
`_distance_squared3d` in `changed_nonexact`. Direct comparison proves the five
accepted bodies themselves remained byte-exact, but the manifest failure is
independently sufficient to reject the candidate.
No second spelling, hand expansion, compiler option, object permutation,
allowlist, adjudication, exception, synthetic caller, or byte-forcing form was
compiled.
## Restoration
The candidate source was removed with `apply_patch`, returning
`source/ai/action_alert.c` byte-for-byte to the committed blob. The generated
object path was resolved inside the authoritative worktree, that single object
was deleted, and one restoration build was run. Its nondeterministic raw
SHA-256 is
`28105c5cf2ead523cf5e98dfb87afb09330164d4e5cb233f3c776167345f92b3`.
The frozen manifest then reports:
- `ok: true`;
- no failures or warnings;
- `changed_nonexact: []`;
- `_action_alert_begin`, `_action_alert_control`,
`_vector_from_points3d`, `_magnitude_squared3d`, and
`_real_random_range` all `still_exact`.
Direct hardened comparison of the same five functions remains
`all_equal: true`. No protected path changed, no tracked path was deleted,
and nothing was pushed.
## Reopen criterion
Do not retry the renamed-inline wrapper. Reopen `_distance_squared3d` only
with genuinely new evidence that reproduces the donor COMDAT emission context
without a synthetic caller—for example an authenticated ordinary definition,
a grounded VC7 inline/COMDAT topology explanation, or another natural
translation-unit use that makes the canonical header definition emit under
the function's real ownership. A direct ordinary copy of the canonical body
is a distinct unmeasured hypothesis, but it is not justified for another
production compile merely by the failed wrapper result.
@@ -1,124 +0,0 @@
# `action_alert.obj` Jonas prefix recovery
This Jonas/Codex ledger records a bounded, fail-closed recovery of the five
requested prefix bodies in `source/ai/action_alert.obj`. Only
`_action_alert_control` was independently strict on the first natural
candidate, so it alone is retained. The object advances from 2/12 to 3/12
exact functions and remains `NonMatching`; no data or whole-object credit is
claimed.
## Scope and provenance
- Authoritative integration base:
`7d2ff479dde012f94cc08857e4cc74650b85b9af`.
- Baseline `action_alert.c` blob:
`45f0eae365e5f6e087507143bd68549c676ab736`.
- Retained `action_alert.c` blob:
`01c9af4f179f4fc7e3792413c7a18cf3d7899c5a`.
- Retained source SHA-256:
`d10627b7393f20bd86037df3da279333b2c0e99c0a7adb24d77320e5865d5865`.
- January executable SHA-256:
`4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`.
- January split-object SHA-256:
`bb7a70aae881cdf5672ae48c3b9c5b7d0cdcc9a30a90730672f04b71fa1cab5f`.
- The raw candidate-object hash is deliberately omitted because the XDK COFF
container timestamp changes across forced builds. The normalized function
hashes below are the reproducible admission identities.
- Clean HCEA semantic donor commit:
`c168af2e747d3095d9a29418ae401f3a39544863`. The screened source blobs
are `a8494315eb9fbbc0b57fe9ae169b76f5203044fe` (setup),
`bdc904682c5a2ba16a90f308cfeef79c97be2a68` (control),
`16f1703d50e1daa0b8f9fa15a6d5aad76c3a484d` (flush position),
`3018bec7791130ca7a11f95f320d0c05cf257f81` (flush structure), and
`c4f463bf4613fc0240debabaa8ad40cc494eb252` (update).
HCEA supplies semantic names and natural control-flow evidence. The January
COFF remains the sole authority for bytes, padding, relocations, and symbolic
destinations.
## One-shot strict result
The five requested bodies were compiled together once with the repository's
natural XDK 3911 command. Each body was compared independently with the
hardened COFF comparator. The four nonexact bodies were then removed without
another source-shape experiment or any code-generation tuning. The later full
build and forced regression are validation replays of this fail-closed final
source.
| Function | Target meaningful | Target padded | Relocations | Target normalized SHA-256 | First candidate result |
| --- | ---: | ---: | ---: | --- | --- |
| `_action_alert_setup` | 126 | 128 | 7 | `225015f380c74c1a19d563730b7820375cd37490eb40cb6d67e3404bc46c9cc0` | rejected: 144 padded, 7 relocations, SHA `47b3dc2358c61240e9d48276b9d00caf88d8384cfc505e764f2929cf32ba7491` |
| `_action_alert_control` | 73 | 80 | 3 | `8be5d8f243b76cbf9dd2c4f7f35b4509c349bc970a9b2fc1518a71e3e6a1fbb3` | **retained: strict exact** |
| `_action_alert_flush_position_indices` | 40 | 48 | 2 | `02db3d8b0b933d6e26b970c3c7a3639e7005e78304692915295d835f61eff579` | rejected: 48 padded, 2 relocations, SHA `90abe46d68627d1a83f1a5a89d4d7ecd6d86b0ee14b514917b19c73f96d17335` |
| `_action_alert_flush_structure_indices` | 39 | 48 | 2 | `1b6fb9035cd64ac3bd1a66e1be208f3dc09ea95d32ef17f960e5bd960540a466` | rejected: 48 padded, 2 relocations, SHA `75fef43de1d32d255e61481ac5402bab1d683f3c801fb2a252438d8ba603434d` |
| `_action_alert_update` | 323 | 336 | 13 | `0b4be7a9c0704dc3075a31fabf85a1f5ab7868be9d5fc3676685252d0aeaaa43` | rejected: 336 padded, 13 relocations, SHA `13ed6e0ef99086fe5eb49463dfa54e086057c8155982114cd78ea8e212608dea` |
The retained control body is exact in padded size, every normalized byte, and
all three relocation addresses, types, destinations, and addends. It uses the
established typed `actor_get` and `actor_definition_get` accessors, sets the
January-proven noncombat idle-look value `1`, tests the named actor-definition
bit 6, and sets both crouch Booleans when that bit is active.
The wave adds 73 meaningful and 80 padded exact code bytes. Together with the
pre-existing exact `_action_alert_begin` and `_real_random_range`, the final
object measures 3/12 exact functions, 101/2,120 meaningful code bytes, and
128/2,240 padded code bytes. The preserved functions were replayed directly:
| Preserved function | Meaningful | Padded | Relocations | Normalized SHA-256 |
| --- | ---: | ---: | ---: | --- |
| `_action_alert_begin` | 1 | 16 | 0 | `499f1f307c1cb989f968a6b7fcaec591e1828877223d0b0e7e8e8b76cde8c9ca` |
| `_real_random_range` | 27 | 32 | 2 | `7ed2e50e8e1a701a98fcceeb6a6b67e05d997f21bdb9ff5ee2e848fd1a8f409a` |
## Fail-closed omissions and policy boundary
The setup, both flushes, and update definitions are absent from the final
candidate. The two large target-only bodies `_code_00000950` and
`_action_alert_perform` were never added or compiled in the candidate lane.
The rejected update's temporary local 0x3C animation-reference view and its
scenario, object, and unit includes were removed with the body. No shared
header, tag-definition storage, configuration, semantic exception, parked
record, or completion label changes.
The retained source is readable typed C with vertically formatted parameters
and an explicit terminal `return;`. It contains no assembly, raw object-header
access, pointer or union pun, inactive-union access, integer-address
reconstruction, volatile scheduling device, force-inline annotation,
optimizer pragma or barrier, synthetic anchor, byte-forcing expression, or
undefined-behavior match.
The target owns 80 ordinary non-code bytes; the final candidate owns none and
receives zero data credit. The existing exact begin and random-range bodies
are textually unchanged.
## Validation
- Complete `halobetacache_build` and `libcmt_build`: pass.
- Direct hardened comparison: retained control plus both preserved functions
pass with exact normalized bytes and relocations.
- Semantic audit: 470 units, 3,995 functions evaluated, 3,855 semantic exact,
101 hidden exact, 36 ordinary-only, 3,916 accepted exact, and zero unit
errors.
- Campaign progress: 375/833 complete objects, 3,903/11,060 exact functions,
472,316/2,198,102 exact code bytes, and 1,835,088/4,176,062 matched data
bytes.
- Halo progress: 273/468 complete objects, 3,736/7,574 exact functions,
459,402/1,770,166 exact code bytes, and 1,829,896/3,923,451 matched data
bytes.
- Object-admission audit: zero candidates and zero revocations; only the
inherited `source/shell/shell_xbox` completion-label contradiction remains.
- Parked-function audit: three active, zero stale, and zero invalid entries.
- Complete tooling suite: 179/179 tests pass.
- Clean committed-state forced single-unit regression snapshot/check: all
three accepted functions remain exact, with no failure or ownership change.
- `git diff --check`, deleted-path, policy-token, and frozen-scope audits:
pass.
The five frozen large-object sources are byte-identical to the integration
base: `vehicles.c` (`4a01dc7c54717e1b063a0cac72b0178b4bb2bf04`),
`ai_debug.c` (`517731845431f1e240b06742ab9ddfbf6e67e929`),
`ai_script.c` (`c3c858af2e4d6bf1df6dccf013b9ba6ee09f332d`),
`actions.c` (`b594c1d52afe683e3fc794be08bf40e2b7045e26`), and
`units.c` (`19a15c4a3a47849ffd8ba0c13989c0c389452519`).
Only `source/ai/action_alert.c` and this new Jonas ledger are tracked changes.
No tracked file is deleted, no object is marked complete, and nothing is
pushed.
@@ -1,212 +0,0 @@
# `action_alert.obj`: safe geometry pair (2026-08-26)
## Outcome
Two ordinary geometry helpers are newly strict-exact:
- `_vector_from_points3d`;
- `_magnitude_squared3d`.
The wave adds two exact functions and 76 meaningful code bytes.
`action_alert.obj` advances from 3/12 to 5/12 exact functions and from 101
to 177 matched meaningful code bytes. It remains incomplete; no completion
label, semantic exception, adjudication, data credit, or comparator waiver was
added.
Source commit:
- `020449c3b3518fd6bbc1dfbe739ab3a15762befa`
(`Match action alert geometry wrappers`).
## Frozen evidence
The untouched translation unit was frozen at commit
`fe2e8631136da957ce6fe286fde456a480cbc2ba` in:
- `build/audit/action_alert_safe_geometry_pair_baseline_20260826.json`;
- manifest SHA-256
`769e1bda19900e12f7c3622d1e4f6632debc1ef84b5fcc6fe42ca72ce10c089d`.
The target functions have these authenticated January fingerprints:
| Function | Meaningful / padded bytes | Relocations | Normalized SHA-256 |
| --- | ---: | ---: | --- |
| `_vector_from_points3d` | 38 / 48 | 0 | `905f56dbe4597d2bacc201456ffc33464b166e0b6392dda24a1a3e109b637aa2` |
| `_magnitude_squared3d` | 38 / 48 | 0 | `0d04579096eeab3a1c45f87774e1eb20ce4856dbac219f624a855e8c9ae99725` |
The canonical bodies and ABIs are fixed by `source/math/real_math.h`.
Direct comparison against the exact same-name definitions in
`build/base/source/math/real_math.obj` proves all 48 padded bytes of each
function. The repository-wide same-name census also found 25 exact donors for
`_vector_from_points3d` and 33 for `_magnitude_squared3d`. Both target
sections have zero relocations, so there is no unresolved callee, owner, or
addend hypothesis. This is a same-name, same-ABI transfer rather than a
masked-shape guess.
`_distance_squared3d` was deliberately excluded. Its target section is 49
meaningful / 64 padded bytes with zero relocations and normalized SHA-256
`f42fa39f289bfa50174aa61e77b345b092df8329d46f43f88ca127227ad1e9c8`,
but its canonical source nests another helper. That makes definition order and
inline topology a separate evidence question; it was not bundled into this
independent two-function wave.
## Claude documentation and tooling audit
All locally reachable Claude records that name this object or the applicable
matching tools were reconciled before candidate selection.
- The Claude residual census at
`C:/halo-worktrees/claude-near-complete-objects-20260816/research/residual_frontier.json`
lists the three geometry functions only as January `target_only` sections.
It records no candidate, residual measurement, source experiment, or fixed
point for the selected pair. The file SHA-256 is
`db87717092d2e7c71160619e9df25f0b940f1762725c81f8bd2d6ece77fbde0d`.
- No reachable `claude/*` branch changes `source/ai/action_alert.c`. The
current history contains the prior Jonas/Codex prefix, control, and random
range recovery only.
- The Claude copies of `docs/matching_methodology.md` and
`docs/shape_transfer_executor_strategy.md` are byte-identical across the
near-complete, finish-hs, and untried worktrees and to the current files.
Their SHA-256 values are respectively
`3eb52f6b2df7b94b4d732161c0f86e2becebbcf0b546824deb957882118b1b5c`
and
`072ee7b2b3a2900e401bec7f69bff475f57639456d9c6270d2ea51189aaefa83`.
- Claude's `docs/house_rules.md`, SHA-256
`9294b81dc2e1c92d75ffd2f9f289287131bdba3564ba95bb054b08189781bfe1`,
requires ordinary C, canonical vector expressions, and evidence before a
compile. The retained component expressions are the canonical header forms
and are corroborated by exact same-name donors.
- Claude's flag-provenance census confirms this TU uses the normal
`/O2 /Oy- /DDEBUG /Dxbox` profile and contradicts `/QIfist` as a whole-TU
alternative. No flag change was considered or used.
- The Claude campaign README identifies
`tools/coff_compare.section_infos_equal` as the acceptance gate. The
current hardened comparator and whole-TU regression manifest were used
directly; no in-memory edit harness, alias adjudication, or comparator
exception was used.
The earlier local record
`docs/object_matching_logs/action_alert_obj_jonas_prefix_recovery.md` was also
read in full. Its one-shot covered setup, control, the two flush helpers, and
update; only control survived. It did not compile or measure either helper in
this wave, so no fixed point was reopened.
## Candidate discipline
The two inline identifiers are locally macro-renamed across the include block,
then ordinary out-of-line definitions are appended after the already accepted
random-range wrapper. `action_alert.c` has an empty private-code tail, so the
append preserves every existing function section index. The macros only avoid
duplicate C definitions; they do not alter optimization, calling convention,
or emitted instructions.
The one normal VC7 candidate compile was strict-exact for both functions on
the first attempt. There was no object permutation, compiler-flag change,
allowlist, adjudication, comparator exception, object patch, intrinsic,
alignment directive, synthetic anchor, or byte-forcing expression.
The preserved first candidate is:
- `build/audit/action_alert_safe_geometry_pair_first_current_tu_20260826.obj`;
- raw SHA-256
`76977841b7e1bac6c02bf3ca86ca56785c5d47a6f3d55bda7019847d270e0ad4`.
## Exact comparison and frozen gate
The hardened comparator reports `all_equal: true` for both additions and all
three previously accepted functions:
- `_vector_from_points3d`;
- `_magnitude_squared3d`;
- `_action_alert_begin`;
- `_action_alert_control`;
- `_real_random_range`.
The frozen gate reports exactly the pair as `newly_exact`, all three inherited
functions as `still_exact`, and `changed_nonexact: []`. Its sole hard finding
is the expected additive symbol inventory from defining the two functions.
## Clean committed state and replay
The clean committed manifest is:
- `build/audit/action_alert_safe_geometry_pair_committed_20260826.json`;
- commit `020449c3b3518fd6bbc1dfbe739ab3a15762befa`;
- SHA-256
`940551915655b88e2affb548bb9e62d687ec109d672b4d02dde0a79cd85af98f`.
Its post-rebuild check returns `ok: true`, no failures, no warnings, no
changed nonexact functions, and all five functions above as `still_exact`.
The generated object path was resolved inside this worktree. That single file
was deleted and rebuilt once from the committed source. Raw base-object hashes
were:
- before replay:
`76977841b7e1bac6c02bf3ca86ca56785c5d47a6f3d55bda7019847d270e0ad4`;
- after replay:
`c7646f7851aa760a7de32c2b52f9b391137eae2ede49cb028cb74740e9810acb`.
VC7 debug/build records are nondeterministic, so raw object hashes are not the
equality oracle. The post-replay manifest is green and direct hardened
comparison remains `all_equal: true`.
Pinned artifacts:
- target split object:
`bb7a70aae881cdf5672ae48c3b9c5b7d0cdcc9a30a90730672f04b71fa1cab5f`;
- source file before/after:
`84e4f2c3591554c6d34dc0701173bbfb11fb137ff4053a571cf3a55041d252ad` /
`0682c4b9fbf896d8434a1a8f6f78b355c2fabf20669601be3d25f64541fee93b`;
- source blob before/after:
`01c9af4f179f4fc7e3792413c7a18cf3d7899c5a` /
`8de8debb9ac7108acc7352c53e36e305f75d06f7`;
- canonical report:
`932d130576c91043f603dec2712ad9afb5dc9bfc98a1d5a991af3ebf482ca4f6`;
- semantic report:
`33c07f434de44fc536e8ba0ddbf98b3d4eb0b9056fe243e84dceed5ec6721bde`;
- VC7 compiler:
`483e00c47bb08d699475a642bcff15b5b2036350b31c540e88a506baf101da11`;
- hardened comparator:
`411e41698a9491a7b69430307d03f844ee3144633e5d3038d3ffde81a8eccfb7`;
- regression gate:
`d09b46dd2331ff7ca677cad78a66c780c4effa41a5389f0c614c9760d7632a5f`.
Supporting audits:
- admission SHA-256:
`f0aa45c22cc604fb74abc388b06dd79a27e7a0b498a5d28837a9e785680c9a36`;
- parked-frontier SHA-256:
`cfac03aea5be607c19a6a40bcdbc1164e71ffb0179602af4bd37b3fd50e26d1d`.
## Canonical progress and gates
After regeneration:
- canonical code: 508,806/2,198,102 bytes and 4,215/11,060 functions;
- complete files: 377/833;
- Halo category: 495,892/1,770,166 code bytes and 4,048/7,574 functions;
- Halo complete files: 275/468;
- semantic audit: 470 units and 4,323 functions evaluated, 4,180 semantic
exact, 114 hidden-exact functions / 64,705 hidden bytes, 4,241 accepted
exact, and zero unit errors.
Repository gates:
- object admission: zero candidates and zero revocations; only the inherited
`shell_xbox` completion-label contradiction remains;
- parked validation: three active, zero stale, zero invalid;
- tooling tests: 179/179 pass;
- Halo and libcmt dry graphs: no work;
- protected-path touches: zero;
- deleted tracked paths: zero;
- no push.
## House-rule audit
Every parameter is on its own line and both functions end with explicit value
returns. The retained source is C89. It contains no assembly, `register`,
`volatile`, barrier, pragma, explicit intrinsic, attribute, raw address,
pointer/integer reconstruction, pointer or union pun, undefined behavior,
synthetic anchor, or byte-forcing form. These functions perform no tag or
object access, so the typed-access rules are not implicated.
@@ -1,119 +0,0 @@
# `action_alert.obj`: setup store-order one-shot rejection (2026-08-30)
## Scope and result
This lane tested one independently sourced ordinary-C shape for
`_action_alert_setup` and no other residual owner. The candidate was rejected
after its first compile. The source was restored immediately; the object
remains **5/12 exact** and **224/2240 padded bytes exact**.
The five inherited exact owners were rechecked after restoration:
- `_action_alert_begin`
- `_action_alert_control`
- `_vector_from_points3d`
- `_magnitude_squared3d`
- `_real_random_range`
## New premise
The fresh premise came from the readable Pastudan history at commit
`1146f5b799111ab72f2f5e37303a4c050945b88b`, file
`src/halo/math/vector_math.c` (the action-alert implementation around lines
368-388 in that snapshot). Unlike the previously tested HCEA-derived shape,
it preserves the January target's post-`csmemset` field-store order:
1. conditional `move_position_order`
2. `target_move_position_index = NONE`
3. `pending_move_position_index = initial_move_position_index`
4. `move_position_increasing = TRUE`
5. `wait_ticks = 0`
6. `must_play_animation = FALSE`
The canonical declaration was retained unchanged:
```c
boolean action_alert_setup(
long actor_index,
short move_position_order,
long initial_move_position_index,
struct alert_state_data *state_data);
```
This was deliberately only a store-order adoption, not the complete Pastudan
lift. The Pastudan snapshot's recovered formal sequence at line 372 is
`int actor_handle, int value, short field, void *state`: its second argument is
full width and its third is narrow. This lane retained the canonical
`long, short, long, pointer` declaration rather than changing a cross-TU ABI
as part of the source-shape test.
The body used typed fields, `actor_get`, the line-35 `state_data` assertion,
`csmemset(state_data, 0, sizeof(*state_data))`, and the store sequence above.
It used no inline assembly, `volatile`, `register`, pragmas, raw structure
offsets, type punning, or byte-forcing constructs.
## Strict comparison
Only `build/base/source/ai/action_alert.obj` was scheduled; the Ninja dry run
showed exactly one `CL` edge. The first and only candidate compile produced:
| Measurement | January target | Candidate |
|---|---:|---:|
| Padded section size | 128 | 128 |
| Relocations | 7 | 7 |
| Normalized SHA-256 | `225015f380c74c1a19d563730b7820375cd37490eb40cb6d67e3404bc46c9cc0` | `daee1bee0eae9da35692ad6ae86ae233ed996083745a65e1f1303842181b29c2` |
All seven relocation offsets and destinations were identical. The first
normalized byte divergence was at section offset `+0x55`: target `0x03`,
candidate `0x04`; 40 normalized bytes differed in total. The local contexts
were:
- target: `0c84c0740433c0eb038b450c668b55106689065f66c74606`
- candidate: `0c84c0740433c0eb040fbf450c668b55106689065f66c74606`
Decoded at `+0x54`, the target has `eb 03 8b 45 0c`: it jumps over a
three-byte `mov eax, dword ptr [ebp+0x0c]`. The candidate has
`eb 04 0f bf 45 0c`: it jumps over a four-byte sign-extending word load. The
first miss therefore reflects the retained `short move_position_order`
formal, not a difference in the Pastudan-derived post-memset store sequence.
This is recorded as provenance evidence, not used as a tuning instruction.
The earlier rejected setup candidate was 144 padded bytes with seven
relocations and normalized SHA-256
`47b3dc2358c61240e9d48276b9d00caf88d8384cfc505e764f2929cf32ba7491`.
The new provenance therefore improved the section extent and relocation
topology, but strict byte identity still failed.
## Restoration and boundary
The setup body was removed immediately, the baseline object was rebuilt only
to restore it, and `tools/coff_compare.py` reconfirmed all five inherited
owners exact. The restored source blob is
`8de8debb9ac7108acc7352c53e36e305f75d06f7`; the January split object SHA-256
is `bb7a70aae881cdf5672ae48c3b9c5b7d0cdcc9a30a90730672f04b71fa1cab5f`.
Do **not** tune this Pastudan store-order candidate from the observed byte
differences and do not retry it. Reopen `_action_alert_setup` only with a new,
independent prototype/source-shape adjudication. A defensible future premise
would need to establish the likely `long, long, short, pointer` prototype and
its cross-translation-unit impact in `source/ai/actions.c`; the byte difference
alone is insufficient. The two flush helpers,
`_action_alert_update`, `_distance_squared3d`, `_action_alert_perform`, and
`_code_00000950` were not compiled in this lane. In particular, current HCEA
flush topology is not a new enough premise to override their existing
fail-closed boundaries.
## Documentation and tooling consulted
- `docs/matching_methodology.md`
- `tools/campaign/README.md`
- `docs/object_matching_logs/README.md`
- the three earlier `action_alert.obj` ledgers
- repository `CLAUDE.md` guidance from the canonical, Stian, and Pastudan trees
- January `build/split/source/ai/action_alert.obj`
- `tools/coff_compare.py` and `tools/campaign/board.py`
- current HCEA action-alert history and the Pastudan snapshot named above
The Discord note that Bungie's fast `ftol` may have been handwritten applies
only as Matrix Math provenance. It did not alter this lane's ordinary-C-only
admission rules.
@@ -1,193 +0,0 @@
# Action Alert: restore genuine math-header ownership (2026-09-05)
## Disposition
Root has applied the single source-only normalization packet to canonical
Action Alert. The actual full canonical Ninja build passes, and the complete
stable sweep remains 6,217 exact functions out of 8,245, with zero losses.
Root's actual all-section comparison and full tooling/tests also pass.
The actual Ninja object is runtime-identical to the reviewed PID candidate:
SHA-256 `c15f370e118de1a78c8cbcab7e3d4cd07f255d2c50af6c4b81e7c2cf391b46da`.
The exact baseline-to-actual changes are only the three selection transitions
and two unreferenced alias removals documented below. All 281 parks validate,
the admission audit has no contradictions or revocations, and 288 tests plus
26 subtests pass. The pytest cache permission warning affects only caching.
Root evidence: `scratch/math-owner-schema-actual-action_alert-20260905.json`,
`scratch/math-owner-schema-actual-summary-20260905.json`, and
`scratch/math-owner-schema-canonical-first-20260905.json`.
The first natural candidate retains all
11 strict exact functions, the identical existing complete residual, all six
runtime data owners, and every gameplay ABI and instruction. No exact bytes
are gained or lost. The object remains 11 exact / 1 residual / 0 unwritten,
not whole-object Matching.
This is an ownership correction, not a new gameplay or residual experiment.
Canonical production source, headers, configuration and build outputs were
not changed by this worker.
## Source provenance and prior adjudication
Read in full before the test:
- `docs/matching_methodology.md` and `tools/campaign/README.md`;
- `action_alert_obj_jonas_prefix_recovery.md`;
- `action_alert_obj_jonas_safe_geometry_pair_20260826.md`;
- `action_alert_obj_jonas_distance_squared_fixed_point_20260826.md`;
- `action_alert_obj_jonas_setup_store_order_rejection_20260830.md`;
- `action_alert_obj_complete_fuzzy_closeout_20260901.md`;
- `action_charge_obj_jonas_math_helper_recovery_rejection_20260826.md`;
- `action_charge_obj_jonas_math_helper_recovery_v2_20260826.md`;
- `action_charge_obj_jonas_real_random_recovery_20260826.md`;
- `game_sound_obj_obstruction_current_owner_boundary_20260905.md`.
The geometry-recovery ledger explicitly introduced local pre-header aliases
and duplicate ordinary external math bodies to add selected code owners while
the real gameplay callers were still unwritten. It described the macros as
duplicate-definition avoidance; they nevertheless changed ordinary COMDAT
ownership and now materialize two additional alias-named helper bodies.
The complete 20260901 reconstruction introduced the genuine gameplay uses.
The old distance fixed point tested a fourth renamed wrapper, not removal of
the existing alias layer to restore normal header topology. The old setup and
next-position source-shape exhaustion is unrelated and is not reopened.
No prior natural owner-header normalization experiment is documented in these
ledgers. The new concrete premise is the demonstrated current canonical
LNK2005 from Action Alert's ordinary selection-1 duplicate when natural real
callers in Game Sound and Observer emit the same header API as selection 2.
`source/math/real_math.h` owns the actual APIs and bodies:
- line1027: `__inline real_vector3d *vector_from_points3d(...)`;
- line1049: `__inline real magnitude_squared3d(...)`;
- line1090: `distance_squared3d`, which genuinely invokes both above;
- line1359: `__inline real real_random_range(...)`.
`source/ai/actions.h:12` already includes that header, and Action Alert also
explicitly includes it. The existing distance callers in update, next-position
and perform plus the random-range caller in perform naturally expose all
three helpers. No synthetic caller, forced-inline annotation, compiler flag,
header edit, or code-generation control is required.
## Exact minimal edit
Apply only the three replacement records in
`scratch/action_alert_natural_math_owners_20260905.edits.json` to
`source/ai/action_alert.c`:
1. Remove three pre-include `#define original original_inline` aliases.
2. Remove their three corresponding `#undef` lines.
3. Remove the duplicate ordinary external definitions of real_random_range,
vector_from_points3d and magnitude_squared3d.
Every include, gameplay body, private declaration, type, constant, enum, and
call remains untouched. This restores existing owner APIs; it does not add
inlining or relax the January point_from_line3d schedule.
## First natural gate and full runtime audit
Commands from the canonical worktree, with ordinary per-TU flags:
```text
python tools/campaign/gate.py source/ai/action_alert --all --out scratch/action-alert-natural-math-baseline-replay-20260905.obj --forbid-emitted-symbol _point_from_line3d
python tools/campaign/gate.py source/ai/action_alert --edits scratch/action_alert_natural_math_owners_20260905.edits.json --all --out scratch/action-alert-natural-math-first-20260905.obj --forbid-emitted-symbol _point_from_line3d
python scratch/action_alert_natural_math_owner_audit_20260905.py
python scratch/action_alert_natural_math_link_review_20260905.py --scan --link
```
Exactly one untouched baseline replay and one natural changed compile were
used. No source retry followed. Both gates report 11E/1R/0U and no forbidden
point_from_line3d owner. The residual next-position remains 768 candidate
versus 784 target padded bytes; its complete section fingerprint is unchanged.
The complete all-section audit includes anonymous/code/data/debug/directive
sections, COMMON and symbol inventories, offsets, storage and type, section
flags and selection, and ordered relocations including full target-symbol
metadata. It does not mistake function-only equality for whole-owner equality.
- Target:12 code owners,6 data owners.
- Actual baseline and untouched replay:14 code owners,6 data owners.
- Candidate:12 code owners,6 data owners.
- No new runtime owner, no COMMON owner or COMMON change.
- The only removed runtime owners are `_vector_from_points3d_inline` and
`_magnitude_squared3d_inline`, each48 padded bytes. Neither exists in January.
- The only inherited runtime changes are COMDAT selection1 to selection2 for
the three actual public math helpers. Each is otherwise completely identical
in bytes, ordered relocations, symbol metadata/offsets and section flags.
- All other inherited code, data and runtime records are completely identical,
including the parked body; there is not even a compiler-local-label change.
- Actual baseline and untouched PID replay have no runtime differences.
- COFF runtime header characteristics are unchanged. Debug/output-path records
are separately preserved in the raw audit and are not runtime credit.
The target selection1 metadata is retained in the audit. This packet does not
patch csplit metadata or claim that its original source-object selection was
recovered from the linked January image. The natural header source generates
ordinary select-any owners and repairs real current-source ownership debt.
## All-owner census and ordinary link checks
The census parsed all833 configured January targets and572 actual canonical
bases, with zero errors. Among current base definitions:
| Helper | Base owners | Selection-1 owners |
|---|---:|---|
| vector_from_points3d |51|Action Alert only|
| magnitude_squared3d |67|Action Alert only|
| real_random_range |8|Action Alert only|
Every other base copy is selection2. Both alias-named extras exist only in
Action Alert; there are zero incoming relocations to either alias across all
target and base objects. Their removal cannot leave a referenced alias owner
unresolved.
All magnitude and random-range copies have the January size/hash. Fifty
vector copies have the January hash; the existing Breakable Surfaces copy has
hash `a947d351c7b0abf577d0b6e41f02a4f0a7418104d3099e62f6835a862b355b57`.
That frozen48-byte/zero-relocation copy differs only by moving the argument
load `mov edx,[ebp+8]` before rather than after the first FLD. All26 decoded
instructions and dataflow otherwise agree. It uses the actual real_math.h
API and is semantically/ABI equivalent, not byte-identical. No expression or
source change was made to suppress or force this ordinary pooled copy.
Ordinary XDK Link.Exe `/NOLOGO /DLL /NOENTRY /NODEFAULTLIB` tests with unchanged
frozen natural Action Alert + Game Sound and Action Alert + reviewed Observer,
each in both input orders, now have no LNK2005. The previous actual-baseline
Game Sound pair has LNK2005 in both orders in its durable rejection ledger.
The new runs still end in expected unrelated LNK1120 unresolveds (57 for Game
Sound,40 for Observer), and none produces a DLL. Thus this is a passed bounded
duplicate-symbol check, not successful whole-link proof. No /FORCE, /IGNORE,
object patches, synthetic stubs, header shim or linker accommodation is used.
## Frozen evidence
All following paths are relative to the canonical worktree.
| Artifact | SHA-256 |
|---|---|
| scratch/action-alert-natural-math-target-20260905.obj |351ff0de9ddfea9b3c03a9038abab75b0c4b48290443b33e09c33105b41ab61a|
| scratch/action-alert-natural-math-before-20260905.obj |b82fd9a16f40c01fc45ac810240ad9ea9e02287ae629827c01306b17f7e38d30|
| scratch/action-alert-natural-math-baseline-replay-20260905.obj |35bbfb9e7f3f0690af27b6772189d4360e1fbddad71ba81b57b98f027397f549|
| scratch/action-alert-natural-math-first-20260905.obj |62ffdb7cd1e7c082b6f03216d4c10bc3437a8f3d0d11d01ba454b0981fc8ac44|
| scratch/action_alert_natural_math_before_20260905.c |3a57d07c1c1173cb26903ca40da1b4fc714d11779e65e34bfe8e6f7a40e1ce6d|
| scratch/action_alert_natural_math_candidate_20260905.c |27d9268805f0e92024ec10304fbb3dee1398d54055cfb01219d8796f7f0f28af|
| scratch/action_alert_natural_math_owners_20260905.edits.json |a91e4428803f9cc697e4d06b52abfbe82c69f8425b9d518360dede80a9624374|
| scratch/action_alert_natural_math_owner_audit_20260905.json |9c4bcb9e971e188518b8dda1a9ea20d894bfbd56f12e7fd7e7426902a5eef889|
| scratch/action_alert_natural_math_all_owners_20260905.json |737872e034ecd35ee428462306ed87cf6b12f244413944be5625f4701c07ac92|
| scratch/action_alert_natural_math_ordinary_links_20260905.json |f1b5c5d828edd62cffa6882d9469c59f50c14a2d985547394852c9a4c169d682|
## Canonical integration status
Root read the complete source packet, manifest and primary scratch ledger,
then applied only the minimal Action Alert source removals. The full canonical
Ninja build passes. The complete stable sweep reports 6,217 exact functions
out of 8,245, with zero exact losses and no new exact credit from this cleanup.
These are actual canonical results, separate from the frozen scratch results
above.
Full actual runtime-owner comparison and the remaining tooling/tests are
pending at the time this durable ledger is authored. Root must complete those
checks before treating this batch as fully verified. The existing residual
and park remain unchanged, and no whole-object completion or new-byte claim
is made. The ordinary two-object linker diagnostics above remain bounded
duplicate checks, not a successful full-game link.
@@ -1,95 +0,0 @@
# action_alert.obj — opus5 150K lane, wave w2 (2026-09-14)
## Baseline and final
| gate | result |
| --- | --- |
| baseline real file | `== exact 11 residual 1 unwritten 0`, guard passes |
| final real file (unchanged) | `== exact 11 residual 1 unwritten 0`, guard passes, park drift 0, fake scan 0 leads |
`git diff --stat -- source/ai/action_alert.c` is empty. No credit is claimed.
## `_action_alert_next_position` (parked, register-allocation)
Target 784 / 18 relocs / nsha `677fd6cc1a9e66de`; real-file candidate 768 / 18 / `4e16c85b544ded33`.
### New evidence
Aligned diff of the real file:
- January preloads `or eax,0xffffffff` before the `vehicle_passenger` test (3 B).
- January's NONE returns are 32-bit (`mov eax,0xffffffff` on the no-position tail, `or eax,-1` in the guard return block at +0x2f8).
- The encounter guard jumps past that block because EAX already holds -1.
Measured VC7 facts:
- `return NONE;` in a `short` function emits a 16-bit `or ax,0xffff`. So does `return result;` when result is constant-propagated (d7).
- Only an assignment to a `short` result variable emits the 32-bit form.
The function is therefore single-exit (brief section 9 single-exit law). The earlier park probes had not tested this with the complete assignment topology.
### Shapes
| shape | result |
| --- | --- |
| v1: init `result = NONE`, three-way `&&` guard, `if (any) {...}`, `result = next` | 784/18 `2c7d5520` (shrink-wrapped pushes, result in ECX) |
| v2: nested guard; v3: result merged into `next_position_index` | identical to v1 |
| v4: no init, `else { result = NONE; }`, `if (!any) result = NONE;` | 768/18 `6ac81fbc` (guards merged into one block, no preload) |
| v5: nested if/else; n1: `else if (encounter == NONE) result = NONE;` | identical to v4 |
| diagnostic d5: guards `return NONE`, init, no-position store, loop `return` | 1 instruction off (`or ax,0xffff`) |
| diagnostic ea: ec without the no-position store | frame 0x24, wrong |
| **ec / d10** | **strict EXACT 784/18 `677fd6cc`; whole TU 12/12 exact** |
The ec / d10 body:
```c
short result = NONE;
if (actor->input.vehicle_passenger || move_position_order == _move_position_order_none)
{
result = NONE; /* redundant with the initialiser */
}
else if (actor->meta.encounter_index != NONE)
{
/* encounter/squad lookup, swarm assert */
if (move_position_order == _move_position_order_repeat && current_position_index != NONE)
result = current_position_index;
else
{
/* availability scan (unchanged) */
if (!any_position_available)
result = NONE; /* redundant with the initialiser */
else if (move_position_order == _move_position_order_random)
result = choose_random_array_element(...);
else
{
/* clamp + do/while traversal (unchanged) */
result = next_position_index;
}
}
}
return result;
```
Scratch candidate: `scratch/workers/model_animations_actor_stimulus_etc/aa_ec.c` (patch `aa_ec.patch`). Its checks:
- the 11 siblings stay exact;
- the `_point_from_line3d` guard passes;
- parkcheck reports drift only on the park itself;
- fake scan finds 0 leads (the scanner does not detect redundant stores);
- the function set is unchanged.
### Disposition
This is not proposed as an admissible reopen. Exactness requires two NONE stores that the initialiser already guarantees. Every non-redundant spelling measured returns to a different fixed point (v1-v5, n1, d11). Brief section 5 prohibits redundant stores. Previous verifiers rejected the `_action_vehicle_desirable` and `_action_obey_command_begin` re-stores. `_extract_sequence` (January-proven redundant logic) was also left parked. This is an **owner-ruling item**.
### Reopen criterion
Either of:
- an owner ruling that admits January-proven redundant default-result stores (land the aa_ec.c body with a disclosure comment and unpark);
- a non-redundant spelling that produces the separate 32-bit NONE materialisations.
## Do-not-repeat
v1-v5, n1, d11 (no-init explicit else after the encounter branch), and ea.
@@ -1,202 +0,0 @@
# ActionCharge: authentic math owners and private names (2026-09-06)
## Result and scope
Starting at published `71128a19fc9707f678f970e380b97ee3181db05e`, remove the
reconstructed ActionCharge-only alias/duplicate-definition packages for
`square_root` and `magnitude_squared2d`. Their existing, identical definitions
in `source/math/real_math.h` now supply the ordinary source call sites.
The header itself is unchanged. No implementation body is rewritten or tuned.
Both helpers still materialize and remain strict exact on the first natural
compile. All **20 inherited exact ActionCharge functions** survive. The
688-byte candidate `action_charge_setup` residual is also byte/relocation
identical to its pre-trial version; `action_charge_perform` remains unwritten.
ActionCharge remains **20 exact / 1 residual / 1 unwritten**, not complete.
Across the campaign all **6,271 strict functions** survive. Meaningful exact
code remains **919,549 bytes**, credited functions **6,225**, matched data
**2,061,024 bytes**, and Matching objects **391/833**. This is source-ownership
progress, **zero new function/byte credit**. The helper bodies were already
counted; changing their owner representation does not count them again.
## Why this is authentic reconstruction
The source previously renamed these two header functions before including
`actions.h`, undefined the aliases afterwards, and supplied duplicate external
C definitions. The historical recovery ledgers explain that those packages
were introduced during reconstruction to reproduce selected-object helper
ownership. They are not original source evidence. Review included the
2026-08-26 math-helper rejection and wave-2 ledgers, real-random recovery,
the 2026-08-28 public residual recovery, and 2026-08-29 helper reintegration.
HCEX's actual debug records authenticate `real_math.h` ownership and report
`WasSpecifiedAsInline=true` and `WasInlined=true` for both functions:
- `square_root`: RVA 0x169DC28, source lines 644-646;
- `magnitude_squared2d`: RVA 0x169DC50, source lines 733-739.
Use `research/tools/DIA2Dump/x64/Release/DIA2Dump.exe -sym <name>` and
`-lines <name>` against
`research/hcea_jun2011_prototype/payload/Halo CE Anniversary (Jun 24 2011)/HCEX_Release.pdb`,
relative to the outer `i-w` workspace. Both line records name
`d:\projects\code\hcex\sources\math\real_math.h`, source MD5
`DE0F004C6EB037D8BE384822F84E002C`.
The January symbol atlas at
`research/halo-symbol-atlas/symbols/halo_ce/4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520.jsonl`
marks adjacent `normalize2d`, `magnitude3d` and `normalize3d` as inline
functions selected from ActionCharge. It omits these smaller two symbols;
do not misrepresent that omission as direct January name/inline evidence.
January's actual helper bytes and the current header bodies independently
agree; the original selected COFF representation is not proof of a hand-written
external wrapper in the original C file.
Current ordinary call sites justify retention without a fabricated anchor:
`magnitude2d` calls `square_root(magnitude_squared2d(v))`, and `magnitude3d`
calls `square_root` on the three-component squared magnitude. Reuse those
unaltered calls and the existing ordinary header definitions. No new inline
annotation, static-inline conversion, qualifier, forced emission, new source
alias, compiler switch, object patch or comparator exception is introduced.
## Full compiled-object proof
Root's complete before/first ordinary COFF fingerprint comparison finds
**38 COFF sections: 36 unchanged and exactly two ownership changes**
(34 unchanged non-debug sections plus both unchanged debug sections).
Only the COMDAT selection of `square_root` and `magnitude_squared2d` changes
from NODUPLICATES (1) to SELECT_ANY (2). Every executable/data byte, relocation,
symbol name/storage, debug section, COMMON owner and other runtime header
property is unchanged. No section is added or removed. The existing selected
`point_from_line3d` body is unchanged; there is no new copy or inline scheduling
change. The first gate retains both exact helpers and all inherited functions.
Normalized helper hashes remain:
- `square_root`, 16 padded bytes, zero relocations:
`0717923dacb4356a94553a32f0ba8a314904fae6d7d0bfc42ecc4af6f10415b7`;
- `magnitude_squared2d`, 32 padded bytes, zero relocations:
`9d8460618a63e41e020b2ce8071486fe278a849abccad44e5ea444da9700d014`.
The genuine header is unchanged, so no other translation unit needs a source
rebuild. A full Ninja run compiles ActionCharge and regenerates the campaign
reports. In this owner-only phase, all other current base objects and every
selected target remain identical to the frozen baseline. The full strict census has zero
gains and zero losses. **650 tests plus 26 subtests** pass; parks remain
**297 active / zero stale / zero invalid**. Admission retains five existing
rejections and has zero candidates, contradictions or revocations. The source
has zero fake-match scanner leads.
## Current owner census and actual link outcomes
The complete 572-base-object census now finds 60 `square_root` definitions
and 15 `magnitude_squared2d` definitions, **all SELECT_ANY**, with identical
target-correct bytes within each family. There is no remaining NODUPLICATES
definition for either name. `dot_product2d` remains seven identical definitions:
ActionCharge's one NODUPLICATES and six SELECT_ANY copies.
Root's actual VC7 `/DLL /NOENTRY /NODEFAULTLIB` links of the frozen complete
PathObstacles and ActionCharge objects, in both input orders, now report only
`dot_product2d` as LNK2005. The previous `square_root` and
`magnitude_squared2d` duplicate errors are gone. The expected 23 unrelated
unresolved dependencies remain and no successful full engine link is claimed.
No forced-multiple-definition flag, object suppression or synthetic stub
removes the remaining conflict.
## Authenticated private names, without behavior changes
After freezing and verifying the two-owner restoration, a separate `named`
phase replaces exactly two address placeholders in declarations, definitions,
calls and the symbol comment:
| January file offset | Old placeholder | Authentic private name |
| ---: | --- | --- |
| 4304 | `code_000010d0` | `action_charge_find_target_range` |
| 9168 | `code_000023d0` | `action_charge_valid_melee_destination` |
The exact January atlas cited above supplies both names at lines 7 and 14,
each attributed to ActionCharge. HCEX Release DIA independently identifies
both as static and not specified inline, with source lines 821-854 and 860-901
in `d:\projects\code\hcex\sources\ai\action_charge.c`. The source was already
static; the two matching `config/symbols.json` entries now record the genuine
names and `static: true` rather than csplit's default external ownership.
The source argument types and all executable statements remain unchanged.
In particular, the melee destination's second argument remains
`real_point3d const *goal`: HCEX's `short goal` type metadata is contradicted
by actual January point loads and by the later decompiler's own corrected
body. January reads `[ESI]`, `[ESI+4]` and `[ESI+8]`, while its caller supplies
the prop's center of mass. Do not import the erroneous short signature.
January's helper does not access or write through `melee_target_point`, and
the caller never consumes that discarded local. The HCEA semantic body and
caller corroborate this. Keeping `(void)melee_target_point;` therefore
preserves actual behavior; do not invent an output write or use the parameter
name as proof of missing executable logic. Whole-TU optimization could remove
historical dead stores, so the audit does not claim to prove the original text
never contained an assignment.
Root's full before-name/after-name fingerprint comparison reports all **38
runtime sections unchanged**, once the two authenticated symbol identities
are mapped for comparison. No instruction, literal, data, relocation offset,
addend, debug record or ownership change exists in the base object beyond
those names. The selected target changes names and the two static-storage
records. Across all 1,405 current objects, only the ActionCharge base and
selected target hashes change; every other object is byte-identical. The
full rebuild and strict census again preserve all 6,271 exact functions.
This audit-name mapping is not a comparator exception or new byte credit.
## Remaining boundary
`dot_product2d` has the same authenticated header-inline provenance, but no
live source caller in current ActionCharge. Its real use belongs to the still
unwritten `action_charge_perform`. Do not invent a caller or a retention anchor
to make an uncalled header definition materialize. Its old duplicate-definition
package remains outside this two-owner restoration and needs the genuine
caller closure. Other inherited ActionCharge aliases/prototype
debts are not certified by this patch, and the large residual is not reopened.
This does not claim a complete engine link or whole-object admission. It
reconciles two real duplicate-owner families while preserving all compiled
behavior and accounting. Active Opus implementations and donor worktrees are
untouched. Original binary evidence and temporary link outputs stay local.
## Frozen evidence
Two independent post-implementation audits pass. The owner audit replays the
full before/first COFF comparison, verifies all 572 named-phase base objects,
and runs the actual VC7 diagnostic links in both orders. Its script hard-pins
the imported comparison tools and linker. The separate private-name audit
checks all section and relocation records, caller targets/addends, exact source
and config changes, and all 1,405 current base/selected object hashes.
- Owner replay script: `scratch/action_charge_two_owner_postimplementation_audit_20260906.py`,
SHA-256 `0cc2e805f87cc55920a6bc3202aa284185e36374cbd7eb335e5c1ad15e86fa9d`.
- Independent owner report: `scratch/action-charge-two-owner-postimplementation-audit-final-20260906.json`,
SHA-256 `096b850bd6c00d0e3b8692dea0fbe0ad4e4789556a868a78ab84ed9b8dc9a397`.
- Independent private-name report: `scratch/action-charge-private-name-postimplementation-independent-20260906.json`,
SHA-256 `380291ccd1dd183ca88231dd08b2b68f5aae04639e60fdfffc767006228b11a4`.
These reports are local frozen evidence, not target binaries to distribute.
Both audits preserve the distinction between the remaining `dot_product2d`
link conflict and the two genuinely resolved duplicate-owner families.
- Baseline: `scratch/action-charge-math-owner-before-20260906.json`,
SHA-256 `34ccf1a6d5e32b4348364235802ba488b62c48317e83c45d0317f9fdc93fa4dc`.
- First: `scratch/action-charge-math-owner-first-20260906.json`,
SHA-256 `fd64316616dca63d976bd3c9ca367b9061a69e80ae0c28f27e61a443cdd210d5`.
- Final: `scratch/action-charge-math-owner-final-20260906.json`,
SHA-256 `a81dcaced90b94c6726aa7004b04bb56213c2f2208efe21a8b1898771183d7c5`.
- Before C/base object: `e8096e28a0a821ae5f97b0461c61ae6f828f51037b0cb62b75f5b1d8fb0be366` /
`45c5386b96ba99b67834dd3ffd85e797dc5fdb8385db4cd4ab1e73d006f8df6d`.
- Pre-name two-owner C/base object: `5f182feceec9ccf33d2d8eade40c88db42c697cbfe4d26f5682fce3121bd193a` /
`8c19c85deb223ff89aeaed6ff768e192139e040496c853b1fb91e31eef5c315b`.
- Unchanged math header: `4e640d1be7b2c843dc7c9acb198afcd5d7d390eb310e5e14eedd484cf99535d4`.
- `scratch/action-charge-math-owner-{before,first}-stable-20260906.json`:
6,271 strict functions, zero gains/losses.
- Named final: `scratch/action-charge-math-owner-named-20260906.json`,
SHA-256 `1d4305f1d603e619889f2e94dd282ca40a364d3115ea447ddb579a7c7e257a88`.
- Named final C: `75961e822a451f5d23910def9cc4661e1fbd0bdefa9caa314f776cf0aa21e81d`;
base object: `cecd4d02fc1dbcf8a355ef289899f7230bd3463b3299abe486cb36ee871ef371`;
selected target: `a906f0ec24b0313a056d4a413eaf85ee32f22f02c45878261bcae4521bfa63c8`;
symbols configuration: `57d80ec4aa5d567067f97ac9f4552bba4d85dc777201d480fdadd47f93b06ccf`.
Loaded 100 of 7750 files, more files were not shown because too many files have changed in this diff. Show more