Compare commits

...
Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
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 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
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
justinfilip 521c44f862 Keep screen fog on the canopy when the field of view is widened. 2026-10-12 20:50:57 -05: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
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
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
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
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
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
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
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
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
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
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
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
MrBruh 634b4197f9 Limit enemy names to the weapon's red reticle range 2026-10-02 20:36:46 +13:00
MrBruh ab96597321 Fix the Android build 2026-10-02 20:34:00 +13: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
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
7399 changed files with 528210 additions and 1626004 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/ringfall-mjolnir-vr#install-on-meta-quest-3--3s
about: Step-by-step installation for the Quest 3 / 3S and the Steam Frame.
+417 -11
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;
# - Ringfall: 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:
# (Ringfall: 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: Ringfall: 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_host_watch_hash.py tools/test_touch_menu.py tools/harness tools/test_profile.py
python tools/test_light_storage.py
- name: Compile cache
run: ccache --show-stats
@@ -107,7 +143,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 +152,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 +198,8 @@ jobs:
retention-days: 3
windows:
# (off: Ringfall: 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
@@ -177,6 +239,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 +256,344 @@ 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: Ringfall: 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
port/third_party/extract-xiso
tools
- uses: actions/download-artifact@v4
with:
pattern: halo-*
path: artifacts
- name: Package
run: |
mkdir release
# (Ringfall: 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/ringfall-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/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 '**Ringfall: 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 | `RingfallMjolnirVR.apk` | [Install on Quest](%s#install-on-meta-quest-3--3s) |\n' "$readme"
printf '| Steam Frame | `Install-Ringfall-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.\n\n'
printf 'Commit: %s\nBuild: %s\n' "$COMMIT_URL" "$RUN_URL"
} > notes.md
gh release create "$tag" release/* --target "$GITHUB_SHA" --latest \
--title "Ringfall: 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: Ringfall: 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:
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
- 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/ringfall-mjolnir-vr-android-release.zip) · [Steam Frame]($latest/download/Install-Ringfall-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)"
+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
+227
View File
@@ -0,0 +1,227 @@
# 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`) |
| `port/linux/game/` | The port's game-side code, compiled as the game's sources are: the distributed netcode (`network_*.c`), co-op, 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`), 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 |
| `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`, `xdk_headers.py`) and tests |
| `docs/` | Longer design notes (`custom_edition_caches.md`, `mods.md`) |
| `pgo/` | Profile-guided optimisation profiles |
| `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 only
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/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_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)
```
`tools/harness/README.md` explains how to add an asset-free test: the code
under test is taken from the sources, not copied.
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.
- `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 CC0. Vendored code goes in `port/third_party/<name>`,
copied unchanged, with a README naming the upstream, the version or commit,
the checksum and what uses it. GPL code (OpenSauce, Reclaimer) may be read
as documentation of file formats, never copied.
### 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/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/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 |
| `port/third_party/*/README.md` | Each vendored library |
| [AUTHORS.md](AUTHORS.md) | Art credits |
+23
View File
@@ -0,0 +1,23 @@
# 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`).
## 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.
+172 -46
View File
@@ -1,77 +1,203 @@
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="Ringfall: 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.
# Ringfall: 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).
Ringfall: 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.** Ringfall: 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)
## Contents
## Build instructions
- [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)
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
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.
- **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.
### Native Linux build
## What you can play
`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.
- **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).
### Debug and release
## What you need
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.
- 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).
### Optimisation
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.
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.
## Install on Meta Quest 3 / 3S
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.
You install the game once with a computer (Windows, macOS or Linux); after
that it updates itself.
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.
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 `RingfallMjolnirVR.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 **Ringfall: 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.
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.
You can delete the disc image from `Download` afterwards. When a new
version comes out, the game asks you at start whether to update.
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):
## Install on Steam Frame
| 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 |
No computer and no keyboard needed.
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.
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-Ringfall-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 **Ringfall: Mjolnir VR** to your Steam
library and offers to switch back to Gaming Mode.
6. Start **Ringfall: Mjolnir VR** from your library (under *Non-Steam*).
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`.
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).
### Frame rate
## Playing
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).
- **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.
### Native Windows build
## Problems and questions
`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).
Please open an [issue](../../issues) and include:
### Android build
- 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).
`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).
What is done and what comes next: [port/android/VR_ROADMAP.md](port/android/VR_ROADMAP.md).
### Matching build
**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.
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.
## Credits
## Where's all the type information?
Ringfall: Mjolnir VR stands on the work of others:
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.
- **[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.
- **[extract-xiso](https://github.com/XboxDev/extract-xiso)**: the disc
image readers follow its format handling.
- 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
Ringfall: 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.
This project's own code is released under [CC0 1.0](LICENSE.md), as the
decompilation it builds on. Third-party code and fonts keep their own
licenses: see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
+46
View File
@@ -0,0 +1,46 @@
# Third-party notices
Ringfall: Mjolnir VR's own code is released under [CC0 1.0](LICENSE.md).
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 | CC0 1.0 | [LICENSE.md](LICENSE.md) |
| [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) |
| [extract-xiso](https://github.com/XboxDev/extract-xiso) | reading disc images (followed by `XisoExtractor.java`, `steam_frame_xiso.py`, `xiso.c`) | BSD-style (in@fishtank.com) | [port/third_party/extract-xiso/LICENSE.TXT](port/third_party/extract-xiso/LICENSE.TXT) |
The disc image readers include this acknowledgement, as extract-xiso's
license asks: *This product includes software developed by in
&lt;in@fishtank.com&gt;.*
## 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
+101 -166
View File
@@ -1,70 +1,23 @@
#!/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
# 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 +26,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 +63,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 +72,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 +84,86 @@ parser.add_argument(
type=str,
help="clang with the arm64_32 target for the Android guest (default: clang)",
)
if not is_windows():
parser.add_argument(
"--wrapper",
metavar="BINARY",
type=Path,
help="path to wibo or wine (optional)",
)
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)",
)
args = parser.parse_args()
try:
check_profile_options(args.profile, args.pgo)
except ValueError as error:
parser.error(str(error))
# path setup
# 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,
)
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
def is_windows() -> bool:
return os.name == "nt"
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]
# build.ninja
out = io.StringIO()
n = ninja_syntax.Writer(out)
n.variable("ninja_required_version", "1.3")
n.newline()
class BuildConfig(TypedDict):
name: str
guid: str
baserom: str
projects: List[BuildConfigProject]
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()
build_config: BuildConfig = json.load(open(config_json_path, "r", encoding="utf-8"))
generate_linux_build(n, sln)
generate_android_build(n, sln)
generate_windows_build(n, sln)
generate_web_build(n, sln)
# solution configuration
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()
sln = SolutionConfig()
sln.build_dir = build_dir
sln.config_dir = config_dir
sln.project_dir = project_dir
sln.tools_dir = tools_dir
# 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() else "linux"
if f"\nbuild {default}: " in out.getvalue():
n.comment("Default rule: the build for this computer")
n.default(default)
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
if not is_windows():
sln.wrapper = args.wrapper
# 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 file generation
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)
with open("build.ninja", "w", encoding="utf-8") as f:
f.write(out.getvalue())
out.close()
+209
View File
@@ -0,0 +1,209 @@
> This is the README of [OpenCE](https://github.com/OpenCommunityEdition/OpenCE),
> the port Ringfall: 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`.
-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.
OpenSauce and Reclaimer are GPL-3.0 and this repository is CC0: no code from
either 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.
-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.
-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`.
@@ -1,68 +0,0 @@
# `action_charge.obj` helper reintegration — 2026-08-29
## Result
The canonical source now measures **20/22 strict-exact functions** and
**1,984/6,176 strict-exact padded bytes**. This restores the best historical
function frontier while preserving the later public lifecycle, control, and
math-helper recoveries.
Relative to the prior canonical 17/22 object, this is a non-regressing gain of
three functions and 640 padded bytes:
| Function | Padded bytes | Result |
| --- | ---: | --- |
| `_code_000010d0` | 272 | strict exact |
| `_collision_test_line` | 80 | strict exact |
| `_code_000023d0` | 288 | strict exact |
The two remaining functions are classified honestly:
- `_action_charge_perform`: unwritten, 3,248 padded bytes.
- `_action_charge_setup`: readable typed-C residual, 688 candidate bytes and
20 relocations versus the 944-byte/29-relocation target.
## Provenance and recovery
The archived ledger at commit
`677defb9d9b4dd0ac4f20bc2c5dbd53bf3e8d7f5` recorded a measured 20/22
experiment, but deliberately restored its source before commit. Its surviving
notes identified the accepted source topology for the two anonymous helpers:
- the range helper uses zero textually on the left for nonnegative checks and
keeps the suicidal/non-suicidal melee limits in separate branches;
- the destination helper calls the ordinary typed `collision_test_line`
wrapper and brackets the call with the AI-melee collision-user scope.
The readable HCEA donor files were used only to recover behavior and named
field access. A complete typed `action_charge_setup` supplies the natural call
sites needed for VC7 to reproduce the target's internal register calling
convention for both anonymous helpers. No standalone ABI shim, raw offset,
representation cast, object patch, compiler flag, or comparator exception was
used.
## Verification
Commands run from the canonical worktree:
```text
python tools/campaign/gate.py source/ai/action_charge --all
ninja build\base\source\ai\action_charge.obj
python tools/campaign/board.py source/ai/action_charge
python tools/campaign/board.py
git diff --check -- source/ai/action_charge.c
```
Measured output:
```text
UNWRITTEN 3248 _action_charge_perform
residual 944 _action_charge_setup [size 688!=944, relocs 20!=29, sha]
== exact 20 residual 1 unwritten 1 (of 22 listed)
objects 275/619 fully exact; bytes 651111/1922669; fns 4699/8246
```
The campaign totals increase from 650,471 to 651,111 strict-exact padded bytes
and from 4,696 to 4,699 strict-exact functions, with the exact-object count
unchanged at 275.
@@ -1,125 +0,0 @@
# `action_charge.obj` Jonas math-helper recovery rejection — 2026-08-26
## Decision
This frozen wave is **rejected wholesale**. All twelve requested math helpers
reproduced their January padded bytes and relocation identities on the only
ordinary candidate compile, but the candidate also emitted an out-of-line
`_magnitude_squared3d` COMDAT that failed the required linker-level ownership
gate. No production source from this wave is retained and no function or data
credit is claimed from it.
The rejection is useful new evidence: the canonical readable body of
`magnitude3d` is byte-correct after inlining, but spelling it as a call to the
header `magnitude_squared3d` helper causes VC7 to materialize an unwanted
out-of-line copy. A later frozen wave may reopen only with a source topology
that removes that helper reference before compilation.
## Frozen provenance
- Branch: `jonas/action-charge-math-recovery-20260826`
- Base commit: `19ed6902ffa755c9b7c7b01cb926ea8af49d5d14`
- January target object SHA-256:
`76C4C0B62850FFAA42FE5027C3E68B2CC42F7640C1156342C9A9CC4D1125AFEA`
- Clean pre-wave base object SHA-256:
`C0683E96025CBAC9980D9779786C89B69164502336E7ABCA23874EB6835C40CE`
- Frozen candidate source Git blob:
`ffc743d088b2c065a53826300514a4e3122ea500`
- Frozen candidate source physical SHA-256:
`769194E601F4CC9132B8AB90CB825DE07A452896647328179D7D5CC3AC771C08`
- XDK 3911 compiler SHA-256:
`483E00C47BB08D699475A642BCFF15B5B2036350B31C540E88A506BAF101DA11`
- Splitter SHA-256:
`708F957C37CAA66DF90ADE35AD3D1E65E2617E2CC2C8C2CCF68D8BF5CB20E752`
- Candidate object: 3,839 bytes, raw SHA-256
`146B19F6DB3DF4163904B15CD5ABD3AB2E0FBA548CB7F2792E100F3425CE306C`.
- Compiler flags and include search path were unchanged:
`/O2 /Oy- /DDEBUG /Dxbox`.
The source passed the XDK `/Zs` syntax-only gate. A Ninja dry run then exposed
exactly one `CL build\base\source\ai\action_charge.obj` edge. It was invoked
once and its first artifact was preserved before any comparison. No spelling,
declaration, scheduling, compiler, or flag retry followed.
## Strict-exact evidence from the rejected artifact
The hardened comparator reported `all_equal: true` for all twelve new helpers
and the three inherited accepted functions. The twelve helpers contribute 479
meaningful / 544 padded target bytes and six relocations.
| Function | Padded bytes | Relocs | Normalized SHA-256 |
| --- | ---: | ---: | --- |
| `_square_root` | 16 | 0 | `0717923dacb4356a94553a32f0ba8a314904fae6d7d0bfc42ecc4af6f10415b7` |
| `_scale_vector2d` | 32 | 0 | `496d82c0615b4ec0cb4211dd21dfbf507d8959357a60895b9fd4d930b554cf95` |
| `_magnitude_squared2d` | 32 | 0 | `9d8460618a63e41e020b2ce8071486fe278a849abccad44e5ea444da9700d014` |
| `_magnitude2d` | 32 | 0 | `17d586569ec882a34a898e8a589fe5140e8d8b79b85e2fd4e7816dfe6210d3fa` |
| `_normalize2d` | 80 | 3 | `8a5659d5dd494fe2d1d880057d1836185ebfb8e81d9d63362a43e6a2cb287890` |
| `_dot_product2d` | 32 | 0 | `defea20c9a86f0deda350c23260a0e093b08da31a2cbcd5e5c08b17164579415` |
| `_point_from_line3d` | 48 | 0 | `9b763841f8519177ac2d3fd471226972b9fa92cf1e9061e9f92359077dc82741` |
| `_scale_vector3d` | 48 | 0 | `6436919fbdc80b5d5fe76d9c92fa146e5ac7d3b4637c9b75aaa62401f0c54a3b` |
| `_magnitude3d` | 48 | 0 | `114f10e7ee4543446daaaafc859a4ec729ba6c8c74c38825d05bd0f00a42352c` |
| `_normalize3d` | 96 | 3 | `67fbd33f204d0c93cce135fcfd37d4ce5b443642edc6ad99230846b91c893e5b` |
| `_dot_product3d` | 32 | 0 | `d564ef69fd99449fdfc29d59d0d844fb17df91369277eaba9574e7c8c3d2ae68` |
| `_subtract_vectors3d` | 48 | 0 | `1d706c27e470449c1ac16f5c5ff47c98ee2be96426f2b8bb949ba79e9d764cac` |
The inherited sentinels remained strict exact:
| Function | Padded bytes | Relocs | Normalized SHA-256 |
| --- | ---: | ---: | --- |
| `_action_charge_begin` | 80 | 3 | `c78c6feeb10fda8857834f5264614aa976498059590ee08304aef25f21af8bf3` |
| `_action_charge_update` | 80 | 2 | `f2591b2de350c20aefef42c38623875dca494bb46a51d06c6dcd5947b035976e` |
| `_real_random` | 16 | 2 | `50eeeb3dc58069325659d8bc0b6f2c17bb9f4fdce278b2f1a37edb64f4fcc60c` |
Both normalize functions reproduced the target relocation offsets, types,
destinations, addends, and local ownership for `0.0f`, `1.0f`, and the
double-precision epsilon literal. The three emitted target-owned `.rdata`
COMDATs were also strict exact: four bytes at
`__real@00000000`, four bytes at `__real@3f800000`, and eight bytes at
`__real@3f1a36e2e0000000`. They receive no credit because the wave is rejected.
## Ownership contradiction
The candidate additionally emitted `_magnitude_squared3d` as a 48-byte,
zero-relocation, select-any code COMDAT. No candidate relocation refers to the
out-of-line copy. Its normalized bytes are strict exact against the January
canonical owner in `source/ai/action_alert.obj`:
- 48 padded bytes;
- zero relocations;
- normalized SHA-256
`0d04579096eeab3a1c45f87774e1eb20ce4856dbac219f624a855e8c9ae99725`.
That cross-object byte equality is insufficient. The acceleration playbook
allows a candidate-only helper COMDAT only when the ordinary linker discards
it. A direct XDK 3911 link of the January canonical `action_alert.obj` and the
candidate `action_charge.obj` was run in both input orders. Both orders fail:
```text
action_charge.obj : error LNK2005: _magnitude_squared3d already defined in action_alert.obj
action_alert.obj : error LNK2005: _magnitude_squared3d already defined in action_charge.obj
fatal error LNK1169: one or more multiply defined symbols found
```
Thus this is not an admissible discardable-helper representation difference.
The target canonical owner is a no-duplicates COMDAT, and the candidate copy
cannot coexist with it. The wave fails before campaign admission regardless of
the twelve function-byte successes.
## Atomic restoration and reopen criterion
The complete macro/definition package was removed after the ownership failure.
The production source is restored exactly to:
- Git blob `3e76470cf8be066072239845cbf08e37aeffa458`;
- physical SHA-256
`3B798CC7F02BCC50741E88D6C735E9F12780DC4AE645BC69DD48262A6E6D67EB`.
Only this rejection ledger is retained. There is no config, comparator,
compiler, protected-file, source, or matching-label change and no push.
The link failure is genuinely new evidence. A new wave may test one frozen
topology: keep the twelve exact external helper bodies, but express
`magnitude3d` directly as the already-inlined three-component squared-magnitude
expression so the header `_magnitude_squared3d` body is never referenced and
VC7 has no reason to materialize the extra COMDAT. That is a new evidence wave,
not a retry of this rejected source.
@@ -1,183 +0,0 @@
# `action_charge.obj` Jonas math-helper recovery wave 2 — 2026-08-26
## Outcome
This wave recovers twelve math helpers as strict exact and retains all three
previously accepted `action_charge.obj` functions. The object advances from
3/22 to **15/22 strict-exact functions**. The twelve-function gain is 479
meaningful / 544 padded code bytes and six relocations. The object remains
`NonMatching`; none of the seven absent behavioral functions is claimed.
Three target-owned read-only constant COMDATs, sixteen bytes total, are also
present with exact bytes and ownership. The ordinary report may credit those
sections directly; no semantic exception or manual data ledger entry is used.
## Why this is a second evidence wave
The first frozen recovery candidate made all twelve helper bodies byte-exact,
but VC7 also materialized an unwanted select-any `_magnitude_squared3d`
out-of-line helper. A direct XDK link against the January canonical owner in
`action_alert.obj` failed with `LNK2005` in both input orders. That entire
candidate was removed and the rejection is recorded in
`action_charge_obj_jonas_math_helper_recovery_rejection_20260826.md`.
The linker failure supplied genuinely new evidence. Wave 2 therefore froze one
new topology: `magnitude3d` spells the already-inlined three-component squared
magnitude expression directly. Its emitted `_magnitude3d` instructions remain
identical, but no source reference remains from which VC7 can materialize the
rejected helper owner.
## Frozen provenance and compile discipline
- Branch: `jonas/action-charge-math-recovery-v2-20260826`
- Evidence base: `511d251d3765b1caac824a47ed8bf330c677cc85`
- January target object SHA-256:
`76C4C0B62850FFAA42FE5027C3E68B2CC42F7640C1156342C9A9CC4D1125AFEA`
- Clean pre-wave base object SHA-256:
`C0683E96025CBAC9980D9779786C89B69164502336E7ABCA23874EB6835C40CE`
- Frozen candidate source Git blob:
`64818f49f816817883be1621a9737166c7615bb7`
- Frozen candidate source physical SHA-256:
`AF8166FC414F459190CE9720B41E5A3BA7858FAFEEB59B226740CF377D0C7E02`
- XDK 3911 `CL 13.00.9254.1` SHA-256:
`483E00C47BB08D699475A642BCFF15B5B2036350B31C540E88A506BAF101DA11`
- Splitter SHA-256:
`708F957C37CAA66DF90ADE35AD3D1E65E2617E2CC2C8C2CCF68D8BF5CB20E752`
- Flags: `/O2 /Oy- /DDEBUG /Dxbox`; project include list unchanged.
- First candidate object: 3,676 bytes, raw SHA-256
`638CE42681DB10907F0C847F2679806E07091A34085E4EE0D50E1AC3DB1E1E67`.
The frozen source passed the XDK `/Zs` syntax-only gate. Ninja dry-run output
contained exactly one `CL build\base\source\ai\action_charge.obj` edge. That
edge was invoked once, and the first object was copied into the frozen audit
packet before comparison. The accepted object subsequently reports no work.
There was no same-wave source, flag, declaration, scheduling, or compiler
retry.
## Strict function evidence
The hardened comparator requires padded size, normalized bytes, relocation
address/type, resolved destination, addend, and ownership to agree. It reports
`all_equal: true` for the twelve recovered helpers and all three inherited
sentinels.
| Function | Padded bytes | Relocs | Normalized SHA-256 |
| --- | ---: | ---: | --- |
| `_action_charge_begin` | 80 | 3 | `c78c6feeb10fda8857834f5264614aa976498059590ee08304aef25f21af8bf3` |
| `_action_charge_update` | 80 | 2 | `f2591b2de350c20aefef42c38623875dca494bb46a51d06c6dcd5947b035976e` |
| `_square_root` | 16 | 0 | `0717923dacb4356a94553a32f0ba8a314904fae6d7d0bfc42ecc4af6f10415b7` |
| `_scale_vector2d` | 32 | 0 | `496d82c0615b4ec0cb4211dd21dfbf507d8959357a60895b9fd4d930b554cf95` |
| `_magnitude_squared2d` | 32 | 0 | `9d8460618a63e41e020b2ce8071486fe278a849abccad44e5ea444da9700d014` |
| `_magnitude2d` | 32 | 0 | `17d586569ec882a34a898e8a589fe5140e8d8b79b85e2fd4e7816dfe6210d3fa` |
| `_normalize2d` | 80 | 3 | `8a5659d5dd494fe2d1d880057d1836185ebfb8e81d9d63362a43e6a2cb287890` |
| `_dot_product2d` | 32 | 0 | `defea20c9a86f0deda350c23260a0e093b08da31a2cbcd5e5c08b17164579415` |
| `_point_from_line3d` | 48 | 0 | `9b763841f8519177ac2d3fd471226972b9fa92cf1e9061e9f92359077dc82741` |
| `_scale_vector3d` | 48 | 0 | `6436919fbdc80b5d5fe76d9c92fa146e5ac7d3b4637c9b75aaa62401f0c54a3b` |
| `_magnitude3d` | 48 | 0 | `114f10e7ee4543446daaaafc859a4ec729ba6c8c74c38825d05bd0f00a42352c` |
| `_normalize3d` | 96 | 3 | `67fbd33f204d0c93cce135fcfd37d4ce5b443642edc6ad99230846b91c893e5b` |
| `_dot_product3d` | 32 | 0 | `d564ef69fd99449fdfc29d59d0d844fb17df91369277eaba9574e7c8c3d2ae68` |
| `_subtract_vectors3d` | 48 | 0 | `1d706c27e470449c1ac16f5c5ff47c98ee2be96426f2b8bb949ba79e9d764cac` |
| `_real_random` | 16 | 2 | `50eeeb3dc58069325659d8bc0b6f2c17bb9f4fdce278b2f1a37edb64f4fcc60c` |
The two normalize functions reproduce all six January DIR32 relocation slots:
- `_normalize2d`: `+0x21`, `+0x2E`, `+0x46`;
- `_normalize3d`: `+0x2C`, `+0x39`, `+0x59`;
- destinations in both cases: the double epsilon, `1.0f`, and `0.0f`, with
zero addends and target-identical local ownership.
## Whole-object ownership audit
The January target owns 22 code sections. The candidate owns exactly fifteen,
all of which are target-owned and strict exact. It has **no candidate-only code
owner**. The seven target-only functions are exactly:
1. `_code_000010d0`
2. `_action_charge_control`
3. `_action_charge_is_leaping`
4. `_collision_test_line`
5. `_action_charge_perform`
6. `_code_000023d0`
7. `_action_charge_setup`
The candidate owns no `.data`, `.bss`, COMMON symbol, or other writable runtime
storage. Its only defined runtime non-function owners are these target-owned
select-any `.rdata` COMDATs:
| Symbol | Bytes | Relocs | Normalized SHA-256 |
| --- | ---: | ---: | --- |
| `__real@00000000` | 4 | 0 | `df3f619804a92fdb4057192dc43dd748ea778adc52bc498ce80524c014b81119` |
| `__real@3f800000` | 4 | 0 | `e00e5eb9444182f352323374ef4e08ebcb784725fdd4fd612d7730540b3e0c8c` |
| `__real@3f1a36e2e0000000` | 8 | 0 | `564be83b341e452f4492fa81f7d5f215f446119f583c33a94b619cd1ee7287fd` |
The `_magnitude_squared3d` symbol and section are absent. Diagnostic XDK links
of the complete January `action_alert.obj` target with the candidate object
succeed in both input orders under `/FORCE:UNRESOLVED` (exit 0); the expected
unresolved project dependencies remain warnings, while the wave-1 `LNK2005`
and `LNK1169` ownership failures are gone.
## Source and policy review
The retained code is ordinary typed C. It keeps one parameter per line, uses
`void` on its own line for the no-argument function, and gives every function
an explicit return. It introduces no assembly, `register`, `volatile`, pragma,
attribute, forced inlining, barrier, byte forcing, raw address, raw object/tag
cast, pointer/integer reconstruction, union pun, or undefined behavior. No
protected Units or game-engine file is touched.
The isolated tooling suite passes 179/179 tests. `git diff --check` is clean,
the candidate object dry run reports no work, and the changed production path
is only `source/ai/action_charge.c`. Whole-campaign reports, regression
snapshot/check, and cumulative-tree validation are recorded in a committed
replay after integration.
## Committed cumulative replay
The evidence branch was merged into `jonas/units-integration-20260824` at
`b08d30922a82ee675400a8d9f78aaef99dbe8dcf`. The cumulative compiler replay
produced a 3,676-byte object with raw SHA-256
`C11BB3BF615299968C93F4BB4857A12B4955D11B8255C6A45F99508E63595CDF`.
Raw object hashes differ normally with the COFF timestamp; hardened comparison
again reports `all_equal: true` for all fifteen code owners and all three
constant owners. The cumulative owner table still contains exactly fifteen
target-owned functions and no candidate-only function, `.data`, `.bss`, or
COMMON owner.
The regenerated ordinary report records the unit as:
- 15/22 matched functions;
- 643/6,032 meaningful code bytes;
- twelve functions and 479 code bytes above the pre-wave 3/22 state;
- 256 target data bytes represented as one aggregate report section.
The three constant COMDATs are independently strict exact, but the ordinary
report does not expose partial per-COMDAT data credit inside its aggregate
`.rdata` measure. The semantic-data gate intentionally permits incomplete-unit
credit only when a verified owner spans the entire remaining data gap. These
three owners span 16 of 256 bytes, so matched campaign data remains unchanged;
no exception, accounting change, or overstated 16-byte claim is introduced.
A committed `tools.regression_gate` snapshot/check for
`source/ai/action_charge` reports:
- all fifteen functions `still_exact`;
- `changed_nonexact: []`;
- `newly_exact: []` after the committed snapshot;
- no failures and no warnings.
The cumulative verification set passes:
- `halobetacache_build libcmt_build`: no work after the replay object;
- semantic report: 470 units, 4,288 functions evaluated, 4,210 accepted exact,
and zero unit errors;
- campaign progress: 507,503 / 2,198,102 code bytes, 4,184 / 11,060
functions, and 1,836,756 / 4,176,062 data bytes;
- Halo category: 494,589 / 1,770,166 code bytes and 4,017 / 7,574 functions;
- tooling tests: 179/179 passed;
- `git diff --check`: clean.
The fail-closed admission audit reports zero completion candidates and zero
revocations. Its sole contradiction is the already-known
`source/shell/shell_xbox` completion label, whose `_main` false positive is
independently rejected by existing semantic policy; this wave neither touches
nor changes it. No push is performed.
@@ -1,109 +0,0 @@
# `action_charge.obj` Jonas public residual recovery — 2026-08-28
## Result
This bounded recovery starts from cumulative integration commit
`9c006fff3ae73abaa3d83ca208aa20ab90785163` and the existing 15/22 exact
partial. It recovers the two remaining public functions previously proven by
the archived full-TU experiment:
| Function | Padded bytes | Relocations | Normalized SHA-256 |
| --- | ---: | ---: | --- |
| `_action_charge_control` | 544 | 6 | `5be694b750701983145a5c29336fe5dc3ffe97cd7b53111013694012215e2095` |
| `_action_charge_is_leaping` | 80 | 3 | `9ddd3d1296e581d078e6958f4039f9bde736698282368436b60d130978e7b275` |
| **Wave gain** | **624** | **9** | |
All fifteen inherited functions remain strict exact. `action_charge.obj`
advances from 15/22 and 720/6,176 padded bytes to 17/22 and 1,344/6,176 padded
bytes. It remains `NonMatching`.
The retained source Git blob is
`7a87fe26032376f229e251ab6cf0f2f7f6f945da`. The January split-object
SHA-256 is `76c4c0b62850ffaa42fe5027c3e68b2cc42f7640c1156342c9a9cc4d1125afea`.
## Provenance and scope
Every current action-charge ledger and the archived frontier ledger at commit
`677defb9d9b4dd0ac4f20bc2c5dbd53bf3e8d7f5`, ledger blob
`bbb630a9fc673dab443948add44b8457e3b465e4`, was read before emission. That
archived full-TU experiment independently measured both retained source shapes
strict exact, then restored its production source because
`action_charge_perform` and `action_charge_setup` remained non-exact. The
current pre-wave source blob was
`64818f49f816817883be1621a9737166c7615bb7`.
The readable semantic oracle is HCEA commit
`570c83fd9c365dad6f2a3e7041705d5b84c7847c`:
- `src/blam/ai/action_charge_control.c`, blob
`b29413ab09c0db5049d66b62cacf7e6d717c3149`, payload SHA-256
`4fd1e1de0fbf920e95a2058ba55d8de27893a99981cfb8119178876970bd2451`;
- `src/blam/ai/action_charge_is_leaping.c`, blob
`38fc8c40afa3cc720f5d8f784c22da25150febcc`, payload SHA-256
`c6857cdfcc0225c97dd8bdfe56a8f5a8ea721881a48ca58e71b8af0911b7a7d5`.
January COFF, current typed actor layouts, and existing exact-history evidence
remain authoritative where the later donor evolved. The source uses
`actor_get` and `actor_definition_get` rather than raw object casts. The only
new header is the existing typed `game/game.h` interface for `game_time_get`;
no shared declaration or storage owner changes.
`action_charge_control` restores the target's exact priority-local topology,
including priority 5 initialized in the fallback block and priority 7 written
only in the locked-aiming arm. It then applies typed crouch, leap, panic,
movement, and combat orders through named actor fields. `action_charge_is_leaping`
forms one typed charge-state pointer, keeps one shared result return, and tests
the authenticated 30-tick leap window.
## Bounded five-body disposition
The historical frontier also proved three private/collision bodies exact:
`_code_000010d0`, `_code_000023d0`, and `_collision_test_line`. They were
included in the one natural recovery packet to test whether the current partial
could own them without the two excluded large callers.
VC7 correctly eliminated all three because the current TU contains no lawful
caller after `action_charge_perform` and `action_charge_setup` are omitted.
They were therefore `UNWRITTEN`, not byte residuals. Their candidate bodies,
prototypes, macro rename, constants, and include support were all pruned before
the retained replay. No synthetic caller, anchor, forced emission, external
ownership rewrite, or zero-credit support body is retained.
The archived ledger already proves the two large residuals are structural:
- `_action_charge_perform` is 3,248 padded bytes with the exact size, frame,
relocation count, and destination sequence but a recovered source topology
that moves 82 of 84 relocation addresses;
- `_action_charge_setup` is 944 padded bytes and needs the original shared-exit
CFG rather than another early-return or `goto` spelling.
Do not retry those fixed points without authentic debug-scope/source topology.
Reopen the three dead helpers only when an admissible authentic caller causes
ordinary target-compatible materialization.
## Policy and validation
The retained code is typed readable C. Each parameter occupies its own line;
both functions end in explicit returns. It contains no assembly, `volatile`,
`register`, pragma, intrinsic, optimizer barrier, raw address or byte-offset
access, pointer/integer reconstruction, punning, inactive-union access,
undefined behavior, synthetic retention, object-byte forcing, comparator
exception, configuration edit, or compiler-flag change.
The isolated retained-only `/Zs` parse and normal XDK 3911 replay passed. Direct
strict comparison returned `all_equal: true` for every one of the 17 emitted
target functions. After copying the identical source blob to cumulative
integration, one normal Ninja edge rebuilt
`build/base/source/ai/action_charge.obj` and
`python tools/campaign/gate.py source/ai/action_charge --all` reported:
```text
exact 17 residual 0 unwritten 5
```
The five unwritten target functions are exactly the two documented large
fixed points and the three pruned dead helpers. `git diff --check` is clean.
Only `source/ai/action_charge.c` and this Jonas-owned ledger are added by this
wave. Units, Vehicles, Claude-active source, shared headers, configuration,
semantic policy, parked records, comparator code, and object status labels are
untouched.
@@ -1,243 +0,0 @@
# `action_charge.obj` Jonas real-random recovery
## Result
This bounded recovery starts from cumulative commit
`c07cbfb90b8bf67a86cb28bc6f3a1dffed70a486`. It adds the ordinary typed
`real_random` wrapper already authenticated by January COFF, the canonical
math interface, and the archived full-TU action-charge experiment.
`_real_random` is strict exact on the frozen recovery compile. The two
inherited lifecycle leaves remain strict exact, so `action_charge.obj`
advances from 2/22 to 3/22 exact functions. It remains `NonMatching` and
receives no data credit.
| Function | Meaningful / padded | Relocs | Normalized SHA-256 | Disposition |
| --- | ---: | ---: | --- | --- |
| `_real_random` | `13 / 16` | 2 | `50eeeb3dc58069325659d8bc0b6f2c17bb9f4fdce278b2f1a37edb64f4fcc60c` | newly exact |
| `_action_charge_begin` | `80 / 80` | 3 | `c78c6feeb10fda8857834f5264614aa976498059590ee08304aef25f21af8bf3` | inherited exact |
| `_action_charge_update` | `71 / 80` | 2 | `f2591b2de350c20aefef42c38623875dca494bb46a51d06c6dcd5947b035976e` | inherited exact |
The accepted object total is 164 meaningful bytes, 176 padded bytes, and
seven relocations across three functions.
## Provenance and recovery status
This is not represented as a compiler-naive first shot. Archived commit
`677defb9d9b4dd0ac4f20bc2c5dbd53bf3e8d7f5` records a 20/22 full-TU
experiment in `docs/object_matching_logs/action_charge_obj.md`, blob
`bbb630a9fc673dab443948add44b8457e3b465e4`. That experiment independently
compiled `_real_random` strict exact, then restored the translation unit
because the large `action_charge_perform` and `action_charge_setup` bodies
remained non-exact. This wave recovers only the already authenticated wrapper;
none of the rejected large-function source, scheduling hypotheses, or
temporary data owners is reused.
The current two-leaf checkpoint is blob
`0ab1bc6655eda440815fd2c185160f8ba96de081`, last clarified by commit
`4053598caa60b40da0e660c138708f85e2a8550b`. The baseline source blob is
`d34d932679150399937475d915ade404f7f94ac9`; the frozen recovery source blob is
`3e76470cf8be066072239845cbf08e37aeffa458`.
The January split object has SHA-256
`76c4c0b62850ffaa42fe5027c3e68b2cc42f7640c1156342c9a9cc4d1125afea`.
The compiler is Microsoft 32-bit C/C++ Optimizing Compiler `13.00.9254.1`,
SHA-256
`483e00c47bb08d699475a642bcff15b5b2036350b31c540e88a506baf101da11`,
with the unchanged XDK 3911 C command and flags.
## Typed interface and source shape
Current `source/math/real_math.h`, blob
`f14567675126171a4b1c2d3052e8058c68c029c2`, declares both callees and owns
the canonical inline behavior:
```c
real real_random(
void)
{
return real_seed_random(
get_global_random_seed_address());
}
```
`actions.h` is the first transitive route to `real_math.h` in this TU. The
inline name is therefore narrowly macro-renamed to `real_random_inline` after
`cseries.h` and before `actions.h`, then undefined after the existing include
list. The explicit external definition above is retained in the public-code
section. No shared-header declaration, duplicate prototype, synthetic caller,
or emission anchor is introduced. Direct symbol inspection proves that no
`_real_random_inline` owner or reference is emitted.
Both dependencies are strict exact in complete
`source/math/random_math.obj`:
- `_get_global_random_seed_address`: 53 meaningful / 64 padded bytes, seven
relocations, normalized SHA-256
`978f6fa37835b338c2088717e09aa32a8e80f074c0c52043c3ee74f9e0c3e9a1`;
- `_real_seed_random`: 48 meaningful / 48 padded bytes, two relocations,
normalized SHA-256
`ad693d3107104928cec2113aa91e03214c37eced08553a417d7f90191ccec3b4`.
## Strict relocation and ownership evidence
The target and candidate padded bytes are identical. Both relocations are
zero-addend REL32 entries:
| Offset | Destination |
| ---: | --- |
| `+0x01` | `_get_global_random_seed_address` |
| `+0x07` | `_real_seed_random` |
The target-authoritative census is 3/22 strict-exact functions against 6,032
meaningful / 6,176 padded target code bytes. The candidate adds exactly one
NODUPLICATES code COMDAT owner `_real_random`, plus undefined references to
the two typed callees and VC7's ordinary compiler-generated `__fltused`
linker marker. The latter is an undefined external, not a runtime owner; the
same natural marker is present in the accepted `action_alert.obj` real wrapper.
There is no owner removal and no candidate `.rdata`, `.data`, BSS, COMMON, or
other runtime non-code section. All fifteen target read-only COMDATs, 243 raw
payload bytes / 256 progress bytes, remain uncredited.
The generic baseline regression gate reports exactly `_real_random` as
`NEWLY_EXACT`, both lifecycle functions as `still_exact`, and
`changed_nonexact: []`. Its nonzero result is confined to the expected new
symbol set plus compiler debug changes in `.debug$F` and `.debug$S`; no
adjudication or comparator exception is added.
## Compile and policy record
The frozen source passed the XDK `/Zs` parse-only check. The subsequent Ninja
dry run exposed exactly one
`CL build\base\source\ai\action_charge.obj` edge. That edge was invoked once,
reported exactly `[1/1] CL build\base\source\ai\action_charge.obj`, and its
output was immediately preserved as
`build/audit/action_charge_real_random_recovery_20260826.obj`, 1,405 bytes,
raw SHA-256
`365281ea87d2f74746742f68b8928223b05bee7abe16d5207734e761ba02ebb5`.
No source spelling, include position, declaration, retry, scheduling tune,
compiler flag, or second candidate compile followed.
The retained source is readable typed C with `void` on its own parameter line
and a direct explicit return. It contains no assembly, `volatile`, `register`,
pragma, intrinsic, attribute, forced inlining, barrier, raw offset or address,
pointer/integer reconstruction, cast or union pun, inactive-union access,
undefined behavior, synthetic caller or anchor, object-byte patch, comparator
exception, or alternate compiler flag.
Direct hardened comparison reports `all_equal: true` for the new wrapper and
both sentinels. The target object dry run reports no work after preservation,
`git diff --check` and the changed-path/protected-file review pass, and all 179
tooling tests pass. Whole-campaign reports and complete Halo/libcmt validation
are deferred to the corrected cumulative-HEAD closure so this isolated
worktree does not rebuild unrelated historical outputs.
Only `source/ai/action_charge.c` and this Jonas-owned ledger are changed. The
approval-gated Units package and its protected neighbors remain frozen. No
push is performed.
## Committed-state replay
Implementation-and-ledger commit
`8ed01222d5e1eec38b3a7a8801c05e52926c753e` was clean before replay. Its
committed source blob remains
`3e76470cf8be066072239845cbf08e37aeffa458`. The clean selected-unit manifest
is `build/audit/action_charge_real_random_committed_20260826.json`, 250,738
bytes, SHA-256
`0fa2c07f0859a868f37e10c0ae0f6094d02a265d26522a00d8f025eaf90945ac`.
The resolved generated object path was verified to remain inside the isolated
worktree. `build/base/source/ai/action_charge.obj` was literally deleted, its
absence was confirmed, the Ninja dry run exposed exactly one expected compiler
edge, and the normal rebuild reported exactly
`[1/1] CL build\base\source\ai\action_charge.obj`. The preserved committed
replay is 1,405 bytes with raw SHA-256
`97d6ffb9f5f320010affd46b659c0de3a2012be0a8376e2e8c9b6697fb9e696f`.
The replay and frozen recovery objects have identical length and differ at
exactly COFF header offsets `+0x4` and `+0x5`, compile-timestamp bytes. Every
modeled section, symbol, meaningful and padded byte, and relocation is
identical. Direct hardened comparison again reports `all_equal: true` for all
three accepted functions. The committed manifest check has no failures or
warnings, no newly exact or changed-nonexact entry, and places
`_action_charge_begin`, `_action_charge_update`, and `_real_random` in
`still_exact`. Both the selected object dry run and normal invocation report
no work afterward.
This section is the only tracked post-implementation change. No push is
performed.
## Corrected cumulative-HEAD closure
The cumulative branch was fast-forwarded from
`c07cbfb90b8bf67a86cb28bc6f3a1dffed70a486` through the isolated
implementation and replay-ledger commits to
`450249b0ead2989462f6e7d1218f0ad9ebd0a7d6`. The implementation source blob
remains `3e76470cf8be066072239845cbf08e37aeffa458`; no protected source, shared
header, storage declaration, configuration, semantic ledger, parked manifest,
or object-status file changed during integration.
From that clean corrected HEAD, the resolved cumulative
`build/base/source/ai/action_charge.obj` was verified inside the cumulative
worktree and literally deleted. The Ninja dry run exposed exactly one expected
compiler edge, and the normal rebuild reported exactly
`[1/1] CL build\base\source\ai\action_charge.obj`. The preserved corrected
replay is
`build/audit/action_charge_real_random_corrected_head_replay_20260826.obj`,
1,393 bytes, raw SHA-256
`f78c30bbff7d78d9b9f576a4b36a1bd5831ef8ba54e1f373b3ebf9102477cb13`.
Direct hardened comparison again reports `all_equal: true` for
`_real_random`, `_action_charge_begin`, and `_action_charge_update`. The
accepted corrected delta remains exactly one function, 13 meaningful bytes,
16 padded bytes, and two relocations, with no changed non-exact function and
no runtime-data gain or regression.
The isolated and cumulative raw objects are root- and timestamp-sensitive.
VC7's `.debug$S` record embeds the absolute output-object path: it is 194
bytes in the isolated `action-charge-three-leaf-20260821` worktree and 182
bytes in `units-integration-20260824`, exactly accounting for the twelve-byte
object-size difference. After report regeneration, the cross-worktree
manifest check reports only this `.debug$S` difference. All three accepted
functions are `still_exact`, `changed_nonexact` is empty, and every modeled
production code and runtime non-code record is stable.
The corrected clean manifest is
`build/audit/action_charge_real_random_corrected_head_manifest_20260826.json`,
250,738 bytes, SHA-256
`a5a9efe588d81de5e8dce55df5a44cbe4627dc0c09500e67f9df4db6da0a616e`.
Its no-build self-check has no failures or warnings, no newly exact or changed
non-exact entries, and retains all three functions in `still_exact`.
Regenerated corrected artifacts are:
- `action_charge_real_random_corrected_head_report_20260826.json`: 1,573,815
bytes, SHA-256
`4ccad6c2d5fd4d48c0e4755d3dd07fddac6a800d15b41620bc01667440b4d6fe`;
- `action_charge_real_random_corrected_head_semantic_20260826.json`: 2,972,764
bytes, SHA-256
`2408321ac6feeab0c9b2ff8de676fd408d3cdb369419ec3afcd7e896f6590a9d`;
- `action_charge_real_random_corrected_head_admission_20260826.json`: 384
bytes, SHA-256
`f0aa45c22cc604fb74abc388b06dd79a27e7a0b498a5d28837a9e785680c9a36`;
- `action_charge_real_random_corrected_head_parked_20260826.json`: 3,950
bytes, SHA-256
`cfac03aea5be607c19a6a40bcdbc1164e71ffb0179602af4bd37b3fd50e26d1d`.
The hardened semantic audit evaluates 4,246 functions, accepts 4,165 exact
functions, and reports zero unit errors. Campaign progress is 375/833 objects,
4,139/11,060 exact functions, 502,200/2,198,102 meaningful code bytes, and
1,835,088/4,176,062 matched data bytes. Halo progress is 273/468 objects,
3,972/7,574 exact functions, 489,286/1,770,166 meaningful code bytes, and
1,829,896/3,923,451 matched data bytes. `action_charge.obj` is 3/22 exact with
164 meaningful exact code bytes and no data credit.
Admission remains zero candidates and zero revocations; only the inherited
`source/shell/shell_xbox` completion-label contradiction remains. Parks remain
three active / zero stale / zero invalid. The selected object and complete
Halo/libcmt dry runs and normal invocations all report no work, and all 179
tooling tests pass.
The approval-gated Units package remains frozen. This closure does not
constitute its required exact approval phrase and authorizes no edit to Units
or its protected neighbors. No push is performed.
@@ -1,165 +0,0 @@
# `action_charge.obj` Jonas two-leaf wave
## Result
This bounded source-only wave starts from authoritative integration commit
`3f2ba63dfb1258f7e36baaec69fe495bcefdc20f`. The three permitted public
action-charge leaves were emitted together in exactly one natural candidate
compile. `_action_charge_begin` and `_action_charge_update` were independently
strict and are retained. `_action_charge_is_leaping` missed and was removed
immediately, without a spelling, declaration, control-flow, or compiler-control
retry.
`action_charge.obj` advances from 0/22 to 2/22 exact functions and remains
`NonMatching`.
| Retained function | Meaningful | Padded | Relocations | Normalized SHA-256 |
| --- | ---: | ---: | ---: | --- |
| `_action_charge_begin` | 80 | 80 | 3 | `c78c6feeb10fda8857834f5264614aa976498059590ee08304aef25f21af8bf3` |
| `_action_charge_update` | 71 | 80 | 2 | `f2591b2de350c20aefef42c38623875dca494bb46a51d06c6dcd5947b035976e` |
| **Wave gain** | **151** | **160** | **5** | |
The hardened COFF comparator proves identical padded bytes and exact
relocation addresses, types, destinations, and addends. The target owns 6,032
meaningful code bytes in 22 functions and 6,176 padded code bytes. Its fifteen
non-code COMDATs contain 243 payload bytes (256 bytes in the progress metric);
all remain outside this wave's credit. The final candidate owns no runtime
non-code data or storage: it emits no `.rdata`, `.data`, `.bss`, or COMMON
symbol.
## Provenance and ABI
- Baseline `source/ai/action_charge.c` blob:
`4a2578245ccc649a0a6c20823cfb8f204cc201e6`.
- Retained source blob/payload SHA-256:
`d34d932679150399937475d915ade404f7f94ac9` /
`78f7439e940d38166d075c19804f74524e0cb784199c533ab97a8c7b785e9bb4`.
- January executable SHA-256:
`4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`.
- January split-object SHA-256:
`76c4c0b62850ffaa42fe5027c3e68b2cc42f7640c1156342c9a9cc4d1125afea`.
- Clean HCEA semantic donor commit:
`c168af2e747d3095d9a29418ae401f3a39544863`. Its
`src/action_charge_begin.c`, `src/action_charge_update.c`, and
`src/action_charge_is_leaping.c` blobs are respectively
`6692a82a1ba231cc7d91c38860096cf6ec80a8e4`,
`3e7d6333d32d89108bfa8b6db76390512094044b`, and
`38fc8c40afa3cc720f5d8f784c22da25150febcc`. Their payload SHA-256 values
are respectively
`9cdc2a06b44a3a69e3418bf79e2aced52111cbd5d9ab9404044c9b472de185c7`,
`a949c7b6fb3ebbd1db455d09f6b0856788d05aa52ce52e0eba3a7b801d1474bd`,
and
`c6857cdfcc0225c97dd8bdfe56a8f5a8ea721881a48ca58e71b8af0911b7a7d5`.
These files supply independently readable behavior, not target-derived
emission anchors.
- Cross-build correspondence commit
`8ffdbab2301d17215b6f4b4a109f0e0a6f09b53d`, blob
`ec76e6464bf79d444779720d5efb9432bf5378d7`, classifies all three names as
exact-unique, ordinary `cdecl`, one-parameter functions. January COFF and
callers remain authoritative for the 32-bit parameter type.
- The existing `struct action_specification` types `begin` and `update` as
`void (*)(long)`. January's action table relocates to the two functions at
`+580` and `+588`. Existing `actions.h` types `action_charge_is_leaping` as
`boolean(long)`, and January `_actor_is_leaping` calls it at relocation
`+33`.
- The earlier evidence-only frontier commit
`677defb9d9b4dd0ac4f20bc2c5dbd53bf3e8d7f5`, ledger blob
`bbb630a9fc673dab443948add44b8457e3b465e4`, independently records all three
public leaves as exact on their first complete readable shapes. It is not a
production-source donor and none of its tuned control/private-helper forms
were reused.
- Compiler: Microsoft 32-bit C/C++ Optimizing Compiler `13.00.9254.1` for
80x86, SHA-256
`483e00c47bb08d699475a642bcff15b5b2036350b31c540e88a506baf101da11`,
with the repository's unchanged XDK 3911 command and flags.
The retained signatures are:
```c
void action_charge_begin(
long actor_index);
void action_charge_update(
long actor_index);
```
Existing `actions.h`, `actors.h`, and `actor_definitions.h` own the action,
actor, charge-state, and firing-variant layouts. The callee's return/argument
types are corroborated by existing source TUs, so the missing declaration is
kept translation-unit-local. The firing-situation enumerator is likewise
source-local because no shared header presently owns it. No shared-header or
storage repair is needed.
## Authenticated behavior and one-shot rejection record
`action_charge_begin` resolves the full 32-bit actor handle through
`actor_get`. For vehicle-strafing charge with the matching firing-variant
special-fire situation, it decrements a positive signed-short deny-attempt
counter. Its target relocations are `DIR32 _actor_data` at `+4`,
`REL32 _datum_get` at `+16`, and
`REL32 _actor_combat_get_firing_variant_definition` at `+37`.
`action_charge_update` resolves the same typed actor handle. During a launched
melee leap, before a melee attack and after the actor is no longer midair, it
increments the signed-short leap-failure timer. Its target relocations are
`DIR32 _actor_data` at `+8` and `REL32 _datum_get` at `+15`.
All three public candidates were compiled together once. The rejected
`_action_charge_is_leaping` target is 66 meaningful/80 padded bytes with three
relocations and normalized SHA-256
`9ddd3d1296e581d078e6958f4039f9bde736698282368436b60d130978e7b275`.
The first natural candidate was also an 80-byte, three-relocation COMDAT, but
its normalized SHA-256 was
`56aa0e8827a4ebecba9e4310f944bc24c07ffb5c2b2e39ef46ea11ad36899f31`.
The target forms one charge-state pointer and shares its epilogue; the readable
HCEA early-return shape instead accesses full actor offsets and duplicates the
return. Consequently the common `game_time_get` relocation occurs at target
`+48` but candidate `+46`. The candidate COMDAT and its support-only prototype
were removed without retry. The one-shot three-body candidate object SHA-256
was `3ee981a618bc3f891786bc839c2c51a3bd11bfb9a16a1e90f382d0569fe3a8af`.
`action_charge_control`, `action_charge_perform`, `action_charge_setup`, both
anonymous `code_` helpers, generic helper emissions, and every data/storage
section were explicitly outside scope and are absent from the final object.
## Policy, scope, and validation
Only `source/ai/action_charge.c` and this new Jonas-owned ledger are changed.
No shared header, storage definition, data section, configuration, semantic
exception, parked record, completion label, protected-five source,
pre-existing Markdown, or Claude-owned file is edited.
The retained source is readable typed C. The guarded decrement remains in the
positive signed-short range. The timer increment is promoted to 32-bit `int`,
so its arithmetic cannot overflow; a hypothetical value above `SHRT_MAX`
narrows according to the implementation-defined two's-complement behavior of
the authenticated VC7/Xbox target. That ordinary target contract is already
used by the project's signed-short AI timers and is not undefined behavior or
an emission control. The source contains no assembly, raw address or
byte-offset dereference, pointer/integer reconstruction, pointer or union pun,
inactive-union access, undefined overflow, volatile scheduling device,
force-inline annotation, optimizer pragma/barrier, synthetic anchor, or
object-byte forcing.
- Forced rebuild of the final retained source: pass. The only defined code
symbols are `_action_charge_begin` and `_action_charge_update`; there is no
runtime `.rdata`, `.data`, `.bss`, or COMMON ownership. Direct hardened
comparison passes both functions.
- Complete `halobetacache_build` and `libcmt_build`: pass (568 remaining edges
after the final translation unit's forced rebuild).
- Semantic audit: 470 units, 4,074 functions evaluated, 3,934 semantic exact,
101 hidden exact / 61,876 hidden code bytes, 3,995 accepted exact, and zero
unit errors.
- Campaign progress: 375/833 complete objects, 3,982/11,060 exact functions,
476,846/2,198,102 meaningful code bytes, and
1,835,088/4,176,062 matched data bytes. This wave adds two functions and 151
meaningful bytes with no data increase.
- Admission audit: zero candidates and zero revocations; only the inherited
`source/shell/shell_xbox` completion-label contradiction remains.
- Parked validation: three active, zero stale, and zero invalid entries.
- Complete tooling suite: 179/179 tests pass; `git diff --check` is clean.
A clean committed-state regression snapshot/check followed by deletion and
forced rebuilding of `action_charge.obj` is the final same-path reproducibility
proof. The post-rebuild hardened comparison must remain strict for both
accepted functions, and the rejected symbol must remain absent. No push is
performed.
@@ -1,120 +0,0 @@
# `action_charge.obj`: setup frame lever and helper-linkage measurement (Opus 5 150K house-clean lane, wave w2, 2026-09-14)
## Result
- `_action_charge_setup` (944 padded, previously a structurally incomplete 688 / 20 residual) is now a complete typed-C body,
landed as **fuzzy** (zero credit): **944 / 29 relocations, instruction-identical to January except the placement of one
`result = FALSE` store** (`mov byte [ebp-1],0` at +0x138 instead of +0xe4).
- A **strict-EXACT** setup candidate exists (`scratch/workers/action_charge/w2/v3.c`, 21/22 object). It is held back because it
needs a January-proven redundant `result = FALSE;` store (see section 4). Owner ruling requested.
- `_action_charge_perform` is unchanged (fuzzy 3216 / 84). The requested helper-linkage measurement is recorded in section 5; the
ruling alone does not produce an exact function.
- Object: `== exact 20 residual 2 unwritten 0` (baseline identical counts). No newly exact function. All 20 baseline-exact rows
keep their listings row for row.
## 1. Baseline and provenance
- Worktree `C:\halo-worktrees\opus5-150k-house-clean-20260914`, base `ea9175577`. Gate without the `_point_from_line3d` guard
(brief 3b: action_charge.obj is the genuine owner; the final object defines exactly one `_point_from_line3d`, strict EXACT,
sha16 `9b763841f8519177`).
- Baseline rows: perform `[size 3216!=3248, sha]`, setup `[size 688!=944, relocs 20!=29, sha]`, 20 EXACT.
- Read: all eight earlier action_charge ledgers, the 100K handoff, the fresh-graphs worker notes and scratch
(`opus5-30k-fresh-graphs-20260914/scratch/workers/action_charge`: `setup_v12.txt`, `t_setup.asm`, `t_perform.asm`).
`branch_sweep`: 12 blobs, none better. A cut-off earlier run in this lane left undocumented probes
(`scratch/workers/action_charge/d_*.c`, `probe/`); used as evidence only.
## 2. New evidence
- **HCEX.pdb (SHIP) locals** (`DIA2Dump -sym`): `action_charge_setup` homes only `short melee_start_tick`,
`short melee_end_tick`, `float melee_start_range`, `float melee_end_range` (all other locals enregistered).
`action_charge_valid_melee_destination` is `static unsigned char (long actor_index, short goal, union real_point3d *melee_target_point)`
with locals `estimated_position`, `collision`. Later build: names and signature shape only.
- **January caller bytes** confirm that signature: setup pushes `[ebp+0xc]` (the short `goal` parameter) and passes
`&prop->center_of_mass` in ESI. The 2026-09-06 ledger's "the point is the second argument, do not import the short
signature" premise is refuted by these bytes; the helper ignores the stack `goal`, and its point parameter is
`melee_target_point`. The helper stays strict EXACT with the authentic signature, and the `(void)melee_target_point;` line is gone.
- `ai_debug.c` charge-decision renderer: `field_198` is the "airborne" (leap) flag, `field_194` is the cannot-move range.
## 3. Setup frame lever (new measured VC7 13.00.9254 law)
January's frame is 0x20 with `leap` (byte) at [-8] and `end_tick` (short) at [-6] in **one** 4-byte cell. The v12 body declares
`leap` and the four tick/range outputs in the same block and gets 0x24 (separate cells). Wrapping the four outputs in a nested
block around the `unit_get_melee_range_and_ticks` if/else (leap stays in the enclosing block) reproduces January's frame and
slot set exactly.
Law: locals declared in the **same** block never share a frame cell. A nested-block local can be packed into, or overlap, an
enclosing local's cell when their live ranges allow it. Probe `scratch/workers/action_charge/probe2/b.c`: f1 (same scope)
frame 20, f2/f3/f4 (outputs nested) frame 16. This contradicts the older note that flattening a block is inert. Only
promotion to the enclosing scope was measured there.
## 4. Setup: the redundant-store blocker
January stores 0 to `result` ([ebp-1]) at +0xe4, which dominates the inhibited test, and again at +0xea (inhibited arm) and
+0x107 (notarget arm). The deep failure arms (no-animation, cannot-move) have no store. Only the literal source
`result = FALSE;` before `if (TEST_FLAG(... inhibited))`, together with `result = FALSE;` in both arms, is strict EXACT
(`w2/setup_exact.txt`).
Non-redundant alternatives measured (all on the nested-scope base):
| Shape | Size / relocs | Outcome |
| --- | --- | --- |
| A1 pre-branch store only | 1024 | tail-duplicated epilogues |
| A4 store at top of melee arm | 1024 | same |
| A6 stores in every failure arm, none before | 1056 | `result` loses its home |
| R1 / R1b / R2 / R3 early `state_data->goal = goal; return ...;` arms | 1024 / 992 / 1024 / 1024 | own epilogues; VC7 does not coalesce return values into `result` |
| **H1** arm stores + `result = FALSE;` first in the melee-attempt arm | **944 / 29** | one store placement differs (**landed, fuzzy**) |
| H3 same store before `prop_get` | 928 | |
| S2a / S5 separate valid/success flag copied into `result` | 1072 / 1072 | |
Owner ruling requested: admit the January-proven redundant store (`w2/v3.c`, the landed file with one `result = FALSE;` moved to
before the inhibited test) for +944 padded exact. The class is the handoff's "January-proven redundant logic"
(`_extract_sequence`).
## 5. Perform: helper-linkage measurement
`w2/v4_inline.c` is the landed file with `__inline` added to the 11 local helper definitions. January's atlas `i` records mark
`normalize2d`, `point_from_line3d`, `magnitude3d`, `normalize3d` and `collision_test_line` as inline COMDATs.
- All 20 other rows stay strict EXACT, with no new owner.
- Perform goes from 3216 to 3264 padded (meaningful about 3250 against January's 3246).
- The two separate `direction = prop->actor_to_prop` copies come back.
What remains after the lever:
- one extra `mov ecx,[ebp-8]` prop reload at +0x256 (the only instruction-count difference);
- the melee-abort block: January keeps `check_range` in BL and reloads the prop, with cells `check_range` [-2] and
`berserk_ranges`+`melee` [-1]; ours keeps the prop in EBX;
- the `&field_114` register;
- x87 scheduling of `lead_fraction * ticks`, the suicide dot product and the ticks `movsx`;
- argument registers of three calls.
Two more shapes were inert: W1 (the weapon pointer in its own block) and W3 (negated-or LOS test). Not landed:
perform is an already-written residual and none of these is strict EXACT.
## 6. Checks (landed real file)
- `gate --all`: `exact 20 residual 2 unwritten 0`. The diff against HEAD's listing is only the setup row
(`[size 688!=944, relocs 20!=29, sha]` becomes `[sha]`).
- Parks: none for the unit.
- Owner census against `build/base`: the only new owners are the target-owned `actor %s melee animation has no damage keyframe`
string and the `state_data` assert literal, which January references as undefined. Pre-existing non-target owners
(`_vector_from_points3d` SELECT_ANY, `__real@41f00000`, `__real@3f000000`, D3D tables) are unchanged.
- `tools/fake_match_scan.py`: 0 leads.
## 7. Proposals
1. Owner ruling: admit `w2/v3.c` (redundant `result = FALSE;` proven by January +0xe4/+0xea/+0x107) as strict EXACT setup.
2. Helper linkage (`__inline` local helper definitions): measured in section 5. It does not give exact credit on its own, so it is
not needed now.
3. Pre-existing debt, not introduced here: action_charge.c carries a consumer-local prototype of units.c's
`unit_get_melee_range_and_ticks`. Its owner would be `source/units/units.h`, which has 66 includers
(declaration-count risk, units.c protected neighbour). Proposed text, placed next to `unit_melee_attack_begin`:
`boolean unit_get_melee_range_and_ticks(long unit_index, boolean secondary, short *melee_tick, real *attack_time, short *frame_count, real *damage_time);`
in house one-parameter-per-line form.
## 8. Reopen criteria
- **setup:** the owner ruling above, or a non-redundant source that emits a dominating FALSE store before the inhibited branch
while keeping both arm stores.
- **perform:** a helper-linkage ruling plus a source-level cause for the +0x256 prop reload and the `check_range` BL allocation.
Do not re-permute declaration order or scopes (fresh-graphs do-not-repeat list).
@@ -1,188 +0,0 @@
# `action_charge.obj`: setup store-position closure proof and perform linkage re-measurement (Opus 5 250K house-clean lane, wave w3c, 2026-09-15)
## Result
- **Nothing landed.** `git diff -- source/ai/action_charge.c` is empty; the object is unchanged at
`== exact 20 residual 2 unwritten 0`, identical row for row to the baseline listing.
- `_action_charge_setup` (944): re-measured at **291 instructions vs January's 291 with exactly ONE aligned
difference** — the position of `mov byte ptr [ebp-1],0` (January `+0xe4`, ours `+0x138`). This wave adds a
**construction proof that no non-redundant source spelling can place that store at `+0xe4`**, so the row is
definitively owner-blocked on the laws_w3 A42 / R12 redundant-store ruling, not on a missing shape.
- `_action_charge_perform` (3248): the `[size 3216!=3248]` gap is **fully attributed** to a single cross-jump —
January emits the `direction = prop->actor_to_prop;` copy twice, ours once. The linkage lever was reproduced and
a strictly better-motivated route for it was found (`p2.c`: delete the per-TU rename package and the 11 local
helper bodies, take the helpers from `math/real_math.h`), producing **byte-identical** output to the previously
declined `__inline` form. Neither reaches exact (3264), so neither is landable; the residual is 59 real
allocation blocks.
## 1. Baseline and provenance
- Worktree `C:\halo-worktrees\opus5-150k-house-clean-20260914`, branch `opus/250k-house-clean-20260915`.
- Gate WITHOUT the `_point_from_line3d` guard (brief 3b exception: this object is the genuine owner). The guard run
fails only on that pre-existing owner: one code definition, 48 bytes, strict EXACT, sha16 `9b763841f8519177`.
No additional candidate-only math owner exists (census below).
- Baseline rows: `residual 3248 _action_charge_perform [size 3216!=3248, sha]`, `residual 944 _action_charge_setup [sha]`,
20 EXACT. No parks for the unit.
- Read in full: all nine action_charge ledgers (2026-08-21 two-leaf, the three 2026-08-26 math-helper ledgers,
2026-08-28 public residuals, 2026-08-29 helper reintegration, 2026-09-06 authentic math header owners,
2026-09-14 fresh-graphs, 2026-09-14 150K w2), `scratch/workers/action_charge.md`, the 100K handoff,
`scratch/w2/laws_w2.md` and `scratch/w3/laws_w3.md` in full.
- `git log --all --oneline -- source/ai/action_charge.c`: 16 refs. `branch_sweep`: 13 unique blobs, HEAD is the best
(`exact 20 residual 2 unwritten 0 sizedelta 32`); nothing to reuse.
- Scratch: `scratch/workers/w3c_action_charge/` — `base.c/.obj`, `p1.c/.obj`, `p2.c/.obj`, `setup_aln.txt`,
`perform_aln.txt`, `perform_aln_full.txt`, `p1_aln.txt`. Worker notes: `scratch/workers/w3c_action_charge.md`.
## 2. `_action_charge_setup` — the shape space is closed
`alndiff` against the HEAD build (`setup_aln.txt`):
```
target 291 instructions, ours 291 instructions
--- delete target[67:68] ours[67:67] @T 0xe4 T e4 mov byte ptr [ebp - 1], 0
--- insert target[88:88] ours[87:88] @O 0x138 O 138 mov byte ptr [ebp - 1], 0
```
(the three other reported blocks are `__FILE__`/`.rdata` label artifacts of `--source`, not code differences).
January's `[ebp-1]` writes: `+0x45 = 1` (the sunk `boolean result = TRUE;` initializer — no entry store, so A42's
"VC7 does not hoist a FALSE into the entry" detector says `+0xe4` is a *separate* source statement), `+0xbc = 0`
(swarm arm), `+0xe4 = 0`, `+0xea = 0` (inhibited arm), `+0x107 = 0` (notarget arm), `+0x308 = 1` (melee success),
`+0x383 = al` (vehicle arm), read once at `+0x39a`.
Context of the divergence:
```
d9 mov cl, byte ptr [eax + 0xb6]
df add esp, 8
e2 test cl, cl
e4 mov byte ptr [ebp - 1], 0 <-- between the compare and the jcc
e8 jns 0xfc
ea mov byte ptr [ebp - 1], 0
ee mov word ptr [edi + 0x190], 3
```
**A38** (initializer / zero-store placement) fires exactly: a store emitted between an `if`'s compare and its `jcc`
belongs to the block *enclosing* that `if`, not to its body — i.e. the source statement sits before
`if (TEST_FLAG(unit->object.damage_flags, _object_melee_attack_inhibited_bit))`.
**Closure proof (new).** Both bodies emit four zero stores to `result`. In any source where every path assigns
`result` exactly once, the fourth store is the default for the deep melee-attempt path, and its only non-redundant
position is inside that arm — which is the landed H1 shape and emits at `+0x138`. Conversely a store emitted at
`+0xe4` necessarily precedes the inhibited test, where it dominates the inhibited and notarget arms; those arms'
own stores cannot be dropped (w2 measured A1 "pre-store only" at 1024 and A6 "arm stores only" at 1056 — removing
them re-creates nine tail-duplicated epilogues). Hence **strict EXACT requires a store that is redundant with
respect to two arms**, and the eleven alternatives already measured in w2 (A1, A4, A6, R1, R1b, R2, R3, H1, H3,
S2a, S5) are exhaustive of the non-redundant space. No shape was spent this wave; the proof replaces the probe.
The strict-EXACT body already exists: `scratch/workers/action_charge/w2/v3.c` (21/22 object) = the landed file with
`result = FALSE;` moved one source line, from the top of the melee-attempt arm to immediately before the inhibited
`if`. Class: laws_w3 C3 "Byte-proven redundant stores/branches (A42)", alongside `_encounter_test_rule` (tr8) and
`_extract_sequence`.
**Owner ruling requested (re-stated, with the new closure proof): +944 padded strict-exact bytes for one moved
source line, zero sibling and zero park risk.**
## 3. `_action_charge_perform` — the `[size]` gap attributed, and the linkage route improved
Production: 3216 / 84, **983 instructions vs January's 982**, relocation multiset identical.
### 3.1 The 32-byte gap is one cross-jump
January emits the `direction = prop->actor_to_prop;` struct copy twice:
```
6c8 mov eax,[ebp-8] <- then-arm of `if (dot_product3d(&direction,&prop->actor_to_prop) < 0.f)`
6cb fld dword ptr [__real@00000000] (distance = 0.f, interleaved)
6d1 mov ecx,[eax+0xe0] / 6d7 mov edx,[eax+0xe4] / 6dd mov eax,[eax+0xe8]
6e3 mov [ebp-0x2c],ecx / 6e6 mov [ebp-0x28],edx / 6e9 mov [ebp-0x24],eax
...
707 mov eax,[ebp-8] ... (second copy, different register schedule) <- inside `if (distance == 0.f)`
```
Ours emits it once at `+0x6e7`: the inlined `dot_product3d(&direction, &prop->actor_to_prop)` strength-reduces the
argument address to `add edx,0xe0` at `+0x6a1`; both copies then become the identical `mov ecx,[edx] …` sequence
and VC7 cross-jumps them. The eight-instruction target block against our one-instruction stub *is* the
`3216 != 3248` delta. The same biased pointer also reorders the inlined dot: January k,j,i (A35(2)
youngest-term-first), ours j,k,i.
**Detector caveat for A35 (new):** a k,j,i vs j,k,i term order can be an addressing-mode artifact of a
strength-reduced `&struct->field` pointer rather than a source expression-order or leaf-age fact. Look for an
`add reg,imm` bias in the candidate before spending an A35 shape on term order.
### 3.2 The linkage lever, reproduced and improved
| Shape | Size / relocs | perform sha16 | Siblings | Owner census | Landed |
| --- | --- | --- | --- | --- | --- |
| HEAD (rename package + 11 local helper definitions) | 3216 / 84 | `2e9beea61ca0c704` | 20 EXACT | unchanged | (in tree) |
| `p1.c` = HEAD + `__inline` on the 11 local definitions (w2 `v4_inline`) | 3264 / 84 | `798a99326c8aff1a` | 20 EXACT | unchanged | no |
| `p2.c` = HEAD with the rename package **and** the 11 local definitions **deleted** (helpers come from `math/real_math.h`) | 3264 / 84 | `798a99326c8aff1a` | 20 EXACT | `+_magnitude_squared3d` (code) | no |
`p1` and `p2` produce **byte-identical** perform sections, so inline *linkage* is the only operative variable; the
local `__inline` copies are not needed to obtain it. `p2` is the better-motivated form: January's map atlas marks
`normalize2d`, `point_from_line3d`, `magnitude3d`, `normalize3d` and `collision_test_line` as inline (`i`) COMDAT
copies, and `p2` removes an A30/R8-class per-TU `#define helper helper_inline` rename package plus 114 lines of
duplicated real_math bodies from a consumer `.c`. Its only cost is one surplus candidate-only COMDAT
`_magnitude_squared3d`, the same real_math.h select-any class as this object's already-disclosed
`_vector_from_points3d`.
Neither reaches 3248, so under brief section 6 (already-written residual: land only if strict EXACT) neither is
landable, and the fresh-graphs lane's decline of the `__inline` route stands. Recorded so a later wave does not
re-litigate the linkage question.
### 3.3 Residual after the lever (59 real blocks, `p1_aln.txt`)
109 aligned blocks, 59 after removing relocation-annotation and branch-offset noise (HEAD base: 67). All are
allocation:
- extra `mov ecx,[ebp-8]` prop reload at `+0x256` (+3 bytes; the only instruction-count difference);
- melee-abort byte pair swapped — January `check_range` at `[ebp-2]`, `berserk_ranges` (and the later `melee`) at
`[ebp-1]`; January also keeps `check_range` in BL across `game_time_get` where ours keeps `prop` in EBX and
reloads the flag (`mov al,[ebp-1]; test al,al` at `+0x387`, +5 bytes);
- `_charge_goal_melee` rematerialised as an immediate by January (`cmp word [edi+4],2`, `mov word [edi+4],2`) vs
pinned in ECX by us (`mov ecx,2` … `cmp word [edi+4],cx`) — the constant-register pinning family, inverted;
- `[ebp-0x14]` vs `[ebp-0x18]` for the tail `unreachable` byte;
- two commutative `fadd` operand orders at `+0x944` / `+0x956` — R3 forbids swapping the source operands for these;
- cdecl-pop (`add esp,N`) placement at `+0x6a2`, `+0x8e7`, `+0x919` (A31(a));
- roughly 35 pure eax/ecx/edx register-name substitutions.
Net 3252 meaningful vs January's 3246.
## 4. Shapes spent and rejected
- setup: 0 this wave (closure proof; the 11 w2 alternatives are exhaustive of the non-redundant space).
- perform: 2 of 5 (`p1`, `p2`), neither exact, neither landed.
- Designed and rejected on paper: setup S1 (split the inhibited else-if into `if`/`else` with one default in the
else) — yields three zero stores, not January's four, so it cannot be exact.
- Not re-spent (prior do-not-repeat): declaration order / block scope / names / dead initialisers of `leap` and the
tick outputs; early-return arms; `check_range` / `berserk_ranges` / `abort_range` / `time` / `target_range` /
`unreachable` declaration order and scope; dot_product3d argument order; `danger + attack` order; moving perform
before the helper package.
## 5. Checks
- Final real-file gate (no guard): `== exact 20 residual 2 unwritten 0`, listing identical to baseline row for row.
- Guard gate: fails only on the pre-existing genuine `_point_from_line3d` owner (one definition, 48 bytes, EXACT).
- Parks: `scratch/parkcheck.py` → "no parks for source/ai/action_charge".
- Owner census vs `build/base`: HEAD `candidate-only vs base: []`; `p1` `[]`; `p2` `[('_magnitude_squared3d','code')]`.
Pre-existing non-target owners (`_vector_from_points3d`, `__real@3f000000`, `__real@41f00000`, the two assert
literals, the D3D tables) unchanged in all three.
- `tools/fake_match_scan.py source/ai/action_charge.c`: 0 review leads.
- `git diff --stat -- source/ai/action_charge.c`: empty.
## 6. Proposals
1. **Owner ruling (setup, A42/R12 class):** admit `scratch/workers/action_charge/w2/v3.c`. One moved source line
(`result = FALSE;` before the inhibited `if`, arms unchanged) for +944 padded strict-exact bytes. This wave adds
the proof that no non-redundant spelling can reach exact, so the ruling is the only remaining path for this row.
2. **Census/policy note (perform):** if a later wave closes the allocator items, the linkage prerequisite should be
taken via `p2.c` (delete the rename package and the local helper bodies) rather than the declined local
`__inline` form; the orchestrator would then need to accept `_magnitude_squared3d` as a real_math.h surplus
select-any disclosure, the same class as this object's existing `_vector_from_points3d`.
## 7. Reopen criteria
- **setup:** the owner ruling in section 6.1. Do not re-spend on store placement, declaration order, block scope or
early-return shapes.
- **perform:** a source-level cause, measured on the `p2` base, for (a) the `[ebp-1]`/`[ebp-2]` byte-pair rank in the
melee-abort block that also puts `check_range` in BL, and (b) the `+0x256` prop reload. Do not re-spend on
declaration order/scope of the abort-block locals, dot_product3d argument order, or commutative `fadd` swaps (R3).
@@ -1,148 +0,0 @@
# `action_charge.obj`: perform reconstruction and setup frame residual (Opus 5 fresh-graphs lane, 2026-09-14)
## Result
- `_action_charge_perform` (3,248 padded, previously UNWRITTEN) is now a complete typed-C body, landed as
**fuzzy** (exact credit 0): 3,216 / 84 relocations vs 3,248 / 84, identical relocation destination multiset.
- `_action_charge_setup` (944-byte residual) was reconstructed to **944 / 29 with identical instruction opcodes**;
only frame-slot displacements differ. It is **not landed** (not strict exact).
- All 20 baseline-exact functions stay strict exact with unchanged normalized hashes.
- Object: `== exact 20 residual 2 unwritten 0 (of 22 listed)` (baseline `exact 20 residual 1 unwritten 1`).
No newly exact function.
## 1. Scope and provenance
- Unit `source/ai/action_charge.c`, target `build/split/source/ai/action_charge.obj`, lane base `47d9dd0da`.
- XDK 3911 `CL 13.00.9254` with `/O2 /Oy- /DDEBUG /Dxbox` (unchanged; `/O2` auto-inlines, see section 6).
- Evidence used: January COFF disassembly (Capstone, relocation-annotated), Ghidra 12.1.2 decompilation
`scratch/ghidra/out/action_charge.obj.decomp.c` (perform 383-832, setup 891-1039), January map atlas
`scratch/atlas/action_charge.obj.txt`, HCEA `src/blam/ai/action_charge_perform.c` / `action_charge_setup.c`
(later build, semantics only), `source/ai/ai_debug.c` charge-decision and melee-check renderers (field meanings),
`source/ai/actor_firing_position.c` weapon minimum-range idiom, `source/ai/action_fight.c`
`actor_perception_unreachable` prototype.
- Prior work read in full: the seven existing action_charge ledgers and the archived frontier ledger
`677defb9d:docs/object_matching_logs/action_charge_obj.md` (its perform/setup bodies were never committed;
branch_sweep over 11 blobs found no body in any lane).
## 2. Validated baseline
| Function | Target padded | Relocs | Baseline | Now |
| --- | ---: | ---: | --- | --- |
| `_action_charge_perform` | 3,248 | 84 | UNWRITTEN | fuzzy 3,216 / 84, sha16 `2e9beea61ca0c704` (target `9ed9d44b1e9fd722`); first divergence +0x256 |
| `_action_charge_setup` | 944 | 29 | 688 / 20 | unchanged in production; best candidate 944 / 29 sha16 `31040fe5ab789ce3` (target `34c0c115890ecab8`); first divergence +0x3 (`sub esp,0x24` vs `0x20`) |
| 20 others | 1,984 | | EXACT | EXACT, hashes unchanged |
Data: production now also owns the target literals `__real@3f5db3d7`, `__real@3cbf258c`, `__real@3e99999a`,
`__real@3d4ccccd`, `__real@3f4ccccd` (perform). `__real@41f00000` and the `!actor->meta.swarm` string are
emitted for references January resolves as undefined externals. The setup-only
`actor %s melee animation has no damage keyframe` string stays absent until setup lands.
## 3. Accepted controls (production)
`action_charge_perform`, placed between the helper package and the private destination helper:
- declarations in January call order: actor, actor definition (`actr`), variant definition (`actv`), firing variant,
charge state, `prop = NULL`, `result = FALSE` (January keeps `[ebp-3]` and returns it on the default path);
- prop-valid branch first with the "no target" arm last (`je` to +0x43c); a separate delayed-projectile arm
(January lays `advancing = TRUE` right after that test and cross-jumps the vehicle-goal arm into it);
- stalking exposure assigned to `state_data->stalking_currently_exposed` and re-read (January materialises it at
32-bit width, which a boolean local does not reproduce);
- close-range bounds with the `actor_firing_position.c` `MAX(minimum_range, weapon->weapon.ai_minimum_target_range)`
idiom (weapon +0x40c);
- melee abort: `MIN(abort_range, maximum_abort_range)` with a named `maximum_abort_range = MAX(0.f, melee_attack_range) + 0.8f`
(January stores it to a slot and compares with `fcom`), asserts at lines 288 and 487;
- melee check debug telemetry into `actor_debug_array` (`last_melee_time`, `field_108/114/120/12C/138/139/13C/148/14C`)
exactly as `ai_debug.c` renders it;
- `dot < 0` if/else distance form (two separate `direction = prop->actor_to_prop` copies in January),
leap arm `distance < lower && unreachable_ticks == 0 && !flying` aborts, ballistic leap with January's argument
mapping (13th = vertical, 14th = horizontal);
- communication immediates as enums: `_ai_communication_melee` (43), `_comm_hostility_enemy` (3);
- tail: `target_prop` re-fetch with `out_of_range = FALSE; if (...) out_of_range = TRUE;` (January `xor cl,cl` form),
single `return result;`.
Supporting source-local additions: includes `ai.h`, `ai_communication.h`, `items/projectiles.h`,
`items/weapon_definitions.h`, `ai_debug.h`; enum constants `_action_charge_prop_facing_central = 2`,
`_action_charge_prop_closing_speed_slow = 1`; TU-local `actor_perception_unreachable` prototype (same as action_fight.c).
The `(real_vector2d *)` casts on 3D vectors follow `actors.c` and January's call operands (the 3D aim vector's address
is passed to `normalize2d`).
## 4. Experiment matrix
Scores are differing instructions after alignment (`scratch/workers/action_charge/score.py`, relocation offsets normalised).
### perform
| Shape | Size / relocs | Diff insns | Kept |
| --- | --- | ---: | --- |
| first complete body | 3280 / 87 | - | no |
| prop-valid first, delayed arm split, exposure via state | 3248 / 87 | - | yes |
| named maximum_abort_range | 3216 / 84 | 144 | yes |
| dot<0 if/else, leap-abort polarity, order fixes (package helpers) | 3216 / 84 | 202 | yes (authentic form; package merges copies) |
| same, helpers marked `__inline` locally (not landed) | 3264 / 84 | 142 | lever only |
| check_range/berserk declaration orders (3 variants) | = | 140-141 | inert |
| target_range/time scopes (3 variants), unreachable scope | = | 140-141 | inert |
| `field_114` component copy | = | 135 but loses January's pointer copy | no |
| `target_prop` + if-form out_of_range, declaration order | = | 127 (`__inline` mode) / 182 (production) | yes |
| abort ternary as if/else, compare operand swap | = | 127 | inert |
| dot_product3d argument swaps, danger/attack sum order | = | 142 | inert (VC7 canonicalises) |
### setup (best candidate `scratch/workers/action_charge/setup_v12.txt`, not landed)
| Shape | Size / relocs | Notes |
| --- | --- | --- |
| single exit + debug decisions + assert + csmemset + keyframe error | 1040 / 29 | 9 tail-duplicated epilogues |
| + `result = FALSE` in inhibited/notarget arms (January stores at +0xbc/+0xe4/+0xea/+0x107) | 1040 / 29 | removing them later re-creates the duplication |
| + `move_range = MAX(minimum_move_range, target_range)` | 928 / 29 | duplication gone |
| + `state_data->leap_possible_if_at_range = real_random() < chance; leap = ...` | **944 / 29** | 32-bit compare width as January; only frame displacements differ |
| frame diagnostics: 16 declaration-order / scope / name / init / `valid` / variant-local variants, `/Fa` listings | 944 / 29 | `leap` and `end_tick` never share January's 4-byte cell |
## 5. Do-not-repeat
- perform: declaration order and block scope of `check_range`, `berserk_ranges`, `abort_range`, `time`,
`target_range`, `unreachable`; dot_product3d argument order; `danger + attack` operand order; moving perform before
the helper definitions (VC7 compiles bottom-up; identical output).
- setup: declaration order, block scope, names, dead initialisers of `leap` / tick outputs; removing `valid`; variant
pointer vs name local; `leap = cmp ? TRUE : FALSE` / `(long)` casts (keep 8-bit width).
- Pure header helpers (removing the rename package) emit candidate-only `_magnitude_squared3d` (LNK2005 history in
`action_charge_obj_jonas_math_helper_recovery_rejection_20260826.md`); do not land that.
## 6. Residual classification
- perform (measured): relocation multiset identical; remaining differences are register choices (melee-abort block
`check_range` in BL vs `prop` in EBX; `field_114` and `field_12C` copy registers; argument registers of
`unit_melee_attack_begin` / `ai_communication_event`), byte-cell packing of `berserk_ranges` / `check_range`, and
x87 operand order of three inlined dot products. **Inference, with a measured lever:** the inherited
NODUPLICATES helper package lets VC7 use normalize3d/normalize2d clobber sets (EDX kept live across the call);
January's helpers were SELECT_ANY inline COMDATs (atlas `i` records), so its caller could not. Marking the local
helper definitions `__inline` keeps all 11 helpers strict exact with no new owner and cuts perform's residual from
182 to 127 instructions. Not landed because it changes exact siblings' linkage.
- setup (measured): instruction stream identical; residual is frame layout only (January frame 0x20 with `leap` at
-8 and `end_tick` at -6 in one cell; candidate 0x24). No source lever found.
## 7. Reopen criteria
- Orchestrator decision on helper linkage (local `__inline` package, or header-owned helpers with January's
`magnitude3d` body that does not reference `magnitude_squared3d`), then continue perform from
`scratch/workers/action_charge/pbase3.txt` on that base.
- For setup: evidence of the original tick-output locals/types that makes `leap` and `end_tick` non-interfering for
VC7's frame packer; land `setup_v12.txt` together with the destination-helper signature
`(long actor_index, short goal, real_point3d const *melee_target_point)` only when strict exact.
## 8. Disposition
Object remains NonMatching: 20 strict exact, `_action_charge_perform` fuzzy (credit 0), `_action_charge_setup`
residual. No config, header, park, flag or comparator change.
## Lead admission notes (2026-09-14)
- The worker's TU-local `actor_perception_unreachable` prototype was replaced
by the owning declaration added to `source/ai/actor_perception.h` in the
separate prerequisite commit `a694cf1b2` (which also removed action_fight.c's
consumer-local copy). `action_charge.c` now includes that header.
- The leap stores were reordered to the natural `leap_pending = TRUE` first,
as measured by the adversarial reviewer; the emitted body is byte-identical
(3216/84, sha16 `2e9beea61ca0c704`).
- The proposal to mark the 11 local math helpers `__inline` was NOT adopted:
it changes COMDAT selection of 11 already-exact sections without an exact
gain and needs its own whole-board review. `_action_charge_perform` remains a
zero-credit fuzzy body; `_action_charge_setup` stays at its baseline residual.
@@ -1,129 +0,0 @@
# `action_converse.obj`: ordinary-C restoration and fuzzy park
## Outcome
`source/ai/action_converse.c` now contains a credible ordinary-C
reconstruction of `action_converse_perform`. The former implementation used
three inline-assembly regions to transcribe January's shared-`TRUE` register
choice. That was an exact machine-code reproduction, not credible original
gameplay source, and is not allowed by the rule limiting sparse inline
assembly to helper and math functions.
The ordinary-C function is intentionally retained at the measured compiler
boundary:
- object-local strict result: `8 / 9` functions, `640 / 896` padded bytes;
- target packet: `256` padded bytes, `8` relocations,
normalized SHA-256
`7a3fad8fb69307c132558b906832d4d31f0334734fc76d18fe773189b5a38d5d`;
- base packet: `256` padded bytes, `8` relocations,
normalized SHA-256
`288862a662942eb5b03ee84809dbd6b6aea1c023d52915f2bc373192e7c212cb`;
- objdiff fuzzy score: `93.14815%`;
- park class: `register-allocation`.
No exact-match credit is claimed for this function.
## Source credibility and semantics
The retained topology is the clean donor preserved at
`C:\halo-worktrees\opus-dual-scheduler-research\dispo-wt`, commit
`5ac58ab8731e9227de6fd87afdf0542ce318aa59`. The same body originated in
commit `b4651a63db29932d5a6640d0c7b5af59a13c8463`.
The function performs a coherent sequence:
1. Resolve the conversation's unit to a prop when no prop is cached.
2. Mark the actor in range when the prop is fully perceived and inside the
requested distance, or is closer than `0.7` world units.
3. Halt movement once in range.
4. Otherwise request movement toward the prop and record failure when that
request fails.
5. Record failure when no prop can be resolved.
This is not an accidental or nonsensical byte match. The state transitions,
callee contracts, field offsets, and relocation identities agree with the
January function. HCEA interface evidence at commit
`570c83fd9c365dad6f2a3e7041705d5b84c7847c` supports the action-table return
contract and the reconstructed behavior. The open Marathon source was
checked as a naming/style reference, but it supplies no stronger homolog for
this Halo-specific conversation action.
The numeric visibility threshold was replaced with the authentic enum
constant `_actor_perception_full`. January accesses `prop->visibility`; an
HCEA reconstruction using `team_index` at that point is inconsistent with the
January offset and was not copied.
The local `true_value` declaration and assignment remain separate even though
the general style preference is to combine them. This is the strongest
measured natural-C packet, and moving its initialization is one of the already
exhausted lifetime shapes.
## Residual compiler boundary
The target and base have the same eight relocation identities in the same
order:
| Relocation | Target | Base |
| --- | ---: | ---: |
| `_actor_data` | `+4` | `+4` |
| `_datum_get` | `+17` | `+16` |
| `_prop_get_base_by_unit_index` | `+67` | `+63` |
| `_prop_data` | `+107` | `+103` |
| `_datum_get` | `+114` | `+110` |
| `__real@3f333333` (`0.7f`) | `+157` | `+153` |
| `_actor_move_halt` | `+186` | `+183` |
| `_actor_move_to_prop` | `+220` | `+216` |
January retains `TRUE` in `EBX` and uses `BL` for the two byte stores. The
lawful VC7 C output folds those values into immediate operands. Five grounded
source shapes were exhausted without finding a credible C lever. The former
asm forced the target register allocation directly and was therefore removed
rather than treated as a reconstruction.
## Prototype ownership cleanup
The same wave removes use-site declarations and places the contracts with the
closest associated headers:
- `prop_get_base_by_unit_index` is declared under `prototypes/PROPS.C` in
`source/ai/props.h`;
- `actor_move_to_prop` and `actor_move_halt` are declared under
`prototypes/ACTOR_MOVING.C` in `source/ai/actors.h`, because
`actor_moving.c` has no dedicated header;
- `actor_move_halt` uses its authentic `boolean` return contract rather than
the inaccurate local `void` declarations. Existing callers ignore the
value, so their call behavior is unchanged;
- duplicate declarations were removed from `actions.c`, `action_charge.c`,
`action_converse.c`, `action_fight.c`, `action_guard.c`, `action_search.c`,
`action_wait.c`, and `ai_communication.c`.
All project real-valued parameters use `real` from `cseries.h`; no raw
`float` interface was introduced.
## Verification
- Focused VC7 compile of `build/base/source/ai/action_converse.obj`: pass.
- Full configured build, semantic report, admission audit, and progress gate:
pass with `0` unit errors.
- `tools.coff_compare` confirms the packet sizes, hashes, and relocation
identities above.
- The pre-change regression manifest reports exactly one intentional change:
the formerly accepted asm transcription is now nonexact. All eight
inherited strict-exact Action Converse functions remain exact.
- Park validation: `13` active, `0` stale, `0` invalid.
- Strict campaign board after the cleanup: `278 / 619` fully exact objects,
`4,857 / 8,246` strict-exact functions, and
`674,823 / 1,922,669` padded bytes. The perform function is counted as
parked, not exact.
- Test suite: `212 passed`.
- `git diff --check`: pass.
- The rebuilt object has no `point_from_line3d` symbol or COMDAT; geometry and
the January inline schedule are untouched.
Raw object hashes for the frozen verification build:
- target `action_converse.obj`:
`9a69bb69f5590a576fd82f635f47fdcb037b6ce5410d5ce2e5b4d3bd92510a35`;
- base `action_converse.obj`:
`e12f04462cff30aa8d59e15ff5d64a786ebc5d19506ed5147103a34848dc731b`.
@@ -1,126 +0,0 @@
# `action_flee.obj` Jonas two-leaf wave
## Result
This bounded source-only wave starts from authoritative integration commit
`a03585774f859697a788a73e4c3fa6bde7c6e3ea`. Six HCEA-backed action-flee
bodies were emitted together in one natural candidate compile. Two were
independently strict and are retained. The four nonexact bodies and their
support-only declaration were removed immediately, without a spelling,
declaration, control-flow, or compiler-control retry. `action_flee.obj`
advances from 0/13 to 2/13 exact functions and remains `NonMatching`.
| Retained function | Meaningful | Padded | Relocations | Normalized SHA-256 |
| --- | ---: | ---: | ---: | --- |
| `_action_flee_end` | 41 | 48 | 3 | `649eb65fd4e8e516d5c8675b4dbf5e08dab7b6d07888e25af8327f32b5c6be98` |
| `_action_flee_blind_panic` | 30 | 32 | 0 | `7bcf33bcce1177b460ded4cc5dee5518afa9db14695e8fdd49652483675b6297` |
| **Wave gain** | **71** | **80** | **3** | |
The hardened COFF comparator proves identical padded bytes and exact
relocation addresses, types, destinations, and addends for both retained
functions. The object now reports 71/3,312 meaningful code bytes and two
exact functions. The target's remaining eleven functions and all 312
non-code bytes stay outside this wave's credit.
## Provenance and signatures
- Baseline `source/ai/action_flee.c` blob:
`73990e6dee5af609201a2b17af56424f3276161a`.
- Retained source blob/SHA-256:
`65b90ce0ba18f6e01c13ef33c8b6bd539db16079` /
`67e862acfb2a040c4a652ecbe85c842d1a9869a3159eefc9bc3059bb351874b7`.
- January executable SHA-256:
`4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`.
- January split-object SHA-256:
`5d6e667103c4b42824a5e1c69fb047860a60d901eda36153162cf19672eff59e`.
- Clean HCEA semantic donor commit:
`c168af2e747d3095d9a29418ae401f3a39544863`. Its exact-name
`action_flee_end.c`, `action_flee_flush_position_indices.c`,
`action_flee_modify_color.c`, `action_flee_replace_prop.c`,
`action_flee_blind_panic.c`, and `action_flee_begin.c` files supply the
six natural typed behaviors.
- January PC disassembly independently fixes 32-bit actor datum handles,
the `actor_get`/`datum_get` path, the named flee-state offsets, the signed
panic interval `[9, 12]`, and both unit blind-running calls. January COFF
remains authoritative for symbol identity, bytes, padding, and relocation
ownership.
- Compiler: Microsoft 32-bit C/C++ Optimizing Compiler `13.00.9254.1` for
80x86, with the repository's unchanged XDK 3911 command and flags.
The retained signatures are:
```c
void action_flee_end(
long actor_index);
boolean action_flee_blind_panic(
short panic_type);
```
The existing `actors.h` owns the typed actor/action layout and `actor_get`
contract. The existing `units.h` declaration owns the stop-running call.
No shared-header repair is needed.
## Authenticated behavior and one-shot rejection record
`action_flee_end` resolves the complete 32-bit actor handle through
`actor_get`; when the actor owns a valid unit datum, it calls the typed
`unit_stop_running_blindly` API. `action_flee_blind_panic` returns true only
for the inclusive signed-short interval from grenade attachment through
burning to death, represented by the source-local enum values 9 and 12.
Both bodies have explicit terminal returns.
All six candidates were compiled together once. The following four misses
were removed without retry:
| Rejected function | Target meaningful / padded / relocs | Target hash | First natural candidate |
| --- | ---: | --- | --- |
| `_action_flee_flush_position_indices` | `39 / 48 / 2` | `8dad23011290d89f5753258c4f1a2c41b21837956d5556a5de2c9a344de11329` | `48 / 2`, SHA `8da380385d9a8cfbb0f1465c1bef5c314add784ef72b616032e8646cded81413`; same relocation identities, different normalized instruction schedule. |
| `_action_flee_modify_color` | `99 / 112 / 4` | `9eef0cd0460952e50234aa38cad6a232cef2ed909f574428956b38026790dfb2` | `80 / 4`, SHA `61e086d8f401f02517b7ad56f1938c82a0121a05b697fba3cc11b86747d8685d`; the optimizer joined the color-copy tails instead of January's duplicated branches. |
| `_action_flee_replace_prop` | `48 / 48 / 2` | `10b5ae0e39ea2d834df55c0984c57bad0465ad8ee69612fc6cc02a1f7762cbf0` | `48 / 2`, SHA `48b2a3f2fe62a1474a20508f3c90d3866f70e390310adee035f55fd0db12bbf4`; same relocation identities, different normalized instruction schedule. |
| `_action_flee_begin` | `92 / 96 / 3` | `009e97240ce60b10098e18f6f625c4b678cd424dd832d92d69e7a3eb485ea9a0` | `96 / 3`, SHA `1aa8574ec4ab3fee338e6ea51c0ad4be4b1e0a7b2098b18442f21131151c2fab`; the unit-start call relocation lands at `+79` instead of January's `+83`. |
All four rejected COMDATs and the begin-only unit-start declaration are
absent from the final object. Reopen them only with independently preserved
January source or a newly authenticated ordinary-C contract; do not tune
their spelling or scheduling from this rejected object.
## Policy, scope, and validation
Only `source/ai/action_flee.c` and this new Jonas-owned ledger are changed.
No shared header, storage definition, data section, configuration, frozen-five
source, semantic exception, parked record, pre-existing Markdown, or
Claude-owned file is edited.
The retained source is readable typed C. It contains no assembly, raw-address
or byte-offset dereference, pointer/integer reconstruction, pointer or union
pun, inactive-union access, undefined overflow, volatile scheduling device,
force-inline annotation, optimizer pragma/barrier, synthetic anchor, or
object-byte forcing. The final COFF defines only two code COMDATs plus debug
metadata; it defines no `.rdata`, `.data`, `.bss`, COMMON symbol, or global
storage.
- Complete `halobetacache_build` and `libcmt_build`: pass.
- Direct hardened comparison: both retained functions pass; all four
rejected symbols are absent from the final COFF symbol table.
- Semantic audit: 470 units, 4,052 functions evaluated, 3,912 semantic
exact, 101 hidden exact / 61,876 hidden code bytes, 3,973 accepted exact,
and zero unit errors.
- Campaign progress: 375/833 complete objects, 3,960/11,060 exact functions,
475,638/2,198,102 meaningful code bytes, and
1,835,088/4,176,062 matched data bytes. This wave adds two functions and
71 meaningful bytes with no data increase.
- Admission audit: zero candidates and zero revocations; only the inherited
`source/shell/shell_xbox` completion-label contradiction remains.
- Parked validation: three active, zero stale, and zero invalid entries.
- Complete tooling suite: 179/179 tests pass; `git diff --check` is clean.
The fail-closed pre-wave check identifies exactly the two retained functions
as `NEWLY_EXACT` and reports `changed_nonexact: []`. Adding the first two
COMDATs necessarily changes the translation unit's debug section and symbol
set, which the generic structural check surfaces for review; direct hardened
comparison independently proves both accepted functions. No adjudication or
configuration exception is added.
A clean committed-state snapshot/check followed by deletion and forced
rebuilding of `action_flee.obj` is the final same-path reproducibility proof.
No push is performed.
@@ -1,133 +0,0 @@
# `action_flee.obj` opus5 150K wave 2: park re-investigation (2026-09-14)
## Scope and provenance
- Translation unit `source/ai/action_flee.c`; target `build/split/source/ai/action_flee.obj`
(January 2002 Xbox debug build, XDK 3911 / VC7 13.00.9254, repository flags unchanged).
- Lane worktree `C:\halo-worktrees\opus5-150k-house-clean-20260914`, wave w2. Worker notes:
`scratch/workers/actions_action_flee_etc.md`; scratch candidates in `scratch/workers/actions_action_flee_etc/`.
- Ledgers read first: `action_flee_obj_jonas_two_leaf_20260821.md`,
`action_flee_obj_update_semantic_owner_boundary_20260905.md`,
`action_flee_obj_threshold_leaf_reconciliation_20260908.md`,
`action_flee_obj_position_helpers_exact_20260909.md`, the three `config/parked.json` entries.
- `branch_sweep.py source/ai/action_flee`: 10 blobs, none better than the current file.
## Baseline (real file, unchanged by this wave)
`== exact 10 residual 3 unwritten 0`, `_point_from_line3d` guard passes. All three residuals are parked:
| Function | Target | Base | Relocs | Gate tag |
| --- | ---: | ---: | ---: | --- |
| `_action_flee_setup` | 272 | 256 | 12 | size, sha |
| `_action_flee_current_position_exposed` | 352 | 352 | 13 | sha |
| `_action_flee_perform` | 864 | 848 | 30 | size, sha |
## Result
Production file **unchanged** (every target is parked). A scratch candidate reopens all three parks:
| Candidate | Gate | Notes |
| --- | --- | --- |
| `scratch/workers/actions_action_flee_etc/flee_c1.c` | 12 exact / 1 residual / 0 unwritten | setup and exposed strict EXACT; perform 864/30 sha-only |
| `scratch/workers/actions_action_flee_etc/flee_c1diag.c` | **13 / 0 / 0** | c1 plus a diagnostic local `unit_is_speaking` prototype (NOT landable; proves the header proposal) |
Diff: `scratch/workers/actions_action_flee_etc/flee_park_reopen_c1.diff`. Every baseline EXACT row stays EXACT,
the guard passes, the owner census against `build/base` shows no new or dropped owners (the three `real_math`
select-any fallbacks are pre-existing), and `tools/fake_match_scan.py` reports 0 leads (HEAD also 0).
## Evidence per function
### Shared: `action_flee_find_flee_position` has a third parameter
HCEX.pdb (SHIP), `DIA2Dump -sym`, gives
`static void action_flee_find_flee_position(long actor_index, unsigned char respect_direction, struct flee_state_data *flee_state_data)`.
January's call sites prove the stack argument:
- setup +0xe1: `push 0; call; add esp,4`;
- perform +0x1a6: `mov ebx,[ebp+8]; push 1; call; add esp,4`.
The callee never reads it, so its body stays EXACT (640/23) with the unused parameter. The candidate declares
`boolean respect_direction` with a one-line disclosure comment. Setup passes `FALSE` and perform passes `TRUE`.
The old two-parameter prototype was an ABI error, not a tie. It cost setup its frame spill: EBX has to carry
`actor_index` into the call, so January spills the actor pointer to `[ebp-8]`.
### `_action_flee_setup`: strict EXACT (272/12, sha 1a415a0adece0dd5)
- The 3-parameter call fixes the size and the frame.
- January loads `force_state_entry` first and emits the stores after `csmemset` as +5, +8, +0xc, +4, +0x1c, +0.
The source order that reproduces this is: forced_flee_ticks, flee_firing_position_index = NONE, panic_type,
flee_from_last_visible_location, allow_occluded_points, flee_prop_index.
- This is store-order recovery for independent field initialisation, per the methodology case `lra_new`.
- Shapes: v1 (prototype only), v2/v3/v4/v5b/c/e (other orders) were sha-only; v5d was EXACT.
### `_action_flee_current_position_exposed`: strict EXACT (352/13, sha 498dc2c046073108)
- **Wrong field in base.** At +0x130, January copies prop_datum +0xbc into `approach_point`. In our `props.h`
layout, +0xbc is `body_position`: `definitely_located` is 0xb8, `abandoned_search` 0xbb, and `head_position` is
+0x104, which is the pointer January passes to `ai_test_line_of_sight`.
- Base copied `head_position`, the same error as the HCEA lift. The second use of `&prop->head_position` created
the extra `[ebp-8]` pointer home, the 0x14 frame (January: 0x10) and the EBX/EDI swap.
- The park's premise, "coherent EDI/EBX allocation, not missing behaviour", is refuted.
- +0x16..+0x22: `exposed = FALSE` is stored after `datum_get`. By the store/call-order law, `actor` is declared
before `exposed`.
### `_action_flee_perform`: strict EXACT only with the proposed prototype (864/30, sha d62f05304574f88e)
January facts that each correct the base:
| January bytes | Source fact |
| --- | --- |
| +0x40 `cmp [esi+2],0; jle; mov [esi+8],-1` | `if (flee_stationary_ticks > 0) { index = NONE; } else { ... }` (base had the inverted polarity) |
| +0x109 `mov byte ptr [eax+0x74], bl` | prop +0x74 is `currently_damaging_me`; base wrote `unopposable_enemy` (+0xa4, HCEA's misnamed lift) |
| +0x101/+0x106 `movsx ecx,cx` / `movsx ecx,dx` | `prop->perception = MAX(prop->audibility, prop->ineffability);` direct, int-promoted |
| visibility, perception, damaging, LOS stores | statement order visibility, perception, currently_damaging_me, line_of_sight |
| +0x255 `or eax,-1; cmp ecx,-1; je` | `cause_unit_index = NONE; if (flee_prop_index != NONE) cause_unit_index = prop_get(...)->unit_index;` |
| +0x27a fall-through | the not-yet-communicated flee event is the then-arm; the idle event is the else-arm |
| +0x2d3 `mov al,[edi+6]; test; jne 0x324` | the line-302 assertion is guarded by `if (!actor->meta.swarm)` |
| +0x1a9 `push 1` | `respect_direction = TRUE` |
| +0x1ea `test al,al; jne; mov [esi+0x10],al` | `unit_is_speaking` returns `boolean` |
The last row is an owner gap. No header declares `unit_is_speaking` (definition: `unit_dialogue.c:389`,
`boolean unit_is_speaking(`), so `action_flee.c` compiles it as a C4013 implicit `int` and emits `test eax,eax`.
With every other fix, the flee_c1 perform differs from January by that single instruction. The diagnostic
candidate, which adds a local prototype, is strict EXACT.
Shapes: v6 (polarity, field, cause, arm order, swarm guard) gave 848. v6p (+ diagnostic prototype) gave 848. v7
(perception after damaging) gave 848. v7b gave EXACT. v7n (v7b without the prototype) gave 864, sha-only.
## Proposals
1. **ORCHESTRATOR-PROPOSAL (header), `source/units/units.h`**. Insert after the `unit_notify_impulse_sound`
prototype, before `unit_make_damage_sound`:
```c
boolean unit_is_speaking(
long unit_index);
```
Evidence:
- January +0x1ea `test al,al`;
- the definition's return type;
- punpckhdq `units.h:557` declares the same prototype at the same neighbourhood.
Consumers: 66 direct includers (`scratch/workers/actions_action_flee_etc/units_h_consumers.txt`). The
declaration-count law applies, so a full build is required.
2. **park-reopen `_action_flee_setup` and `_action_flee_current_position_exposed`** from `flee_c1.c`. Both are strict
EXACT without any header change.
3. **park-reopen `_action_flee_perform`** from `flee_c1.c` once proposal 1 lands. If proposal 1 does not land, the
reopen of (2) still changes perform's call site (`push 1`), so perform's park must be re-baselined to
864/30 sha cefb8e42bcdf2d49.
## Do-not-repeat
- The two-parameter `find_flee_position` prototype.
- `head_position` as the exposure approach point.
- `unopposable_enemy` in perform's arrival block.
- The `<= 0` stationary polarity.
- A short perception local.
## Reopen criteria
- setup and exposed: met (January-proven ABI and field facts).
- perform: land the `unit_is_speaking` prototype in its owner header.
@@ -1,57 +0,0 @@
# `action_flee.obj` - opus5 150K house-clean lane, wave w3 (2026-09-14)
## Scope and baseline
- Translation unit `source/ai/action_flee.c`; target `build/split/source/ai/action_flee.obj` (January 2002 Xbox debug, VC7 13.00.9254).
- Worker notes: `scratch/workers/w3_path_smoothing_actor_firing_position_etc.md`; scratch dir `scratch/workers/w3_path_smoothing_actor_firing_position_etc/`.
- Baseline real-file gate (guard passes): `== exact 12 residual 1 unwritten 0`. The only residual, `_action_flee_perform` (864/30), is parked
(park base 864/30 `cefb8e42bcdf2d49`, target `d62f05304574f88e`).
- Ledgers read: `action_flee_obj_jonas_two_leaf_20260821.md`, `action_flee_obj_update_semantic_owner_boundary_20260905.md`,
`action_flee_obj_threshold_leaf_reconciliation_20260908.md`, `action_flee_obj_position_helpers_exact_20260909.md`,
`action_flee_obj_opus5_150k_w2_20260914.md`; laws `scratch/w2/laws_w2.md` (C2, A7, R8, R9).
- `branch_sweep.py`: best historical blob 10/3/0 (below the current file).
## Result
**The real file is unchanged** (final gate 12/1/0, guard passes, park drift 0/1, fake scan 0 leads). One header packet is proposed,
measured with a shadow-header consumer census.
## `_action_flee_perform` - park reopen via a genuine units.h packet (no body change)
Law A7 (missing prototype via the genuine owner). `CL /Zs /W3` (`scratch/w3/c4013.txt`) reports two implicit declarations:
`unit_start_running_blindly` (line 414) and `unit_is_speaking` (line 689). Their definitions are `units.c:3897`
(`void unit_start_running_blindly(long unit_index)`; only a units.c-local prototype exists) and `unit_dialogue.c:389`
(`boolean unit_is_speaking(long unit_index)`). No header declares either; `units.h` owns both prototype sections (UNITS.C and
UNIT_DIALOGUE.C). January +0x1ea `test al,al` proves the boolean return; punpckhdq `units.h:557` declares `unit_is_speaking`
right after `unit_notify_impulse_sound`.
Shadow-header method: `shadow_gate.py` compiles each unit inside a full copy of the `source/` headers (so every quoted include
resolves in the shadow tree) and dumps per-function size/relocations/normalized sha. 76 units transitively include `units.h`.
| trial | units.h change | action_flee | 76-consumer deltas vs pristine |
| --- | --- | --- | --- |
| P | none | 12/1, perform = park base | - |
| H1 | + `unit_is_speaking` only | 13/0 | perform -> target; speech_timers park load pair; `_physics_update_old` (non-exact); **`_unit_preprocess_node_orientations` EXACT -> residual** |
| H2 | + both prototypes | **13/0** | **exactly two**: perform -> target; `_ai_communication_update_speech_timers` park flips to its second documented flag-neutral load order (672/43 `885fdb81966284ac`) |
Sum of strict-exact rows over the 76 units: P 2402 -> H2 2403, zero losses. H1 is rejected (declaration-count victim).
ORCHESTRATOR-PROPOSAL (exact diff `scratch/workers/w3_path_smoothing_actor_firing_position_etc/units_h_packet_H2.diff`, CRLF):
```c
/* prototypes/UNITS.C, immediately before unit_stop_running_blindly */
void unit_start_running_blindly(
long unit_index);
/* prototypes/UNIT_DIALOGUE.C, immediately after unit_notify_impulse_sound */
boolean unit_is_speaking(
long unit_index);
```
Consumers: 66 direct includers (`units_h_direct_includers.txt`), 76 compiled transitive consumers (`units_h_consumers.txt`).
Same commit: unpark `_action_flee_perform` (864, no source change) and re-baseline the `_ai_communication_update_speech_timers`
park base to `885fdb81966284acf5ba7138a4bbbd2221e7c61c89114fdd6f53b96ba89b81a0`. A full ninja plus stable sweep is required.
## Reopen criteria
- `_action_flee_perform`: met when the H2 packet lands (not the single-prototype variant).
@@ -1,109 +0,0 @@
# `action_flee.obj` position-helper packet (2026-09-09)
## Result
Two previously unwritten January functions are now strict COFF matches in a
single, caller-connected translation-unit reconstruction:
| January owner | Recovered private name | Meaningful | Padded | Relocations | Normalized SHA-256 |
| --- | --- | ---: | ---: | ---: | --- |
| `_code_00003210` | `action_flee_find_flee_position` | 629 | 640 | 23 | `058dad92555bc68a3f7deb04f41c3b1995d70808b593c142c7f534383e18b0b7` |
| `_code_000039e0` | `action_flee_at_flee_position` | 318 | 320 | 13 | `85c493abe1c07f9d923b165c98d0ec59098a6c21ef04a0954f8c5451507804fa` |
The packet therefore contributes **947 meaningful exact code bytes** and
**960 padded exact bytes**. The focused gate reports 9 exact, 3 residual and
1 unwritten function in `action_flee.obj`; the inherited baseline was 7 exact.
No whole-object completion is claimed.
## Name and behavior evidence
The independent HaloCEA reconstructions at
`research-cache/halocea-full-blobs-20260830/src/blam/ai/` recover the three
private names and their caller relationships:
- `action_flee_find_flee_position.c`
- `action_flee_current_position_exposed.c`
- `action_flee_at_flee_position.c`
- `action_flee_setup.c`
- `action_flee_perform.c`
January remains the byte and branch-semantics authority. The first exact
owner carries January's line-522 non-swarm assertion, selects and commits a
cover/panic firing position, and computes an approach point through the real
path subsystem. It uses `actor_get`, `actor_definition_get`, `prop_get` and
`TAG_BLOCK_GET_ELEMENT`, plus declarations from the owning `ai.h`, `path.h`,
and `actor_perception.h`; no raw datum/tag casts or local foreign prototypes
were introduced.
The three ACTOR_PERCEPTION.C situation declarations live in the dedicated
owner header. Their earlier placement in the middle of the heavily shared
`actors.h` perturbed C2's declaration-position-sensitive allocation in
`units.obj` and reopened one otherwise exact owner. Isolating the declarations
keeps ActionFlee fully typed while preserving all 189 exact `units.obj`
functions and the existing code generation of unrelated AI consumers.
The two previously undeclared PATH.C routines use an opt-in declaration block
at the end of `path.h` for the same reason. The genuine `path_state_find`
return type is corrected in the ordinary owner declaration, while the two new
declarations are visible only to ActionFlee. This preserves the closer parked
`encounter_update_respawn` schedule instead of silently degrading a fuzzy
owner merely to expose unrelated declarations.
The second exact owner carries January's line-470 assertion, validates the
current encounter firing position, honors the already-at-destination fast
path, and compares the squared body distance against the actor's destination
tolerance. January's final branches show that a tracked prop with line-of-
sight value clear or occluded prevents this fallback arrival result; no prop,
or a different line-of-sight state, permits it. This differs from the later
HaloCEA lift's final polarity, so the donor was used only as a semantic/name
map and the January branch behavior was preserved. The final boolean is
normalized with the ordinary `result = !result; return result;` idiom; the
open Marathon cseries source uses the same toggle-then-return spelling in
`cseries.lib/macintosh_utilities.c`, so this is not an invented dependency or
optimizer directive.
## Honest residuals retained with zero credit
The coherent callers needed to make the private static owners emit are kept as
the best currently reviewed typed reconstructions, but they are not exact and
receive zero matched-byte credit:
| Function | January | Candidate | Relocations (target/candidate) | Objdiff |
| --- | ---: | ---: | ---: | ---: |
| `action_flee_setup` | 272 padded / 265 meaningful | 256 padded | 12 / 12 | 80.265305% |
| `action_flee_current_position_exposed` | 352 padded / 347 meaningful | 352 padded | 13 / 13 | 83.46342% |
| `action_flee_perform` | 864 padded / 857 meaningful | 848 padded | 30 / 30 | 77.46595% |
Their normalized target/candidate SHA-256 pairs are respectively:
- setup: `1a415a0adece0dd541b62c1c9ada51604d58751e66d769d88619d76db5471d79` /
`cfc7ace7bcb857227c8c0559c80681a4f9ff2b142fa9df3e57c59af75960a898`
- current-position exposure: `498dc2c046073108d309d0057bb5fc0405a8112c5532e90ef7ec5e79f21d7708` /
`bbfe7ce7c3bafd235c0528f0480f5836b1d5016ec27e364ef77db6381c616c60`
- perform: `d62f05304574f88eefd7c025a452910d821e3986442777adcb223dc0d2085dc2` /
`e4b2a3cecd2525f5989ea94abd238fd8cc687627248f91ba39f597fe4ae66f7e`
These are ordinary typed C with named constants and subsystem APIs. They
contain no volatile/register forcing, pragma, assembly, raw offset,
representation pun, fake dependency, undefined behavior or nonsensical
branch. They should be revisited only with authoritative January source/local
records or another natural same-compiler donor, not expression-order search.
## Verification boundary
The final PID-isolated campaign gate compiled only `source/ai/action_flee.c`,
compared every target function, and rejected any emitted
`_point_from_line3d` owner. The guard passed. Because the semantic symbol
renames are part of this packet while the shared split object intentionally
remained untouched, the focused gate used temporary in-memory aliases from
the recovered private names to the old `_code_*` split labels. No Ninja,
configure, shared build output or unrelated source file was touched.
The ordinary `real_math.h` distance expression also emits select-any fallback
copies of `_distance_squared3d` (64 bytes), `_vector_from_points3d` (48 bytes)
and `_magnitude_squared3d` (48 bytes). Each has zero relocations and exactly
matches the corresponding January-selected owner in `action_alert.obj`
(`f42fa39f289bfa50`, `905f56dbe4597d2b`, and `0d04579096eeab3a`
respectively). They receive no target-owned or progress credit; this packet
does not alter their ownership. No `_point_from_line3d` definition or
reference is emitted.
@@ -1,45 +0,0 @@
# ActionFlee small-leaf reconciliation (2026-09-08)
## Result
This reconciliation admits three complete Fable-lane leaves into their real
`action_flee.c` owner and moves their public declarations from the unrelated
`actions.c` dispatcher into the closest associated header, `actions.h`.
| January owner | meaningful bytes | padded bytes | relocations | verdict |
| --- | ---: | ---: | ---: | --- |
| `_action_flee_flush_position_indices` | 39 | 48 | 2 | exact |
| `_action_flee_modify_color` | 99 | 112 | 4 | exact |
| `_action_flee_replace_prop` | 48 | 48 | 2 | exact |
The batch adds **186 meaningful / 208 padded exact code bytes** and three
strict functions. The two inherited exact ActionFlee owners remain exact.
## Source and ownership review
The implementations use the existing typed `actor_get` accessor, the genuine
`flee_state_data` fields, `NONE`, project booleans, and the canonical ARGB
color owners. Parameters remain one per line and every void body ends with an
explicit `return;`. No inline control, assembly, raw offset, address-derived
name, fabricated dependency, or manual bit logic is introduced.
The declarations belong to `actions.h` because this translation unit has no
dedicated header. All ActionFlee declarations now live there, including the
setup/dispatch callbacks and `action_flee_blind_panic`; their duplicate local
declarations were removed from `actions.c` and `actor_type_grunt.c` without
changing either consumer's code owners.
## Focused evidence
The canonical source gate reports `5 exact / 0 residual / 8 unwritten` for
`action_flee.obj`; `actions.obj` remains `58 exact / 5 residual / 0 unwritten`.
Both gates reject any emitted `_point_from_line3d`, and the changed-file
fake-match scan reports zero findings. The packet author also compared all 22
other initially compilable `actions.h` consumers without a count change; the
enclosing canonical build compiles the complete consumer set. The separate
`ai_debug.c` Path-interface repair receives no ActionFlee proof or credit.
The full-build, rename-stable sweep, park validation, admission audit and test
suite are recorded in the enclosing reconciliation batch. These three bodies
receive exact credit only if that global admission gate reports no inherited
loss.
@@ -1,189 +0,0 @@
# `action_flee.obj` update semantic and owner boundary, 2026-09-05
## Result
`action_flee_update` is **withheld**. This review admits neither exact nor
fuzzy code and creates no park. The current production object remains at its
published two strict functions; the attempted update remains unwritten.
Two independent blockers were demonstrated:
1. The required public-API ownership repair changes two inherited strict Race
functions and the existing AI speech-timer park in a full 84-object header
trial. The entire four-file API packet was restored; there is no
caller-local declaration or header-position workaround.
2. The first natural 160-byte body writes the positive-panic timestamp to the
wrong, adjacent semantic member. January writes actor `+0x39C`; the
candidate writes `+0x398`. This is not an ordinary scheduling residual and
is not eligible for a fuzzy park.
The member discrepancy is now resolved by a complete neighboring-use census:
the existing layout is correct. Actor `+0x398` is
`emotions.last_flee_failed_time`, while `+0x39C` is
`emotions.flee_with_friends_disable_time`. A new source manifest containing
only that semantic member correction is preserved pending a future genuine
API context. It was deliberately not compiled against restored, incorrectly
owned declarations.
## January behavior and ABI
The authoritative January `_action_flee_update` is 152 meaningful / 160
padded bytes, has four relocations, and contains 54 decoded instructions.
Its normalized SHA-256 is
`c5e5c557ca1dc9c70ea3fe4dbfff11451fc6a5b7a1b71921f839386588fbe2d8`.
Its call/data relocations, in order, are `_actor_data`, `_datum_get`,
`_unit_start_running_blindly`, and `_game_time_get`.
The target establishes the following source contract:
- `actor_index` is a complete 32-bit datum handle. The prologue loads the
full dword argument and passes it to `datum_get`; the HCEA donor's
`uint16_t` parameter is not the January ABI.
- `actor->meta.unit_index` at actor `+0x18` is also consumed and passed as a
complete 32-bit handle.
- `forced_flee_ticks` at `+0x9C`, `flee_stationary_ticks` at `+0x9E`, and
`panic_type` at `+0xA8` are signed 16-bit values.
- `flee_timer` at `+0xB4` increments every call with 32-bit machine wrapping.
- A positive forced-flee countdown decrements once. A positive stationary
countdown decrements once; only its `1 -> 0` transition can start blind
running, and only when the unit is not `NONE` and the signed panic value is
in the inclusive `[9, 12]` interval.
- The blind-running decision naturally calls the already strict
`action_flee_blind_panic(short)` source helper. The compiler may inline its
range test; the helper must not be manually expanded or artificially
retained.
- Independently of the unit/countdown path, any signed `panic_type > 0`
calls `game_time_get`, adds 750 ticks (`25*TICKS_PER_SECOND`) modulo 32
bits, and stores the result at actor `+0x39C`.
- When both calls occur, `_unit_start_running_blindly` precedes
`_game_time_get`. The function returns `void` and preserves its callee-saved
state and stack.
## The adjacent fields are not a missing-layout gap
Three independent January-backed uses distinguish the two adjacent dwords:
- In `_action_flee_perform`, after setting the flee state's
`unable_to_flee` byte, the target calls `_game_time_get` and at function
offset `+0x1C3` stores EAX to actor `+0x398`.
- `_actor_action_handle_panic_transition` tests actor `+0x398` against
`NONE`, calls `_game_time_get`, reloads `+0x398`, adds seven ticks, and
compares it with the current time. Current source names that value
`last_flee_failed_time`.
- The strict January/canonical `_actor_stimulus_prop_fleeing` calls
`_game_time_get` and at function offset `+0x58` compares it with actor
`+0x39C`. Its natural typed source names that value
`flee_with_friends_disable_time`; the function is strict exact at
263 meaningful / 272 padded bytes and 14 relocations, normalized SHA-256
`13df393dee019d54339f963e17edd5d85610d11ce48fe5c2e357240b22f65da1`.
The canonical `actor_emotion_data` already declares those members in that
order. Its base is actor `+0x350`, so relative offsets `+0x48` and `+0x4C`
produce the observed actor offsets `+0x398` and `+0x39C`. The HCEA
`actor_emotion_data.h` independently corroborates both names and offsets, but
its `action_flee_update.c` incorrectly selects `last_flee_failed_time` and is
not authoritative. The extracted January PDB census contains no member-level
type record for these fields, so no direct PDB-name claim is made.
The behavior is also coherent: a live positive panic refreshes a 25-second
cooldown that suppresses group/friend-flee propagation, while a failed flee
attempt records its separate, short-lived failure time at `+0x398`. No
padding, field insertion, or four-byte schema shift is proposed.
## First natural body and executable behavior review
The first natural source used `actor_get`, the existing typed flee-state and
emotion layouts, the already strict blind-panic helper, and owner APIs. Its
focused gate was `2 exact / 1 residual / 10 unwritten`; both inherited strict
functions and the hard `_point_from_line3d` guard were preserved. The new
function is also 160 bytes with four relocations and 54 instructions. Its
normalized SHA-256 is
`6e9a8d4d811187492ebf30f9a1942589321ce1e5beedf2c30045564be563bb28`.
Aligned disassembly finds exactly one instruction difference:
```text
target +0x8F mov dword ptr [esi + 0x39c], eax
candidate +0x8F mov dword ptr [esi + 0x398], eax
```
The bounded Unicorn harness executes only `_action_flee_update` and stubs
only its three genuine call boundaries: `_datum_get`,
`_unit_start_running_blindly`, and `_game_time_get`. It verifies exact API
arguments, event order, all actor bytes outside target-written fields,
callee-saved registers, and stack balance. Its 47 cases cover:
- signed countdown boundaries `-32768, -1, 0, 1, 2, 32767`;
- `NONE` and full-width `0x89ABCDEF` unit handles;
- panic boundaries `-32768, -1, 0, 1, 8, 9, 10, 12, 13, 32767`;
- actor handles `0x00000007`, `0x12340007`, and `0xFFFF0007`;
- zero, sign-boundary, and all-one flee counters; and
- timestamp addition across 32-bit wrap.
The frozen January target passes **47/47**. The first candidate passes
**27/47** and fails exactly the 20 cases with positive panic. Every failure
has the same adjacent-field difference spanning actor bytes
`[0x398, 0x3A0)`: the candidate overwrites `last_flee_failed_time` and leaves
`flee_with_friends_disable_time` unchanged. Countdown behavior, complete
handles, calls, call order, and counter behavior agree in the remaining
observations. This is direct behavioral rejection of the candidate, not a
request to adapt the expected offset.
## Required owner-header repair and failed blast
A source-faithful production packet requires these ownership changes as one
coherent boundary:
- declare `void action_flee_update(long actor_index)` in `actions.h` and
remove its consumer-local declaration from `actions.c`;
- declare `void unit_start_running_blindly(long unit_index)` in `units.h`
and remove its owner-C local declaration from `units.c`; and
- include `game/game.h` from `action_flee.c` for the already owned
`game_time_get` API.
Root tested the pure owner-header migration across the complete 84-object
consumer union before admitting the body. It materially changed
`_race_touch_flag` (336 padded bytes) and
`_race_engine_player_update` (160 padded bytes), losing both inherited Race
stricts. It also changed the normalized body of the existing
`_ai_communication_update_speech_timers` park. The packet was therefore
withheld without header-position permutations, local-prototype fallback, or
shim headers.
After restoring all four API files, the full build returned to 6,217 strict
owners out of 8,245 with zero losses, and all 281 parks validated. Production
`action_flee.c`, the owner headers, and configuration remain unchanged by
this review.
## Frozen evidence
| Artifact | SHA-256 | Disposition |
| --- | --- | --- |
| `scratch/action-flee-update-target-frozen-20260905.obj` | `5D6E667103C4B42824A5E1C69FB047860A60D901EDA36153162CF19672EFF59E` | January target |
| `scratch/action-flee-update-natural-api-trial-20260905.obj` | `FA607D970E3E97D39519086C1FFA09008E98659211C35E2D2BF760D485E8B6D9` | rejected wrong-member candidate |
| `scratch/action_flee_update_natural_20260905.edits.json` | `2D9D0F48A831E2B2E9FF8EA1D1B8C1E6C2BAA797C79CDECA8CB8CD5B241B3197` | immutable first-source manifest |
| `scratch/action_flee_update_semantic_corrected_pending_20260905.edits.json` | `14D98FCD9964F932CDF92AB99364B974EEE009E4B75CB0B6A97C0577F980C58E` | one-member source correction, intentionally uncompiled |
| `scratch/action_flee_update_behavior_review_20260905.py` | `927D2B29FAF1A400B7429F60FE24146E8DC520CC71D617C156FD45E76DDAE1C2` | bounded behavior harness |
| `scratch/action-flee-update-target-behavior-reviewed-20260905.json` | `910C06F6630F38AC25B3B2E2F3A013BDC8CC577C717235515C98776FF620DE69` | target 47/47 pass |
| `scratch/action-flee-update-candidate-behavior-reviewed-20260905.json` | `EB6EC2F6A66739A828B6C2A2DCA431A9185EEF45015B427930DB1AB8CFF0D4C0` | candidate 27/47 pass, 20 semantic failures |
| `scratch/action-flee-api-failure-summary-20260905.json` | `6C68F7B76ACC6120F8D50A121FEAA206918763BCE5EEE83B69BACDEF168A88BD` | 84-object owner-header failure census |
| `scratch/action-flee-api-restored-20260905.json` | `5EF39E7AD811893F6E1FFAA2F46F960D06592AEDB144E958FF646E386B72CFB7` | restored stable census |
| `scratch/action-flee-api-parks-restored-20260905.json` | `92ED21DB874053C53684CDD519F70A2D1ACAAD96F76F9E0E8E0CD866879DB739` | restored 281-park validation |
| `scratch/actor-stimulus-prop-fleeing-canonical-20260904.obj` | `2FE9919CFE7DF2B7E8A7C6A895EB2DEECBA75FF12856BEB162E6377F1B731764` | strict `+0x39C` consumer |
Harness reproduction uses frozen objects only:
```text
python scratch/action_flee_update_behavior_review_20260905.py scratch/action-flee-update-target-frozen-20260905.obj --output scratch/action-flee-update-target-behavior-reviewed-20260905.json
python scratch/action_flee_update_behavior_review_20260905.py scratch/action-flee-update-natural-api-trial-20260905.obj --output scratch/action-flee-update-candidate-behavior-reviewed-20260905.json
```
## Reopen condition
Reopen only as a coherent owner-header trial after the Race and speech-timer
collateral can be reconciled by independently authenticated source/API
evidence. At that point compile the preserved corrected manifest once in the
genuine header context and repeat the behavior, full owner, no-point, park,
and whole-tree stable checks. Do not patch the old object, shift the actor
layout, restore caller-local externs, permute declarations, or classify the
wrong-member candidate as fuzzy progress.
@@ -1,152 +0,0 @@
# ActionObey public API and structure-flush leaf: independent admission review
## Conclusion
The frozen public-API owner packet is **admission-safe and runtime-neutral**. The structure-index flush leaf is also technically sound: it adds exactly one genuine public function owner, is a strict all-section match to January, preserves every inherited section, has the correct 32-bit C ABI and owner metadata, and matches January behavior in 1,024 bounded executions. It adds **46 meaningful / 48 padded code bytes** and no data credit.
One provenance caveat remains for the root's policy decision. The body is typed, idiomatic, semantically corroborated, and contains no byte-forcing construct, but the Fable report explicitly says its exact source shape corrected the earlier 2026-08-21 miss by introducing the local `state_data` pointer. It therefore must not be described as an independently natural first candidate. If admission requires first-shot provenance rather than an authenticated exact reconstruction, this leaf needs explicit adjudication; its object, ABI, runtime, API-owner, and no-surplus evidence otherwise pass.
This review does not cover the parallel shared swarm/enum schema packet. No production, donor, build, configuration, park, or frozen object was changed, and no compile or gate was run.
## Source packet and owner boundary
There is no `action_obey.h`. `source/ai/actions.h` is the genuine narrow owner because it already owns `action_specification`, all obey control/state records, the other action-family public declarations, and an explicit `prototypes/ACTION_OBEY.C` section. The frozen API change adds these nine target-public declarations there:
```c
boolean action_obey_command_list_setup(
long actor_index,
short command_list_index,
struct obey_state_data *state_data);
void action_obey_flush_command_indices(
long actor_index);
void action_obey_flush_structure_indices(
long actor_index);
void action_obey_advance_command_list(
long actor_index);
void action_obey_begin(
long actor_index);
boolean action_obey_perform(
long actor_index);
void action_obey_update(
long actor_index);
void action_obey_control(
long actor_index);
void action_obey_end(
long actor_index);
```
`action_obey_describe_command` was already present in that owner section. Every added symbol is an external January function (`storage=2`, `type=0x20`, `value=0`, NODUP function COMDAT). The signatures agree with the existing consumer-local declarations, the `action_specification` callbacks, January's ordinary 32-bit stack accesses, and the retained HCEA semantics. In particular, `action_obey_command_list_setup(long, short, obey_state_data *)` preserves the pre-existing `actions.c` declaration exactly.
The paired cleanup removes from `actions.c` only the now-owned local declarations for `action_obey_command_list_setup` and the five action-table callbacks (`begin`, `perform`, `update`, `control`, `end`). It later removes the local `actor_action_change` declaration from `action_obey.c`; `actions.h` already owns that exact three-argument declaration. Moving declarations does not create a definition, helper, or alternate ABI.
The three no-current-cross-TU-call declarations (`flush_command_indices`, `flush_structure_indices`, and `advance_command_list`) are not synthetic helpers: all three are genuine external January owners. Later/HCEA callers also corroborate their public nature. `perform` and `control` are presently unwritten in canonical, but `actions.c` genuinely references both from the obey action table; their prototypes are necessary public contracts rather than promises invented for this leaf.
The frozen public API source hashes are:
- before: `actions.h` `f62df99f9ad20305ed01ebd09e120378aa513874dc3b990c2a07775a41e98086`, `actions.c` `3dbb2e77c2f4fd40cc0376589b9d3024f47f7a47d6e8375d8b0e9a3a616e44a2`;
- after: `actions.h` `962973471e872cf3f159ef06d1f8209c9a962a8fae5cac8bbb18e2e7d21146ac`, `actions.c` `f6347eb2573a36f12cf0c9253da66504441b8743269e8847fd3866770571d7ec`.
At review time the current `actions.c` still has the frozen after hash, while `actions.h` has subsequently changed to `ea1bec48dfcc5bdad4dc01137e8c9d4a1f6aa878ee6f3eae4d0ef13e64aee62` from the root's parallel schema work. The nine API declarations themselves remain textually unchanged. This report makes no claim about that later schema content.
## Public API dependent audit
The capture script resolved all real `actions.h` dependencies from the Ninja graph and froze 47 objects. The before and after unit lists are identical. Independent review of the runtime summary found:
- 47/47 units preserved;
- 3,079 runtime sections unchanged;
- 54 changes classified solely as compiler-local label spelling across 22 units;
- zero review-required units;
- zero runtime sections added, lost, or genuinely changed;
- zero genuine function-owner changes and zero new helpers;
- zero COMMON additions/removals;
- zero new `_point_from_line3d` emission or reference.
The full stable verdict maps are byte-identical: both contain 8,245 entries and share SHA-256 `223b39e9a64198e10ff6dab9fe24a92b1d715b87e385c9389fee4188d6a0f458`. The captured park census is 284 active, 0 stale, 0 invalid. Thus the API move has no inherited exact/residual loss and no ordinary-link owner side effect.
Primary frozen files:
- `scratch/action-obey-public-api-before-20260905.json` — SHA-256 `22082d37d81366a681520949d9502107f7ea43b8e185f29936643cd07c89d0d5`;
- `scratch/action-obey-public-api-after-20260905.json` — SHA-256 `49d6f456321f119a67d25ac7324d82b4fc282d43cf1e86044f4a342e43284f65`;
- `scratch/action-obey-public-api-runtime-audit-20260905/summary.json` — SHA-256 `6f473b63e0918f9f085c664dbccebcac2c28668cfc2196a95e5a510504f70e9e`;
- `scratch/action-obey-public-api-parks-20260905.json` — SHA-256 `6c212dbc1d83de169bccde32a1c1ed3acc281d998bc8ec42ebe9e1b37e850cfa`.
## Leaf body and source authenticity
The current leaf is:
```c
void action_obey_flush_structure_indices(
long actor_index)
{
struct actor_datum *actor = actor_get(actor_index);
struct obey_state_data *state_data = &actor->state.action_data.obey;
if (state_data->complex_control.destination_valid)
{
state_data->complex_control.destination_surface_index = NONE;
}
return;
}
```
This is a genuine typed implementation. It uses the owning records, an existing typed actor accessor, the named valid/surface fields, and the repository's `NONE` constant. It contains no raw offset, cast reconstruction, aliasing trick, inline/optimizer directive, synthetic anchor, or surplus wrapper.
Three independent sources support the behavior:
1. January PC code loads the full dword argument from `[ebp+8]`, passes that full handle with `_actor_data` to `_datum_get`, reads the byte at actor `+0xCC`, materializes the obey-state base at actor `+0x9C`, and conditionally stores `0xFFFFFFFF` at state `+0x40` (actor `+0xDC`).
2. The retained HCEA function tests `obey.complex_control.destination_valid` and sets `destination_surface_index=-1` on true.
3. The adjacent accepted canonical obey wrappers use the same `actor` then `state_data` local source pattern.
The HCEA extraction spells this one function's actor parameter `uint16_t`, unlike the surrounding `int` functions. That later-platform/decompiler spelling is not PC ABI authority. January's dword load, the target relocation/call sequence, the callback-family contract, and the runtime harness's untruncated handle prove canonical `long` is correct.
The caveat is derivation history, not source semantics. The 2026-08-21 natural batch produced a different 46/48 candidate hash and removed the miss after one attempt. Fable later recorded that the exact body added the `state_data` pointer missing from that attempt. Because it explicitly consumed the previous mismatch boundary, this implementation cannot inherit the earlier ledger's first-shot label even though the resulting ordinary C body is authentic in form.
## Exact object and owner evidence
January target object SHA-256 is `0e4f889a35b97c44f6ae91e4906b547bd64f2ff12b9f399e02b46f02b69633fb`. The frozen owner-clean candidate object SHA-256 is `543ef02661eafabe28197ce23ddd345fdfb4818d543017219ce933d56562da0e`.
For `_action_obey_flush_structure_indices`, target and candidate agree on:
- 46 meaningful bytes and a 48-byte padded section (two terminal NOPs);
- normalized SHA-256 `9f0facf2a5e64545b5b323119ea1ce6c45813a307bf0fb256308aab64fe0db82`;
- relocation `+0x08`, `IMAGE_REL_I386_DIR32`, to `_actor_data`;
- relocation `+0x0F`, `IMAGE_REL_I386_REL32`, to `_datum_get`;
- `.text` flags `0x60501020`, external storage 2, function type 32, value 0, COMDAT selection 1.
The first leaf comparison (before to after) adds exactly one runtime section and one genuine function owner. It changes no inherited runtime section, adds/removes no COMMON, emits no forbidden point helper, and has no debug delta. The later owner cleanup (removing the redundant `actor_action_change` local declaration) preserves all 12 runtime sections and every program symbol identity; it creates no new helper or owner.
Frozen leaf chain:
- before manifest `ee343e4c289c0b81eaa4972b030f1f1db83a150d1cd13096a5393e82c3f0c91e` / object `10ee91a816e424347da9cc3ccf6c820e6f4b7f4f4aba09d303eef26a0b547c5f`;
- body-after manifest `928b2bde2a3aa4c568d726ff87457c2ff91ad64f47f0b8d22aa8fdc9c47ae426` / object `9edaf5bafdd56b1ab3864f7b3a777e6b8ad688084571346efc2b1b0d6429a531`;
- owner-clean manifest `d8435f09c9c20d28562d4c62b5b587a57df19a99c2961c6d3255313e6ee3f581` / object `543ef02661eafabe28197ce23ddd345fdfb4818d543017219ce933d56562da0e`.
The isolated replay object (`69ae82ce4432cbe0df25eeb0d4e5912e16e19c8527a3aef7e645d31fac58d238`) defines the nine inherited exact functions plus this leaf—ten function owners total—and no unexpected code owner. The full before/after runtime report identifies only the new leaf; the owner-clean comparison is completely unchanged.
## Runtime differential
The final scratch runtime harness tests target, owner-clean actual, and isolated objects over all 256 possible `destination_valid` byte representations and four prior surface values (`0`, `17`, `0x80000000`, `0xFFFFFFFF`), or 1,024 cases per object. All three produce trace SHA-256 `999e2273e77f3347e5871616830525f72c861071faf31f8e1757762f2767becd`.
It also verifies:
- one and only one `_datum_get(_actor_data, actor_index)` call;
- the full 32-bit handle reaches the accessor;
- the surface changes to `NONE` iff the valid byte is nonzero;
- no other modeled actor byte changes;
- callee-saved registers, stack cleanup, and direction flag behavior;
- exact target section and owner metadata before execution.
The stated limit is appropriate: this models a valid actor handle and the `datum_get` contract; it is not a whole actor subsystem or image-link test. There is no uncovered input branch inside this leaf once those API preconditions hold.
Evidence:
- `scratch/action-obey-structure-leaf-owned-runtime-20260905.json` — SHA-256 `65e17c98c27a46eb49b0e25e92e95984d7d9b291e9b8dc11ec60f80d632b3752`;
- runtime script SHA-256 `d686a0c4c2251909cc6d7d4428091f318f2f867c35841c09a5441a6ae034b704`;
- owner-clean all-section summary SHA-256 `97a66ae9554056837a796f433621a966546b8ade2f21fdde4da1da4d9ceb5bf6`.
## Accounting and admission statement
Before this leaf, canonical `action_obey.obj` had 9/27 exact functions and 439 meaningful / 480 padded code bytes. This packet advances it to **10/27 exact**, **485 meaningful**, and **528 padded** code bytes. The incremental claim is exactly one function and **46 meaningful / 48 padded bytes**. It claims no `.rdata`, `.data`, BSS, COMMON, or other data owner.
Technical verdict: **PASS** for API ownership, exactness, ABI, runtime semantics, inherited-owner preservation, and accounting. Provenance verdict: **review required only for the “natural first candidate” label**; the frozen record establishes that it is a later exact correction. Do not roll this caveat into the separate shared-schema trial, and do not use this leaf as blanket precedent for the larger Fable ActionObey donor cluster.
@@ -1,98 +0,0 @@
# ActionObey command-owner trial: rejected and restored
Date: 2026-09-05. Baseline: `c32247e7651d680dcd133d72b2fbf4e8009cc124`.
## Disposition
No source or byte credit from this shared-header trial is retained. Its full
12-file source delta was reversed with a scoped patch; no checkout/reset/clean
or donor edit was used. The completed earlier shared swarm/biped/API packet
and structure-cache leaf remain intact.
The trial moved actor fire-target/combat-status enums to `actors.h`, moved
the unit animation impulse domain to `units.h`, and put command modifier
domains plus complete animation/script/recording reference records in
`ai_scenario_definitions.h`. Corresponding local shadows in nine C files
were removed. Datum field types and layouts were not changed; vehicle seat
desire ownership and Units implementation/private-name cleanup were not added.
The source proposal's first patch accidentally contained a redundant
`struct ai_command_definition;` forward declaration. This was removed as
an editing correction before the measured after snapshot. It was not
retained or searched as a compiler-order device.
## Measured failure
Actual Ninja dependencies identified **86** consuming base objects. The
complete strict sweep fell **6,233 -> 6,232**: the previously exact
`source/units/units::_unit_preprocess_node_orientations`, 1,920 padded bytes,
became residual. The full owner comparison found:
- 6,270 unchanged runtime sections;
- 84 changes restricted to compiler-local label spelling;
- one changed runtime owner, the Units function above;
- no added forbidden `_point_from_line3d` definition/reference.
The trial was rejected rather than exchanging inherited exact progress for
new dependency coverage. No header ordering, source spelling, flags, or
local facade variants were searched to rescue it.
## Restoration proof
The restored full sweep is **6,233 exact**, zero gains and zero losses.
All **6,355** runtime sections across the 86 affected objects compare
unchanged after restoration. Units is back to 189/189 exact.
The first restored capture correctly refused when its Ninja-dependency
database hash changed during enumeration. No compiler processes remained;
the next unchanged-state capture succeeded. The failed attempt wrote no
manifest or object snapshot and was not accepted as evidence.
Frozen evidence paths, relative to the canonical repository:
- `scratch/action_obey_command_owners_capture_20260905.py`
- `scratch/action-obey-command-owners-before-20260905.json`,
SHA-256 `34c077c0b3cbe43476e5e9dadc1a24fc22d29c462db23976b8ae1b8a45d8702d`
- `scratch/action-obey-command-owners-after-20260905.json`,
SHA-256 `090874e7e335ae137a823e4de032de6122ca89bee8860bfd397fec166a08a404`
- `scratch/action-obey-command-owners-restored-20260905.json`,
SHA-256 `23e6ff746d10fa75f5c23449f16903138281cac98ca62b54da20053adcd4523a`
- corresponding `*-stable-before/after/restored-20260905.json` snapshots;
- `scratch/action-obey-command-owners-runtime-20260905/summary.json`;
- `scratch/action-obey-command-owners-restored-runtime-20260905/summary.json`;
- measured corrected-build and restored-build logs under the same prefix.
The capture folders preserve every proposed/restored source file and all 86
actual objects. This is a rejected trial, not proof that every source-authentic
owner reconstruction must fail.
## Corrections discovered during independent prerequisite review
The complete private `action_obey_command_begin` and
`action_obey_command_perform` proposals remain scratch-only and uncompiled.
Later direct PDB checks refined the initial proposed owner declarations:
- The true wait names are `_ai_atom_wait_modifier_alerted`,
`_ai_atom_wait_modifier_visible_enemy`, and
`_ai_atom_wait_modifier_told_to_advance`, values 0..2. The adjacent
"always" literal belongs to loop modifier zero, not a fourth wait value.
January's out-of-domain wait behavior still returns true.
- PDB records authenticate all fourteen unit animation impulse names and
the full three scenario reference layouts. Their `unused` fields are
genuine records, not fabricated opaque prefixes.
- PDB constants belong to unnamed enums. Descriptive named enum tags in the
initial proposal were reconstructions, not recovered source names.
- The fire-target terminal is DB-spelled `number_of_actor_fire_targets`;
the previously suggested uppercase spelling was a style reconstruction.
- Directly named crouch/targeting/initiative/die modifier constants can
replace the private begin proposal's remaining raw boolean-valued literals
when the proper shared owner is integrated.
These corrections are preserved in the prerequisite inventory and do not
authorize a definition-order or equivalent-spelling search. A future coherent
owner packet must use the refined evidence, preserve actual dependent
objects, and keep private functions private. No consumer-local enum/type/
prototype shadow or fake external private ABI is an acceptable workaround.
The independently acceptable smaller callback graph does not need this
failed owner packet; it is tracked separately.
@@ -1,176 +0,0 @@
# `action_obey.obj`: inline math wrapper trio (2026-08-26)
## Outcome
Three ordinary inline math wrappers are newly strict-exact:
- `_vector_from_points2d`;
- `_cross_product3d`;
- `_negate_vector3d`.
The wave adds three exact functions and 122 meaningful code bytes.
`action_obey.obj` advances from 6/27 to 9/27 exact functions and from 317 to
439 matched meaningful code bytes. It remains incomplete; no completion
label, semantic exception, adjudication, data credit, or comparator waiver was
added.
Source commit:
- `ef95dd016320fed49b3945c7443513b33bd005fe`
(`Match action obey inline math wrappers`).
## Frozen evidence
The untouched translation unit was frozen at commit
`f529d037bc66823234e5e7a48bfdda08563b1ae8` in:
- `build/audit/action_obey_inline_math_baseline_20260826.json`;
- manifest SHA-256
`6b18cb311648b730e7264cd328aaffe96cc12e534c256011ab9174adb54dca3a`.
The target functions have these authenticated January fingerprints:
| Function | Meaningful / padded bytes | Relocations | Normalized SHA-256 |
| --- | ---: | ---: | --- |
| `_vector_from_points2d` | 29 / 32 | 0 | `5c9ee61634eac376cfc65890719accfeb6e0f5db37c55ab93b4da88bcbfe4f80` |
| `_cross_product3d` | 60 / 64 | 0 | `660da515fee7a623e6230a78b71749517ee5ad376db4126e5c8fe79414d2623a` |
| `_negate_vector3d` | 33 / 48 | 0 | `3265a51c2e64caa7a94fc32386ad62d6a774c929993624a9e83f35fbc2f2afcd` |
This was a same-name, same-ABI transfer rather than a masked-shape guess.
Current rebuilt objects already emit each exact function from the canonical
inline bodies in `source/math/real_math.h`. Representative accepted donors
include:
- `vector_from_points2d`: `real_math.obj`, `collision_bsp.obj`, and
`collision_features.obj`;
- `cross_product3d`: `actor_type_infection.obj`, `ai_debug.obj`,
`players.obj`, `matrix_math.obj`, `random_math.obj`, `real_math.obj`,
`objects.obj`, `bsp3d.obj`, and `collision_bsp.obj`;
- `negate_vector3d`: `players.obj`, `real_math.obj`, `models.obj`,
`objects.obj`, and `render_sky.obj`.
The repository header independently authenticates the ordinary C bodies,
operand ordering, return types, and parameter types. All three targets have
zero relocations, so there is no unresolved owner or addend hypothesis.
## Candidate discipline
The three 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. Appending preserves every existing function
section index. The macros only avoid duplicate C definitions; they do not
change optimization, calling convention, or emitted instructions.
The one normal VC7 candidate compile was strict-exact 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.
## Exact comparison and frozen gate
The hardened comparator reports `all_equal: true` for the three new wrappers
and all six previously accepted functions:
- `_vector_from_points2d`;
- `_cross_product3d`;
- `_negate_vector3d`;
- `_random_range`;
- `_action_obey_flush_command_indices`;
- `_action_obey_advance_command_list`;
- `_action_obey_begin`;
- `_action_obey_end`;
- `_action_obey_update`.
For each new wrapper, target and candidate agree on every padded byte,
normalized hash, zero-relocation inventory, and function-section ownership.
The frozen gate reports exactly the three wrappers as `newly_exact`, the six
accepted siblings as `still_exact`, and `changed_nonexact: []`. Its sole
expected structural finding is the additive symbol inventory from defining
the wrappers.
## Clean committed state and replay
The clean committed manifest is:
- `build/audit/action_obey_inline_math_committed_20260826.json`;
- commit `ef95dd016320fed49b3945c7443513b33bd005fe`;
- SHA-256
`c00d5c50ed2e757ddbdd96bbca96764cdc85e5a885fdc8466e96751f8a4581ed`.
Its pre-rebuild and post-rebuild checks return `ok: true`, no failures, no
warnings, no changed nonexact functions, and all nine 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:
`318e7b5cf227a3b0f6ea4258074ce0b52b10201b90bbac8835900be0020945f9`;
- after replay:
`fe1ece76d2544a0d01b58cdd49927a0d962c616b591441ca8c48b57393e5aa63`.
VC7 debug/build records are nondeterministic, so raw object hashes are not the
equality oracle. The post-replay committed manifest is green and the direct
hardened comparison remains `all_equal: true`.
Pinned artifacts:
- target split object:
`0e4f889a35b97c44f6ae91e4906b547bd64f2ff12b9f399e02b46f02b69633fb`;
- source file:
`dbcc7c6d642babfa08ec5a1090661fc9fe35d50bb2f415700dd27d89350785dd`;
- source blob before/after:
`fed1a96e20b6e9e41353ef77085488e19929f948` /
`770920ff65e3ecddff29ba1d83b12f4b189ce89a`;
- canonical report:
`3bab75fe187a50dd5fe76f472abe144ab33e97bca3031f67d89162f59bbd3eec`;
- semantic report:
`8b568709e2d795289d4c420b475f6c6e0a80646925d53145ed21703cd029ee60`;
- VC7 compiler:
`483e00c47bb08d699475a642bcff15b5b2036350b31c540e88a506baf101da11`;
- hardened comparator:
`411e41698a9491a7b69430307d03f844ee3144633e5d3038d3ffde81a8eccfb7`;
- regression gate:
`d09b46dd2331ff7ca677cad78a66c780c4effa41a5389f0c614c9760d7632a5f`;
- matching methodology:
`3eb52f6b2df7b94b4d732161c0f86e2becebbcf0b546824deb957882118b1b5c`.
Supporting audits:
- admission SHA-256:
`f0aa45c22cc604fb74abc388b06dd79a27e7a0b498a5d28837a9e785680c9a36`;
- parked-frontier SHA-256:
`cfac03aea5be607c19a6a40bcdbc1164e71ffb0179602af4bd37b3fd50e26d1d`.
## Canonical progress and gates
After regeneration:
- canonical code: 508,669/2,198,102 bytes and 4,211/11,060 functions;
- complete files: 377/833;
- Halo category: 495,755/1,770,166 code bytes and 4,044/7,574 functions;
- Halo complete files: 275/468;
- semantic audit: 470 units and 4,319 functions evaluated, 4,176 semantic
exact, 114 hidden-exact functions / 64,705 hidden bytes, 4,237 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 each wrapper ends with an explicit
value return. The retained source is C89. It contains no assembly, `register`,
`volatile`, barrier, pragma, explicit intrinsic, attribute, raw address,
pointer/integer reconstruction, undefined behavior, synthetic anchor, or
byte-forcing form. These wrappers perform no tag or object access, so the
typed-access rules are not implicated.
@@ -1,133 +0,0 @@
# `action_obey.obj` Jonas public-lifecycle salvage
## Result
This bounded source-only wave starts from canonical commit
`3f2ba63dfb1258f7e36baaec69fe495bcefdc20f`. Six ordinary public,
one-argument stack-ABI bodies were emitted together in one natural XDK compile
and measured independently. Five were strict in that first complete batch and
are retained. `_action_obey_flush_structure_indices` missed and was removed
without a second source shape or tuning pass. `action_obey.obj` advances from
0/27 to 5/27 exact functions and from 0/11,585 to 290/11,585 meaningful code
bytes. It remains `NonMatching` and receives no data credit.
| Function | Target meaningful / padded | Relocations | First candidate normalized SHA-256 | Decision |
| --- | ---: | ---: | --- | --- |
| `_action_obey_flush_command_indices` | `106 / 112` | 6 | `32a82f8ac1a4940e43f61ca190846d4dd6d6d2970adedde76ab0e42837ca3c03` | **retained: strict exact** |
| `_action_obey_flush_structure_indices` | `46 / 48` | 2 | `685bb16bd7a8c09586002344b247d5aa94fe50371bf33b398b82896932470495` | rejected: target hash `9f0facf2a5e64545b5b323119ea1ce6c45813a307bf0fb256308aab64fe0db82` |
| `_action_obey_advance_command_list` | `46 / 48` | 4 | `e1cc926aa1c23e5f6c1e9bb46b326736e53bff9e460248611ef98d43f48a8faf` | **retained: strict exact** |
| `_action_obey_begin` | `46 / 48` | 4 | `e1cc926aa1c23e5f6c1e9bb46b326736e53bff9e460248611ef98d43f48a8faf` | **retained: strict exact** |
| `_action_obey_end` | `46 / 48` | 4 | `e1cc926aa1c23e5f6c1e9bb46b326736e53bff9e460248611ef98d43f48a8faf` | **retained: strict exact** |
| `_action_obey_update` | `46 / 48` | 4 | `e1cc926aa1c23e5f6c1e9bb46b326736e53bff9e460248611ef98d43f48a8faf` | **retained: strict exact** |
The five retained functions total 290 meaningful bytes, 304 padded bytes, and
22 relocations. Each passes the hardened comparator in padded size, every
normalized byte, and every relocation address, type, symbolic destination,
and addend. Explicit terminal `return;` statements were added to satisfy the
house source form; final validation replays prove that these style-only
statements do not change any retained section.
## Provenance and one-shot boundary
- Baseline and restored atomic `action_obey.c` Git blob:
`febbd96e39c7519dc21dd86a35bbe6fe7b6a4579`.
- Retained `action_obey.c` Git blob / stable committed-payload SHA-256:
`e0bb4361e9e4a4bf353e5cbf58037fee9b9e0169` /
`e9160d896d224885bc51bee44329aca58cee420efb52c45875be1cee6d76e4ca`.
The SHA is over `git cat-file blob e0bb4361...`, not checkout bytes.
- January executable SHA-256:
`4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`.
- January split `action_obey.obj` SHA-256:
`0e4f889a35b97c44f6ae91e4906b547bd64f2ff12b9f399e02b46f02b69633fb`.
- The preserved atomic base object is only the restored 476-byte shell,
SHA-256
`0c7ed4643c2075fe416eacfa9a464d49c59b8d32241755cc743e953793824454`;
it is not a candidate oracle and preserves none of the historical bodies.
- The supplied historical commit `677defb9d9b4dd0ac4f20bc2c5dbd53bf3e8d7f5`
is the action-charge frontier. The independently authenticated action-obey
atomic ledger is `f164672e22f55ade17493b2030d561d806bab2ec`, whose parent is
`983f99f6ea9de2d43f4c17297017982ca42ac883`.
- Clean HCEA semantic-oracle commit:
`c168af2e747d3095d9a29418ae401f3a39544863`. The six screened body blobs are
`7c6b1802eda90af54c7c0c81a0b3136aeac3fdff` (flush command),
`bf1a73ee05723cd4d0fe9a63a6945f3fa1995d8b` (flush structure),
`6599ec2fbe3b660229e331b71b789bd9d79149c8` (advance),
`b9c9eef8510f14c386742e4405e2d7e7e52bf7ce` (begin),
`80fb2698ca87abb6b554f6bf4a8cc0e633c838fd` (end), and
`7ac747c1b0688fc4a38dc0a99d4ef63a84e9aaa7` (update).
- Compiler: Microsoft 32-bit C/C++ Optimizing Compiler 13.00.9254.1 for
80x86, SHA-256
`483e00c47bb08d699475a642bcff15b5b2036350b31c540e88a506baf101da11`,
with unchanged `/O2 /Oy- /DDEBUG /Dxbox` flags.
HCEA was used only for semantics. January's symbols, stack access,
disassembly, literals, exact byte counts, and relocations supplied the PC
source shape. Every selected target symbol is a COFF external function of type
`0x20`, reads its sole `long` argument from `[ebp+8]`, and returns with the
ordinary C stack ABI. Exactly one new exploratory six-body object was emitted.
The rejected body was then deleted; later builds are validation replays of the
retained source, not alternate candidate searches.
## Retained behavior and typed state
`action_obey_flush_command_indices` obtains the typed actor and its embedded
obey state, validates the command-list index against the current scenario, and
either marks the state finished and transitions to no action or invokes the
private individual iterator with the command-index flush callback. The
advance, begin, end, and update wrappers obtain the same typed state and invoke
that iterator with their respective private callbacks.
The translation unit locally declares the callback function type and ordinary
typed undefined functions. Compile-time proofs preserve the January layouts:
simple control `0x24`, complex control `0x58`, obey state `0x84`, and the obey
state at actor offset `0x9C`. It uses named fields and existing typed accessors.
There is no raw address, byte-offset dereference, pointer or union pun,
inactive-union access, volatile scheduling device, force-inline annotation,
optimizer pragma/barrier, assembly, synthetic anchor, or object-byte forcing.
## Fail-closed scope, signatures, and ownership
The final candidate defines exactly the five retained public functions. The
rejected `_action_obey_flush_structure_indices` definition is absent. The
other 21 January functions are also absent, including every private
`code_*` helper, perform/control/setup/describe, and generic inline emission.
The six private callbacks/iterator used by the retained bodies are ordinary
typed undefined relocations, never definitions.
The final object consists only of `.drectve`, `.debug$S`, and five `.text`
COMDATs. It defines no `.rdata`, `.data`, `.bss`, COMMON symbol, or other
storage owner; January's 1,748 data bytes remain wholly unclaimed. Only
`source/ai/action_obey.c` and this ledger change. No header, caller,
configuration, semantic exception, parked record, existing matching log, or
protected source changes, and no path is deleted.
The current model is consumer-safe without a shared declaration. The only
textual begin/update/end references are in the disabled action table in
`actions.c`, while `actions.h` already models the relevant action callbacks as
ordinary `void(long)` functions and declares no conflicting cross-TU
signature. The private callback declarations likewise have no definitions or
conflicting declarations elsewhere in production source.
## Validation
- Complete `halobetacache_build` and `libcmt_build`: pass (569/569 actions).
- Direct hardened final comparison: all five retained functions remain strict;
the rejected wrapper, the other 21 target functions, and all data owners are
absent.
- Semantic audit: 470 units, 4,077 functions evaluated, 3,937 semantic exact,
101 hidden exact / 61,876 hidden code bytes, 3,998 accepted exact, and zero
unit errors.
- Campaign progress: 375/833 complete objects, 3,985/11,060 exact functions,
476,985/2,198,102 meaningful code bytes, and
1,835,088/4,176,062 matched data bytes. Halo contributes 3,818/7,574
functions and 464,071/1,770,166 code bytes.
- Object admission: zero candidates and zero revocations; only the inherited
`source/shell/shell_xbox` completion-label contradiction remains.
- Parked validation: three active, zero stale, and zero invalid entries.
- Complete tooling suite: 179/179 tests pass.
- `git diff --check`, deleted-path, and banned-policy scans are clean.
A clean committed-state regression snapshot/check followed by deletion and
forced rebuilding of `action_obey.obj`, another regression check, and a direct
hardened replay is the final same-path reproducibility proof. No push is
performed.
@@ -1,162 +0,0 @@
# `action_obey.obj`: random-range wrapper (2026-08-26)
## Outcome
`_random_range` is newly strict-exact. The wave adds one exact function and
27 meaningful code bytes. `action_obey.obj` now reports 6/27 exact functions
and 317 matched meaningful code bytes. It remains incomplete; no completion
label, semantic exception, adjudication, or data credit was added.
Source commit:
- `7bce0f67e990e5dd75e4392cbc9e6cfa4c32f6cf`
(`Match action obey random range`)
## Frozen evidence and donors
The untouched translation unit was frozen at commit
`3f2e6a76cb9cf575954a57e1bf3813cd312b0025` in:
- `build/audit/action_obey_random_range_baseline_20260826.json`;
- manifest SHA-256
`cc41ccbacdd5ab85bced427218def879a69dab6df5cc8f13fe9a051070f670de`.
January's `_random_range` is 27 meaningful / 32 padded bytes, with normalized
SHA-256
`7ed2e50e8e1a701a98fcceeb6a6b67e05d997f21bdb9ff5ee2e848fd1a8f409a`.
It has exactly two `IMAGE_REL_I386_REL32` relocations:
- byte 12 to `_get_global_random_seed_address`;
- byte 18 to `_seed_random_range`.
The repository shape atlas independently finds two exact 32-byte, two-call
donors with identical relocation-masked instruction bytes:
- `source/ai/action_alert:_real_random_range`;
- `source/camera/bored_camera:_real_local_random_range`.
The authenticated inline definition in `source/math/real_math.h` fixes the
recipient's short-returning semantics and exact destinations:
```c
return seed_random_range(
get_global_random_seed_address(),
lower_bound,
upper_bound);
```
Thus the transfer is jointly proven by January's bytes and relocations, two
accepted ABI-shape donors, and the canonical repository source body. It is
not a masked-byte guess.
## Candidate discipline
The header inline identifier is renamed locally across the include block, then
the ordinary out-of-line wrapper is defined after all five previously accepted
functions. The macro only avoids a duplicate C definition; it does not alter
optimization, calling convention, or emitted instructions.
The one normal VC7 candidate compile was strict-exact. There was no
emitted-object permutation, compiler-flag change, allowlist, adjudication,
comparator exception, object patch, raw intrinsic call, or byte-forcing
expression.
## Exact comparison and frozen gate
The hardened comparator reports `all_equal: true` for the new wrapper and all
five previously accepted functions:
- `_random_range`;
- `_action_obey_flush_command_indices`;
- `_action_obey_advance_command_list`;
- `_action_obey_begin`;
- `_action_obey_end`;
- `_action_obey_update`.
For `_random_range`, target and candidate agree on all 32 padded bytes,
normalized hash, relocation count, both relocation addresses/types, and both
exact destinations. The pre-commit frozen gate reports `_random_range` as
`newly_exact`, all five accepted siblings as `still_exact`, and
`changed_nonexact: []`. Its sole hard finding is the expected additive symbol
inventory caused by defining the new function.
## Clean committed state and replay
The clean committed manifest is:
- `build/audit/action_obey_random_range_committed_20260826.json`;
- commit `7bce0f67e990e5dd75e4392cbc9e6cfa4c32f6cf`;
- SHA-256
`506b7f5ff0129697a6a59056c2bf4783c36f9ccce32de4ea2346b4fa3fa60ed0`.
Its pre-rebuild and post-rebuild checks both return `ok: true`, no failures,
no warnings, no changed nonexact functions, and all six functions above as
`still_exact`.
The generated object path was resolved inside this worktree. That single
generated file was deleted and rebuilt exactly once. Raw base-object hashes
were:
- before replay:
`8d320430f41df2e30729f3f419bca7c16091972b06fef62273d4b6c4bfb630c0`;
- after replay:
`09f54824c750107064118aabebf38a202c03c236cd83ba5e423c4816a4fffdf3`.
VC7 debug/build records are nondeterministic, so raw object hashes are not the
equality oracle. The post-replay committed manifest is green and the direct
hardened comparison remains `all_equal: true`.
Pinned artifacts:
- target split object:
`0e4f889a35b97c44f6ae91e4906b547bd64f2ff12b9f399e02b46f02b69633fb`;
- source file:
`2794ea8b908210ea9bd5a94693ede143c95db1850bac1803ddb33f8269148f83`;
- source blob before/after:
`e0bb4361e9e4a4bf353e5cbf58037fee9b9e0169` /
`fed1a96e20b6e9e41353ef77085488e19929f948`;
- canonical report:
`28e97215980d2cb6afb13e04cffda0b6ed2d893155847215138c2925250ea822`;
- semantic report:
`4e06bd4620272dbf7d9eebcf36e682271599afae808d7536152cc834031dbc7a`;
- VC7 compiler:
`483e00c47bb08d699475a642bcff15b5b2036350b31c540e88a506baf101da11`;
- hardened comparator:
`411e41698a9491a7b69430307d03f844ee3144633e5d3038d3ffde81a8eccfb7`;
- regression gate:
`d09b46dd2331ff7ca677cad78a66c780c4effa41a5389f0c614c9760d7632a5f`;
- matching methodology:
`3eb52f6b2df7b94b4d732161c0f86e2becebbcf0b546824deb957882118b1b5c`.
## Canonical progress and gates
After regeneration:
- all categories: 23.13% matched and 14.21% linked, 377/833 complete files;
- code: 508,520/2,198,102 bytes and 4,207/11,060 functions;
- data: 1,836,762/4,176,062 bytes;
- Halo category: 4,040/7,574 functions;
- semantic audit: 470 units and 4,315 functions evaluated, 4,172 semantic
exact, 114 hidden-exact functions / 64,705 hidden bytes, 4,233 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;
- added-source banned-construct matches: zero;
- no push.
## House-rule audit
Each parameter is on its own line and the function ends with an explicit
return statement. The source is C89. The retained code contains no assembly,
`register`, `volatile`, barrier, pragma, explicit intrinsic, attribute, raw
address, pointer/integer reconstruction, undefined behavior, synthetic anchor,
or byte-forcing form. This wrapper performs no tag or object access, so the
typed-access rules are not implicated.
@@ -1,114 +0,0 @@
# ActionObey (opus5 100K consolidated lane, waveB, 2026-09-14): describe_command and command_begin closed
Worker lane `opus/100k-consolidated-20260914` (worktree `C:\halo-worktrees\opus5-100k-consolidated-20260914`).
Only `source/ai/action_obey.c` changed. Two already-written residuals became strict EXACT;
`_action_obey_control` remains a documented register tie.
## 1. Scope and provenance
- Unit `source/ai/action_obey.c` vs January split `build/split/source/ai/action_obey.obj`.
- Compiler VC7 13.00.9254 (XDK), repository flags for the unit (`/c /O2 /Oy- /DDEBUG /Dxbox`), unchanged.
- Names were already authentic (January map atlas, orchestrator commit `47d9dd0da`); no rename needed.
- Evidence: January COFF (relocations, instructions via `tools.campaign.alndiff`), Ghidra decompilation
`C:/halo-worktrees/opus5-30k-fresh-graphs-20260914/scratch/ghidra/out/action_obey.obj.decomp.c:300-538`,
HCEA `research-cache/halocea-full-blobs-20260830/src/blam/ai/action_obey_command_begin.c` and
`action_obey_control.c` (semantics/order hints only).
- Prior ledgers read: `action_obey_obj_opus5_fresh_graphs_20260914.md` (including the lead admission),
`action_obey_obj_private_closure_plan_20260905.md`, `action_obey_obj_api_leaf_independent_review_20260905.md`,
and the fresh-graphs worker report.
## 2. Validated baseline (lane HEAD)
`== exact 24 residual 3 unwritten 0 (of 27 listed)`, `_point_from_line3d` guard passed, no parked entries.
| Row | Target (padded / relocs / sha16) | Baseline candidate | First divergence |
| --- | --- | --- | --- |
| `_action_obey_command_begin` | 4320 / 167 / `5b56fdd8b88a4456` | 4336 / 167 | `sub esp,0x184` vs `0x174` |
| `_action_obey_control` | 992 / equal | 992 / equal, sha differs | +0x33e register pair |
| `_action_obey_describe_command` | 1936 / 153 / `d14c27f63086d980` | 1824 / 147 | pause arm `jmp` into a shared tail |
Final: `== exact 26 residual 1 unwritten 0 (of 27 listed)`, guard passed, 0 parked drift (no entries), owner
census clean (no new code/data owner vs `build/base`; the header-inline SELECT_ANY math copies are pre-existing and
already disclosed), `tools/fake_match_scan.py` 0 leads.
## 3. Accepted controls (retained in production)
### `_action_obey_describe_command` (now EXACT 1936/153)
- `relocdiff --count-by-target`: every literal/call count equal except `__snprintf` 26 vs 20, so the gap was six
cross-jump merges, not missing code.
- January's animate and recording NONE arms read `mov eax, offset "NONE"; push eax` after
`mov eax, offset "<error>"`: the name is a variable, exactly like the script arm. Source now uses the
script-arm idiom for all three tag-reference atoms: `name = "<error>"; if (index == NONE) name = "NONE";
else if (valid) name = element; _snprintf(..., name);`. VC7 duplicates that small join (snprintf plus return)
into both predecessors; the script join also loads a modifier table, so it is not duplicated. Ghidra shows two
calls only because it decompiles the duplicated join.
- HEAD's separate `_snprintf(..., "NONE")` calls pushed an immediate. That made those arms identical to other
0x10 tails and moved VC7's cross-jump hosts across the whole function.
### `_action_obey_command_begin` (now EXACT 4320/167)
1. **Look-arm duration re-store.** January's `_ai_atom_look` valid-point arm contains
`mov ecx,[command+4]; mov [ebp-0x28],ecx` next to the point-index store, so `look_duration = command->parameter1`
is really stored twice. The same shape appears in look_random, which assigns the duration inside its valid arm.
The earlier lead removed this line as a "duplicated store". The instruction is in January, and removing it
again measures [sha].
2. **Declaration order.** The look prologue stores point, prop, unit, then duration (-0x14, -0x30, -0x2c,
-0x28), so `look_duration` is declared after the three NONE indices. The old order measures [sha].
3. **Scoped prop search.** In January, `prop_iterator` (-0x1c..-0x14) and the per-player `head_position`
(-0x20..-0x14) use overlapping storage, so their scopes are disjoint. In HEAD the prop iterator was scoped to the
whole look_player branch, which enclosed the player loop. That pushed `data_iterator` into its own 16-byte class
(frame +16) and split January's {direction, facing, head_position, vehicle_origin, prop_iterator,
actor_definition} class. The prop search is now in its own block inside the look_player branch. Without it the
size is 4336.
4. **running_jump store order.** `delay_ticks` is written before `pause_timer`, matching the already-exact
targeted_jump arm, which writes delay_ticks, the velocities, then pause_timer. This fixes January's EAX reload of
`simple_control` after `test al,al`.
## 4. Experiment matrix
| Candidate | Change | Result | Kept |
| --- | --- | --- | --- |
| describe v1 | animate/recording as script-style if / else-if + one `_snprintf` | 1936/153 EXACT | yes |
| begin b1 | + look-arm duration re-store | 4336, look diffs reduced | yes (step) |
| begin b2 | + duration declared last | 4336, only frame + running_jump register | yes (step) |
| begin b3 | + prop search block | 4320/167, frame 0x174, one EAX/ECX pick | yes (step) |
| begin b4 | + delay_ticks before pause_timer | EXACT | yes |
| begin n1 | b4 minus re-store | [sha] | no |
| begin n2 | b4 with old declaration order | [sha] | no |
| begin n3 | b4 without prop block | [size 4336!=4320] | no |
| control c1 | alignment_vector declared before facing | identical to HEAD | no |
| control c2 | HCEA ternary `jump_alignment_vector = normalize2d(...) == 0 ? *global_forward2d : facing;` first | worse (264 insns, register swaps spread to 0x2b7) | no |
| control c3 | same ternary into the local, store in place | identical to HEAD | no |
Every whole-TU gate kept all other rows unchanged (full `--all` listing diffs).
## 5. Do-not-repeat
- describe_command: the return/break mixes from the earlier lane. The cause was the NONE-arm shape, now closed.
- control: declaration order of facing/alignment_vector; ternary alignment forms; alignment store position;
Ghidra single overwritten local; jump-block scope local.
## 6. Residual classification
- `_action_obey_control` (992, [sha]): ABI/register tie, measured. January gives alignment i->EDX and j->ECX
(facing arm loads j first; join stores +0x448 before +0x444). Ours gives the inverse. Every other instruction is
equal.
## 7. Reopen criteria
- control: a verified January idiom where an enregistered 2D vector pair is copied j-first, or new type evidence for
`orders.move.jump_alignment_vector`.
## 8. Disposition
- `_action_obey_describe_command` and `_action_obey_command_begin`: strict EXACT, eligible for Matching
(normalized sha16 equal to January).
- `_action_obey_control`: still active residual, no credit.
- Lesson: both closed rows had been labelled cross-jump/frame-packing ties. In each case the relocation count or
slot overlap pointed to a concrete source shape. Read `relocdiff --count-by-target` and slot overlaps before
classifying a tie.
## Orchestrator admission note (checkpoint 2)
Only `_action_obey_describe_command` was admitted (1,936 padded). The strict-exact `_action_obey_command_begin` form restores `look_duration = command->parameter1;` in the look arm, directly after the identical declaration initializer. That is a redundant store, prohibited by the house rules, and the fresh-graphs lane removed it for the same reason. Its hunks, including the bare prop-iterator block and the pause_timer/jump reorder, were reverted. The worker file is preserved as `scratch/orch/action_obey_waveB.c`. Reopen only with evidence of a non-redundant January statement shape.
@@ -1,88 +0,0 @@
# ActionObey (opus5 150K house-clean lane, wave w2, 2026-09-14): command_begin blocker proved, control left exhausted
Worker lane `opus/150k-house-clean-20260914` (base `12f7375d4`). Only `source/ai/action_obey.c` was in scope.
**No production source changed.** `git diff --stat -- source/ai/action_obey.c` is empty, and the final real-file gate
matches the baseline row for row.
## 1. Baseline (real file, whole-TU gate, `_point_from_line3d` guard passed)
`== exact 25 residual 2 unwritten 0 (of 27 listed)`. This unit has no `config/parked.json` entries.
| Row | January (padded / relocs / sha16) | Real file |
| --- | --- | --- |
| `_action_obey_command_begin` | 4320 / 167 / `5b56fdd8b88a4456` | 4336 / 167, frame `0x184` vs `0x174` |
| `_action_obey_control` | 992 / equal | 992, [sha] only |
Inherited exact rows from earlier lanes were not touched: `_vehicle_possibility_qsort`, `_action_obey_describe_command`,
`_action_obey_command_perform`, `_action_obey_individual_perform`, `_action_obey_perform`.
## 2. `_action_obey_command_begin`: NOT LANDED (policy-blocked, reopen criterion proved unsatisfiable)
Prior state: the 100K lane's exact file (`opus5-100k-consolidated-20260914/scratch/orch/action_obey_waveB.c`) differs from
the real file in four hunks. The orchestrator rejected it twice because one hunk restores
`look_duration = command->parameter1;` inside the `_ai_atom_look` valid arm, a store that repeats the declaration
initializer. The admission note asked for a non-redundant January statement shape.
### January evidence (target COFF via `tools.campaign.alndiff`)
- Look prologue `+0x1ca..0x1e2`: `mov ecx,[ebp-4]` (command), `mov edx,[ecx+4]` (parameter1). It then stores the three
NONE indices and `mov [ebp-0x28],edx` (duration).
- Look valid arm `+0x205..0x213`: `mov eax,[ebp-4]; mov edi,ecx; mov ecx,[eax+4]; mov [ebp-0x14],edi; mov [ebp-0x28],ecx; jmp +0x418`.
- Across the whole function, `[ebp-0x28]` has exactly six uses:
- writes: `+0x1e2` (prologue), `+0x210` (look valid), `+0x2a4` and `+0x2b9` (look_random valid, `fstp`);
- reads: `+0x418` (the join, `fld` then compare against 0.0) and `+0x4fa` (pause_timer).
- Neither look_player nor look_object writes it. All six invalid look/look_random exits (`+0x1ee`, `+0x1ff`, `+0x229`,
`+0x23a`, `+0x24a`, `+0x255`) jump straight to `+0x418` without a store.
### Dominance argument
The join reads the duration first on every path. On the invalid edges and on the look_player/look_object paths, the only
reaching definition is the prologue store of `command->parameter1`. The source must therefore contain a dominating
`look_duration = command->parameter1` (the declaration initializer or an equivalent statement). The only alternative is
reading an uninitialized local, which is undefined behaviour and prohibited, and VC7 would not synthesize the prologue store
in that case anyway.
The look-valid store copies the same value, from the same memory, into the same variable. Nothing writes `[ebp-0x28]` or
`command->parameter1` between the two stores. Every C statement that emits these three instructions is therefore
value-redundant.
HCEA (`halocea-review/src/blam/ai/action_obey_command_begin.c:261-267`, a later build) has no such store, so it offers no
alternative January spelling. **The requested reopen criterion cannot be met on January's bytes.**
### Measurements (reference only, whole-TU with guard)
| Candidate | Change | Result |
| --- | --- | --- |
| `b4_waveB.c` | 100K exact form (includes the re-store) | 4320/167 EXACT; all other rows unchanged. Not landable. |
| `n1_no_restore.c` | b4 minus only the re-store | 4320 [sha]. The three look-arm instructions are missing, and the -0x14/-0x28/-0x2c slot permutation cascades through the look_random, look_player and look_object arms. |
No further shapes were tried: the dominance argument covers the remaining space. Classification: policy-blocked. January
contains a value-redundant store, so this is not a register or scheduling tie.
### Disposition and reopen
- Stays a zero-credit fuzzy genuine caller in the real file.
- The row can close only through an explicit owner decision to admit a January-proven redundant store. The evidence for
that decision is the 100K exact file plus the proof above. Do not requeue it for workers.
## 3. `_action_obey_control`: SKIPPED (exhausted, on the brief's do-not-spend list)
No new source or type evidence was found, so no shapes were spent.
Re-reading the four differing instruction pairs (`+0x33e..0x391`) gives one refinement to the 100K classification:
- In both builds, the pair member held in ECX is loaded first in the facing arm and stored first at the join.
- The `global_forward2d` arm loads in memory order in both builds.
So the "j-first copy, +0x448 before +0x444" ordering follows from the register choice (January: i in EDX, j in ECX; ours:
the reverse). It is not a separate statement-order fact. The residual is a pure ECX/EDX permutation of an enregistered
`real_vector2d` (class C). HCEA `action_obey_control.c:170-180` uses the same ternary-copy semantics that were already tried.
Reopen criterion is unchanged from the 100K ledger: a verified January idiom that copies an enregistered 2D pair with the
second member in ECX, or new type evidence for `orders.move.jump_alignment_vector`.
## 4. Final checks
- Final real-file gate equals the baseline (`diff` of the `--all` listings is empty); guard passed.
- No parks for this unit, so no park drift; owner census unaffected (no edit); `tools/fake_match_scan.py` reports 0 leads.
- Worker notes: `scratch/workers/action_obey.md`; candidates and objects: `scratch/workers/action_obey/`.
@@ -1,178 +0,0 @@
# ActionObey fresh-graphs lane (opus5, 2026-09-14): header-blocked private graph, measured in scratch
Worker lane `opus/30k-fresh-graphs-20260914` (base `c3e257e9a`, lane HEAD `47d9dd0da`).
**No production source changed.** `git diff -- source/ai/action_obey.c` is empty; the
final real-file gate equals the baseline. All progress below is scratch-only evidence
for an orchestrator header packet.
## 1. Scope and provenance
- Unit `source/ai/action_obey.c` (SHA-256 `bfc668a6...5fe9`) vs January split
`build/split/source/ai/action_obey.obj` (SHA-256 `d52f369d...600e`).
- Compiler VC7 13.00.9254 (XDK), flags `/c /O2 /Oy- /DDEBUG /Dxbox`.
- Names: January map atlas `scratch/atlas/action_obey.obj.txt` (all seven non-exact rows
already carry their authentic names after orchestrator commit `47d9dd0da`).
- Semantics: HCEA `research-cache/halocea-full-blobs-20260830/src/blam/ai/action_obey_command_{begin,perform}.c`,
`action_obey_individual_perform.c`, `action_obey_control.c`; Ghidra
`scratch/ghidra/out/action_obey.obj.decomp.c`; canonical uncompiled proposals
`scratch/action_obey_command_{begin,perform}_proposal_20260905.c`; Fable donor
`C:/halo-worktrees/fable-50k-small-families-20260904` (`f9dd42218`). No HCEA/Stian body
exists for `action_obey_describe_command` (PC debug only).
## 2. Validated baseline (lane HEAD)
`== exact 20 residual 1 unwritten 6 (of 27 listed)`, `_point_from_line3d` guard passed,
no parked entries for this unit.
| Row | Target | Status |
| --- | ---: | --- |
| `_action_obey_command_begin` | 4320 / 167 relocs / `5b56fdd8b88a4456` | UNWRITTEN |
| `_action_obey_command_perform` | 1408 / 51 / `afc109dc8ddf7e3f` | UNWRITTEN |
| `_action_obey_control` | 992 | residual [sha] |
| `_action_obey_describe_command` | 1936 / 153 / `d14c27f63086d980` | UNWRITTEN |
| `_action_obey_individual_perform` | 288 / 11 | UNWRITTEN |
| `_action_obey_perform` | 192 / 8 | UNWRITTEN |
| `_vehicle_possibility_qsort` | 64 / 0 | UNWRITTEN (no caller emits it) |
## 3. Results (scratch whole-TU gate, prerequisites declared scratch-only)
`scratch/workers/action_obey/b10.c`: `== exact 24 residual 3 unwritten 0`, all 20 baseline
exact rows still exact, point guard passed, `tools/fake_match_scan.py` 0 leads.
| Row | Scratch result | Candidate text |
| --- | --- | --- |
| `_action_obey_perform` | **strict EXACT** | `scratch/workers/action_obey/perform.inc` |
| `_action_obey_individual_perform` | **strict EXACT** | `BEST_individual_perform.inc` |
| `_action_obey_command_perform` | **strict EXACT** (bytediff 0) | `BEST_command_perform.inc` |
| `_vehicle_possibility_qsort` | **strict EXACT** (via genuine caller) | `qsort_new.inc` |
| `_action_obey_command_begin` | 4336 / 167, frame-packing residual | `BEST_command_begin.inc` |
| `_action_obey_describe_command` | 1824 / 147, cross-jump residual | `describe_v1.inc` (+ dialogue include) |
| `_action_obey_control` | unchanged (register/aggregate-copy tie) | — |
Post-header landing file with no scratch shadows: `scratch/workers/action_obey/LANDING_after_headers.c`.
## 4. Why nothing landed (owner block)
Every new body needs declarations that belong to shared headers (brief §1 forbids worker
header edits and TU-local shadows):
- `describe_command`, `command_begin`: `struct ai_{animation,script,recording}_reference_definition`
(January element sizes 0x3c/0x28/0x28; PDB records).
- `command_begin`/`command_perform`: go_to, look, animation_mode, crouch, grenade, vehicle,
animate, action, targeting, initiative, wait, die modifier domains; actor fire-target and
combat-status domains; unit animation impulses.
- `individual_perform` requires both private bodies; `perform` requires `individual_perform`;
the qsort comparator is emitted only through `command_begin` (static COMDAT law).
- Owner census: the graph also emits SELECT_ANY copies of `distance_squared3d`, `dot_product2d`,
`dot_product3d`, `vector_from_points3d`, `real_random_range`, which January references as
undefined externals (per-caller real_math inline boundary; base already emits the
magnitude/normalize/scale/square_root family).
Exact ORCHESTRATOR-PROPOSAL text, conflict lists and consumer lists:
`scratch/workers/action_obey.md` (end of file) and `scratch/workers/action_obey/prereq.inc`.
## 5. Accepted source controls (evidence per shape)
- **command_perform**: single `done` result returned once (every January epilogue is `mov al,bl`);
each guard path assigns its result explicitly (`else { done = TRUE; }`) — removing any one of
the eight explicit TRUE results changes bytes (measured individually); the TRUE-atom case group is
the last case (its `mov bl,1` block falls into the return block); `done = dot_product3d(...) >
current_command->parameter1` for move_direction; flying/2D facing arms share one 0.984 compare via
`goto facing_done` (HCEA CFG); assert line 983 names `current_command`; action result
`!complex_control || !complex_control->play_action`.
- **individual_perform**: `command_valid = FALSE; if (index < count) command_valid = TRUE;`
(January's branchy stores; `= index < count` emits setl and swaps two byte slots).
- **vehicle_possibility_qsort**: `<` → -1, `>` → 1, else 0 with three returns (January reloads `a`
between compares); HEAD's `a >= b ? a > b : -1` body is the wrong shape.
- **describe_command**: function-scope 5-entry look-modifier table, block-local tables per case
(packed into the same top slots), 11-entry action table, 256-byte teleport buffer (frame 0x140
exact); `dialogue_get_vocalization_name` must be prototyped (`units/dialogue_definitions.h`)
— without it C4013 promotes the short argument with movsx.
- **command_begin**: see §6 v10; ABI EAX=complex_control / ECX=unit_index reproduced.
## 6. Experiment matrix (bounded)
describe_command: v1 all-break 1824/147; v2 all-return identical; v3/v4 mixed return/break
1872/149 and 1824/147; v1b + dialogue include 1824/147 (vocalize fixed). Residual = VC7
cross-jump selection only.
control: single overwritten local (Ghidra form) byte-identical to HEAD; alignment store before
targeted / first → worse; jump-block scope local → identical. Residual = reversed field order of a
register-pair struct copy.
command_perform: v1 1456/52 → v2 goto facing 1424 → v3 HCEA returns 1408/51 (wrong ABI:
complex_control register-passed) → v12 mixed 1408 regalloc right → v14 explicit guard results →
v18 TRUE group last (1 byte) → v19b/v20 EXACT. Uninitialised-`done` probe rejected (UB, and wrong).
individual_perform: donor body 272; A (FALSE/if TRUE) EXACT; ternary next index 272; C 288 sha.
command_begin: v1 4368/179 (switch-compiled sub-dispatch) → v2 if/else chains 4320/167 →
v3 single exit → v5 `short command_number` → v6 two distance locals → v7 double duration store
(refs match January) → v10 scenario local / surface arg / vocalize order / biped ternary 4336/167.
Probes inert or worse: short index local, declaration order, real_point3d facing, direction and
iterator scope moves.
## 7. Do-not-repeat
- describe_command return/break mixes; control statement moves and local scoping; command_begin
declaration-order permutations (density ranking ignores order off ties).
- Never use an uninitialised result, TU-local enum/struct shadows, or non-static externs for the
private pair.
## 8. Residual classification
- control: register/store-order tie (measured).
- describe_command: cross-jump granularity (measured instruction-level; source cause unknown).
- command_begin: frame-allocation packing — January classes {direction, teleport facing,
head_position, vehicle_origin, prop_iterator, actor_definition} (end-aligned at -0x14) and
{object_iterator, data_iterator} at -0x40; ours forms {object_iterator, facing, prop_iterator,
actor_definition}, {direction, head, origin} and a lone data_iterator (+16 bytes). Inference, not proof.
## 9. Reopen criteria
- Header packet A–C (and census decision D) landed → gate `LANDING_after_headers.c`; land the four
exact rows, park or keep begin/describe fuzzy.
- command_begin: an evidence-led function-scope promotion that reproduces January's packing classes.
- describe_command: a source-level reason for January's sparser cross-jumping.
## 10. Disposition
Still active; zero new production credit from this lane. Four rows are strict EXACT in a complete
scratch TU pending owner-header prerequisites.
## Lead admission (2026-09-14): graph landed
The worker's scratch-only prerequisites were resolved as follows, and the
complete private graph was then admitted:
- **Header prerequisite `ef7bc742a`**: `source/ai/ai_scenario_definitions.h` now
owns the command-list atom modifier enums and the ai animation/script/recording
reference definitions (HCEX PDB names; January element sizes 0x3C/0x28/0x28).
`action_alert.c`'s local reference struct was removed. The full build showed
no inherited strict loss; two already-parked fuzzy functions were re-measured
(documented in the park evidence).
- **TU-local enums with disclosure**: the actor fire-target and combat-status
levels cannot be added to `actors.h` in this lane because the active
`actor_perception.c` already defines the combat-status enumerators locally (a
redefinition would break it). They follow canonical's existing TU-local practice
(copies in actors.c, actor_combat.c, actor_perception.c, ai_script.c). The unit
animation impulse enum is TU-local because units.obj is outside this lane
(units.c declares only the count locally).
- **Adversarial review** accepted `_action_obey_command_perform` (1,408),
`_action_obey_individual_perform` (288), `_action_obey_perform` (192) and
`_vehicle_possibility_qsort` (64) as strict exact, and
`_action_obey_describe_command` (1824/147 vs 1936/153) as fuzzy. It rejected
the first `_action_obey_command_begin` spelling for byte-inert house-rule
defects. The lead removed the duplicated `look_duration` store, collapsed the
extra look_player braces, re-indented the command block (whitespace only) and
rewrote the enum provenance comments accurately. The re-gate is unchanged:
24 exact / 3 residual / 0 unwritten, fake scan 0 leads.
- **Disclosed owner census**: header-inline SELECT_ANY COMDAT copies
`_distance_squared3d`, `_dot_product3d`, `_real_random_range`,
`_vector_from_points3d` (January references them as undefined externals) and
an unreferenced `_dot_product2d` copy. This is the same systemic class as the
base object's pre-existing magnitude/normalize/scale/square_root copies, and
no new non-inline code owner was added.
- `_action_obey_command_begin` (4336/167 vs 4320/167) and
`_action_obey_describe_command` land as zero-credit fuzzy genuine callers.
`_action_obey_control` is unchanged.
@@ -1,191 +0,0 @@
# ActionObey partial callback graph reconciliation
Date: 2026-09-05. Parent: `c32247e7651d680dcd133d72b2fbf4e8009cc124`.
## Accepted result
The verified partial graph advances ActionObey from **10/27 to20/27 strict
exact target functions**, **485 to2,436 meaningful code bytes**, and
**528 to2,560 padded bytes**. New credit is **1,951 meaningful /2,032 padded
bytes and10 functions**. Seven target functions remain unwritten; this is not
a completed object.
Canonical totals after the independently checked accounting repair are
**912,298 /2,198,102 meaningful exact code bytes**, **6,197 /11,060 credited
functions**, **2,061,020 /4,176,062 matched data bytes**, and **391/833 Matching
objects**. The separate stable owner sweep advances6,233 to**6,243 /8,245**,
with zero inherited exact losses. All298 tests plus26 subtests pass.
There are284 valid fuzzy parks, zero stale/invalid entries, and no revoked
object admissions.
| New target function | Meaningful bytes | Padded bytes |
| --- | ---: | ---: |
| action_obey_command_end |549|560|
| action_obey_individual_setup |110|112|
| action_obey_individual_flush_command_indices |49|64|
| action_obey_individual_begin |71|80|
| action_obey_individual_advance |20|32|
| action_obey_individual_end |72|80|
| action_obey_individuals_iterate |290|304|
| action_obey_command_list_setup |383|384|
| action_obey_directmovement_update_facing |336|336|
| action_obey_individual_update |71|80|
## Genuine source and private caller closure
Only `source/ai/action_obey.c` changes implementation. No shared header
from the failed command-owner trial is retained. Public prototypes remain
in the existing ACTION_OBEY owner section of `actions.h`; all nine newly
implemented private functions are static with semantic names. The matching
nine `config/symbols.json` records are edited in place, preserving the
23,225-record order and every other record.
The public setup routine and five existing public wrappers retain the real
iterator/callback graph. The end callback retains command_end; the update
callback retains directmovement_update_facing. No dummy caller, emission
anchor, forced inline, private extern ABI, or consumer-local owner declaration
is introduced. The excluded private begin/perform pair and its individual
perform caller are not declared as fake externs.
The source consumes the already accepted full swarm/obey/unit/biped records,
typed datum/object/tag access, project boolean/real types and flag macros,
named command cases, original assertions and PC-only loop diagnostics.
Parameters/prototypes retain the house format and explicit returns.
The structure-cache leaf is unchanged.
The real math owner supplies cross_product3d and negate_vector3d through the
new facing caller. Their two include-time aliases and manual duplicate
bodies are removed atomically. Both existing target bodies remain strict
exact. Their selection metadata changes from manually emitted NODUPLICATES
to natural header SELECT_ANY; that ownership difference is disclosed, not
mistaken for identical object metadata. The other two inherited manual
aliases, random_range and vector_from_points2d, are unchanged pending their
genuine private-command callers. This partial packet does not endorse them
as final source topology.
## Provenance adjudication
Fable's preserved donor is
`C:/halo-worktrees/fable-50k-small-families-20260904`, commit
`f9dd42218b3325e1b3a63d7860e5309f69fd3d8e`; it remains read-only.
These are authenticated reconstructions, **not a claim that all Fable bodies
were first-shot natural matches**. Its `scratch/reports/action_obey.md`
records an earlier early-return setup shape changed to the ordinary single
success-result form, a command_end case ordering correction to January's
physical case-block order, and a signed-short swarm flag correction.
The signed field is now independently PDB-proven. The retained success-result
control flow is meaningful, follows the engine's common single-result
style, and matches January's state-store/dataflow. The command case order
corresponds to the actual January layout without changing behavior or adding
nonsensical control flow. No spelling, lifetime, declaration-order, flags,
assembly, or source-shape search was conducted during this integration.
This adjudication does not reopen exhausted tuning recipes elsewhere.
## Accounting repair, not a relaxed match
Ordinary objdiff reports command_end at85.26786% and the facing helper
at96.77419%, even though complete code and ordered relocation checks pass.
Two narrow semantic ledger entries restore their549+336 meaningful bytes:
- command_end:560 bytes,24 relocations, normalized SHA-256
`f59f0185a672ce6409fe3d3c3a439e517483593f6ffc9bf5eff4b18dd4420a61`;
- facing:336 bytes,17 relocations, normalized SHA-256
`17b28fabb3ad4366074689d2f9be1364ccf07cdc0c7a185dbd4dff25b00e3f97`.
The discrepancy is compiler-local switch-label representation. Every build
must still pass the unchanged strict comparator; no target bytes, branch
destinations, relocation identities, or comparison policy were altered.
Without these ledger entries the report showed911,413 bytes and6,195
functions. The additional885 bytes are part of this same1,951-byte packet,
not another reconstruction gain.
## Whole-object boundary
The candidate naturally emits five math functions absent from this
ActionObey target: square_root16, scale_vector3d48, magnitude_squared3d48,
magnitude3d48 and normalize3d96 padded bytes. All are SELECT_ANY. Only
normalize3d has incoming candidate relocations, from the facing function at
167 and217; the other four are header emission. The full572-base/833-target
owner census finds byte/relocation-identical available implementations for
each helper. **None receives ActionObey target credit.**
Code equality is not link compatibility. Ordinary frozen actual base/base
links with ActionCharge fail in both input orders with LNK2005 for
square_root, scale_vector3d, magnitude3d and normalize3d: ActionCharge owns
NODUPLICATES copies. Both links also retain37 unrelated unresolved externals
and produce no final binary. No FORCE, IGNORE, stub, object patch, selection
patch, or synthetic discard pass was used. Full Ninja success means
compilation/reporting, not successful final-image linkage.
An explicit ActionObey whole-object rejection records this ownership boundary
and the remaining inherited alias topology. Target-function progress remains
valid separately. Reopening requires authentic source/owner evidence and
ordinary compatible canonical linkage, not manual expansion or forced
inlining to hide helper emission.
## Verification evidence
All paths below are relative to canonical:
`C:/Users/isabe/Documents/Codex/2026-07-13/i-w/work/rasterizer-model-wrapper-alias-20260829`.
- Frozen `scratch/action-obey-partial-graph-before-20260905.json` and
`*-after-20260905.json` include source, target/base objects, symbol manifests,
and hashes of all572 base objects. Only ActionObey changes; all571 other
complete base-object hashes are identical.
- Whole frozen base hashes: before
`5335c76a7e9833303b69e3a2d3a8e71fb73ae34e2b83aac4d22642a11c6659d6`;
after `f339f7776b58f5d044015dc363a709ac649878c918a61591a6e21a71ebedb4f9`.
Renamed January target:
`13a4e47de2c79a3122033539284d148cae052f5f008f084a3d15862dda844368`.
- `scratch/action-obey-partial-graph-stable-after-20260905.json`:
ten gains/2,032 padded bytes, zero losses.
- Gate:20 exact, zero residual, seven unwritten; no emitted
`_point_from_line3d`. No candidate code/data/BSS/COMMON owner is silently
excluded from the complete-object inventory.
- Independent object audit root replay:
`scratch/action-obey-partial-graph-independent-object-root-20260905.json`,
SHA-256 `321305aa1268865dfc91d598ab827ab24de37a3e9e1bfcd99880a7e3eb71c104`.
It verifies exact positional symbol edits, all sections and all new owners.
- Math-owner census root replay:
`scratch/action-obey-partial-graph-math-comdat-root-20260905.json`,
SHA-256 `6f236108159648eccef28a68c9c07e221abeedf76c268a30c093c971aaf586b6`.
- Ordinary links:
`scratch/action-obey-partial-graph-ordinary-links-20260905.json`,
SHA-256 `8e0a4d74b64caae7d1867b47a1d5e50cfdf1993f260746760190eaf7eefd0ce5`.
- Public verification tool:
`tools/audit/action_obey_callbacks_runtime_differential.py`,
SHA-256 `1caf0250cc996639dd792145894cfbd381fc61546c380f4d625e7b6c18833697`.
Its target-first cases cover setup9, flush44, advance256, in target,
actual and isolated objects. Real six-argument callback ABI, complete
modeled0x24/0x58 state, API arguments/order, caller stack frame, ESP,
nonvolatile registers and DF all pass. Setup's authentic null-reference
assertion/noreturn exit is covered. Tag indices/handles stay within the
documented valid model; this is not a whole-game execution claim.
- `tools/test_action_obey_callbacks_runtime_differential.py` adds ten
domain/negative-control tests. Full `tools` suite:298+26 pass.
- Final accounted build:
`scratch/action-obey-partial-graph-accounted-build-20260905.log`.
Parks/admission reports have no stale park or revocation.
- Units remains189/189 exact. Both protected bitmap source hashes are
unchanged.
Reproduction of the bounded public oracle:
```text
python -B tools/audit/action_obey_callbacks_runtime_differential.py --actual build/base/source/ai/action_obey.obj
```
The tool requires its documented frozen target and retained HCEA donor
evidence (or explicit paths), and refuses changed section/hash evidence.
## Remaining reconciliation
The coherent larger command-owner trial was rejected after a Units
regression and fully restored; see
`action_obey_obj_command_owner_trial_rejected_20260905.md`.
Private begin/perform source proposals and corrected PDB prerequisite evidence
remain scratch-only, uncompiled, and uncredited. Seven target functions,
remaining data, helper ownership, and two inherited aliases still prevent
ActionObey completion. Fable Bitmaps and other lane packets remain separate
pending work; historical lane totals are not added to canonical.
@@ -1,142 +0,0 @@
# Fable `action_obey` private-closure intake (2026-09-05)
Read-only/source-plan audit. No production, donor, build, configuration, or generated-object file was changed or compiled. This note does not change the earlier intake accounting: the initial Fable report started at 9 exact / 0 residual / 18 unwritten, and the reported donor `+11` remains donor evidence rather than admitted campaign progress. This audit admits zero functions.
## Conclusion
The two missing routines can be restored as a coherent, genuine same-translation-unit closure:
```c
static boolean action_obey_command_begin(
long actor_index,
long unit_index,
short command_list_index,
struct obey_individual_simple_control *simple_control,
struct obey_individual_complex_control *complex_control);
static boolean action_obey_command_perform(
long actor_index,
long unit_index,
short command_list_index,
struct obey_individual_simple_control *simple_control,
struct obey_individual_complex_control *complex_control);
```
January proves that these are private register-ABI functions, not public ABI. `_code_000060c0` receives the complex-control pointer in EAX and unit index in ECX; actor index, command-list index, and simple-control pointer remain stack arguments. `_code_000071a0` receives unit index in EAX and its other four arguments on the stack. Those allocations are consistent with VC7 optimizing the ordinary source signature above after it sees the same-TU callers. The audit aliases should name the private sections, but production source must not declare either body non-static or manufacture an external relocation.
Once both bodies exist, the retained `action_obey_individual_perform` has a real private callee closure and can continue to support public `action_obey_perform`. There is no missing third private body behind either routine. The remaining preconditions are authentic shared schema and owner prototypes, not an ABI invention.
## Primary evidence
- January PC object: `build/split/source/ai/action_obey.obj`, SHA-256 `0e4f889a35b97c44f6ae91e4906b547bd64f2ff12b9f399e02b46f02b69633fb`.
- `_code_000060c0`: 4,320 padded bytes, 167 relocations, normalized SHA-256 `5b56fdd8b88a4456f893740a2f972a21d114b032c636f6b90c9d86e0114fd640`.
- `_code_000071a0`: 1,408 padded bytes, 51 relocations, normalized SHA-256 `afc109dc8ddf7e3f4ee6ed60f606b1e597702f063a81488fb2f8fd24f3713b07`.
- Semantic donor, read in full:
- `research-cache/halocea-full-blobs-20260830/src/blam/ai/action_obey_command_begin.c`, SHA-256 `4373d536adedb3dbdadfeaf62cfd1369ba2d1c53e92acae8477b6837a0a9461f`.
- `research-cache/halocea-full-blobs-20260830/src/blam/ai/action_obey_command_perform.c`, SHA-256 `bd22ee99d261de2f8b1db20ca548bb2158031a7e0d907bdfd78a9813842bd531`.
- `research-cache/halocea-full-blobs-20260830/src/blam/ai/action_obey_individual_perform.c`, SHA-256 `05d10cbc643b6b1b3eee5fe725bb257188a9521ea09df5deeb1370775532dade`.
- `research-cache/halocea-full-blobs-20260830/src/blam/ai/action_obey_directmovement_update_facing.c`, SHA-256 `3295935a5a1602fe4cdbfba17bf2762734699ef1e1bc981aae5c346a6bdcb042`.
- Retained PC source is not a full donor: Stian's `src/halo/ai/action_obey.c` is zero bytes; Punpckhdq retains the January symbol/literal manifest only; Pastudan history commit `ba72a7d7` contains only a raw-float `cross_product3d` reconstruction. It independently corroborates that math operation, but supplies neither private body.
- Fable source reviewed: `C:/halo-worktrees/fable-50k-small-families-20260904/source/ai/action_obey.c`, SHA-256 `067b4900d5494c5341ca609337f0d220dedd4f95778a935d601427de96575a59`.
HCEA is an Xbox/PPC semantic donor. January PC disassembly, relocation destinations, literals, assertion strings, record strides, and register/stack use take precedence wherever the two diverge.
## Natural `command_begin` source plan
Start by resolving the typed actor, command list, current command, point block, actor definition, and actor variant. Return false if `current_command_index` is outside the command count. Dispatch on the 28 `ai_atom_type` values and use the existing simple/complex control fields:
1. `pause`: convert seconds to ticks (`parameter1 * 30`).
2. `go_to` / `go_to_and_face`: validate point indices; set destination point/surface/keep-moving state; call `actor_move_to_point`; optionally call `actor_move_keep_moving_past_destination`; for the facing form, set the typed facing point.
3. `move_direction`: sample actor body position or `object_get_origin`; for a point command call `vector_from_points3d` then `normalize3d`, otherwise use `vector3d_from_angle`; set the facing mode, halt the actor when it owns the unit, and set direct-movement flags.
4. `look`, `look_random`, `look_player`, `look_object`: resolve a command point, active prop, nearest player, or named object; use `random_range(low, high + 1)` and `real_random_range` for the random look; construct a complete local `direction_specification` directly and call `actor_look_secondary`; store the duration in ticks. Do **not** import HCEA's one-argument/variadic `direction_get_empty` declaration: January has no such relocation and writes the local type/object/point fields directly.
5. `animation_mode`, `crouch`, `shoot`, `grenade`: validate domains and populate complex control. Grenade setup takes the grenade type from the actor variant, calls `unit_add_grenade_type_to_inventory`, and initializes trajectory/target/timer state.
6. `vehicle`: iterate `_object_mask_vehicle`, keep at most 16 typed `vehicle_possibility` records, compute distance, `qsort` with the already genuine same-TU `vehicle_possibility_qsort`, and try candidates through `actor_action_try_to_enter_vehicle` using the ordinary empty string literal. The record is TU-private; no public header is owed.
7. `running_jump`, `targeted_jump`: reject the actor's mounted unit case; set jump bits/timers/velocity. January uses `dot_product3d` for the non-actor unit's facing/velocity test.
8. `script`, `animate`, `recording`: call `hs_wake_by_name`; resolve the animation reference/default object animation graph and call `unit_start_user_animation`; update typed biped runtime flags; or resolve/play a recorded animation.
9. `action`, `vocalize`: map modifiers to animation impulses/communication types, or fill a real `unit_speech_item`, initialize its `ai_information_packet`, and call `unit_speak`.
10. `targeting`, `initiative`, `wait`, `loop`, `die`, `move_immediate`, `set_radius`, `teleport`: update the named flag/state fields. Teleport filters with `_object_mask_biped`, reads `biped_definition.biped.flags`, uses full 3D facing for flying/climb-anything bipeds and a typed 2D facing otherwise, then calls `object_set_position`, `object_reset`, `object_compute_node_matrices_recursive`, `actor_input_sample_position`, and `actor_move_halt`.
The PC-only debug epilogue must also be retained. When `ai_debug` is enabled, January formats `<no encounter>` or `encounter/squad`, calls `action_obey_describe_command(..., temporary, 256)`, and reports `"%s: %s #%d%s: %s"` with either `""` or `" FAILED"`. HCEA returns immediately after the switch and does not contain this epilogue.
PC-shape corrections relative to HCEA include ordinary `random_range` / `real_random_range`, `vector_from_points3d`, `distance_squared3d`, and `dot_product3d` calls; direct construction of the look-direction record; the PC debug epilogue; the January empty-string literal; and typed biped/object/tag access. HCEA's seed calls, manually expanded vector/distance products, `nullptr`, raw object header casts, and its `direction_get_empty` facade are semantic hints only.
## Natural `command_perform` source plan
Resolve the typed command and actor, default `done` to true, and use the January switch grouping:
- `pause` and all look atoms, plus `move_immediate`: finish when `pause_timer == 0`.
- `go_to` / `go_to_and_face`: for the actor's own unit with complex control, test `actor_path_at_destination`; apply keep-moving tolerance/velocity logic; wait ten ticks while the actor is still moving; when destination facing is active, assert at source line 983 that the current atom is `_ai_atom_go_to_and_face`, normalize a 3D or 2D vector to the facing point, compare against `0.98400003f`, then halt on completion.
- `move_direction`: obtain actor body position or object origin, form a vector from the stored start position, and compare its dot product with the commanded travel distance.
- Immediate atoms (`animation_mode`, `crouch`, `vehicle`, `script`, `targeting`, `initiative`, `loop`, `die`, `set_radius`, `teleport`) return true.
- `shoot`: require the actor unit and complex control; compare the manual fire target with the ordered point (`distance_squared3d < 0.25f`); otherwise set pause from `actor_variant_definition.ranged_combat.first_burst_delay_upper_bound * 30`, floored at 60.
- `grenade`: inspect `unit.grenade_throw_state` after start, or call `actor_combat_plan_grenade_trajectory` and depress the trigger when the unit is not busy.
- Jump atoms: require the jump flag, use actor `input.in_midair` or `unit_flying_through_air`, and clear the timer after the jumped bit and airborne state coincide.
- `animate`, `recording`, `action`, `vocalize`: inspect `unit.animation.state`, `recorded_animation_controlling_unit`, `complex_control->play_action`, or `unit.speech.current.priority` respectively.
- `wait`: preserve the January three-way combat-status/told-to-advance ladder.
Use `vector_from_points3d`, `distance_squared3d`, `magnitude_squared3d`, `dot_product3d`, `normalize3d`, and `normalize2d` as the typed math vocabulary instead of HCEA's manually expanded products. January's relocations authenticate every non-inlined call.
## Inline-math ownership and true callers
The four currently exact out-of-line bodies are authenticated `source/math/real_math.h` inline operations, but the present macro-renaming plus hand-appended definitions are a temporary facade. Preserve their current exact credit until the real callers are present, then remove the four include-time renames and four manual definitions atomically and let ordinary header ownership materialize them. A normal compile must prove all four sections remain exact before that cleanup is retained.
| Target section | January | True source caller | PC evidence |
| --- | --- | --- | --- |
| `_random_range` | 32 bytes, 2 relocs | `action_obey_command_begin`, `look_random` | call relocation at private-body `+0x25f`; body calls `get_global_random_seed_address` and `seed_random_range` |
| `_vector_from_points2d` | 32 bytes, 0 relocs | `action_obey_command_perform`, non-flying destination-facing branch | the helper is inlined into `+0x211..+0x22e`: two point-coordinate subtractions followed by `normalize2d`; emitted wrapper plus inline sequence identifies the reference |
| `_cross_product3d` | 64 bytes, 0 relocs | `action_obey_directmovement_update_facing` | two direct call relocations at `+0x9e` and `+0xd0` |
| `_negate_vector3d` | 48 bytes, 0 relocs | `action_obey_directmovement_update_facing` | backwards/right cases are inlined at `+0x72..+0x88` and `+0x123..+0x138` |
For the 2D facing call, use a genuinely typed `real_point2d`/`real_vector2d` projection or authentic shared view. Do not recover it with a pointer cast between unrelated point/vector types, an inactive-union read, or a prefix facade merely to provoke the wrapper.
## Owner headers and API closure
Most dependencies already have the correct owner declarations and should be consumed by includes, not redeclared in `action_obey.c`:
| Owner | Required declarations/types |
| --- | --- |
| `source/ai/actions.h` | shared obey simple/complex/state types; `actor_action_try_to_enter_vehicle`; public ActionObey declarations |
| `source/ai/actors.h` | `actor_datum`, `direction_specification`, movement/look/path/grenade APIs, `actor_input_sample_position` |
| `source/ai/actor_definitions.h` | actor/variant tags, idle-look range, ranged first-burst delay, grenade properties |
| `source/ai/ai_scenario_definitions.h` | command-list/command/point records and all atom/command-list/modifier domains |
| `source/ai/props.h` | `prop_iterator`, `prop_iterator_new/next`, `prop_get_active_by_unit_index` |
| `source/ai/ai_communication.h` and `source/ai/ai.h` | communication enums and complete `ai_information_packet` |
| `source/game/players.h`, `source/memory/data.h` | `player_data`, `player_datum`, typed `data_iterator_new/next` |
| `source/objects/objects.h`, `source/objects/object_definitions.h` | object iterators/accessors/origin/name/position/reset/node matrices, masks, damage flags, animation graph tag |
| `source/units/units.h`, `source/units/bipeds.h`, `source/units/biped_definitions.h` | unit speech/animation/grenade/facing APIs and complete unit/biped runtime/tag fields |
| `source/scenario/scenario.h`, `source/scenario/scenario_definitions.h` | `global_scenario_get`, command/reference blocks, encounter/squad debug names |
| `source/cutscene/recorded_animation_definitions.h`, `source/cutscene/recorded_animations.h` | scenario animation lookup and recorded-animation control/playback |
| `source/hs/hs.h` | `hs_wake_by_name` |
| `source/math/real_math.h` | all point/vector/random helpers and constants |
| `<stdlib.h>` plus project cseries/error headers | `qsort`, memset/string/format/error/debug support |
Two genuine owner-prototype gaps were found:
1. `actor_move_keep_moving_past_destination(long)` is defined by `source/ai/actor_moving.c` but is absent from `source/ai/actors.h`; `source/ai/action_charge.c` currently carries a foreign C-local declaration. Add the owner prototype to `actors.h` and remove consumer-local copies.
2. `recorded_animation_play(long, short)` is defined by `source/cutscene/recorded_animations.c` but is absent from `source/cutscene/recorded_animations.h`; `source/hs/hs.c` currently declares it locally (with a `word` second parameter). Add the definition-compatible owner prototype to `recorded_animations.h` and remove/repair foreign copies.
No `direction_get_empty` declaration should be added: it is not a January dependency. `vehicle_possibility_qsort`, `action_obey_directmovement_update_facing`, `action_obey_command_begin`, and `action_obey_command_perform` remain genuine statics in this TU. `qsort` comes from the standard header, not a handwritten extern.
## Shared schema/enum prerequisites
Before integration, the owning headers must supply one authentic definition for:
- `obey_individual_simple_control` (0x24), `obey_individual_complex_control` (0x58), and `obey_state_data` (0x84), including direct-movement/jump union arms and every destination/action/shoot/grenade field;
- the complete shared actor/swarm layouts already being audited separately; no obey-only prefix, opaque tail, or parallel incompatible record is acceptable;
- `direction_specification`, `unit_speech_item`, `ai_information_packet`, `vehicle_possibility` (TU-private), and the command/reference records with January strides: command list 0x60, command 0x20, point 0x14;
- the command-list flags, simple-control bits, atom modifier domains (look, move facing, grenade, animate, action, loop), unit animation impulses, biped runtime flags, and existing biped-definition/communication/speech/object domains in their natural owners.
Current Fable-local enums explicitly labeled “belongs in ...” should migrate to those owners with the full packet; they are not a reason to create another private copy. The exact signedness of the shared swarm component flags remains part of the separate schema closure and must not be inferred from this pair of functions.
## Admissible packet boundary
A coherent implementation packet is:
1. land the authentic shared schema and the two owner-prototype fixes;
2. include the actual owners;
3. add both private static bodies together, including the PC-only assertion/debug behavior and ordinary typed accesses;
4. keep `action_obey_individual_perform` calling those statics and remove the two non-static placeholder declarations;
5. reconcile the four math wrappers only after their true calls are present, in the same verified packet;
6. add audit-only aliases from the two authentic names to `_code_000060c0` / `_code_000071a0` for measurement.
This packet is source/API/schema feasible and does not require an external facade. `command_perform` is the smaller, higher-confidence body; `command_begin` is large but semantically closed. Neither should be admitted alone if the goal is to retain public perform without unresolved private ABI.
@@ -1,455 +0,0 @@
# ActionObey begin/perform owner prerequisites (read-only audit, 2026-09-05)
## Scope and disposition
This is the complete owner-prerequisite inventory for the frozen private
`action_obey_command_begin` and `action_obey_command_perform` source
proposals. The published baseline inspected was
`c32247e7651d680dcd133d72b2fbf4e8009cc124`; concurrent root work after that
checkpoint is outside this audit. No production source, header, build
configuration, target metadata, gate, or object was changed here.
The two source proposals remain research, not admitted code:
- `scratch/action_obey_command_begin_proposal_20260905.c`, SHA-256
`630ee045a17f3a095929516cd524c1ee40a385eb08d75a2b133ed99ebcca5ead`;
- `scratch/action_obey_command_perform_proposal_20260905.c`, whose final hash
is recorded at the end of this ledger. The earlier performer SHA-256
`23cb0320a7b64abb4111cb3269983f50b5ff3ca7287edb455349d04a9b737ce3`
is retained as history: it used three unauthenticated `until_...` wait
names and incorrectly assigned an adjacent `"always"` literal to the wait
domain. The corrected proposal uses the direct PDB names and preserves
January's out-of-domain wait result of `TRUE`.
Root already exercised one coherent 86-consumer owner-header trial from the
6,233-strict checkpoint. It lost the existing strict
`Units::_unit_preprocess_node_orientations` owner (6,233 -> 6,232) and was
fully restored without a name/order/source-shape retry. The frozen evidence
is:
- `scratch/action-obey-command-owners-before-20260905.json`, SHA-256
`34c077c0b3cbe43476e5e9dadc1a24fc22d29c462db23976b8ae1b8a45d8702d`;
- `scratch/action-obey-command-owners-after-20260905.json`, SHA-256
`090874e7e335ae137a823e4de032de6122ca89bee8860bfd397fec166a08a404`;
- `scratch/action-obey-command-owners-restored-20260905.json`, SHA-256
`23e6ff746d10fa75f5c23449f16903138281cac98ca62b54da20053adcd4523a`;
- stable before/restored reports, both SHA-256
`f714aec1804b522fbe405ea393c82a21c79569f20a19a365a43a47a77b662642`.
Consequently, this ledger is an actionable future boundary, not a request to
repeat that failed trial. The begin source itself remains frozen even where
this audit identifies better owner-constant spellings.
## Completeness verdict
The two fragments have exactly these missing shared prerequisites:
1. two complete actor constant domains in `source/ai/actors.h`;
2. twelve complete command-modifier domains and three complete scenario AI
reference records in `source/ai/ai_scenario_definitions.h`;
3. the complete 0..13 unit-animation-impulse domain in
`source/units/units.h`;
4. removal of the one current scenario-record shadow in
`source/ai/action_alert.c` and the enumerator shadows listed below; and
5. direct inclusion of `units/unit_definitions.h` and
`cutscene/recorded_animations.h` by `action_obey.c` when the begin body is
integrated.
No additional public API is missing. The private begin/perform functions,
`vehicle_possibility_qsort`, and
`action_obey_directmovement_update_facing` remain same-TU `static`
declarations. `vehicle_possibility` remains a genuine private 8-byte record;
it is not a shared-header type. The runtime vehicle-seat-desire enum is not
part of this packet: command validation uses the separate five-value
`ai_atom_vehicle_modifier` domain and forwards its short value through the
already-owned actor API.
## Exact actor-owner declarations
`source/ai/actors.h` is the genuine owner. DIA reports both domains as
`enum <unnamed-tag>`, so use anonymous blocks and leave every datum backing
field as `short`:
```c
enum
{
_actor_fire_target_none = 0,
_actor_fire_target_prop = 1,
_actor_fire_target_manual_point = 2,
number_of_actor_fire_targets = 3,
};
enum
{
_actor_combat_status_none = 0,
_actor_combat_status_wary = 1,
_actor_combat_status_investigate = 2,
_actor_combat_status_definite = 3,
_actor_combat_status_certain = 4,
_actor_combat_status_clear_los = 5,
_actor_combat_status_dangerous = 6,
_actor_combat_status_visible = 7,
NUMBER_OF_ACTOR_COMBAT_STATUS_LEVELS = 8,
};
```
Direct `HCEX_Release.pdb -sym` results authenticate every value enumerator
and number. The lower-case `number_of_actor_fire_targets` spelling is the
database spelling; an upper-case replacement is not authenticated. The
terminal counts are domain/database corroboration rather than separately
emitted PDB constants. `actor_control_data.current_fire_target_type` at
`+0x1A0` and the actor-state combat-status fields remain `short`; declaring a
four-byte C enum field would corrupt the authenticated layout.
Conflicting current declarations to remove in a coherent future packet:
- fire-target `none`/`prop` from the mixed blocks in
`source/ai/actor_combat.c` and `source/ai/actors.c`;
- the full combat-status block in `source/ai/actors.c`;
- the partial combat-status blocks in `source/ai/actor_stimulus.c`,
`source/ai/actor_type_infection.c`, and `source/ai/actor_type_flood.c`;
- only `_actor_combat_status_certain` from the mixed anonymous block in
`source/ai/ai_communication.c` (the following
`_ai_information_none = 0` remains an explicit reset); and
- the differently named sole alias
`_action_charge_combat_status_clear_line_of_sight = 5` in
`source/ai/action_charge.c`, changing its one consumer to
`_actor_combat_status_clear_los`.
The complete conflict/use census and direct offset proof are preserved in
`scratch/action_obey_actor_enum_owner_packet_20260905.md`.
## Exact command-modifier owner declarations
All of these belong in `source/ai/ai_scenario_definitions.h`. Use anonymous
blocks: DIA exposes the source domains as anonymous enums, and command
storage remains `short atom_modifier`.
```c
enum
{
_ai_atom_go_to_modifier_stop_at_point = 0,
_ai_atom_go_to_modifier_keep_moving = 1,
NUMBER_OF_AI_ATOM_GO_TO_MODIFIERS = 2,
};
enum
{
_ai_atom_look_modifier_idle_aim = 0,
_ai_atom_look_modifier_idle_turn_around = 1,
_ai_atom_look_modifier_idle_look = 2,
_ai_atom_look_modifier_force_facing = 3,
_ai_atom_look_modifier_force_aim_weapon = 4,
NUMBER_OF_AI_ATOM_LOOK_MODIFIERS = 5,
};
enum
{
_ai_atom_animation_mode_modifier_noncombat = 0,
_ai_atom_animation_mode_modifier_asleep = 1,
_ai_atom_animation_mode_modifier_combat = 2,
_ai_atom_animation_mode_modifier_panic = 3,
NUMBER_OF_AI_ATOM_ANIMATION_MODE_MODIFIERS = 4,
};
enum
{
_ai_atom_crouch_modifier_disable = 0,
_ai_atom_crouch_modifier_enable = 1,
NUMBER_OF_AI_ATOM_CROUCH_MODIFIERS = 2,
};
enum
{
_actor_atom_grenade_modifier_toss = 0,
_actor_atom_grenade_modifier_lob = 1,
_actor_atom_grenade_modifier_bounce = 2,
NUMBER_OF_AI_ATOM_GRENADE_MODIFIERS = 3,
};
enum
{
_ai_atom_vehicle_modifier_any_non_driver = 0,
_ai_atom_vehicle_modifier_gunner = 1,
_ai_atom_vehicle_modifier_passenger = 2,
_ai_atom_vehicle_modifier_driver = 3,
_ai_atom_vehicle_modifier_any_seat = 4,
NUMBER_OF_AI_ATOM_VEHICLE_MODIFIERS = 5,
};
enum
{
_ai_atom_animate_modifier_relative_movement = 0,
_ai_atom_animate_modifier_absolute_movement = 1,
_ai_atom_animate_modifier_absolute_movement_no_collision = 2,
_ai_atom_animate_modifier_no_interpolation_relative_movement = 3,
_ai_atom_animate_modifier_no_interpolation_absolute_movement = 4,
_ai_atom_animate_modifier_no_interpolation_absolute_movement_no_collision = 5,
NUMBER_OF_AI_ATOM_ANIMATE_MODIFIERS = 6,
};
enum
{
_ai_atom_action_modifier_berserk = 0,
_ai_atom_action_modifier_surprise_front = 1,
_ai_atom_action_modifier_surprise_back = 2,
_ai_atom_action_modifier_evade_left = 3,
_ai_atom_action_modifier_evade_right = 4,
_ai_atom_action_modifier_dive_forward = 5,
_ai_atom_action_modifier_dive_back = 6,
_ai_atom_action_modifier_dive_left = 7,
_ai_atom_action_modifier_dive_right = 8,
_ai_atom_action_modifier_vehicle_woohoo = 9,
_ai_atom_action_modifier_vehicle_scared = 10,
NUMBER_OF_AI_ATOM_ACTION_MODIFIERS = 11,
};
enum
{
_ai_atom_targeting_modifier_enable = 0,
_ai_atom_targeting_modifier_disable = 1,
NUMBER_OF_AI_ATOM_TARGETING_MODIFIERS = 2,
};
enum
{
_ai_atom_initiative_modifier_enable = 0,
_ai_atom_initiative_modifier_disable = 1,
NUMBER_OF_AI_ATOM_INITIATIVE_MODIFIERS = 2,
};
enum
{
_ai_atom_wait_modifier_alerted = 0,
_ai_atom_wait_modifier_visible_enemy = 1,
_ai_atom_wait_modifier_told_to_advance = 2,
NUMBER_OF_AI_ATOM_WAIT_MODIFIERS = 3,
};
enum
{
_ai_atom_die_modifier_normal = 0,
_ai_atom_die_modifier_silent = 1,
NUMBER_OF_AI_ATOM_DIE_MODIFIERS = 2,
};
```
The PDB directly authenticates the go-to values, all three wait values, and
the exact value names used by begin for crouch enable, targeting enable,
initiative enable, and silent death. It also authenticates look values 1..4
and every animate/action value. The retained database headers below
authenticate the complete look domain (including idle-aim 0), animation-mode,
grenade, vehicle, animate, and action domains. Paired opposite values and
terminal counts for the two-valued domains, plus the go-to/wait counts, are
complete-domain corroboration from the retained Demon header; do not describe
those counts as separately emitted PDB symbols.
There is no wait `always` member. The adjacent January `"always"` literal is
`_ai_atom_loop_modifier_always = 0`; the already-owned loop value
`_ai_atom_loop_modifier_until_told_to_advance = 1` remains unchanged. The
already-admitted reconstructed move-facing block also remains unchanged and
is not another prerequisite domain.
The frozen begin proposal still spells five comparisons numerically. A
future semantic application should use the genuine names without treating
that as a source-shape experiment:
- go-to `== 1` -> `_ai_atom_go_to_modifier_keep_moving`;
- crouch `== 1` -> `_ai_atom_crouch_modifier_enable`;
- targeting `== 0` -> `_ai_atom_targeting_modifier_enable`;
- initiative `== 0` -> `_ai_atom_initiative_modifier_enable`;
- die `== 1` -> `_ai_atom_die_modifier_silent`.
No exact-name local shadows of these twelve domains exist in canonical
source. The present `ai_scenario_definitions.h` loop and move-facing blocks
must not be duplicated.
## Exact scenario-reference records
These are complete tag-block element records and belong in
`source/ai/ai_scenario_definitions.h`, adjacent to the other command-list
schema. Direct `HCEX_Release.pdb -type` output authenticates every field
name, field type, offset, and total size; none of the fields below is a
speculative tail name.
```c
struct ai_animation_reference_definition
{
char animation_name[TAG_STRING_LENGTH + 1];
struct tag_reference animation_graph;
unsigned long unused[3];
};
struct ai_script_reference_definition
{
char script_name[TAG_STRING_LENGTH + 1];
unsigned long unused[2];
};
struct ai_recording_reference_definition
{
char recording_name[TAG_STRING_LENGTH + 1];
unsigned long unused[2];
};
typedef char ai_animation_reference_definition_size_assert[
sizeof(struct ai_animation_reference_definition) == 0x3C ? 1 : -1];
typedef char ai_script_reference_definition_size_assert[
sizeof(struct ai_script_reference_definition) == 0x28 ? 1 : -1];
typedef char ai_recording_reference_definition_size_assert[
sizeof(struct ai_recording_reference_definition) == 0x28 ? 1 : -1];
```
The exact offsets are animation name `+0`, tag reference `+0x20`, unused
dwords `+0x30`; script/recording name `+0`, unused dwords `+0x20`.
The only current declaration conflict is the complete local
`struct ai_animation_reference_definition` at
`source/ai/action_alert.c:76`. It must be removed so that the already-present
`#include "ai_scenario_definitions.h"` supplies the owner. Its local
`long unused[3]` has the right width but the PDB owner type is
`unsigned long`. There is no current script- or recording-reference shadow.
The `scenario` structure's corresponding members remain `struct tag_block`;
no scenario prefix or retyping is required.
## Exact unit-animation-impulse owner declaration
`source/units/units.h` is the genuine public constant owner. Again use an
anonymous enum and keep the existing public function parameters as `long`
and the private mapper's parameters as `short`:
```c
enum
{
_unit_animation_impulse_berserk = 0,
_unit_animation_impulse_signal_move = 1,
_unit_animation_impulse_signal_attack = 2,
_unit_animation_impulse_signal_warn = 3,
_unit_animation_impulse_surprise_front = 4,
_unit_animation_impulse_surprise_back = 5,
_unit_animation_impulse_evade_left = 6,
_unit_animation_impulse_evade_right = 7,
_unit_animation_impulse_dive_front = 8,
_unit_animation_impulse_dive_back = 9,
_unit_animation_impulse_dive_left = 10,
_unit_animation_impulse_dive_right = 11,
_unit_animation_impulse_vehicle_celebrate = 12,
_unit_animation_impulse_vehicle_panic = 13,
NUMBER_OF_UNIT_ANIMATION_IMPULSES = 14,
};
```
Direct `HCEX_Release.pdb -sym` queries return every value enumerator with
exactly these names and values. The terminal count 14 is supported by the
complete January range/switch and database domain, not a separately emitted
PDB constant. The immutable HCEA header's older `GUESS` prose predates this
direct PDB check and does not downgrade the now-authenticated value names.
Current canonical has no local value-name shadow. Remove only
`NUMBER_OF_UNIT_ANIMATION_IMPULSES = 14` from the mixed block near
`source/units/units.c:708`; retain `_unit_seat_unknown8_bit = 8`.
For a complete semantic migration, replace the raw 0..13 cases in both
switches of private `code_00198e40` and the raw 12..13 range tests in private
`code_00198fd0` with these owner constants. The PDB authenticates
`code_00198e40` as
`static short unit_animation_impulse_get_index(short, short *)`; that private
symbol rename is useful metadata cleanup but is not an API prerequisite and
must not be put in a header.
## Prototype and include boundary
The corrected performer needs no prototype or type beyond the four owner
domains it names: go-to, wait, fire-target, and combat-status. Its callees
are already declared by their genuine headers.
The begin proposal needs no additional shared type/prototype beyond the
modifier domains, unit impulse, and three reference records above. Two
direct-owner includes are required when it is placed into the real TU:
- `#include "units/unit_definitions.h"` for the
`unit_definition_get(index)` owner macro and complete
`struct unit_definition` dereference;
- `#include "cutscene/recorded_animations.h"` for
`boolean recorded_animation_play(long unit_index, short animation_index)`.
Preserve the existing direct `#include "math/real_math.h"`; it owns the
normalization/vector APIs, including the accepted 3D-prefix-as-2D convention.
`actor_move_keep_moving_past_destination(long)` is already correctly owned by
`actors.h` and matches the definition in `actor_moving.c`. Do not add any
caller-local extern declarations.
All other required owners are already present: `actions.h`, `actors.h`,
`actor_definitions.h`, `ai_communication.h`, `props.h`, `objects.h`,
`scenario.h`, `units.h`, `bipeds.h`, `hs.h`, and the recorded-animation,
object-definition, player, data, and C runtime owners selected in the natural
candidate skeleton.
## Evidence and provenance
Authoritative local artifacts:
- PDB:
`../../research/hcea_jun2011_prototype/payload/Halo CE Anniversary (Jun 24 2011)/HCEX_Release.pdb`,
SHA-256
`0fd11b084fd2169cafe071fd8c3e4083d6e9564f01e7964f6c26e5a03eee4dff`;
- secondary PDB:
`../../research/hcea_jun2011_prototype/payload/Halo CE Anniversary (Jun 24 2011)/HCEX.pdb`,
SHA-256
`f55cfe957da8079a62a26a6753f1ddbb700b067a66de0bd5db5aac182d985ff1`;
- DIA reader: `../../research/tools/DIA2Dump/x64/Release/DIA2Dump.exe`,
SHA-256
`5be3b890fb5c3b7c74b7c3ce7c275a70f48813c388d3611c41e69c0ac0626243`;
- January `action_obey.obj`: current split artifact SHA-256
`13a4e47de2c79a3122033539284d148cae052f5f008f084a3d15862dda844368`;
the earlier frozen source-body audit recorded SHA-256
`0e4f889a35b97c44f6ae91e4906b547bd64f2ff12b9f399e02b46f02b69633fb`
before later semantic target-metadata regeneration;
- retained full-domain header:
`../../research/players_update_cluster_research/upstream/demon/source/ai/ai_scenario_definitions.h`,
SHA-256
`d650fce71f3f8f1cc9e1c5e02b350bb56b65b8de385ef1a7d93257410de2106c`.
Immutable HCEA database extracts, used as corroborative reconstructions rather
than original source:
```text
33fb4dd6bdc543a755d068186d18f472e679bcfd12835ffa020866dbef33db27 ai_atom_animation_mode_modifier.h
b9cf105849bd637b2c1b7011578ddb204f67e35ac5dce696eb6217a12323e412 actor_atom_grenade_modifier.h
586f4f8473f15d34370598a74ae3637a4d377ca37b938398de892ca0633deed8 ai_atom_vehicle_modifier.h
66b289cef8e9af8e497f552bdbf2a149b0396d6814800f0db1d7d82f73aa0570 ai_atom_look_modifier.h
df5ed09edc01002f3568b353cdf5ae24784a294b8ddcc92d9b6ccf3c9171851c ai_atom_animate_modifier.h
1919399bdc4cf367a73e1e71d7c17abdf5a2c5b481b40b97ea6e020362c832fc ai_atom_action_modifier.h
6d367a2adacf78a8c89b7e8a2cc566572e5fd0419fe92fd979affcd5abe74020 unit_animation_impulse.h
c34d83224cf95b071a9de847bdd1a98e63bde836ae6d622beb3351a990b2da72 ai_animation_reference_definition.h
16f251ee304111c333f39f2a495da1b65dcd91d154a151592a8f2f81ac63330f ai_script_reference_definition.h
b96e456964ea532b65afac96fbf4360f8cee954d51beb0ab374127a0ced4f68d ai_recording_reference_definition.h
```
All ten files are under
`../../research-cache/halocea-full-blobs-20260830/src/headers/`.
Final supporting scratch revisions:
```text
630ee045a17f3a095929516cd524c1ee40a385eb08d75a2b133ed99ebcca5ead action_obey_command_begin_proposal_20260905.c
1a9392214a2ca5be3eb0f214d38cc0c67f3d18ddabaa339df27165c7726c774b action_obey_command_perform_proposal_20260905.c
46e78d3c070638fab00d3f0c5334088b91146f92224330a5964ee4b358d8cd57 action_obey_actor_enum_owner_packet_20260905.md
1e554e3d6f43775d645302ffb4d0eb297e7e65d51983a938b006282f836a23af action_obey_command_begin_proposal_issues_20260905.md
```
## Provenance limits and final boundary
- Enum tag names are not PDB-authenticated; use anonymous enums.
- PDB proves the value enumerators described above. Terminal counts and the
non-emitted counterparts in two-valued domains are domain-completion
evidence from the retained source/database and January range behavior.
- The three scenario-reference records are stronger: every field name/type,
offset, and size is direct PDB evidence. No field is a speculative facade.
- HCEA `src/blam` bodies and extracted headers are binary reconstructions, not
recovered first-party source.
- The frozen begin's numeric boolean comparisons are known owner-name debt,
not an invitation to edit or compile another variant now.
- The failed 86-consumer owner trial is the current integration blocker.
Reopen only with a genuinely changed shared-header/consumer context, not
spelling, enum-tag, declaration-order, include-order, or compiler-steering
experiments.
@@ -1,163 +0,0 @@
# ActionObey: shared owners and structure-cache flush reconciliation
Date: 2026-09-05. Parent publication: `31a1927c951479106d0475ee99e1fb236a15e89d`.
## Accepted checkpoint and accounting
This checkpoint adds the genuine `action_obey_flush_structure_indices` leaf
and repairs the shared types, enum owners, and public API declarations needed
by the larger Fable ActionObey packet. It does not import that packet wholesale.
- ActionObey advances from 9/27 to **10/27 exact functions**, 439 to **485
meaningful code bytes**, and 480 to 528 padded bytes.
- Incremental credit is **46 meaningful / 48 padded bytes**, one function,
and no data. The complete-object count does not change.
- The final canonical build reports **910,347 / 2,198,102 meaningful exact
code bytes**, **6,187 / 11,060 credited functions**, **2,061,020 / 4,176,062
matched data bytes**, and **391/833 Matching objects**.
- The complete stable sweep advances 6,232 to **6,233** exact owners out of
8,245 measured owners, with zero inherited exact losses. This owner census
is not the same denominator or admission policy as the credited-function total.
Canonical root:
`C:/Users/isabe/Documents/Codex/2026-07-13/i-w/work/rasterizer-model-wrapper-alias-20260829`.
Donor root, read-only throughout:
`C:/halo-worktrees/fable-50k-small-families-20260904`, tip
`f9dd42218b3325e1b3a63d7860e5309f69fd3d8e`.
## Source ownership and credibility
`actions.h` owns the public ActionObey declarations. The packet adds the nine
genuine target-public declarations in its existing ACTION_OBEY section and
removes their six consumer-local copies from `actions.c`. The redundant
`actor_action_change` declaration in `action_obey.c` is removed; its existing
owner declaration is unchanged.
The leaf uses `actor_get`, the complete `obey_state_data`, the named
`destination_valid` and `destination_surface_index` fields, `NONE`, and an
explicit return. January independently materializes the obey state at actor
offset 0x9C, tests the byte at 0xCC, and conditionally stores -1 at 0xDC.
Provenance adjudication: this is an authenticated exact reconstruction, **not
a natural first-shot result**. Fable's report says the local `state_data`
pointer corrected an earlier missed candidate. That history remains visible
in the independent review. The accepted pointer is ordinary meaningful source,
matches adjacent action-family source organization, and is independently
supported by the target state-base computation. It is not an inert lifetime
anchor, aliasing trick, or invented control flow. This ruling does not authorize
replaying exhausted spelling/order searches on other functions.
The shared owner correction is one coherent packet:
- `actors.h` owns the full 0x40 `swarm_component_datum`, the 0x14
`swarm_wander_control`, the five component flags, and `swarm_component_get`.
Direct HCEX and HCEX_Release PDB records prove signed `short flags`, the
explicit byte `pad` at 0x1B, and the complete anonymous wander/obey union at
0x1C. The wander record has ordinary implicit alignment, not an invented
named padding field. Local partial/duplicate views are removed from
`actors.c`, `actor_type_infection.c`, and `actor_perception.c`.
- `actions.h` owns the five simple-control bits. Existing 0x24/0x58/0x84
obey record shapes are unchanged. Infection's local copies are removed.
- `bipeds.h` owns the six directly authenticated runtime bits. Bit zero is
airborne, not limping. The five misleading uses in `units.c` and one in
`players.c` are renamed; duplicate local enums in Infection, Bipeds, and
PlayerControl are removed. All values and underlying datum storage remain
unchanged. Units remains **189/189 exact**.
- `ai_scenario_definitions.h` owns the five authenticated command-list bits,
loop-until modifier, and four movement-facing values. The latter names are
explicitly descriptive reconstructions from January behavior, not claimed
recovered spellings. Unneeded later manual-BSP and count names are not imported.
- `actors.h` owns `actor_move_keep_moving_past_destination(long)` and
`action_charge.c` loses its foreign local declaration.
- `recorded_animations.h` owns the genuine play/play-and-delete/play-and-hover
signatures `(long, short)`. `hs.c` includes that owner and loses the three
mismatched local `(long, word)` declarations. Definition and caller code
are unchanged.
No private begin/perform facade, flat obey-only swarm view, new raw offset,
new inline directive, or new math wrapper is introduced by this checkpoint.
It does not bless the inherited ActionObey math-wrapper/callback debt: that
requires the real private caller graph and remains the next reconciliation step.
## Verification and the two residual changes
The public-header phase freezes all 47 actual Ninja consumers: 3,079 runtime
sections unchanged, 54 compiler-local-label spelling changes, and no runtime,
owner, COMMON, or forbidden point-helper addition. All 6,232 exact owners survive.
The subsequent shared-schema phase freezes all **58 actual consumers**, not
merely lexical include matches. Its full runtime comparison has 4,544 unchanged
sections, 62 compiler-local-label-only changes, and exactly two residual code
changes. Neither was accepted from strict-function counts alone:
1. `infection_swarm_control` remains 3,360 padded bytes with 103 relocations.
Three clear-bit masks use shorter signed-immediate encodings after the
authenticated signed-word field correction. Exhaustive evaluation covers
every 16-bit flag value at all three sites: **196,608 comparisons pass**.
The first two sites explicitly zero upper EAX. The third does not require
that assumption: only AL is tested and AX stored; TEST replaces the flags,
and both successor paths fully define EAX before reading it full-width.
All 130 direct branches, both table jumps, all 103 relocation records,
shifted local labels, exact allowed alignment bytes, and the masked
80-byte table/tail are checked. No owner, data, COMMON, or definition changes.
2. `ai_communication_update_speech_timers` remains 672 bytes with the same 43
ordered relocations. Its only change swaps the independent loads of DI
from `[EBP+0x10]` and EBX from `[EBP-8]` in 0x167..0x16E. All 27 branches
are unchanged; none enters the block interior; there is no indirect jump
or relocated operand in the block. The rest of the bytes are identical.
Its honest fuzzy score returns from 97.76382% to the previously observed
**97.67839%**. This is explicitly disclosed, not hidden as an exact gain.
No speech-timer source or scheduling hint was changed.
The first Ninja invocation compiled the consumers but correctly stopped its
progress step on the now-stale speech-timer evidence. Only after full semantic
review and independent proof was that park remeasured. Final Ninja succeeds.
Root independently replays the hardened residual proof with the same JSON hash.
The final 58-object freeze matches the reviewed after-freeze across all **4,608
runtime sections** with no changes at all.
Final checks:
- Complete stable sweep: +1 exact function / 48 padded bytes, zero losses.
- Leaf: **1,024 cases each** against the January target, final actual object,
and isolated build. All 256 flag-byte representations and four surface
values; full modeled actor-state writes, call arguments, stack, preserved
registers, and direction flag checked. Limit: valid modeled actor handle
and `datum_get` contract, not a whole-game execution test.
- Separate VC7 scratch TU: **24 compile-time layout/value checks** pass;
it emits no code and earns no progress credit.
- **288 tests plus 26 subtests pass** after the final park update.
- Parks: **284 active, zero stale, zero invalid**.
- Admission: zero candidates, zero contradictions, five existing displayed
rejections, zero revocations. No new `_point_from_line3d` emission/reference.
- Protected bitmap source/header hashes are unchanged. No source implementation
in the new Opus rasterizer/device/widget reservation is modified.
## Frozen evidence and continuation
Key manifests (all under canonical `scratch/`):
- `action-obey-public-api-before-20260905.json` / `...after...`:
`22082d37d81366a681520949d9502107f7ea43b8e185f29936643cd07c89d0d5`
/ `49d6f456321f119a67d25ac7324d82b4fc282d43cf1e86044f4a342e43284f65`.
- `action-obey-shared-schema-before-20260905.json` / `...after...` / `...final...`:
`90becfaff10da7f9e549e43d28385da41c37675d51d3c9319347bb7b71600253`
/ `b7fef54d4e8030efd58299c5dd290f7e3e9a0ba0c80e8bd8aaa70a929d95c831`
/ `d88762b5f013727b37c746f8e290a5719779e40ec00a9ae10b5accc6e9fd24bb`.
- `action_obey_shared_schema_residual_independent_proof_v2_20260905.json`
and root replay both hash
`2a673deafe4714f0ad74ff074feb24d8cb34cb4f87fe6485fbca89d5911c2e4f`.
- `action-obey-structure-leaf-final-runtime-20260905.json`:
`b4ce9623dafa0c31d541f6a6aa16528c3c843522f542c47e367c45566b08e059`.
See the sibling API/leaf independent review, shared-schema evidence, and
shared-schema residual proof ledgers for source hashes and detailed ownership.
The private-closure plan is preserved separately. Both real private command
bodies, further actor/modifier enum ownership, and ordinary math-header
materialization remain scratch research until their coherent packet passes.
Do not count the donor's remaining exact claims or these proposals as integrated.
The separate Bitmaps intake preserves 18 distinct donor exact candidates /
4,736 padded bytes, with prototype/ownership/provenance issues still awaiting
reconciliation. It adds no canonical credit in this checkpoint. The Fable lane
as a whole remains unfinished; donor committed and dirty work is preserved.
@@ -1,340 +0,0 @@
# ActionObey shared swarm schema and enum-owner audit (2026-09-05)
## Disposition
The shared `swarm_component_datum` / `swarm_wander_control` schema is
authenticated strongly enough for one coherent owner-header trial. The
natural owner is `source/ai/actors.h`: that header already owns
`swarm_datum`, `SWARM_COMPONENT_DATUM_SIZE`, `swarm_data`,
`swarm_component_data`, and the neighboring actor/swarm accessors.
The decisive evidence is direct DIA type information in both `HCEX.pdb` and
`HCEX_Release.pdb`, not the Fable donor's flat local view. In particular,
`swarm_component_datum.flags` is a signed `short`, the byte at `+0x1B` is an
explicit member named `pad`, and the `+0x1C` tail is an anonymous union of
`wander` and `obey`. `swarm_wander_control` has no named member at `+2`;
that two-byte interval is ordinary alignment before its vector.
This is a read-only/source-ownership result. No production, donor, header,
configuration, build, or object file was changed or compiled by this audit.
The current root-owned ActionObey API/flush-leaf work was observed but not
modified or re-audited here.
## Direct PDB schema
Reproduction tool:
`C:/Users/isabe/Documents/Codex/2026-07-13/i-w/research/tools/DIA2Dump/x64/Release/DIA2Dump.exe`
Primary type source:
`C:/Users/isabe/Documents/Codex/2026-07-13/i-w/research/hcea_jun2011_prototype/payload/Halo CE Anniversary (Jun 24 2011)/HCEX.pdb`
SHA-256: `f55cfe957da8079a62a26a6753f1ddbb700b067a66de0bd5db5aac182d985ff1`
Independent same-result PDB:
`C:/Users/isabe/Documents/Codex/2026-07-13/i-w/research/hcea_jun2011_prototype/payload/Halo CE Anniversary (Jun 24 2011)/HCEX_Release.pdb`
SHA-256: `0fd11b084fd2169cafe071fd8c3e4083d6e9564f01e7964f6c26e5a03eee4dff`
The relevant commands are `DIA2Dump.exe -sym <name> <pdb>`. Both PDBs give
the same two principal layouts:
| type/member | offset | DIA type |
| --- | ---: | --- |
| `swarm_component_datum` | | size `0x40` |
| `identifier` | `0x00` | `short` |
| `flags` | `0x02` | `short` |
| `position` | `0x04` | `real_point3d` |
| `surface_index` | `0x10` | `long` |
| `combat_target_prop_index` | `0x14` | `long` |
| `attached_to_unit_ticks` | `0x18` | `unsigned char` |
| `ground_ticks` | `0x19` | `unsigned char` |
| `attack_delay_ticks` | `0x1A` | `unsigned char` |
| `pad` | `0x1B` | `unsigned char` |
| `wander` | `0x1C` | `struct swarm_wander_control` |
| `obey` | `0x1C` | `struct obey_individual_simple_control` |
| `swarm_wander_control` | | size `0x14` |
| `pause_ticks` | `0x00` | `unsigned char` |
| `move_ticks` | `0x01` | `unsigned char` |
| implicit alignment | `0x02..0x03` | no DIA member |
| `vector` | `0x04` | `real_vector3d` |
| `angle` | `0x10` | `float` |
The already-shared ActionObey arm in `actions.h` also agrees with the PDB:
`obey_individual_simple_control` is `0x24`, with bytes at `+0/+1`, signed
`short pause_timer` at `+2`, bytes at `+4/+5`, and anonymous
`directmovement` / `jump` arms at `+8`. The direct-movement arm is `0x1C`
(`short facing +0`, vector `+4`, start position `+0x10`); the jump arm is
`0x0C` (`short delay_ticks +0`, reals `+4/+8`). Those existing records need
no shape change.
`DIA2Dump -sym swarm_component_data HCEX.pdb` identifies the global as
`struct data_array *`, matching the existing `actors.h` declaration.
## Source-authentic owner shape
The following is the evidence-backed shape for `actors.h`. The enumerator
count is deliberately omitted because the named `NUMBER_OF_*` symbol is not
present in either PDB and no current consumer needs it.
```c
enum
{
_swarm_component_attacking_in_melee_bit = 0,
_swarm_component_attached_to_unit_bit,
_swarm_component_wander_bit,
_swarm_component_obey_bit,
_swarm_component_obey_desire_jump_bit,
};
struct swarm_wander_control
{
byte pause_ticks;
byte move_ticks;
real_vector3d vector;
real angle;
};
struct swarm_component_datum
{
short identifier;
short flags;
real_point3d position;
long surface_index;
long combat_target_prop_index;
byte attached_to_unit_ticks;
byte ground_ticks;
byte attack_delay_ticks;
byte pad;
union
{
struct swarm_wander_control wander;
struct obey_individual_simple_control obey;
};
};
```
Owner assertions should lock, at minimum:
- `sizeof(swarm_wander_control)==0x14`, vector `+4`, angle `+0x10`;
- `sizeof(swarm_component_datum)==0x40`, flags `+2`, position `+4`,
surface `+0x10`, target prop `+0x14`, the four bytes `+0x18..+0x1B`, and
both union arms `+0x1C`; and
- the already-proven composed access
`obey.directmovement.vector==+0x28`.
The retained HCEA generated header spells an explicit `pad[2]` in
`swarm_wander_control`. That is a reconstruction convenience, not a named
member recovered by DIA, and must not be promoted into the owning schema.
## Signed flags: independent January corroboration
January `_code_00005350` (`action_obey_individuals_iterate`, `0x130` padded)
zeros exactly `0x24` bytes at component `+0x1C`, then loads the word at `+2`
and emits:
```text
xor eax,eax
mov ax,word ptr [esi+2]
and eax,0xfffffffb
or eax,8
mov word ptr [esi+2],ax
```
This is the component's `wander`-clear / `obey`-set operation. The frozen
Fable measurement records that `short flags` reproduces the
`and eax,0xfffffffb` encoding while its prior unsigned `word` spelling emits
the different `and eax,0xfffb` form. That code observation corroborates the
direct PDB `short`; it is not being used as a spelling-search instruction.
The committed Fable source must therefore not contribute its flat
obey-only component record, even though that local record also uses `short`.
Its source SHA-256 is
`067b4900d5494c5341ca609337f0d220dedd4f95778a935d601427de96575a59`.
## Enum owners and provenance tiers
### `actors.h`: component flags
Direct `HCEX.pdb` constants authenticate all five spellings and values:
| enumerator | value |
| --- | ---: |
| `_swarm_component_attacking_in_melee_bit` | 0 |
| `_swarm_component_attached_to_unit_bit` | 1 |
| `_swarm_component_wander_bit` | 2 |
| `_swarm_component_obey_bit` | 3 |
| `_swarm_component_obey_desire_jump_bit` | 4 |
These belong with `swarm_component_datum.flags` in `actors.h`.
`NUMBER_OF_SWARM_COMPONENT_FLAGS` is absent from the PDB and unused by the
candidate; do not add it merely because a later extracted DB header has it.
### `actions.h`: simple-control flags
Direct `HCEX.pdb` constants authenticate all five spellings and values:
| enumerator | value |
| --- | ---: |
| `_obey_simple_directmovement_bit` | 0 |
| `_obey_simple_directmovement_update_continuously_bit` | 1 |
| `_obey_simple_jump_bit` | 2 |
| `_obey_simple_jump_jumped_bit` | 3 |
| `_obey_simple_jump_targeted_bit` | 4 |
They belong beside `obey_individual_simple_control.simple_control_flags` in
`actions.h`. `NUMBER_OF_OBEY_SIMPLE_FLAGS` is absent from the PDB and unused,
so the proposed owner packet need not invent it.
### `bipeds.h`: runtime biped flags
The PDB defines `_biped_datum.flags` as `unsigned long` and directly
authenticates the complete six-name set:
| enumerator | value |
| --- | ---: |
| `_biped_airborne_bit` | 0 |
| `_biped_slipping_bit` | 1 |
| `_biped_absolute_movement_bit` | 2 |
| `_biped_no_collision_bit` | 3 |
| `_biped_movement_passes_through_bipeds_bit` | 4 |
| `_biped_limp_body_physics_active_bit` | 5 |
These belong next to `_biped_datum.flags` in `bipeds.h`.
`NUMBER_OF_BIPED_FLAGS` is not returned by DIA and is unnecessary.
The existing header's `_biped_limping_bit=0` is not an authentic alias:
`DIA2Dump -sym _biped_limping_bit` returns no symbol, while bit 0 is directly
named `_biped_airborne_bit`. It is also not unused. Current source has five
uses in `units.c` (lines 2364, 4219, 4302, 6547, and 10051 at audit time) and
one in `players.c` (line 2111). The strongest semantic anchors select
`_unit_state_melee_airborne` and `_unit_state_dying_airborne` when the bit is
set. A coherent owner repair must rename those six uses to
`_biped_airborne_bit`, remove the wrong placeholder, and not retain it as a
second same-value name.
### `ai_scenario_definitions.h`: command-list flags and atom modifiers
`ai_command_list_definition.flags` is directly `unsigned long` at `+0x20`.
The PDB authenticates exactly these five command-list names:
| enumerator | value |
| --- | ---: |
| `_ai_command_list_allow_initiative_bit` | 0 |
| `_ai_command_list_allow_targeting_bit` | 1 |
| `_ai_command_list_disable_looking_bit` | 2 |
| `_ai_command_list_disable_communication_bit` | 3 |
| `_ai_command_list_disable_falling_damage_bit` | 4 |
January `action_obey_command_list_setup` independently reads masks
`1/2/4/8`, and `action_obey_individual_begin` reads mask `0x10`.
`_ai_command_list_manual_structure_bsp_bit=5` and
`NUMBER_OF_AI_COMMAND_LIST_FLAGS=6` occur in later extracted DB/Demon
material but are not returned by either PDB and are unused by the evidenced
eleven-function donor subset. They must not be included in this PDB-grounded
packet.
`ai_command_definition.atom_modifier` is directly a signed `short` at `+2`.
The PDB authenticates only
`_ai_atom_loop_modifier_until_told_to_advance=1`; January command-end compares
the word against 1. The donor's `_ai_atom_loop_modifier_always=0` and count
are not needed and have no direct PDB result, so omit them.
The direct-movement facing domain is the one remaining naming caveat.
January `_code_00005f70` proves a signed-short read at simple-control `+8`, an
unsigned range check for values `0..3`, and these behaviors:
- 0: copy the base facing;
- 1: negate the base facing;
- 2: store the normalized perpendicular;
- 3: negate that perpendicular;
- any other value: leave the output unchanged.
This authenticates the values and semantics of the donor spellings
`_ai_atom_move_facing_forwards/backwards/left/right`, and the natural owner is
the atom-modifier domain in `ai_scenario_definitions.h`. It does **not**
authenticate those exact source spellings: DIA returns no such names and the
retained HCEA function explicitly records that its enum oracle found none.
If descriptive reconstructed enum names are allowed with an explicit
provenance comment, those four donor names are semantically credible. If an
original-name requirement applies, the private direct-movement/update pair
remains withheld; raw integers or a TU-private enum are not acceptable
workarounds.
## Duplicate/facade removal inventory
One coherent shared-owner trial must address all current production views,
not only the donor file:
| file | current debt | owner-coherent action |
| --- | --- | --- |
| `source/ai/actors.c` | partial `swarm_component_datum` with unsigned `word flags` and `unknown_tail`; local accessor macro; two local layout typedefs | remove the partial type; consume the owner type; relocate/remove redundant asserts; use the shared typed accessor if installed |
| `source/ai/actor_type_infection.c` | local component/wander types (`word flags`, synthetic named `word pad`); local component and obey-simple enums; local biped-airborne enum; local accessor and layout typedefs | remove duplicate types/enums/asserts and consume `actors.h`, `actions.h`, and `bipeds.h` owners |
| `source/ai/actor_perception.c` | 0x40 prefix facade `actor_perception_swarm_component_view`; local accessor; raw `TEST_FLAG(...,1)` | remove facade/assert, use `swarm_component_datum`, and name bit 1 `_swarm_component_attached_to_unit_bit` |
| `source/units/bipeds.c` | local bit 0 / bit 5 enum | remove it after the complete owner enum lands |
| `source/game/player_control.c` | local `_biped_airborne_bit=0` enum | remove it after the owner enum lands |
| `source/units/units.c`, `source/game/players.c` | six uses of wrong `_biped_limping_bit` name | migrate all six to direct-PDB `_biped_airborne_bit` |
| prospective Fable `source/ai/action_obey.c` body | flat obey-only component, local component/simple/biped/command/modifier enums, local accessor | do not import those definitions; consume the owning headers and shared full-union type |
`actor_type_infection.c` also copies the field into a local
`word component_flags`. DIA proves the **record field** is signed; it does
not recover that local variable's source type. Do not retune this local as
part of the ownership packet without January function evidence—the header
correction alone removes the false record declaration.
Because `actors.h` already owns `swarm_get`, `swarm_try_and_get`, and the
`swarm_component_data` declaration, a single typed `swarm_component_get`
macro there is the natural source-owner cleanup for the three current local
macros and the future ActionObey caller. Macro ownership is a source-design
inference (PDBs do not retain macros), not a recovered original symbol; root
may instead retain identical local macros, but the perception facade's return
type still must be corrected.
## Trial boundary and dependency scope
This audit found 43 direct lexical includers of `actors.h` (42 C files plus
`ai_debug.h`), 27 direct includers of `actions.h`, 13 of
`ai_scenario_definitions.h`, and 13 of `bipeds.h`. These are only source-text
counts, not a substitute for the build dependency graph; the root-owned
trial must freeze and compare every actual dependent object and all inherited
strict/residual owners.
The first coherent trial should therefore be one ownership packet:
1. install the PDB-authenticated full schema and five component bits in
`actors.h`;
2. remove the three production type facades and migrate perception's raw bit;
3. install the five PDB-authenticated obey-simple bits in `actions.h`;
4. install the six PDB-authenticated biped bits in `bipeds.h`, remove three
local enum bodies, and repair all six wrong bit-0 consumer names;
5. install only the PDB-authenticated command-list bits 0..4 and loop-until
value 1 in `ai_scenario_definitions.h`;
6. decide explicitly whether the four behavior-authenticated but
source-name-unproven move-facing enumerators are acceptable; and
7. only then compile the unchanged natural ActionObey donor-body subset under
real owner headers, with a complete dependent-object/runtime census.
No enum counts, manual-BSP bit, flat union, synthetic wander pad, raw bit
numbers, prefix views, or TU-private fallback enums are part of this
recommendation.
## Evidence read
- `scratch/fable_action_obey_intake_20260905.md`, SHA-256
`587bda8055043f26957944b40939f0a7e9a831957c8cace07e0d3c007238eef6`;
- current `actors.h/.c`, `actor_type_infection.c`, `actor_perception.c`,
`actions.h`, `ai_scenario_definitions.h`, `bipeds.h/.c`,
`player_control.c`, `units.c`, and `players.c`;
- the committed Fable `action_obey.c` at
`f9dd42218b3325e1b3a63d7860e5309f69fd3d8e`, its saved
`scratch/reports/action_obey.md`, and its frozen exactness notes;
- complete January disassemblies for `_code_00005350`,
`_action_obey_command_list_setup`, `_code_00005290`, `_code_00004fb0`, and
`_code_00005f70` from `build/split/source/ai/action_obey.obj`;
- direct DIA queries against both PDBs for all types and enumerators recorded
above; and
- retained HCEA generated headers/functions and Demon command-schema source,
used only as corroboration where explicitly identified, never as January
original-source proof.
@@ -1,83 +0,0 @@
# ActionObey shared-schema residual proof (final)
Date: 2026-09-05
Scope: the two `runtime_or_owner_changed_REVIEW` sections selected by the frozen
58-consumer shared-schema audit. This proof reads only the frozen before/after
inventories and objects. It does not read live objects or source, compile,
run a gate, refresh a park, or decide packet ownership.
## Frozen inputs
- Before inventory SHA-256:
`90becfaff10da7f9e549e43d28385da41c37675d51d3c9319347bb7b71600253`
- After inventory SHA-256:
`b7fef54d4e8030efd58299c5dd290f7e3e9a0ba0c80e8bd8aaa70a929d95c831`
- Infection objects:
`8bb55ad955ced508c5db6cd768ef437575b3b6e6a83f2140757282854197f531`
-> `80caf88f6bc3db25ecffa0488ac480eba0997618be066f5d2cfca6b8fd76de62`
- Communication objects:
`329718faba04602a5809c17849c23f824ee483b4f854c58bc62d11c7c3e97141`
-> `be4f557e04d2b99155594435035bef9705c6e317e85162efe9494d1f5909ed5e`
## Result
Both residuals preserve their public ABI, COFF owner/linkage/type/value,
section flags/alignment/COMDAT selection, relocation identities and topology,
control-flow topology, and observable behavior for a valid call.
### `_infection_swarm_control`
- Size remains 3,360 bytes and all 103 relocations map exactly. Thirty-eight
relocation source offsets move with the shortened encodings. The six
changed internal table destinations map as
`1254->1252` three times, `1701->1697` twice, and `1793->1789` once; the
table slots themselves remain fixed.
- All 130 direct branches and both indirect table jumps map exactly. No branch
enters either alignment/trailing-padding interior.
- The only executable changes are three `AND EAX, imm32` encodings becoming
their sign-extended `imm8` forms: `0x0000fffe -> 0xfffffffe` twice and
`0x0000fff7 -> 0xfffffff7` once. Exhaustive evaluation of all 65,536 16-bit
flag inputs at all three sites (196,608 comparisons) is exact.
- The first two sites explicitly zero EAX before loading AX. At the third
site, upper EAX is not assumed zero: only AL is tested and AX is stored;
`TEST AL,1` overwrites the `AND` flags before the branch, and each successor
fully defines EAX before any full-width read.
- The exact allowed padding bytes are asserted, not merely described: old
`LEA ESP,[ESP]` becomes `JMP 0x940` plus skipped `LEA EBX,[EBX]`; the old
post-`RET` `NOP` becomes post-`RET` `LEA ECX,[ECX]`.
- After masking relocation slots, the complete 80-byte `[0xcd0,0xd20)` table
and tail are byte-identical, SHA-256
`eb8ad26cf1b215d3d0731e00af0d53823547fa6cd4254298bafa1996d58f96c1`.
- No runtime/data section was added or lost, no COMMON changed, no definition
transition occurred, and the complete function-owner name inventory is
unchanged. Shifted compiler-local labels are fully accounted for by the
same offset map.
### `_ai_communication_update_speech_timers`
- Size remains 672 bytes and all 43 relocation records are byte-for-byte
identical.
- The only change is the order of adjacent loads in `[0x167,0x16e)`:
`DI <- word [EBP+0x10]` and `EBX <- dword [EBP-8]`. They read distinct
stack slots, write disjoint registers, and do not touch flags or memory.
- Bytes before `0x167` and from `0x16e` onward are identical. All 27 direct
branches are identical, two enter at the block boundary `0x167`, the jump at
`0x165` rejoins at `0x16e`, no branch targets the strict interior, there are
zero indirect jumps, and the block contains zero relocations.
- No runtime/data section was added or lost, no COMMON changed, no definition
transition occurred, and the complete function-owner name inventory is
unchanged.
This is a frozen-object equivalence proof for these two deltas, not a blanket
exception for future schema packets or other compiler-shape changes. It does
not replace the root owner's admission, park, build, or stable-gate decision.
## Artifacts
- Hardened proof script SHA-256:
`b9aa2a4c8b4509c368b9bbd96d5907abd4944202b635edc29c852807e2df74b0`
- Final v2 JSON SHA-256:
`2a673deafe4714f0ad74ff074feb24d8cb34cb4f87fe6485fbca89d5911c2e4f`
- Preserved preliminary v1 JSON SHA-256:
`b44e5ed4c6d5e469a0ae821fbd4b91aa6af04393242238d0df7e4670081226c8`
@@ -1,121 +0,0 @@
# `action_search.obj` exact-match strategy ledger
## Outcome
`source/ai/action_search.obj` is strict-exact: all 9 functions and all 4
owned `.rdata` COMDAT sections match the January 2002 target in padded size,
normalized bytes, relocation count, relocation address/type, and resolved
destination/addend.
The reconstruction began from the HCEA-to-January bridge packet, but every
prototype, field offset, constant, call, and source shape was independently
gated against the January object. Cross-build evidence was used only as a map;
January bytes remained the authority.
## Final function inventory
| Function | Padded bytes | Relocations | Result |
| --- | ---: | ---: | --- |
| `action_search_setup_target` | 128 | 7 | exact |
| `action_search_setup_pursuit` | 256 | 10 | exact |
| `action_search_setup_undirected` | 112 | 7 | exact |
| `action_search_update` | 384 | 9 | exact |
| `action_search_flush_position_indices` | 48 | 2 | exact |
| `action_search_flush_structure_indices` | 48 | 2 | exact |
| `action_search_control` | 336 | 3 | exact |
| `action_search_begin` | 144 | 7 | exact |
| `action_search_perform` | 848 | 24 | exact |
## Closeout discoveries
### `action_search_setup_pursuit`
The initial reconstruction used an outer eligibility condition plus a nested
firing-position guard. That produced the right behavior and size but the wrong
cold-epilogue ordering. The exact source is one chained guard containing
`firing_position_index != NONE`. VC7 then reuses `BX` for the firing-position
index and automatically emits the three January return paths in the correct
order. This also satisfies the project's preference for one source-level
return.
General lesson: when several failed predicates share one machine epilogue,
write them as one short-circuit condition before trying manual early returns or
gotos. The compiler's SSA split may create multiple machine returns even when
the C has one return.
### `action_search_perform`
Three source details were required:
1. Keep the maximum-searcher comparison explicitly `short`. Without the cast,
integer promotion creates a `movsx` and a 32-bit compare instead of the
January 16-bit compare.
2. Write the movement dispatch as a flat chain beginning with
`if (at_destination)`, followed by the type-0 and type-1 branches. The
semantically equivalent nested `if (!at_destination) ... else ...` makes
VC7 share the two move-call suffixes in the opposite direction.
3. Declare one function-scope `struct prop_datum *prop` and reuse it for both
prop lookups. Separate expression temporaries create an EAX/ECX mirror in
the examined-pursuit block; the reused named local reproduces January's
register coloring exactly.
The measured rejects included nested versus combined searcher conditions,
integer-promoted thresholds, early returns, a `switch`, explicit shared-result
gotos, declaration-order changes, `register`, and one- and two-barrier tail
experiments. All were reverted when they failed strict comparison.
## House-rule audit
- Parameters are one per line.
- Typed actor, prop, scenario, and tag-block accessors are used.
- Void functions end with explicit `return;` where their exact source permits.
- No inline assembly, volatile byte forcing, undefined behavior, compiler flag
change, fuzzy credit, or comparator weakening is present.
- `action_search_update` retains its early machine-required exit; forcing a
single source return would change exact codegen, so this is the documented
exception to the preferred single-return rule.
## Current-campaign revalidation (2026-08-13)
This atomic lane began at authoritative campaign tip `ea55318e`, where this
translation unit was still a 38-line skeleton and reported 0/9 exact functions.
Before reconstructing it again, the lane audited all local worktrees and
branches and found Claude/Fable's completed but unintegrated object at commit
`bd438b42`. Reusing that proven local result was both faster and safer than
repeating the source-shape search above.
The commit was cherry-picked into a new isolated worktree, configured against
the installed XDK 3911 compiler and pristine January executable, and rebuilt
from source. The hardened comparator independently reproduced every function
hash and relocation identity:
| Function | Padded bytes | Relocations | Normalized SHA-256 |
| --- | ---: | ---: | --- |
| `action_search_setup_target` | 128/128 | 7/7 | `cbf6d948839e88e8e724d10a38ac71c300c6e37440a51b2882923df9dc0a3cff` |
| `action_search_setup_pursuit` | 256/256 | 10/10 | `4347d9f3a5b25001bcdbf6f61d729394de70ebb908f1688e459654c2f10778e3` |
| `action_search_setup_undirected` | 112/112 | 7/7 | `dd5c14cc93d1ad3a623a90cc663dd57f974c84bdf8416e95d97219b585f1d933` |
| `action_search_update` | 384/384 | 9/9 | `86d23b9bcdbf51d55040b1aeb6342c751a18fdf31f4350606d8b07386b444d64` |
| `action_search_flush_position_indices` | 48/48 | 2/2 | `0092a713331f5179dbbff0cc7a9c576e7ebbe07d887f6ba6661da5713a0acd53` |
| `action_search_flush_structure_indices` | 48/48 | 2/2 | `828e1700de76a884cac45684d4e9eb16164aac1d8554ab01f9491b715d870b62` |
| `action_search_control` | 336/336 | 3/3 | `0152f72402462585a66e38af4f580d73fb27cfebe5d70f432efbd0d105926951` |
| `action_search_begin` | 144/144 | 7/7 | `f60a7b46a9e6640afb10e4cc818304c1a6d499f7d9eed99aa42e25b4e29488b9` |
| `action_search_perform` | 848/848 | 24/24 | `91321b997bee74aa54c2fea51c3c3ba35c30bedf5e4856f88b259984597d267d` |
The object owns no `.data` or `.bss`. Its four target-owned `.rdata` COMDATs
are the source path and the float constants `0.64f`, `6.25f`, and `0.49f`; all
four compare strict exact. Their normalized SHA-256 values are respectively
`cfd78efaa8180437a616198604af560523d4c0c77a2106385c170a77946efffd`,
`74fdba32aaac063fe5801c661ff89edff6e7852c241a36064ce88505748a5894`,
`c1080e69acd873d362d47f9e25351f5237bcf82e3374c88040dc7140aa378747`,
and `eeacb7c70a55988e1772bae66236c28b361bba5de119afb4075e752269a6890a`.
The `state_data` assertion string is target-external and candidate-owned due to
ordinary COMDAT folding; its relocation spelling and bytes are nevertheless
strictly resolved at every use by the hardened comparator.
The source was also manually re-audited against Berth's rules. Parameters are
one per line; `void` functions end with explicit `return;`; actor, prop, tag,
and tag-block access go through the typed subsystem interfaces; and the one
early return in `action_search_update` is retained only because January's
machine control flow requires it. No raw object/tag casts, alignment controls,
assembly, `volatile`, forced inline, undefined behavior, magic byte forcing,
or flag/comparator exception is present.
@@ -1,130 +0,0 @@
# `action_uncover.obj` credible fuzzy reconstruction and park
## Result
`source/ai/action_uncover.obj` now has source for every January function:
- 8 strict-exact functions;
- 1 measured credible residual;
- 0 unwritten functions.
The retained `_action_uncover_perform` is coherent typed C. It has the exact
544-byte padded envelope, 164-instruction count, and all 19 January relocation
addresses, types, destinations, and addends. Its objdiff score is 98.77778%.
Only 43 normalized byte positions differ, all within the contiguous
`+0xA2..+0xD3` pursuit-point copy schedule. The eight established exact
siblings remain exact, so the object retains 2,112 strict-exact padded code
bytes out of 2,656 and has no unwritten byte scope.
## Evidence and source reconstruction
The January split object and its disassembly remain authoritative. The frozen
ordinary-C candidate from the earlier `jonas/action-uncover-completion-20260828`
wave supplied the strongest same-compiler source topology. The following
cross-build material was read as corroboration, not as byte authority:
- HCEA commit `570c83fd9c365dad6f2a3e7041705d5b84c7847c`, whose manually recovered
`action_uncover_perform.c` agrees on the selector/change short ABI and the
high-level state transitions;
- Stian and pastudan retail-Xbox raw-pointer lifts, which corroborate the
branch flow and record offsets but are not admissible source;
- `punpckhdq/halo`, which still contains only the function stub;
- the open Marathon source tree, which has no direct firing-position/uncover
homolog and therefore supplies style precedent only.
The restored body:
1. asserts that the actor is not a swarm actor;
2. gates work on the actor timeslice, passenger state, and completion state;
3. constructs the firing-position search from either the pursuit location or
the last visible target position;
4. selects a candidate and updates the target-visibility or inspected-location
state using named line-of-sight and pursuit enums;
5. changes the actor's firing position and records failure when no position can
be adopted.
The source uses `boolean`, `real`, `real_point3d`, named enum constants, typed
records, and the existing C-series memory helper. It contains no inline
assembly, `volatile`, raw byte emission, optimizer directive, artificial
dependency, address-placeholder name, or helper call introduced only to steer
code generation.
## Residual boundary
| Measurement | January | Retained candidate |
| --- | ---: | ---: |
| Meaningful / padded bytes | 533 / 544 | 533 / 544 |
| Instructions | 164 | 164 |
| Relocations | 19 | 19 |
| Normalized SHA-256 | `ddd3ed044dc0de47ecb6ba83ac683372ff87b3cb7d8fee318d20da5467c39ed8` | `f6c5eb3588f3efccd66611ba0c133b20dc3fb449fad4894f3022b8e9c6f04bd0` |
| Objdiff fuzzy score | 100% | 98.77778% |
January loads all three `real_point3d` dwords before interleaving the
independent enable, surface, cluster, and point stores. VC7 lowers the natural
aggregate assignment with the same compact source `lea` but alternates the
point loads and stores. Code before and after that window realigns exactly.
The prior aggregate, scalar, alias, helper, field-order, scope, barrier, and
45 documented defined-C topology families are exhausted. Forcing the remaining
schedule would require gameplay assembly or artificial source, so the function
is parked as `instruction-scheduling` in `config/parked.json`. Reopen only for
authenticated January source/local records, a strict same-compiler C donor, or
a new natural lifetime fact that preserves the frame, relocations, compact
source `lea`, and exact downstream suffix.
## Prototype ownership and shared-header audit
`actor_active_select_firing_position` and `actor_change_firing_position` are
owned by `actor_firing_position.c`. Their forward-declared record tags and
short-width prototypes now live in the closest associated owner header,
`actors.h`, rather than in action translation units. The local full work-record
definitions remain in their consuming TUs because the owner object is still
largely unwritten and moving the large incomplete layouts into a shared header
would unnecessarily expose provisional definitions.
January callee disassembly independently establishes the narrow ABI: the
selector returns in `AX`, and the changer consumes the position index through
`DI` and returns in `AX`. Existing exact callers were reconciled to `short`
locals before removing their duplicate declarations. Rapid same-command-line
gates then compared the edited header against the pre-edit header for all 41
direct C includers and all five additional C consumers reached through
`ai_debug.h`. Every per-function exact/residual/unwritten set was unchanged.
In particular:
- `action_avoid.obj`: 6/6 exact;
- `action_fight.obj`: 6/6 exact;
- `action_guard.obj`: 15/15 exact;
- `units.obj`: 189/189 exact;
- `objects.obj`: 121/121 exact.
The same cleanup removed the final address-style private name from the touched
`action_guard.c`. The pinned HCEA PDB authenticates
`action_guard_speak_post_combat(long)` in `action_guard.obj`, and the recovered
HCEA body corroborates the January function's one-shot post-combat speech
semantics. January's PDB omits this private static, so the name is recorded as
later-build PDB provenance rather than claimed as January-name-proven. The
renamed January function remains strict exact.
## Ownership and validation
Relative to the declaration-only baseline, the only new owned production
sections are `_action_uncover_perform` itself and its three expected strings
(the swarm assertion, target-visibility diagnostic, and pursuit-inspection
diagnostic). No helper COMDAT was added. In particular, the candidate symbol
table contains no `point_from_line3d` symbol. Direct target comparison reports
all 304 target-owned data bytes exact.
Canonical integration validation completed after regenerating the split and
running a full Ninja build:
- canonical campaign gate: `8 exact / 1 residual / 0 unwritten`;
- exact-set comparison: no lost exact sibling;
- direct distance audit: 544 bytes, 43 differing normalized positions, 19
relocations, identical relocation address and destination sequences;
- objdiff: 98.77778% for `_action_uncover_perform`, 100% for all eight siblings,
and 100% for target-owned data;
- parked-function validator: 1 active, 0 stale, 0 invalid;
- fake-match scan across all edited AI source/header files: 0 findings;
- full tool suite: 254 passed;
- semantic report: 0 unit errors;
- common-key exact regression sweep: 0 losses;
- `git diff --check`: clean.
@@ -1,136 +0,0 @@
# `action_uncover.obj` perform ABI reconciliation and scheduler closeout
## Result
`source/ai/action_uncover.obj` remains honestly incomplete at 8/9 strict
functions and 2,112/2,656 padded code bytes. `_action_uncover_perform` is
still absent from the production candidate object; no residual body, owned
string, completion label, park, semantic exception, compiler option, or
comparator exception is admitted.
This wave resolves the contradictory selector-width history and exhausts a
new, bounded set of ordinary-C copy topologies. The best lawful candidate
has the exact 544-byte envelope, all 19 relocations, the same 164-instruction
count, and the complete January call/ABI schedule. Its only remaining
difference is
the already isolated pursuit-point copy scheduler: 43 normalized byte
positions in one contiguous `+0xA2..+0xD3` window.
## Evidence read first
The current recovery record
`docs/object_matching_logs/action_uncover_obj_jonas_recovery.md`, the fuller
historical strategy log at commit
`1633ccff6d9baa3f01b8e4eb3648b5880932e8be`, and the preserved earlier
cross-build closeout log were read in full. The historical log is decisive:
January callee disassembly proves all three related types are narrow:
- `actor_active_select_firing_position` returns `short`;
- the receiving `selected_firing_position_index` local is `short`;
- `actor_change_firing_position` receives that argument as `short` and returns
`short`.
The 2026-08-26 follow-up in the current recovery record narrowed the selector
and local while retaining a `long` consumer parameter. Its resulting
sign-extension was therefore an incomplete ABI experiment, not evidence of
January-width drift. Repeating the complete three-part short topology makes
the call window strict: January and candidate both emit
`add esp, 0x18; mov ebx, eax; cmp bx, -1`, with no later sign extension.
HCEA independently corroborates the three short types, but January code is the
authority.
## Frozen best candidate
| Measurement | January | Best ordinary-C candidate |
| --- | ---: | ---: |
| Meaningful / padded bytes | 533 / 544 | 533 / 544 |
| Relocations | 19 | 19 |
| Instructions | 164 | 164 |
| Normalized SHA-256 | `ddd3ed044dc0de47ecb6ba83ac683372ff87b3cb7d8fee318d20da5467c39ed8` | `f6c5eb3588f3efccd66611ba0c133b20dc3fb449fad4894f3022b8e9c6f04bd0` |
The immutable best scratch source snapshot is
`scratch/_gate_26956.c`, SHA-256
`d136beeae2cbe62fa65e46cfc3c16777cd8ae18ad70d2a51977d96feacf6199a`.
Its phase-local object is `scratch/_gate_26956.obj`, SHA-256
`0ae37faec0fc7446e23ffae6ad373a05a9dec640e0b486ae6c75de768afb41cd`.
The January split object is SHA-256
`82fd7ef88e212ebca0a7afe5e7eae37eef82054020988046090774865e5bb135`.
Raw candidate COFF identity is phase-local because debug records contain the
output path; the normalized function hashes are the reproducible identities.
Every relocation address, type, symbolic destination, and addend agrees. Code
before and after the pursuit-copy window realigns exactly. January uses one
compact source `lea`, loads all three point dwords, then schedules the stores
among the independent surface, cluster, and enable operations. VC7 lowers
the best aggregate source with the same compact `lea` but interleaves each
point load and store.
## New compiler probes
Twenty-five production-neutral variants were compiled from the immutable
snapshot. They cover ordinary inline pointer/value aggregate helpers,
`const` variants, by-value and return-by-value helpers, scalar/array/union
initializers, comma declarations and expressions, branch lifetimes, enclosing
aggregate layouts, and supported alias qualifiers. They add the following
facts to the historical E01-E20 matrix:
- aggregate, value-identity, pointer-local, and `const` forms all canonicalize
byte-for-byte to the `f6c5eb35...` best candidate;
- an inline helper taking a `real_point3d` by value and spelling scalar stores
does create January's compact `lea; load x; load y; load z` prefix, but VC7
also materializes a 12-byte argument temporary, shifts the frame, duplicates
stores, and diverges globally. It remains 544/19 but has normalized hash
`3169a4b10565db618e9129673a3ac294cd6c0103cc23345879cde972f58eb6cf`
and raw object SHA-256
`3c7338bd4dfd682a40fe591a937580be7ddc81674981c570e010a003c65030bb`;
- scalar return-by-value helpers remain out of line at 576 bytes / 20
relocations;
- a named enclosing target subobject rounds from 22 to 24 bytes, shifts every
following offset, and yields a nonviable 544/19 object with raw SHA-256
`dfde9276e1a93f833186af26bd492ff1e62f09fd7ffef490fc1c2fccc4e343d2`;
- XDK 3911 rejects `__restrict`, so this compiler supplies no supported
non-alias qualifier to test;
- the January PDB module stream for `action_uncover.obj` is only 124 bytes of
object/compiler records and contains no local-variable records.
The research harness is
`scratch/uncover_codegen_probe_subagent.py`, SHA-256
`176ae1e683a25e6850f75fb98fbae48b81fef7bb4dbee41afb010f371e19d66a`.
Scratch artifacts are evidence only and are not tracked or published.
## Public-source provenance
The user-supplied repositories were searched at their current public heads and
through relevant history.
- `punpckhdq/halo` is the same build-2342 project, but commit
`fc47f9a15a0aea7f0081394c4d66db5c68ab5323` and all 141 reachable commits
contain only the original `action_uncover.c` symbol/string stub.
- HCEA commit `570c83fd9c365dad6f2a3e7041705d5b84c7847c` contains the strongest typed
cross-build reconstruction. Its own comments identify the body as a manual
disassembly recovery after decompiler failure; it is not lost Bungie C.
- Stian/pastudan target retail Xbox `01.10.12.2276`, not January debug build
2342. Their bodies are Ghidra/LLM raw-pointer lifts. The two historical
enable/point statement orders are non-admissible and are already subsumed by
the measured scalar and field-order families.
- Baboon, demon, nimbus, halopc-restored, and xboxrecomp contain no January
function source or compiler-spelling record. Xboxrecomp pull request 5 is
useful runtime/recompilation infrastructure, but adds no source-provenance
control for this scheduler.
No authentic January/Bungie source or new admissible defined-C spelling was
found.
## Disposition
The production source is intentionally unchanged: the eight established exact
functions remain, `_action_uncover_perform` remains declaration-only, and the
two perform-owned strings remain absent. This preserves the atomic/fail-closed
boundary and all house rules.
Do not repeat the historical aggregate, scalar, alias, helper, field-order,
scope, barrier, representation-copy, or E01-E20 families, nor the 25 variants
above. Reopen only for authentic January source/local records, a same-compiler
strict-exact C donor, or a genuinely new defined-C lifetime dependency that
retains the compact base `lea` and load-all/store-all schedule without a new
temporary, frame change, relocation, or forbidden compiler control.
@@ -1,227 +0,0 @@
# `action_uncover.obj` Jonas recovery checkpoint
## Scope and provenance
This is a bounded partial recovery on authoritative integration commit
`2ef097537488b0c71589004a0047dc9a8c02b3e1`. The donor was the clean
`action_uncover.c` working-copy artifact in
`work/action-search-closeout-20260801` at
`10a2244a0d3e6a70b5945995700ff9d9b219dc92`. Its Git blob identity was
verified before transplant as
`a6c042c1d124b7bd6148ca1b3b34d26244f887db`.
Only `source/ai/action_uncover.c` is transplanted and adapted. No shared
header, configuration, semantic ledger, park entry, status label, existing
object log, or Claude-authored Markdown changes. The unit remains
`NonMatching`.
## Frozen-header compatibility bridge
The donor's dirty worktree also contained an unrelated `ai_debug.h` edit
that split `__unknown3C[105]` into `__unknown3C[104]` plus the named
`print_uncovering` Boolean. That header edit is not imported.
The local source instead uses the read-only
`ACTION_UNCOVER_DEBUG_PRINTING_ENABLED()` accessor over the already declared
`ai_debug.__unknown3C[104]` subobject. The frozen header declares that storage
as `char[105]`; the accessor reads the actual one-byte character subobject and
uses it only as a truth value, so it does not depend on signedness. A
zero-emission compile-time assertion requires the member identity to remain at
`+0xA4`. The independent
`hs_globals_external.c` registration names that same address
`ai_print_uncovering`. There is no cast, pointer arithmetic, write, union
overlay, or helper body.
A controlled A/B XDK build of the original named-field donor and the
frozen-header bridge proved the bridge code-neutral before the nonexact body
was removed. The final declaration-only build was then independently replayed:
all eight retained runtime function sections remain identical to the target in
size, normalized bytes, and relocations. The bridge therefore changes only
source/debug description, not the credited runtime code or data.
## Strict code result
The final forced XDK 3911 measurement reaches the fail-closed donor boundary:
8/9 strict-exact target functions, 2,054 meaningful bytes, 2,112 padded
bytes, and 71 relocations. Every credited function is exact in padded size,
normalized bytes, relocation count, relocation address/type, symbolic
destination, and addend.
| Function | Meaningful | Padded | Relocations | Normalized SHA-256 |
| --- | ---: | ---: | ---: | --- |
| `_action_uncover_setup_target` | 121 | 128 | 7 | `d2a5512fda5465b171d041acc0a9f229d8923ecbd07f095eb88bf7aebf4f7793` |
| `_action_uncover_setup_pursuit` | 247 | 256 | 10 | `9bb3ea855bc4ffb388e7ed9ec451eb2b63944b724094b75ffd87396b498721f2` |
| `_action_uncover_control` | 365 | 368 | 5 | `ac833f1fbfe0375f3e11ebd1132430a2c2b6ded9e21549c2d0f9fe09dc231e8d` |
| `_action_uncover_flush_position_indices` | 49 | 64 | 2 | `d32f3d33556ecc4034d286cc8dcaef9cbdc100164d9882af21f6eb706b8c8b9a` |
| `_action_uncover_flush_structure_indices` | 39 | 48 | 2 | `828e1700de76a884cac45684d4e9eb16164aac1d8554ab01f9491b715d870b62` |
| `_action_uncover_modify_color` | 102 | 112 | 4 | `c4db7ebcc9c18e628518139c9c96e35ada43909042d81c6b990e6e6342b659a4` |
| `_action_uncover_begin` | 367 | 368 | 18 | `eb0c3080244d12fedb171518d45dee6d575c7cb3c9f1d9f84a26e88c350aec9a` |
| `_action_uncover_update` | 764 | 768 | 23 | `47b5885aad0b232ad22cdee9177393a8fd77ac16931ae1c28e9a131dd1ee9356` |
The source also makes VC7 emit the header-inline
`vector_from_points3d`, `magnitude_squared3d`, and
`distance_squared3d` COMDATs. They are candidate-owned folded copies, not
target functions in this translation unit, and receive zero function credit.
## Fail-closed residual
`_action_uncover_perform` is deliberately absent from the candidate object.
The source retains only its public prototype. The final candidate symbol
table and `_action_uncover_update` relocation inventory contain no perform
symbol or relocation; no section, body, helper, or anchor is emitted. There
is no perform code credit.
For provenance, the rejected donor body measured 533 meaningful / 544 padded
bytes with 19 relocations. Its normalized SHA-256 was
`9c05cfd15a565d4fe76c1dca3670578a42f95173100d3ab91b17cba46fd453cf`,
against target
`ddd3ed044dc0de47ecb6ba83ac683372ff87b3cb7d8fee318d20da5467c39ed8`.
That nonexact body and its exclusively owned strings are not retained.
The donor already established the two residual scheduling windows: the
12-byte pursuit-position copy interleaves loads and stores instead of loading
all three dwords first, and the post-select `add esp, 0x18` / `mov ebx,
eax` order is exchanged. No tuning is performed after reproducing this
measured donor subset.
## RDATA and ownership boundary
Ordinary objdiff reports 82.608696% fuzzy RDATA similarity, but no
`matched_data` value because the aggregate target boundary is incomplete.
The campaign therefore receives 0/304 ordinary data credit. Independent
owner-by-owner comparison proves eleven physically owned target COMDATs,
218 bytes total, exact in owner identity, logical size, flags/alignment,
payload, and zero-relocation shape:
| Literal/constant owner | Bytes |
| --- | ---: |
| source path | 35 |
| begin-uncover format | 63 |
| `pursuit` | 8 |
| `target` | 7 |
| `un` | 3 |
| uncover-done format | 24 |
| unknown-reason text | 17 |
| location-inspected text | 19 |
| persistent-timer format | 20 |
| timer-finished format | 18 |
| `60.0f` | 4 |
Another eleven target bytes are the folded
`"state_data"` assertion-expression COMDAT. The split action object records
that owner undefined, while VC7 emits the normal local select-any copy.
Direct comparison with `cachebeta.exe` at the independently recorded symbol
file offset 2,371,308 proves both sides are the same eleven bytes with
SHA-256
`8eeb47459ccd3b18728d86838a1447f42b0207e2ef5ff5996fdaefa90d849878`.
This is ownership attribution, not a semantic-data exception. The strict
independently proven retained-data boundary is therefore 229/304 bytes.
The two target-only perform strings total the missing 75 bytes: 31 bytes for
the inspected-pursuit message and 44 bytes for the unable-to-see-target
message. They disappear with the rejected body. Two candidate-only folded
COMDAT definitions total five bytes: the empty string and `30.0f`. They have
no target-owned section in this split object and receive zero data credit.
## Source and policy audit
The retained reconstruction is readable typed C. Parameters are vertically
formatted and every function has an explicit return. It introduces no
assembly, volatile scheduling device, force-inline control, optimizer pragma,
barrier, undefined aliasing, pointer pun, raw-address arithmetic, data write
through the compatibility bridge, byte-forcing expression, or synthetic
anchor.
The frozen `vehicles`, `ai_debug`, `ai_script`, `actions`, and
`units` source/config/log surfaces are unchanged from `2ef09753`.
## Validation
- full `halobetacache_build` and `libcmt_build`: pass in the 572-edge
consolidated build;
- semantic audit: 470 units, 3,948 functions evaluated, 3,808 semantic exact,
3,869 accepted exact, zero unit errors;
- campaign progress: 375/833 objects, 3,856/11,060 functions,
467,019/2,198,102 code bytes, and 1,846,712/4,176,062 data bytes;
- Halo progress: 273/468 objects, 3,689/7,574 functions,
454,105/1,770,166 code bytes, and 1,841,520/3,923,451 data bytes;
- object admission: zero candidates, zero revocations; only the inherited
unrelated `shell_xbox` contradiction remains;
- parked-function audit: 3 active, 0 stale, 0 invalid;
- tooling suite: 179/179 tests pass;
- configuration remains `NonMatching`; no semantic or parked exception is
added.
Reproducibility identities before the final commit:
- adapted source Git blob:
`a6b3cd4af7259f2268f762f4135ffb48a855e63d`;
- adapted source SHA-256:
`57735f1cf03c852c642a070c35f0eda684ff7ec0d3d39c38589cf601c209e3cb`;
- January target object SHA-256:
`82fd7ef88e212ebca0a7afe5e7eae37eef82054020988046090774865e5bb135`.
The raw candidate COFF hash is intentionally omitted because the timestamped
object container changes across forced builds; the per-function normalized
hashes above are the reproducible code identities.
A final committed-state snapshot and forced same-path rebuild/check are
performed after the local checkpoint commit. Nothing is pushed.
## HCEA short-width follow-up (2026-08-26)
The user-supplied HCEA corpus was refreshed at commit
`570c83fd9c365dad6f2a3e7041705d5b84c7847c`. Its
`src/blam/ai/action_uncover_perform.c` adds two source facts that were absent
from the earlier PDB-only packet: the later-build selector is declared to
return `int16_t`, its receiving local is `int16_t`, and the pursuit point is
spelled as three indexed `n[]` assignments. These are cross-build hypotheses;
January code remains authoritative.
The clean pre-wave regression manifest is
`build/audit/action_uncover_short_return_baseline_20260826.json`, SHA-256
`6fefc18d013dd0bd3250b91a0ade399fd2c7a120b058cee97377d3a17508acba`.
The preserved preimage object has SHA-256
`c9af8e7d7d762362e93db4a0a15d380d8f04ab0f64039755a23bab5d97569999`.
All candidate source packets passed VC7 `/Zs` before their ordinary compile.
Three evidence-separated measurements were made:
| Candidate | Source SHA-256 | Object SHA-256 | Normalized perform SHA-256 | Result |
| --- | --- | --- | --- | --- |
| HCEA scalar point copy, short selector return and local | `5e44b5ceeda63c464f4c256582a67a944dd0f0b0edc2c1d9f1135ee2687779d0` | `9607a4d247ce07b7bf7cf76cc64948eba677deef98f19a505fb540ab5f04d509` | `1a81f8957d0af644dc2fb00a9b59b28f127819bed8cdffdb0d7edcf459736a54` | rejected |
| Aggregate point copy, short selector return and local | `a86776739746ae14fca69bd4cc90729583e7134cbfbb5b3f53200285b414a6bd` | `597fe15cb624f8a1c06bf0785136b7d2e8644a9c5f4936b34f4a27f15d105ac5` | `1ad7a09e123a93efdc1ea16451b49765676871fb5b11e0ac2ffd2a215fe63e67` | rejected |
| Aggregate point copy, January `long` selector return, short local | `5d3c695fb508e98af777e90ec3144c5c8abb2d247033ce373d5bb346bc448aaa` | `24be90ad9b6f332a3e848b5603447f86f05172560f66ec4300a1cdd5a721083d` | `300bba8c0c0ea302c17b22c44e2cbb8da3ad2e8031ee7cb9546e84e4faf5fa56` | rejected |
The first artifact proves the indexed scalar spelling is not January's
source shape. It replaces the compact source-base `lea` with three full
displacement loads, adds four bytes before the selector call, and shifts that
relocation from `+262` to `+266`.
The two aggregate artifacts restore the 544-byte envelope and every
relocation through the selector call. Narrowing the receiving local does
produce January's desired post-call order exactly:
`add esp, 0x18; mov ebx, eax`. It also requires a three-byte sign extension
before `actor_change_firing_position`, shifting only that final relocation
from January `+504` to candidate `+507`. Restoring the selector declaration to
`long` does not remove that consumer-side extension. Thus HCEA's short width
is later-build drift, not January's exact ABI.
Both aggregate artifacts retain the other known residual: January loads all
three pursuit-point dwords through one compact base `lea` before any of the
three destination stores, while VC7 interleaves each load and store. The
fresh HCEA source therefore closes the provenance question but supplies no
legal-C spelling that satisfies both scheduling windows simultaneously. The
prior aggregate, scalar-lifetime, representation-copy, alias, helper, field-
order, and scope do-not-repeat list remains in force.
No rejected perform body or perform-owned data is retained. The source was
restored byte-for-byte to its pre-wave Git state and rebuilt normally. The
restored object SHA-256 is
`558369011d594c5e95ef4b43db52e15ebceddae92b87fa88a95b213a75b4f70d`.
The frozen regression gate passes with all eight accepted functions under
`still_exact`, no changed nonexact function, no failure, and no warning.
`action_uncover.obj` remains 8/9 exact and incomplete. Reopen only for a new
January-side source/provenance record or a defined-C control that produces the
compact load-all/store-all point copy without introducing the final
sign-extension tradeoff.
@@ -1,32 +0,0 @@
# action_uncover.obj — opus5 150K lane, wave w2 (2026-09-14)
## Baseline and final
| gate | result |
| --- | --- |
| baseline real file | `== exact 8 residual 1 unwritten 0`, guard passes |
| final real file (unchanged) | `== exact 8 residual 1 unwritten 0`, guard passes, park drift 0, fake scan 0 leads |
`git diff --stat -- source/ai/action_uncover.c` is empty. No credit is claimed.
## `_action_uncover_perform` (parked, instruction-scheduling): SKIPPED-EXHAUSTED
Target 544 / 19 / `ddd3ed044dc0de47`; candidate 544 / 19 / `f6c5eb3588f3efcc`.
The residual was re-disassembled. The only differing window is +0xa2..+0xd3:
- January: `lea edx,[esi+0xb0]`, then loads x/y/z into eax/ecx/edx, then stores x, (surface load), y, (cluster load), enable=1, z, surface, cluster.
- VC7: alternates each point load with its store.
No lever from brief section 9 applies:
- no call inside the window (store/call order);
- equal frame;
- no x87;
- no inline helper;
- one return;
- no accessor-binding difference (same compact lea).
The do-not-repeat lists already cover the remaining candidates: E01-E20, the 25 probe variants of 2026-08-28, and 45 defined-C families. No shape was compiled.
Reopen criterion (unchanged): authentic January source or local records, or a same-compiler donor for the load-all/store-all copy schedule.
@@ -1,94 +0,0 @@
# `action_vehicle.obj` evaluate-seat symbol reconciliation (2026-09-13)
## Scope and ruling
January target file offset `0x000094C0` is authentically
`action_vehicle_evaluate_seat`. `config/symbols.json` therefore maps the old
address placeholder `_code_000094c0` to
`_action_vehicle_evaluate_seat`. This is a symbol-identity correction only;
no function body, prototype, linkage, compiler flag, or source schedule changes
in this packet.
The corrected mapping makes the already reconstructed private caller
`action_vehicle_find_impromptu_seat` strict-exact. The evaluate-seat body
itself remains fuzzy and receives no exact credit.
## Same-build name evidence
The exact January/Halo CE atlas record is:
```text
{"off":"0x4094c0","name":"_action_vehicle_evaluate_seat","lib":"action_vehicle.obj","tier":"exact"}
```
It is record 53 of:
```text
research/halo-symbol-atlas/symbols/halo_ce/4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520.jsonl
```
The file's SHA-256 is
`851802ac508bbcf7cd2896c6cfddd1e0f81510b7b9ef9ea02f39f913c123578e`.
Subtracting the image base `0x400000` places the symbol at target file offset
`0x94C0`, exactly the `config/symbols.json` record being corrected. The
adjacent same-build records independently anchor the object topology:
- `0x4093B0`: `_action_vehicle_desirable`;
- `0x409890`: `_action_vehicle_find_destination`;
- `0x40A1B0`: `_action_vehicle_find_impromptu_seat`.
This direct same-build map, not a behavioral guess, authorizes the production
name and relocation identity.
## Corroboration and limits
Stian's commit `32a94ebf59066e34057e6e6bfe625d64c61b1be5` introduced the
same name after decoding the action-definition table and cross-referencing the
call graph. That is useful independent semantic analysis, but it is not
original-symbol evidence and was not used alone to admit this mapping.
The HCEA atlas also contains exact PDB records for
`action_vehicle_evaluate_seat`, including `0x83821698` in
`567b63bd5f1c5aed0f99b8bc2bb70c9be655d8f6df81d2cbb20c725b33fd6084.jsonl`.
Those records corroborate the spelling and role across builds; they are not
interchangeable January layout evidence.
The supplied `h1_tags/*_symbols.exe` images contribute no name proof here.
They contain references to missing external PDBs, zero COFF symbols, and no
embedded original function-name records. Analysis-generated Ghidra names are
not treated as Bungie symbols.
## Focused verification
Before target-map regeneration, the universal identity-only scan reports the
sole mismatch in the 304-byte caller:
```text
source/ai/action_vehicle::_action_vehicle_find_impromptu_seat
@0x0072 target _code_000094c0 + 0
ours _action_vehicle_evaluate_seat + 0
```
The ordinary focused gate reports 9 exact, 5 residual, and 3 unwritten
functions because its existing split target still carries the old placeholder.
Running the same gate with the authenticated in-memory alias
`action_vehicle_evaluate_seat=code_000094c0` proves the post-regeneration
identity result without disturbing the shared build directory:
```text
EXACT 304 _action_vehicle_find_impromptu_seat
residual 976 _code_000094c0 [size 960!=976, relocs 31!=30, sha]
== exact 10 residual 5 unwritten 2 (of 17 listed)
```
Thus the caller is strict-exact at 292 meaningful / 304 padded bytes with
three matching relocations after the map correction. The current semantic
ledger had already admitted those 292 meaningful bytes through its ordinary
normalized-code proof despite the unresolved callee spelling. This rename
upgrades that existing result to strict relocation identity; it must **not**
be booked again as a new 292-byte campaign gain.
The reconstructed evaluate-seat body is 960 padded bytes against January's
976 and has 31 relocations against 30. It remains a best-source fuzzy body,
earns zero exact bytes here, and must not be described as closed merely because
its original name is now known.
@@ -1,220 +0,0 @@
# ActionVehicle Fable independent intake (2026-09-06)
## Decision
The smallest house-rule-clean packet is **`_action_vehicle_control` only**.
Keep `_action_vehicle_begin` held.
The control packet is three production-source edits for a later owner trial:
1. Add the donor control body and its TU-local
`_action_vehicle_idle_look_type = 4` enum to
`source/ai/action_vehicle.c`.
2. Add `void action_vehicle_control(long actor_index);` to the genuine owner
header, `source/ai/actions.h`, under an `ACTION_VEHICLE.C` prototype block.
3. Remove the now-redundant consumer-local declaration of
`action_vehicle_control` from `source/ai/actions.c`.
Do not copy the whole donor file: it also contains the held begin body. No new
header include, symbol alias, macro, global, static helper, data owner, or
configuration change belongs in the control-only packet.
`_action_vehicle_begin` is held because its HCEA/prior-rejected contract
already had the same three semantics: reset the counter, store
`game_time_get()`, and copy `body_position`. Fable's only material source-shape
change is the `struct vehicle_state_data *state_data` alias. Exactness therefore
comes from a new lvalue topology, not new semantic provenance, and does not
satisfy the explicit 2026-08-21 reopen condition. If begin is ever lawfully
reopened, it additionally needs the genuine `#include "game/game.h"`; the
genuine public declaration of `game_time_get` is `source/game/game.h:171`.
## Why control is a genuine semantic reconciliation
The later-build HCEA source uses the wrong January priorities:
- lock-facing: `_primary_priority_exact_facing` (canonical value 2);
- path-present: `_primary_priority_facing` (canonical value 1).
The January target machine code instead writes 4 and 3. Canonical
`source/ai/actors.h:62-66` authentically names those values
`_primary_priority_locked_aiming` and `_primary_priority_aiming`. The Fable
control body used those two names in its literal first saved source, before any
candidate object existed. This is a code-proven semantic correction to the
HCEA contract, not an alias-only scheduling retry.
The remaining contract is also owner-backed:
- `actors.h:210-214` owns `_direction_specification_movement = 0` and
`_direction_specification_vector = 4`;
- `actors.h:265` owns `actor_get`, `actors.h:1322` owns `actor_data`, and
`actors.h:1157` owns the `actor_path_has_path` prototype
(`source/ai/actor_moving.c:310` defines it);
- `actions.h:246` owns `struct vehicle_state_data`, and its action-data union at
`actions.h:389` owns the `vehicle` arm;
- the schema has `lock_facing` at vehicle offset 44 and
`destination_facing` at offset 60. January reads actor offsets `0xC8` and
`0xD8`, fixing the vehicle arm base at `0x9C` and independently confirming
both fields;
- `actors.h:582` owns the short `idle_look_type`. The value 4 is present as a
TU-local action constant in both `source/ai/action_charge.c:127` and
`source/ai/action_avoid.c:35`; no shared idle-look enum exists.
The target control has 175 meaningful / 176 padded bytes and three
relocations: `DIR32 _actor_data` at `+4`, `REL32 _datum_get` at `+17`, and
`REL32 _actor_path_has_path` at `+89`. Its normalized SHA-256 is
`81612c85cf900b15c2de8ea6d52646f5727a96fbea5e2f13018e6f9613fcd293`.
The first code-producing Fable candidate and committed donor object are strict
equal on all bytes and relocation identity.
Both target and candidates define `_action_vehicle_control` as external
function symbols (`storage=2`, `type=0x20`, section-relative value 0). There is
no private/static implementation symbol to rename.
## Literal-first versus first-code-producing provenance
The Fable report's phrase "EXACT - first compile" is accurate only when
"compile" means the first code-producing compile:
- `scratch/_gate_16992.c`, created `2026-09-04T22:46:23.2697770Z`, is the
literal first saved source. It already contains the final state alias,
locked-aiming/aiming branches, direction choices, and tail stores, but uses
undeclared `_idle_look_combat`. No `_gate_16992.obj` is preserved.
- `scratch/_gate_35296.c`, created
`2026-09-04T22:46:38.1663797Z`, differs only by adding the TU-local
`_action_vehicle_idle_look_type = 4` declaration and renaming that one use.
`_gate_35296.obj` is the first preserved code-producing object, and control
is strict exact there.
- The parse/name repair is disclosed. It did not change control flow, field
access, priorities, call identity, or emitted runtime data, and is accepted
here as pre-code-generation semantic-name closure backed by the two sibling
owners above. It must not be described as literal-first-source exact.
- `_gate_35296.c` and the committed donor `source/ai/action_vehicle.c` are the
same Git blob, `0e89db5e7b3072e1ebb5220f7d5a436428b57d69`.
The earlier canonical ledger is not superseded silently. It records the
one-shot HCEA control miss as normalized SHA
`3f96cbf0cdc3a58d1013ac2d44df261acc0d3b68c37ebab639956ddfa1a046ad`
against the January hash above and requires new authenticated contract
evidence. The 4/3 priority correction is that new evidence for control. No
equivalent semantic delta exists for begin.
## Caller and owner closure
The complete current source search finds only one consumer:
- `source/ai/actions.c:764-765` declares control locally;
- `source/ai/actions.c:967` stores its address in the vehicle action callback
table.
Current base `actions.obj` carries one `DIR32` reference at `.rdata+536` to an
undefined external function symbol `_action_vehicle_control`; January split
`actions.obj` carries the same reference at `.rdata+536`. The corresponding
begin reference is `.rdata+524`, but begin remains out of scope. No other
current base or January split object contains either callback symbol.
`actions.c` already includes `actions.h`, so moving the control declaration to
the genuine header and removing the consumer-local declaration leaves the
same signature visible at the callback table. Adding a new `action_vehicle.h`
would be an artificial facade and is not admissible. Leaving only a C-local
declaration is likewise not owner closure.
Because `actions.h` is shared, a later trial must freeze and compare all of its
actual consumers plus `actions.obj` and `action_vehicle.obj`; this intake did
not query Ninja or compile anything.
## COFF delta
Against the current canonical base object, `_gate_35296.obj`,
`ng_action_vehicle.obj`, and the donor final object all add exactly two named
external code owners: begin and control. For the proposed subset, admit only
control.
All seven inherited exact owners remain exact:
`_action_vehicle_update`, `_action_vehicle_flush_structure_indices`,
`_point_from_line2d`, `_distance_squared2d`, `_distance2d`,
`_negate_vector2d`, and `_object_get_bounding_sphere`.
The candidate has no COMMON symbols, BSS section, new writable/read-only data
owner, or named static code helper. Its only additional undefineds are
`_actor_path_has_path` and `_game_time_get`; the latter belongs solely to the
held begin body. Section-symbol `.text` entries with storage 3 are compiler
section bookkeeping, not private helpers. The local enum emits no symbol or
storage.
## Pinned artifacts
Canonical frontier: commit
`c2561af50c7fda9c5ea3326b35b4ba10f90d17f5`.
- January target:
`build/split/source/ai/action_vehicle.obj`, 11,826 bytes,
SHA-256 `49F00A0115CECF578E51102FC33F665F876A4FAFCB78EE3B53520545406EF769`.
- Current canonical candidate baseline:
`build/base/source/ai/action_vehicle.obj`, 3,084 bytes,
SHA-256 `84FE91F3F197946CA8EC1EED569AFBF77774B44B6F2CEAE4A8540396979E4120`.
- Current callback consumer:
`build/base/source/ai/actions.obj`, 56,841 bytes,
SHA-256 `05FB549A9F707E1E97EE9A44D11CF9AE7AC534752A00ECF22652C60C3374D9D4`;
January `build/split/source/ai/actions.obj`, 53,049 bytes,
SHA-256 `624BB4506ADFF68E9888A20E44042F28EC7F7E4FDC33115C64C25BEB1270B5B5`.
- Canonical source:
`source/ai/action_vehicle.c`, SHA-256
`22560B33AD7A0250E1ADBD5F500C52DF4102FEAC97E2A280E1E2D1F29428529F`,
Git blob `6f689f90311fc56f1f3c0d58c593df81390efdca`.
- Canonical owner header:
`source/ai/actions.h`, SHA-256
`EA1BEC48DFCC5BDAD4DC01137E8C9D4A1F6AA878EE6F3EAEF4D0EF13E64AEE62`,
Git blob `5df907fab02c19fd14904ede814923362e2309f5`.
- Canonical consumer source:
`source/ai/actions.c`, SHA-256
`F6347EB2573A36F12CF0C9253DA66504441B8743269E8847FD3866770571D7EC`,
Git blob `b1f6be227e232de1a7a5dec92710b5dd8aaea3e6`.
- Canonical schema/API source:
`source/ai/actors.h`, SHA-256
`411A70414C9ED5605A6AB01D14503D7E4B82F2B04BE1D12865CD6D96F45D0B8A`,
Git blob `ea715dbedf141721eb4c1bf4585bab5cb7c905be`.
- Main prior ledger:
`docs/object_matching_logs/action_vehicle_obj_jonas_one_leaf_20260821.md`,
SHA-256 `C01350FD8E5C4CBAF48026405D7480E2297FAB4CC307BA0D322693E931627372`.
- HCEA begin source:
`C:/Users/isabe/Documents/Codex/2026-07-13/i-w/research-cache/halocea-full-blobs-20260830/src/blam/ai/action_vehicle_begin.c`,
SHA-256 `4820BFAFAACD281D4F1DBA31312158B158AF55AAD5DAFF4D04E98D247763A3B0`.
- HCEA control source:
`C:/Users/isabe/Documents/Codex/2026-07-13/i-w/research-cache/halocea-full-blobs-20260830/src/blam/ai/action_vehicle_control.c`,
SHA-256 `96F8D95AA428D7AA9F1535A4C2847B8CDA6D97F8C7467E02B883B2C1924C3716`.
Donor frontier: commit
`f9dd42218b3325e1b3a63d7860e5309f69fd3d8e`, parent
`96086c9c4c187dd539af13bdaa9252d6e46ad8f6`.
- Donor report: `scratch/reports/action_vehicle.md`, SHA-256
`36D66D460BD2A3F7E0D7C1C623F76A580BA012B22E979AA0A8226E3E3A4043C6`.
- Donor facts: `scratch/facts/action_vehicle.md`, SHA-256
`24D06EEAC322DE860653F348308B302404CEEF1B0B9DD31C4C7D6A927DAC052F`.
- Literal first source: `scratch/_gate_16992.c`, 4,808 bytes, SHA-256
`5300FDEB147AA82B39298432663C63F4D7D8E12452B36B386524F7849F7D0F8D`,
Git blob `ea342b9e03c72d12584831d6fef4f2a95d3c8f99`.
- First code-producing source: `scratch/_gate_35296.c`, 4,869 bytes,
SHA-256 `F8F6B69F62B253478B494FE68AF1DABBE987175660E0C41C976EA23F54266F99`,
Git blob `0e89db5e7b3072e1ebb5220f7d5a436428b57d69`.
- First code-producing object: `scratch/_gate_35296.obj`, 3,651 bytes,
SHA-256 `3AAC2DB9C10B2A07E9614358D93F416E5C1FBC7B8EAAD75EA355BF10F6C8E66B`.
- Saved later natural object: `scratch/ng_action_vehicle.obj`, 3,651 bytes,
SHA-256 `702BFF861ACA3E3D961157DCDBD7D7196A812D5F5EEA3CDFD5A06F9EDD721431`.
- Committed donor source: `source/ai/action_vehicle.c`, SHA-256
`3E5997DDB9769E1E80682B21E117986BACC2DD1BFD238877ACA392F3A8990A7F`,
Git blob `0e89db5e7b3072e1ebb5220f7d5a436428b57d69`.
- Donor final object: `build/base/source/ai/action_vehicle.obj`, 3,667 bytes,
SHA-256 `0736AE24E228A91848D26B73F8FE86D72EC9B7ACEA9FF42ACA9337D349E4D207`.
The differing raw candidate-object hashes are explained by producer/debug
record paths and timestamps; the hardened function comparison is strict equal
for both proposed functions and reports zero inherited losses.
## Scope statement
This was read-only over canonical production files and the donor. No source,
header, configuration, object, Ninja state, or donor file was changed; no
compile, configure, Ninja, or commit command was run. This report is the only
new file.
@@ -1,139 +0,0 @@
# ActionVehicle: Fable control reconciliation (2026-09-06)
## Scope and verified runtime gate
The one genuine `action_vehicle_control` correction matches on its first
canonical compile and in the ordinary Ninja output. It adds **175 meaningful
/ 176 padded exact code bytes** and one function. ActionVehicle advances
**7 -> 8 / 17 exact**, with zero residual and nine unwritten functions; it is
not a complete object. `action_vehicle_begin` is deliberately not imported.
The local strict sweep is **6,256 exact / 8,245**, gaining precisely this
function with no inherited exact losses. Official local totals are
**916,726 / 2,198,102 meaningful exact code bytes**, **6,210 / 11,060 credited
functions**, **391 / 833 Matching objects**, and unchanged **2,061,020 /
4,176,062 data bytes**. The preceding published geosphere commit is
`b90402d960a75d312f2a3895c43850e9cca4ccb8` on both authorized refs.
Independent runtime checks now pass. These packet-local numbers precede the
independent HUD helper's four meaningful bytes and are not alone a claim of
publication. The joint local checkpoint is 916,730 meaningful bytes.
## Why this is a legitimate contract correction
The prior 2026-08-21 HCEA-based control attempt used the canonical facing
priorities 2/1. January writes **4/3**, independently named in `actors.h` as
`_primary_priority_locked_aiming` and `_primary_priority_aiming`. Fable's
literal first saved source already contains this semantic correction.
The only later pre-emission repair supplies a semantic TU-local name for
the proven idle-look value 4; the first preserved code-producing object is
exact. That parse/name correction is disclosed, not misrepresented as
literal-first-source exactness. Root made no post-compile source variants.
Begin has no analogous contract correction: its stronger donor shape only
introduces a local state pointer over already-correct reset/time/copy logic.
Its old source-shape boundary therefore remains held. Do not import the
whole donor source or count both claimed 256 padded bytes.
The independent source/history/COFF intake is
`action_vehicle_obj_fable_control_intake_20260906.md`. Root read all seven
prior ActionVehicle ledgers, the donor report/facts and complete control
diff, the HCEA control body, and the genuine current type/enum/API owners.
The user-authorized Fable donor remains read-only at
`C:/halo-worktrees/fable-50k-small-families-20260904`, tip
`f9dd42218b3325e1b3a63d7860e5309f69fd3d8e`.
## Genuine owner boundary and full consumer verification
The sole prerequisite moves the unchanged public `action_vehicle_control`
prototype from the consuming `source/ai/actions.c` to the genuine owning
`source/ai/actions.h`, under its normal source-family section. The existing
header owns `vehicle_state_data`; `actors.h` already supplies typed actor
access, priorities, direction kinds and the real `actor_path_has_path` API.
No new facade header, duplicate C-local prototype, global, alias, schema,
compiler option or configuration exception is introduced.
Before that edit root captured all **47 actual Ninja consumers**, both
sources, the header, January target, configuration and all 572 base hashes.
The schema-only build preserves all 6,255 exact functions. Across the full
runtime closure, **3,107 sections are unchanged and 53 differ only in
compiler-local-label spelling**. There is no lost/changed runtime content,
new COMMON storage, or new `point_from_line3d` definition/reference.
After adding the body, the same inherited 3,160 runtime sections survive.
ActionVehicle alone adds the intended control section. It adds no helper,
data or BSS owner. Existing helper copies are unchanged, not new progress.
The genuine Actions callback table retains its `DIR32` control reference
at `.rdata+536`; no artificial caller keeps the body alive.
Source uses `actor_get`, authentic named fields and enums, a real vector
copy, explicit `FALSE` flags and terminal return, with one parameter per
line. The locked branch copies the saved facing and skips path lookup;
the unlocked branch chooses aiming/movement only when the real path API
succeeds. All three branches set idle look and clear the six genuine
shoot/movement order flags. No assembly, punning, raw offset, manual
helper expansion, register/volatile steering or fake matching is introduced.
## Exact and frozen identity
Control target/first/actual agree on normalized SHA-256
`81612c85cf900b15c2de8ea6d52646f5727a96fbea5e2f13018e6f9613fcd293`.
All three ordered relocations agree: `DIR32 _actor_data` at +4,
`REL32 _datum_get` at +17, `REL32 _actor_path_has_path` at +89.
The target symbol is an external cdecl public function, not a private
address-named body requiring an invented ABI.
All paths below are canonical-relative:
| Artifact | SHA-256 |
| --- | --- |
| `scratch/action-vehicle-fable-control-before-20260906.json` | `fa933a6deb8788e398a06037db7730cace93f8ab47a7efae636b012ae97430d4` |
| `scratch/action-vehicle-fable-control-schema-20260906.json` | `6ddf088f8265e4fc707d16a76711df93682d5406965628a4a85bbb22b9462289` |
| `scratch/action-vehicle-fable-control-final-20260906.json` | `081017c7ca9d82bf5b240e64a27115ec3d187901a7f2b04f77e291182b8ddf7b` |
| frozen January object | `49f00a0115cecf578e51102fc33f665f876a4fafcb78ee3b53520545406ef769` |
| `scratch/action-vehicle-fable-control-first-20260906.obj` | `6bc330954c197c8a5dc54c3ed269e39b5d894edafc4d0a4f265b235686b46c2f` |
| frozen actual object | `0f80fc38c93779cce5e03eceea3dffa9788386a497300ac6c18e307c66c4f82c` |
| frozen ActionVehicle source | `a45f810e182ca07d962da22deb8f9ea0063382650205248d74f66530f7a7725b` |
Capture: `scratch/action_vehicle_fable_control_capture_20260906.py`.
Full runtime comparisons:
`scratch/action-vehicle-fable-control-{schema,final}-runtime-20260906/summary.json`.
Final stable snapshot:
`scratch/action-vehicle-fable-control-final-stable-20260906.json`.
Build/gate logs: `scratch/action-vehicle-fable-control-{first,schema-build,final-build}-20260906.log`.
## Independent execution
Root reviewed the complete public oracle and its 24 fixture-free tests, then
replayed it against the frozen January, first reconciled candidate and actual
owner-correct object. All **18 cases pass for each object**, with identical
trace SHA-256
`87fe79ca103e97d9c177ba4def03ab82e8e91b2ce4c9f41253b761cba9341ed7`.
All four pinned real-byte mutants are detected by both semantic checks and
target/candidate comparison.
The oracle checks the entire seeded 0x800-byte actor-memory envelope, precise
write order/values, genuine API call order and forwarded handle bits, caller
return/argument window, stack balance, clear DF and preserved nonvolatile
registers. Locked facing exercises raw vector-word copies, including unusual
bit patterns; unlocked paths vary the boolean AL byte while poisoning upper
EAX and volatile registers. There is no floating-point arithmetic in this body.
The envelope is a test boundary, not a `sizeof(actor_datum)` claim. Arbitrary
and NONE-shaped handles are forwarding tests with the datum stub returning
the modeled owner; they do not claim those handles are valid engine data.
The void function's volatile registers are differential observations only.
Only genuine `datum_get` and `actor_path_has_path` dependencies are stubbed.
| Artifact | SHA-256 |
| --- | --- |
| `tools/audit/action_vehicle_control_runtime_differential.py` | `1f6a27acb013afe57f9ebf43a0020e04fb0720dd56f07653e21dd61ce3af2f79` |
| `tools/test_action_vehicle_control_runtime_differential.py` | `cc4f093dec9026fbff5c4495f70ccf3e67915bbed985f46d5952b68478df7327` |
| `scratch/action-vehicle-control-public-runtime-root-20260906.json` | `71a50adc3ddea8014a8bf19dc538d970966b47534485db21b017f3e60d455b34` |
The subsequent ModelAnimations owner-only experiment is completely restored:
all 1,523 sections across 13 consumers match the pre-trial state, including
Units. No graph implementation was compiled. Its rejected boundary is in
`model_animations_obj_fable_node_owner_trial_rejected_20260906.md`.
Final joint publication checks are recorded in the reconciliation backlog;
remote refs must be verified against the resulting commit, not this ledger.
@@ -1,128 +0,0 @@
# Action-vehicle bounding-sphere recovery (2026-08-27)
## Result
The first and only ordinary code-producing candidate makes
`_object_get_bounding_sphere` strict exact in
`source/ai/action_vehicle.obj`. The object advances from 6/17 to 7/17 exact
functions and from 165/4,909 to 294/4,909 meaningful code bytes. It remains
`NonMatching`; no data, completion-label, semantic-exception, or
parked-function credit is claimed.
All six inherited exact owners remain exact: `_action_vehicle_update`,
`_action_vehicle_flush_structure_indices`, `_point_from_line2d`,
`_distance_squared2d`, `_distance2d`, and `_negate_vector2d`.
## Frozen target and provenance evidence
January defines an external cdecl
`void(long, real_point3d *, real *)` owner with 129 meaningful and 144 padded
bytes. It retrieves a typed all-mask object, asserts non-null `center` and
`radius` outputs with canonical `objects.h` line metadata 535 and 536, copies
the named three-float bounding-sphere center, copies the radius, and returns.
Its nine relocations are fully fixed: `REL32` to
`_object_get_and_verify_type`; two references to the canonical objects-header
path literal; references to the `center` and `radius` assertion literals; and
two ordered `REL32` pairs to `_display_assert` and `_system_exit`.
The canonical inline implementation in `source/objects/objects.h`
independently supplies the exact signature, typed `object_get` access path,
assertion strings and line numbers, named fields, copy order, and explicit
terminal return. The HCEA later-build source at commit
`570c83fd9c365dad6f2a3e7041705d5b84c7847c` independently corroborates the
typed object datum and sphere-field semantics; its source blob is
`3bed3193ca7ec40b5f6c81955fc1452b7b5daaf9`. HCEA omits January's assertions
and is semantic/type provenance only; January PC COFF remains the sole padded
byte and relocation authority.
The complete Claude campaign tree, Stian sources and verification metadata,
all current object ledgers, and all repository revisions were searched before
emission. None contains a prior action-vehicle candidate or fixed-point record
for this owner. The existing global and HUD copies authenticate the shared
contract but do not claim this translation unit's COMDAT ownership.
The adjacent `_action_vehicle_begin` and `_action_vehicle_control` owners were
excluded before emission because the 2026-08-21 action-vehicle ledger already
preserves their natural HCEA-backed misses and requires new January evidence
before any retry.
## Retained typed source and house-rule audit
A translation-unit-local macro rename preserves the canonical inline owner
under a private name while allowing January's external COMDAT to be defined
from the same typed body:
```c
#define object_get_bounding_sphere object_get_bounding_sphere_inline
#include "actors.h"
#undef object_get_bounding_sphere
void object_get_bounding_sphere(
long object_index,
real_point3d *center,
real *radius)
{
struct object_datum *object = object_get(object_index);
match_assert("..\\objects\\objects.h", 535, center);
match_assert("..\\objects\\objects.h", 536, radius);
*center = object->object.bounding_sphere_center;
*radius = object->object.bounding_sphere_radius;
return;
}
```
Every parameter is on its own line, the C89 declaration precedes statements,
the canonical typed object-access macro replaces any raw access cast, the body
uses named fields, and the void function ends with an explicit return. It
contains no assembly, `register`, `volatile`, optimizer barrier, pragma,
intrinsic, attribute, raw address, byte-offset access, pointer/integer
reconstruction, pointer or union pun, undefined behavior, synthetic anchor,
object patch, comparator exception, or byte forcing.
## Immutable artifacts and strict comparison
- Base commit: `6d23d0f7bc976fcd24480411ee6e4878a758d8df`.
- Pre-wave source blob: `96eb18be20f7f1aa3e40e65063e9d65f5b57650c`.
- Frozen retained source blob: `6f689f90311fc56f1f3c0d58c593df81390efdca`.
- January split object SHA-256:
`49F00A0115CECF578E51102FC33F665F876A4FAFCB78EE3B53520545406EF769`.
- Baseline candidate object SHA-256:
`0B473CDE87C91DB8E453A108F777B1138C5556CC44B31303F509AEA8B634B048`.
- First-shot object SHA-256:
`4A44DE94FA7BC403290B54809F4DC08EAD97D2DC6DC50EC57724626599A96CE4`.
- Compiler: Microsoft XDK 3911 CL 13.00.9254.1 with the unchanged natural
`/O2 /Oy- /DDEBUG /Dxbox` flags and include graph.
The hardened comparator proves all 144 padded bytes, all nine relocation
addresses, types, destinations, addends, and ownership exact. Target and
candidate share normalized SHA-256
`65d07f3bd1140a75fa9122ef9ec31f739c4a744a117752b38f576b2493457fef`.
The exact-set comparison reports only `_object_get_bounding_sphere` as newly
exact, preserves all six inherited exact owners, and reports no loss.
## One-shot discipline and validation
`git diff --check` and the production compiler's no-output `/Zs` check passed
before emission. Exactly one normal selected-object compiler edge produced the
retained artifact. No assertion spelling, macro, field, declaration, include
topology, flag, or compiler retry occurred.
The complete graph and campaign gates pass:
- 377/833 complete objects overall and 275/468 for Halo;
- 511,119/2,198,102 meaningful code bytes and 4,240/11,060 functions overall;
- 498,205/1,770,166 Halo code bytes and 4,073/7,574 Halo functions;
- data unchanged at 1,836,762/4,176,062 overall;
- semantic audit: 470 units, 4,352 functions evaluated, 4,206 semantic exact,
4,266 accepted exact, and zero unit errors;
- admission: zero candidates, contradictions, or revocations;
- parks: three active, zero stale, zero invalid;
- tooling tests: 179/179 pass, with only the inherited pytest cache-permission
warning.
No protected source, configuration entry, completion label, push, amend, or
history rewrite is involved.
@@ -1,112 +0,0 @@
# Action-vehicle 2D-distance recovery (2026-08-27)
## Result
The first and only ordinary code-producing candidate makes `_distance2d`
strict exact in `source/ai/action_vehicle.obj`. The object advances from 4/17
to 5/17 exact functions and from 95/4,909 to 132/4,909 meaningful code bytes.
It remains `NonMatching`; no data, completion-label, semantic-exception, or
parked-function credit is claimed.
The four inherited exact owners, `_action_vehicle_update`,
`_action_vehicle_flush_structure_indices`, `_point_from_line2d`, and
`_negate_vector2d`, remain exact.
## Frozen target and provenance evidence
January defines an external cdecl `real(real_point2d const *, real_point2d
const *)` owner with 37 meaningful and 48 padded bytes. It subtracts the two
point components, squares and adds them on the x87 stack, applies `fsqrt`, and
returns the result. The function has no relocation.
The canonical inline implementation in `source/math/real_math.h` supplies the
same signature and computes the magnitude of the vector from `a` to `b`. The
HCEA later-build source at commit
`570c83fd9c365dad6f2a3e7041705d5b84c7847c` independently spells the same two
component differences and square root. Its source blob is
`3757f43b014e35fe4945b9b678c7857cec58613b`. HCEA supplies semantic and type
provenance only; January PC COFF remains the sole padded-byte authority.
The complete Claude campaign tree, Stian action-vehicle sources and
verification metadata, all current ledgers, and all repository revisions were
searched before emission. None contains an earlier action-vehicle definition,
candidate artifact, or fixed-point record for this owner. The immediately
preceding point-from-line ledger explicitly excluded it to preserve the present
independent one-shot boundary.
`_distance3d` and `_distance_squared2d` remained outside this wave. Their
different x87 stack shapes require separate frozen evidence and production
shots.
## Retained typed source and house-rule audit
The existing `actions.h` include brings in the canonical inline math header.
A translation-unit-local macro rename retains the inline spelling privately
while allowing January's external owner to be defined from the canonical
body:
```c
#define distance2d distance2d_inline
#include "actions.h"
#undef distance2d
real distance2d(
real_point2d const *a,
real_point2d const *b)
{
real_vector2d v;
return magnitude2d(vector_from_points2d(a, b, &v));
}
```
Every parameter is on its own line, the C89 local declaration precedes the
statement, typed helpers and named fields provide the arithmetic, and the
function ends with an explicit value return. It contains no assembly,
`register`, `volatile`, optimizer barrier, pragma, intrinsic, attribute, raw
address, byte-offset access, pointer/integer reconstruction, pointer or union
pun, undefined behavior, synthetic anchor, object patch, comparator exception,
or byte forcing.
## Immutable artifacts and strict comparison
- Base commit: `608181dec91c7d657c503d864c115354ed9946a7`.
- Pre-wave source blob: `d6af877431ebe5ae8d02c961bb091aa2f6d17683`.
- Frozen retained source blob: `8b35c87381770b48b7a6118555b6a60c61b54757`.
- January split object SHA-256:
`49F00A0115CECF578E51102FC33F665F876A4FAFCB78EE3B53520545406EF769`.
- Baseline candidate object SHA-256:
`4A2BBD6AFD7DB99DC2636DE3AD3F590B0B52A3D969D450347FB3F0B366BC75F1`.
- First-shot object SHA-256:
`5901C77965196428278E8290250BAB1B493581466B74A2FF60D5CF70CDEE0285`.
- Compiler: Microsoft XDK 3911 CL 13.00.9254.1 with the unchanged natural
`/O2 /Oy- /DDEBUG /Dxbox` flags and include graph.
The hardened comparator proves all 48 padded bytes and zero-relocation
identity exact. Target and candidate share normalized SHA-256
`3c78450b16a81d94946b346e70e6b6c0b6d43ce353c74c1a18cd64d8e37eff9d`.
The exact-set comparison reports only `_distance2d` as newly exact, preserves
all four inherited exact owners, and reports no loss.
## One-shot discipline and validation
`git diff --check` and the production compiler's no-output `/Zs` check passed
before emission. Exactly one normal selected-object compiler edge produced the
retained artifact. No expression, helper spelling, declaration, include
topology, flag, or compiler retry occurred.
The complete graph and campaign gates pass:
- 377/833 complete objects overall and 275/468 for Halo;
- 510,957/2,198,102 meaningful code bytes and 4,238/11,060 functions overall;
- 498,043/1,770,166 Halo code bytes and 4,071/7,574 Halo functions;
- data unchanged at 1,836,762/4,176,062 overall;
- semantic audit: 470 units, 4,350 functions evaluated, 4,204 semantic exact,
4,264 accepted exact, and zero unit errors;
- admission: zero candidates, contradictions, or revocations;
- parks: three active, zero stale, zero invalid;
- tooling tests: 179/179 pass, with only the inherited pytest cache-permission
warning.
No protected source, configuration entry, completion label, push, amend, or
history rewrite is involved.
@@ -1,138 +0,0 @@
# Action-vehicle 3D-distance fixed point (2026-08-27)
## Result
Two separately frozen, evidence-backed waves for `_distance3d` were cleanly
rejected. Both candidates preserve all five inherited exact functions in
`source/ai/action_vehicle.obj`, match January's 64-byte padded extent, and
contain zero relocations, but neither matches the target's x87 instruction
schedule. All candidate source was removed and the exact pre-wave source blob
was restored.
The object therefore remains at 5/17 exact functions and 132/4,909 meaningful
code bytes. No function, data, completion-label, semantic-exception, or
parked-function credit is claimed.
## Frozen target and provenance evidence
January defines an external cdecl `real(real_point3d const *, real_point3d
const *)` owner with 51 meaningful and 64 padded bytes. It loads the three
component differences, squares `x`, then `y`, accumulates those two, squares
and accumulates `z`, exchanges the result into the expected x87 slot, applies
`fsqrt`, drops the two residual stack values, and returns. It has no
relocation. Its normalized SHA-256 is
`4cdb6852c1705b5f4af050b829c52d1342a122dccb39f2291f477c6829908b84`.
The canonical inline implementation in `source/math/real_math.h` supplies the
fixed signature and nested vector/magnitude semantics. The independent HCEA
later-build source at commit `570c83fd9c365dad6f2a3e7041705d5b84c7847c`
spells three scalar differences followed by the left-associated sum of their
squares. Its source blob is
`0918df559cfdfb35e458f74fc6ebd1592f9eda97`. HCEA is semantic and source-shape
provenance only; January PC COFF remains the sole padded-byte authority.
The complete Claude campaign tree, Stian action-vehicle source and verification
metadata, every current ledger, and all repository revisions were searched.
None provides another January-authenticated source shape or a prior attempt for
this owner.
## Wave one: canonical nested helper
The first frozen wave used the canonical inline body under a source-local macro
rename:
```c
real distance3d(
real_point3d const *a,
real_point3d const *b)
{
real_vector3d v;
return magnitude3d(vector_from_points3d(a, b, &v));
}
```
It emits the same loads, three differences, 64-byte padded extent, and zero
relocations, but squares `z`, then `x`, then `y`. Its normalized SHA-256 is
`01d7809ae796fc6e9a4a6c2ca73f1e156770bdcff33dd17fc212c50d7279a841`.
The immutable candidate object SHA-256 is
`995D0A95996A30F24DFA5D09CD6CDE66D1AC8429AD61F14F1B0C93929C95003C`.
The emitted x87 order and the independent HCEA scalar spelling constituted
genuine new evidence, so the canonical source was restored before opening a
separate second wave.
## Wave two: donor-derived scalar topology
The second frozen wave used the HCEA scalar topology while retaining current
typed names and the canonical square-root helper:
```c
real distance3d(
real_point3d const *a,
real_point3d const *b)
{
real dx = b->x - a->x;
real dy = b->y - a->y;
real dz = b->z - a->z;
return square_root(dx*dx + dy*dy + dz*dz);
}
```
It also emits the same loads, differences, extent, and zero-relocation shape,
but squares `z`, then `y`, then `x`. Its normalized SHA-256 is
`21014f2a01d8cc834186684d7f719a227bf489bc06c34d4890f9b4a862f356f5`.
The immutable candidate object SHA-256 is
`245CE326C975C10738CA9B8E74CF7ECD07E7A6C419C31DA7029C8A667FA55B61`.
No further retry is justified by current evidence. Reopen this owner only with
a new January-era source donor, compiler intermediate evidence, or a lawful
source topology independently explaining the target's exact x87 ownership and
drop order.
## Immutable baseline and restoration
- Base commit: `6076c6b4953ded466b8f280fc5177999de537d2e`.
- Pre-wave and restored source blob:
`8b35c87381770b48b7a6118555b6a60c61b54757`.
- Canonical-wave source blob:
`3dba79474fb4e09b49778ac577f78c27e08654b6`.
- Scalar-wave source blob:
`1ce486341aed567b5b8b77198d74156e28bf5387`.
- January split object SHA-256:
`49F00A0115CECF578E51102FC33F665F876A4FAFCB78EE3B53520545406EF769`.
- Frozen baseline candidate object SHA-256:
`5901C77965196428278E8290250BAB1B493581466B74A2FF60D5CF70CDEE0285`.
- Compiler: Microsoft XDK 3911 CL 13.00.9254.1 with the unchanged natural
`/O2 /Oy- /DDEBUG /Dxbox` flags and include graph.
Each wave passed `git diff --check` and a no-output `/Zs` parse before its one
normal code-producing compile. Neither used assembly, `register`, `volatile`,
optimizer barriers, pragmas, intrinsics, attributes, raw addresses,
pointer/integer reconstruction, pointer or union puns, undefined behavior,
synthetic anchors, object patches, comparator exceptions, or byte forcing.
After restoration, the complete exact-set comparison again contains exactly
`_action_vehicle_update`, `_action_vehicle_flush_structure_indices`,
`_point_from_line2d`, `_distance2d`, and `_negate_vector2d`, with no lost or
new owner.
## Validation
The restored complete graph and campaign gates pass at the unchanged campaign
totals:
- 377/833 complete objects overall and 275/468 for Halo;
- 510,957/2,198,102 meaningful code bytes and 4,238/11,060 functions overall;
- 498,043/1,770,166 Halo code bytes and 4,071/7,574 Halo functions;
- data at 1,836,762/4,176,062 overall;
- semantic audit: 470 units, 4,350 functions evaluated, 4,204 semantic exact,
4,264 accepted exact, and zero unit errors;
- admission: zero candidates, contradictions, or revocations;
- parks: three active, zero stale, zero invalid;
- tooling tests: 179/179 pass, with only the inherited pytest cache-permission
warning.
No protected source, configuration entry, completion label, push, amend, or
history rewrite is involved.
@@ -1,109 +0,0 @@
# Action-vehicle squared 2D-distance recovery (2026-08-27)
## Result
The first and only ordinary code-producing candidate makes
`_distance_squared2d` strict exact in `source/ai/action_vehicle.obj`. The
object advances from 5/17 to 6/17 exact functions and from 132/4,909 to
165/4,909 meaningful code bytes. It remains `NonMatching`; no data,
completion-label, semantic-exception, or parked-function credit is claimed.
All five inherited exact owners remain exact: `_action_vehicle_update`,
`_action_vehicle_flush_structure_indices`, `_point_from_line2d`,
`_distance2d`, and `_negate_vector2d`.
## Frozen target and provenance evidence
January defines an external cdecl `real(real_point2d const *, real_point2d
const *)` owner with 33 meaningful and 48 padded bytes. It loads the two
component differences, squares `x`, squares `y`, accumulates them, drops the
residual x87 value, and returns. The function has no relocation. Its byte body
is exactly the non-square-root prefix of the already accepted adjacent
`_distance2d` owner.
The canonical inline implementation in `source/math/real_math.h` supplies the
same signature and computes the squared magnitude of the vector from `a` to
`b`. The HCEA later-build source at commit
`570c83fd9c365dad6f2a3e7041705d5b84c7847c` independently spells the two
component differences and their sum of squares. Its source blob is
`c99e46c512b4f42215c6bf887b00b63d51084011`. HCEA supplies semantic and type
provenance only; January PC COFF remains the sole padded-byte authority.
The complete Claude campaign tree, Stian action-vehicle source and verification
metadata, all current ledgers, and all repository revisions were searched
before emission. None contains an earlier action-vehicle definition,
candidate artifact, or fixed-point record for this external owner. Mentions in
other object ledgers concern independent translation-unit copies and confer no
ownership here.
## Retained typed source and house-rule audit
The existing `actions.h` include brings in the canonical inline math header.
A translation-unit-local macro rename retains that inline spelling privately
while defining January's external owner from the canonical body:
```c
#define distance_squared2d distance_squared2d_inline
#include "actions.h"
#undef distance_squared2d
real distance_squared2d(
real_point2d const *a,
real_point2d const *b)
{
real_vector2d v;
return magnitude_squared2d(vector_from_points2d(a, b, &v));
}
```
Every parameter is on its own line, the C89 local declaration precedes the
statement, typed helpers and named fields carry the arithmetic, and the
function ends with an explicit value return. It contains no assembly,
`register`, `volatile`, optimizer barrier, pragma, intrinsic, attribute, raw
address, byte-offset access, pointer/integer reconstruction, pointer or union
pun, undefined behavior, synthetic anchor, object patch, comparator exception,
or byte forcing.
## Immutable artifacts and strict comparison
- Base commit: `9b60e35bab6620162e3e985960a693e9d48d7dce`.
- Pre-wave source blob: `8b35c87381770b48b7a6118555b6a60c61b54757`.
- Frozen retained source blob: `96eb18be20f7f1aa3e40e65063e9d65f5b57650c`.
- January split object SHA-256:
`49F00A0115CECF578E51102FC33F665F876A4FAFCB78EE3B53520545406EF769`.
- Baseline candidate object SHA-256:
`3502C791766695EDF523005BD35BD478CA266EA3930809B4D1A345C221A6DCC4`.
- First-shot object SHA-256:
`0B473CDE87C91DB8E453A108F777B1138C5556CC44B31303F509AEA8B634B048`.
- Compiler: Microsoft XDK 3911 CL 13.00.9254.1 with the unchanged natural
`/O2 /Oy- /DDEBUG /Dxbox` flags and include graph.
The hardened comparator proves all 48 padded bytes and zero-relocation
identity exact. Target and candidate share normalized SHA-256
`e12f5936181ec0b4f78951a003a60fa0a613f8f5345003e293f2b3a4a180c4f2`.
The exact-set comparison reports only `_distance_squared2d` as newly exact,
preserves all five inherited exact owners, and reports no loss.
## One-shot discipline and validation
`git diff --check` and the production compiler's no-output `/Zs` check passed
before emission. Exactly one normal selected-object compiler edge produced the
retained artifact. No expression, helper spelling, declaration, include
topology, flag, or compiler retry occurred.
The complete graph and campaign gates pass:
- 377/833 complete objects overall and 275/468 for Halo;
- 510,990/2,198,102 meaningful code bytes and 4,239/11,060 functions overall;
- 498,076/1,770,166 Halo code bytes and 4,072/7,574 Halo functions;
- data unchanged at 1,836,762/4,176,062 overall;
- semantic audit: 470 units, 4,351 functions evaluated, 4,205 semantic exact,
4,265 accepted exact, and zero unit errors;
- admission: zero candidates, contradictions, or revocations;
- parks: three active, zero stale, zero invalid;
- tooling tests: 179/179 pass, with only the inherited pytest cache-permission
warning.
No protected source, configuration entry, completion label, push, amend, or
history rewrite is involved.
@@ -1,108 +0,0 @@
# Action-vehicle vector-negation recovery (2026-08-27)
## Result
The first and only ordinary code-producing candidate makes
`_negate_vector2d` strict exact in `source/ai/action_vehicle.obj`. The object
advances from 2/17 to 3/17 exact functions and from 35/4,909 to 60/4,909
meaningful code bytes. It remains `NonMatching`; no data, completion-label,
semantic-exception, or parked-function credit is claimed.
Both inherited exact owners, `_action_vehicle_update` and
`_action_vehicle_flush_structure_indices`, remain exact.
## Frozen target and provenance evidence
January defines an external cdecl `real_vector2d *(real_vector2d const *,
real_vector2d *)` owner with 25 meaningful and 32 padded bytes. It loads the
two source floats in order, negates each with x87 `fchs`, stores them to the
corresponding result fields, leaves the result pointer in `EAX`, and returns.
The function has no relocation.
The canonical inline implementation in `source/math/real_math.h` independently
supplies the exact signature and named-field semantics. The HCEA later-build
source at commit `570c83fd9c365dad6f2a3e7041705d5b84c7847c` independently performs the same
two component negations and returns the result pointer. Its source blob is
`2c138a6375cc6957f91a3af3756ff5309f12518b`. HCEA supplies semantic and type
provenance only; January PC COFF remains the sole padded-byte authority.
The complete Claude campaign tree at
`0e1fc785b7f5fb8c8d6fec8cae667005aa663957`, Stian sources and verification
metadata, all current object ledgers, and all repository revisions were
searched before emission. They contain no earlier action-vehicle definition,
candidate artifact, or fixed-point record for this owner. The broader Claude
and Stian action-vehicle reconstructions concern larger actor-action bodies and
do not alter this standalone math leaf's signature or ownership.
## Retained typed source and house-rule audit
The existing `actions.h` include brings in the canonical inline math header.
A translation-unit-local macro rename keeps that inline implementation
available as `negate_vector2d_inline` while allowing January's external owner
to be defined naturally:
```c
#define negate_vector2d negate_vector2d_inline
#include "actions.h"
#undef negate_vector2d
real_vector2d *negate_vector2d(
real_vector2d const *a,
real_vector2d *result)
{
result->i = -a->i;
result->j = -a->j;
return result;
}
```
Every parameter is on its own line, the implementation uses the named typed
fields, and the function ends with an explicit value return. It contains no
assembly, `register`, `volatile`, optimizer barrier, pragma, intrinsic,
attribute, raw address, byte-offset access, pointer/integer reconstruction,
pointer or union pun, undefined behavior, synthetic anchor, object patch,
comparator exception, or byte forcing.
## Immutable artifacts and strict comparison
- Base commit: `21ab835f9748f2423d8c6a77c0da99f2b42eebc3`.
- Pre-wave source blob: `21e056ab7bf30cb7f746e8a610ca15146a4785ae`.
- Frozen retained source blob: `18146aae0ffe9fe419384a27c204a04aa7d276f0`.
- January split object SHA-256:
`49F00A0115CECF578E51102FC33F665F876A4FAFCB78EE3B53520545406EF769`.
- Baseline candidate object SHA-256:
`E87C78FD8A92B66B61018183132ED60DD1CEE3AAB8CCF80FF471F9B8818CF276`.
- First-shot object SHA-256:
`128B7726809D6A1814973851CB71E5F5B1023F34D4A012AD297482FA1B3EDF5D`.
- Compiler: Microsoft XDK 3911 CL 13.00.9254.1 with the unchanged natural
`/O2 /Oy- /DDEBUG /Dxbox` flags and include graph.
The hardened comparator proves all 32 padded bytes and zero-relocation
identity exact. Target and candidate share normalized SHA-256
`5a29b0bda878f7b2a152c71c0056cd52ba442629e5177fb7f7617ac83bc3eed3`.
The exact-set comparison reports only `_negate_vector2d` as newly exact,
preserves both inherited exact owners, and reports no loss.
## One-shot discipline and validation
`git diff --check` and the production compiler's no-output `/Zs` check passed
before emission. Exactly one normal selected-object compiler edge produced the
retained artifact. No field spelling, statement order, include topology,
flag, or compiler retry occurred.
The complete graph and campaign gates pass:
- 377/833 complete objects overall and 275/468 for Halo;
- 510,885/2,198,102 meaningful code bytes and 4,236/11,060 functions overall;
- 497,971/1,770,166 Halo code bytes and 4,069/7,574 Halo functions;
- data unchanged at 1,836,762/4,176,062 overall;
- semantic audit: 470 units, 4,348 functions evaluated, 4,202 semantic exact,
4,262 accepted exact, and zero unit errors;
- admission: zero candidates, contradictions, or revocations;
- parks: three active, zero stale, zero invalid;
- tooling tests: 179/179 pass, with only the inherited pytest cache-permission
warning.
No protected source, configuration entry, completion label, push, amend, or
history rewrite is involved.
@@ -1,124 +0,0 @@
# `action_vehicle.obj` Jonas one-leaf wave
## Result
This bounded source-only wave starts from authoritative integration commit
`d3453bd46972f3c24148b2de14f495286fa3637c`. The three permitted HCEA-backed
action-vehicle bodies were emitted together in exactly one natural candidate
compile. `_action_vehicle_flush_structure_indices` was independently strict
and is retained. `_action_vehicle_begin` and `_action_vehicle_control` missed
and were removed immediately, without a spelling, declaration, control-flow,
or compiler-control retry. The pre-existing exact `_action_vehicle_update`
body and bytes are unchanged.
`action_vehicle.obj` advances from 1/17 to 2/17 exact functions and remains
`NonMatching`.
| Retained function | Meaningful | Padded | Relocations | Normalized SHA-256 |
| --- | ---: | ---: | ---: | --- |
| `_action_vehicle_flush_structure_indices` | 34 | 48 | 2 | `0463c71d743e4bc0945ec5b28c91d1650764463a866ff5e552074a88f4ad1130` |
| **Wave gain** | **34** | **48** | **2** | |
The hardened COFF comparator proves identical padded bytes and exact
relocation addresses, types, destinations, and addends. The object now reports
35/4,909 meaningful code bytes and two exact functions. The target's remaining
fifteen functions and all 116 non-code bytes stay outside this wave's credit.
## Provenance and signatures
- Baseline `source/ai/action_vehicle.c` blob:
`77f976dc9617cb72d35c768e2fa2b839be7d7ad5`.
- Retained source blob/committed-blob SHA-256:
`21e056ab7bf30cb7f746e8a610ca15146a4785ae` /
`ef2f07e6e1a4ede19ce03b834a6de720c9cf1eca465299f3e80e39b1c3286308`.
- January executable SHA-256:
`4cc87b45f721270392a96f1674ed2b5cd4a7bb4355faeab4531d1cf1884d9520`.
- January split-object SHA-256:
`49f00a0115cecf578e51102fc33f665f876a4fafcb78ee3b53520545406ef769`.
- Clean HCEA semantic donor commit:
`570c83fd9c365dad6f2a3e7041705d5b84c7847c`. Its exact-name
`src/blam/ai/action_vehicle_begin.c`,
`src/blam/ai/action_vehicle_flush_structure_indices.c`, and
`src/blam/ai/action_vehicle_control.c` files supply the three natural typed
behaviors. The retained donor file's blob is
`99133bb4ae11f72755ea63cc95f4fede8c9d4182`.
- January PC disassembly independently fixes the 32-bit actor datum handle,
`actor_get`/`datum_get` access path, destination-surface field store, exact
call identities, code extents, padding, and relocation ownership. January
COFF remains authoritative for acceptance.
- Compiler: Microsoft 32-bit C/C++ Optimizing Compiler `13.00.9254.1` for
80x86, SHA-256
`483e00c47bb08d699475a642bcff15b5b2036350b31c540e88a506baf101da11`,
with the repository's unchanged XDK 3911 command and flags.
The retained signature is:
```c
void action_vehicle_flush_structure_indices(
long actor_index);
```
Existing `actors.h` and `actions.h` own the typed actor/action layout and
`actor_get` contract. Existing `cseries.h` owns `NONE`. No shared-header or
storage repair is needed.
## Authenticated behavior and one-shot rejection record
The retained body resolves the full 32-bit actor handle through `actor_get`
and invalidates
`actor->state.action_data.vehicle.destination_surface_index` with `NONE`.
Its target relocations are `DIR32 _actor_data` at `+8` and
`REL32 _datum_get` at `+15`.
All three candidates were compiled together once. The following two misses
were removed without retry:
| Rejected function | Target meaningful / padded / relocs | Target hash | First natural candidate |
| --- | ---: | --- | --- |
| `_action_vehicle_begin` | `76 / 80 / 3` | `5e5ac406a3045fd7df7a8414f08608cd1ece5bd940ea167941fb293bd192a8f0` | `80 / 3`, SHA `fef3ce0d9fc229b139957cfc30fdcc6d1a4a212be1536f2ebc864411f2046c65`; the relocation addresses and identities are equal, but normalized instruction bytes differ. |
| `_action_vehicle_control` | `175 / 176 / 3` | `81612c85cf900b15c2de8ea6d52646f5727a96fbea5e2f13018e6f9613fcd293` | `176 / 3`, SHA `3f96cbf0cdc3a58d1013ac2d44df261acc0d3b68c37ebab639956ddfa1a046ad`; the relocation addresses and identities are equal, but normalized instruction bytes differ. |
For `_action_vehicle_begin`, the common relocations are `DIR32 _actor_data`
at `+8`, `REL32 _datum_get` at `+16`, and `REL32 _game_time_get` at `+35`.
For `_action_vehicle_control`, they are `DIR32 _actor_data` at `+4`,
`REL32 _datum_get` at `+17`, and `REL32 _actor_path_has_path` at `+89`.
Both rejected COMDATs and their candidate-only includes/constants are absent
from the final object. Reopen either body only with independently preserved
January source or a newly authenticated ordinary-C contract; do not tune from
the rejected objects.
## Policy, scope, and validation
Only `source/ai/action_vehicle.c` and this new Jonas-owned ledger are changed.
No shared header, storage definition, data section, configuration, semantic
exception, parked record, completion label, frozen-five source, pre-existing
Markdown, or Claude-owned file is edited.
The retained source is readable typed C. It contains no assembly, raw address
or byte-offset dereference, pointer/integer reconstruction, pointer or union
pun, inactive-union access, undefined overflow, volatile scheduling device,
force-inline annotation, optimizer pragma/barrier, synthetic anchor, or
object-byte forcing. The final COFF's only action-vehicle code symbols are the
pre-existing `_action_vehicle_update` and retained
`_action_vehicle_flush_structure_indices`; it adds no owned storage.
- Complete 569-edge `halobetacache_build` and `libcmt_build`: pass.
- Direct hardened comparison: both `_action_vehicle_update` and
`_action_vehicle_flush_structure_indices` pass; both rejected symbols are
absent from the final COFF symbol table.
- Semantic audit: 470 units, 4,053 functions evaluated, 3,913 semantic exact,
101 hidden exact / 61,876 hidden code bytes, 3,974 accepted exact, and zero
unit errors.
- Campaign progress: 375/833 complete objects, 3,961/11,060 exact functions,
475,672/2,198,102 meaningful code bytes, and
1,835,088/4,176,062 matched data bytes. This wave adds one function and 34
meaningful bytes with no data increase.
- Admission audit: zero candidates and zero revocations; only the inherited
`source/shell/shell_xbox` completion-label contradiction remains.
- Parked validation: three active, zero stale, and zero invalid entries.
- Complete tooling suite: 179/179 tests pass; `git diff --check` is clean.
A clean committed-state snapshot followed by deletion and forced rebuilding
of `action_vehicle.obj` is the final same-path reproducibility proof. The
post-rebuild hardened comparison remains strict for both accepted functions.
No push is performed.
@@ -1,112 +0,0 @@
# Action-vehicle point-from-line recovery (2026-08-27)
## Result
The first and only ordinary code-producing candidate makes
`_point_from_line2d` strict exact in `source/ai/action_vehicle.obj`. The object
advances from 3/17 to 4/17 exact functions and from 60/4,909 to 95/4,909
meaningful code bytes. It remains `NonMatching`; no data, completion-label,
semantic-exception, or parked-function credit is claimed.
The three inherited exact owners, `_action_vehicle_update`,
`_action_vehicle_flush_structure_indices`, and `_negate_vector2d`, remain
exact.
## Frozen target and provenance evidence
January defines an external cdecl
`real_point2d *(real_point2d const *, real_vector2d const *, real,
real_point2d *)` owner with 35 meaningful and 48 padded bytes. It computes the
two components in order as `t*v + p`, stores them to the result point, leaves
the result pointer in `EAX`, and returns. The function has no relocation.
The canonical inline implementation in `source/math/real_math.h` independently
supplies the exact signature, parameter order, named fields, arithmetic order,
and return value. Its source blob is
`39a62c3da5b6a9003dfeb420ed554d1718596541`. January PC COFF remains the sole
padded-byte authority.
The complete Claude campaign tree, HCEA and Stian action-vehicle sources and
metadata, all current ledgers, and all repository revisions were searched
before emission. None contains a prior action-vehicle definition, candidate
artifact, or fixed-point record for this owner. The existing path-structure
ledgers mention the same canonical inline helper only as a spelling option in
other translation units; they neither define nor claim this external owner.
The adjacent `_distance2d` and `_distance3d` leaves were deliberately excluded
from this wave because their nested magnitude helpers introduce separate x87
stack-scheduling evidence. This wave froze only the direct arithmetic leaf.
## Retained typed source and house-rule audit
The existing `actions.h` include brings in the canonical inline math header.
A translation-unit-local macro rename retains that inline owner under a private
name while allowing January's external function to be defined naturally:
```c
#define point_from_line2d point_from_line2d_inline
#include "actions.h"
#undef point_from_line2d
real_point2d *point_from_line2d(
real_point2d const *p,
real_vector2d const *v,
real t,
real_point2d *result)
{
result->x = t*v->i + p->x;
result->y = t*v->j + p->y;
return result;
}
```
Every parameter is on its own line, the implementation uses named typed
fields, and the function ends with an explicit value return. It contains no
assembly, `register`, `volatile`, optimizer barrier, pragma, intrinsic,
attribute, raw address, byte-offset access, pointer/integer reconstruction,
pointer or union pun, undefined behavior, synthetic anchor, object patch,
comparator exception, or byte forcing.
## Immutable artifacts and strict comparison
- Base commit: `dccc548cc21c1dfc6a3ab8e1083888794141e506`.
- Pre-wave source blob: `18146aae0ffe9fe419384a27c204a04aa7d276f0`.
- Frozen retained source blob: `d6af877431ebe5ae8d02c961bb091aa2f6d17683`.
- January split object SHA-256:
`49F00A0115CECF578E51102FC33F665F876A4FAFCB78EE3B53520545406EF769`.
- Baseline candidate object SHA-256:
`128B7726809D6A1814973851CB71E5F5B1023F34D4A012AD297482FA1B3EDF5D`.
- First-shot object SHA-256:
`4A2BBD6AFD7DB99DC2636DE3AD3F590B0B52A3D969D450347FB3F0B366BC75F1`.
- Compiler: Microsoft XDK 3911 CL 13.00.9254.1 with the unchanged natural
`/O2 /Oy- /DDEBUG /Dxbox` flags and include graph.
The hardened comparator proves all 48 padded bytes and zero-relocation
identity exact. Target and candidate share normalized SHA-256
`d90e91bc9340636a11d9fe06c53bd87fad47428860899fd41c08c65be2cbe273`.
The exact-set comparison reports only `_point_from_line2d` as newly exact,
preserves all three inherited exact owners, and reports no loss.
## One-shot discipline and validation
`git diff --check` and the production compiler's no-output `/Zs` check passed
before emission. Exactly one normal selected-object compiler edge produced the
retained artifact. No expression, statement order, include topology, flag, or
compiler retry occurred.
The complete graph and campaign gates pass:
- 377/833 complete objects overall and 275/468 for Halo;
- 510,920/2,198,102 meaningful code bytes and 4,237/11,060 functions overall;
- 498,006/1,770,166 Halo code bytes and 4,070/7,574 Halo functions;
- data unchanged at 1,836,762/4,176,062 overall;
- semantic audit: 470 units, 4,349 functions evaluated, 4,203 semantic exact,
4,263 accepted exact, and zero unit errors;
- admission: zero candidates, contradictions, or revocations;
- parks: three active, zero stale, zero invalid;
- tooling tests: 179/179 pass, with only the inherited pytest cache-permission
warning.
No protected source, configuration entry, completion label, push, amend, or
history rewrite is involved.
@@ -1,131 +0,0 @@
# `action_vehicle.obj`: opus5 100K consolidated lane (2026-09-14)
## 1. Scope and provenance
- Source TU: `source/ai/action_vehicle.c`; target object: `build/split/source/ai/action_vehicle.obj`
(January split, SHA-256 `49F00A01...F769` per earlier ledgers).
- Compiler: Microsoft XDK 3911 CL 13.00.9254 with the repository's unchanged flags
(measured only through `tools/campaign/gate.py`; no ninja/configure run by this worker).
- Lane: `opus/100k-consolidated-20260914`, HEAD `b8cd50113`; worker claim `waveA:action_vehicle`.
- Evidence used: January COFF (code, relocations, literals); January map atlas
`scratch/atlas/action_vehicle.obj.txt`; HCEA `action_vehicle_{desirable,find_destination,setup_specific,setup_impromptu}.c`
for semantics only. Prior ledgers read: `action_vehicle_obj_evaluate_seat_symbol_reconciliation_20260913`,
`..._fable_control_intake_20260906`, `..._fable_control_reconciliation_20260906`,
`..._jonas_distance3d_fixed_point_20260827`, `..._jonas_one_leaf_20260821` and the grep hits in the
units/path_obstacles/fable intake ledgers. `branch_sweep.py source/ai/action_vehicle`: no blob with more exact rows.
- Per-function worker report (full measurements): `scratch/workers/action_vehicle.md`.
## 2. Validated baseline
Gate at lane HEAD: `== exact 10 residual 7 unwritten 0 (of 17 listed)`; no parked entries.
| Residual | Target padded / relocs / nsha16 | Base padded / relocs | First divergence |
| --- | --- | --- | --- |
| `_action_vehicle_desirable` | 272 / 7 / e3b1ecf64be4fb82 | 288 / 7 | callee-saved push `edi` vs `esi` (private ABI), dead-test layout |
| `_action_vehicle_evaluate_seat` | 976 / 30 / 9f67690555e799a6 | 960 / 31 | `mov eax,[ebp+8]` before actor load; `_vehicle_specific_definition_get` call vs `_tag_get` |
| `_action_vehicle_find_destination` | 1232 / 34 / 698d66d07e83af5b | 1264 / 34 | frame `0x464` vs `0x468`; register params `ecx`/`eax` |
| `_action_vehicle_perform` | 768 / 18 / e61cabd696765a6f | 768 / 18 | actor/vehicle register swap `edi`/`esi` |
| `_action_vehicle_setup_impromptu` | 464 / 18 / 0347ef4f8993dc07 | 464 / 18 | inlined desirable square order; find_destination call ABI |
| `_action_vehicle_setup_specific` | 336 / 15 / aa10ba89a138d06b | 336 / 15 | inlined desirable dead test (branchy vs branchless) |
| `_distance3d` | 64 / 0 / 4cdb6852c1705b5f | 64 / 0 | x87 square order (exhausted, 2026-08-27) |
## 3. Accepted controls (retained in production)
`_action_vehicle_desirable` is strict EXACT (272 padded, 272 meaningful, 7 relocations) in the whole-TU
real-file gate `== exact 11 residual 6 unwritten 0`. Two source changes, both in `action_vehicle.c`:
1. The TU-local `#define REAL_MATH_EXTERNAL_MAGNITUDE_SQUARED3D` / `#undef` pair was removed, which restores
the header's default inline linkage. January inlines `magnitude_squared3d` inside the distance test and calls it
out of line for the velocity test. With the header default, VC7 makes the same per-call choice by itself.
2. The body now reads `boolean desirable = FALSE;` followed by `if (dead) desirable = FALSE; else if (scripted)
desirable = TRUE; else { ...; if (already_inside || distance_squared3d(&body, &origin) < max*max) {...} }`.
Binary support for each part:
- January computes the three coordinate differences *after* the `already_inside` test, with no `fstp` cleanup.
So the distance is an inline helper in the `||` right operand, not a set of pre-computed locals.
- `xor bl,bl` comes before the dead test, which is the initializer.
- The not-dead body is placed after the return, which is the if/else-if chain.
- January's `setup_specific` inlines desirable with scripted=TRUE as branchless `shr cl,2; not cl; and cl,1`.
VC7 only if-converts an if/else whose arms both store constants.
- January's `setup_impromptu` inlines desirable, and its dead path jumps to the `xor bl,bl` block that it
cross-jumps with the velocity FALSE store. Without an explicit dead-arm store, the dead path jumps straight to the
upright test (measured variant s4). So the dead-arm assignment is January's, even though it restates the initializer.
This match makes no header, config or prototype change. The owner census shows one extra header-inline select-any
COMDAT, `_magnitude_squared3d`: January references it undefined, and the same COMDAT is already emitted by many
build/base AI objects. There is no data, BSS or COMMON change. The fake scan reports 0 leads.
## 4. Experiment matrix (all via `gate.py --source`; siblings stayed at 10 exact unless noted)
| Function | Shape | Size / relocs | Result |
| --- | --- | --- | --- |
| desirable | v0 named dx/dy/dz (base) | 288 / 7 | residual |
| desirable | s3 `distance_squared3d` with external magnitude define | 240 / 8 | out-of-line distance_squared3d |
| desirable | s4 s3 without the define | 272 / 7 sha | layout/ABI still off |
| desirable | va chain, no init, else-FALSE on distance fail | 272 / 7 sha | reverted |
| desirable | **vd** init FALSE + dead-arm FALSE + else-if chain | 272 / 7 | **EXACT, landed** |
| desirable | vh va + init | 272 / 7 sha | reverted |
| desirable | d1 `magnitude_squared3d(vector_from_points3d())` | sha | desirable lost exact, reverted |
| find_destination | f1 MAX / early marker vector / 3D offset cast / chosen.z | 1248 / 34, frame 0x464 | not landed |
| find_destination | f2 f1 + `0.35f*0.35f` + bsp local first | 1248 / 34 | not landed |
| find_destination | f3 helper args without POINT_FROM_LINE3D define (diagnostic) | guard FAIL (`_point_from_line3d` emitted) | discarded |
| find_destination | f4 f2 + scalarised 3D helper vectors | 1232 / 34 sha, frame 0x460, ABI unchanged | not landed |
| find_destination | f5 destination operand order | no change | discarded |
| evaluate_seat | e0 `#include "units/vehicle_definitions.h"` only | 960 / 31 (reloc name fixed) | not landed alone (no row change) |
| evaluate_seat | e1 2D helpers on 3D storage (casts, diagnostic) | 976 / 32 | discarded |
| evaluate_seat | e2 e1 + single-exit nesting | 976 / 32 | discarded |
| evaluate_seat | e3 e2 + explicit prop-loop distance | 992 / 31 | only site-2 `normalize2d` inlined vs January call |
| evaluate_seat | e4/e5/e6/e8/e3ord variants of e3 | 992 / 31 | site-2 still inline |
## 5. Do-not-repeat list
- **Measurement trap:** a `sed 's/...\r$//'` on this LF file silently kept the magnitude define, and about 25
"inline budget" probes were built against the wrong base. The real per-call inline boundary in desirable was the
TU-local define alone. Always `grep -c` the define after generating a variant base.
- desirable: named-local distance forms, `magnitude_squared3d(vector_from_points3d())`, the explicit else-FALSE
chain, and chain + init + else-FALSE are all measured non-exact.
- find_destination: the `_point_from_line3d` inline/out-of-line split cannot be reproduced without emitting the
forbidden COMDAT. Destination operand order and bsp-in-arguments are measured inert.
- evaluate_seat: none of these changes the site-2 `normalize2d` inline choice: explicit to_seat vector,
vector_from_points3d facing, assigned result, conditional context, January function order.
- `_distance3d`: see the 2026-08-27 ledger (two waves exhausted).
## 6. Residual classification
- **find_destination (policy + ABI).** Measured facts:
- January passes `ecx=actor_index, eax=vehicle_index`.
- The actor pointer is homed and all body accesses go through one derived pointer.
- The destination uses the inline `point_from_line3d` body order, while the refine offset calls `_point_from_line3d`.
- The offset vector is a real_vector3d used as perpendicular2d/normalize2d storage.
- The approach update is `MAX`, the literal is `0.35f*0.35f`, and the bsp fetch comes first.
Inferred: the register ABI follows from the body shape, which needs the forbidden COMDAT and a representation cast.
- **evaluate_seat (policy + one inline decision).** Measured facts:
- The implicit `vehicle_specific_definition_get` call is a missing include.
- The body is single-exit.
- The facing is a 3D vector normalized as 2D.
- The distance is `MIN(distance2d, distance2d)`, and January owns the 2D helper COMDATs.
Unresolved: VC7 inlines the second `normalize2d` in our build, while January calls it.
- **setup_specific, setup_impromptu, perform (ABI/register).** They are blocked by find_destination's private ABI.
setup_impromptu also differs in one x87 square order in its inlined desirable copy.
- **_distance3d.** x87 scheduling, exhausted.
## 7. Reopen criteria
- find_destination: an owner ruling that allows a select-any `_point_from_line3d` COMDAT here, or a C spelling that
produces the out-of-line offset call without it; plus admission of the offset-vector storage cast.
Start from `scratch/workers/action_vehicle/f/f4.c`.
- evaluate_seat: owner admission of the 2D-helper-on-3D-storage casts, plus evidence for the site-2 `normalize2d`
inline choice. Start from `scratch/workers/action_vehicle/e/e3.c`, with the `vehicle_definitions.h` include.
- setup_specific, setup_impromptu, perform: re-gate as soon as find_destination matches January's ABI.
- `_distance3d`: the 2026-08-27 criteria still apply.
## 8. Disposition
`action_vehicle.obj` is now 11/17 strict exact (+272 padded bytes: `_action_vehicle_desirable`). It is still active
and not Matching. No function is parked or fuzzy-landed by this lane.
## Orchestrator admission note (checkpoint 1)
`_action_vehicle_desirable` was **not admitted**. The adversarial verifier rejected the worker's strict-exact form because its dead-vehicle arm re-stores `desirable = FALSE` after the identical initializer, which is a redundant store. The orchestrator also measured the single-assignment form (no initializer, explicit `else desirable = FALSE`): 10 exact / 7 residual, so `desirable` is not exact. `action_vehicle.c` was reverted to HEAD. The `_point_from_line3d` emission and 2D representation-cast proposals are declined under the lane rules.
@@ -1,201 +0,0 @@
# `action_vehicle.obj`: opus5 150K house-clean lane, wave w1 (2026-09-14)
## 1. Scope and provenance
- Source TU: `source/ai/action_vehicle.c`. Target: `build/split/source/ai/action_vehicle.obj` (January split).
- Lane `opus/150k-house-clean-20260914`, base `12f7375d4`. Only `source/ai/action_vehicle.c` was modified.
No header, config, build, or tool file was touched. No ninja, configure, pytest, or git write was run.
- Measurement: `tools/campaign/gate.py`, `alndiff`, `relocdiff`, `tinfo`, `scratch/parkcheck.py`, and `tools/fake_match_scan.py`.
A `/Zs /W3` warning census was also run with the unit's own cflags.
- Ledgers read: all 11 `action_vehicle_obj_*` logs, including `opus5_100k_20260914` and its admission note, plus the
`opus5_100k_consolidated_HANDOFF_20260914` rejections. The 100K worker report is at
`C:\halo-worktrees\opus5-100k-consolidated-20260914\scratch\workers\action_vehicle.md`.
- `git log --all` shows 14 commits for the file. `branch_sweep`: the best historical blob has 9 exact functions.
- Evidence sources:
- January COFF: code, relocations, and COMDAT set.
- HCEA `action_vehicle_{desirable,evaluate_seat,find_destination}.c`: semantics and topology only.
- January `actors.obj` `.rdata` assert literals (section 5).
- Per-function worker report: `scratch/workers/action_vehicle.md`. Candidates are in `scratch/workers/action_vehicle/`.
## 2. Baseline (real file at lane base, guard on)
`== exact 10 residual 7 unwritten 0 (of 17 listed)`. The unit has no parks.
| Residual | Target padded / relocs | Base tags |
| --- | --- | --- |
| `_action_vehicle_desirable` | 272 / 7 | size 288 |
| `_action_vehicle_evaluate_seat` | 976 / 30 | size 960, relocs 31 |
| `_action_vehicle_find_destination` | 1232 / 34 | size 1264 |
| `_action_vehicle_perform` | 768 / 18 | sha |
| `_action_vehicle_setup_impromptu` | 464 / 18 | sha |
| `_action_vehicle_setup_specific` | 336 / 15 | sha |
| `_distance3d` | 64 / 0 | sha |
## 3. Landed (final real-file gate `== exact 12 residual 5`, guard passed)
| Function | Padded | Meaningful | Relocs | Normalized sha16 |
| --- | ---: | ---: | ---: | --- |
| `_action_vehicle_perform` | 768 | 767 | 18 | e61cabd696765a6f |
| `_action_vehicle_setup_specific` | 336 | 328 | 15 | aa10ba89a138d06b |
The gain is **1,104 padded / 1,095 meaningful** code bytes. All 10 baseline-exact rows are unchanged. The row diff is:
- perform and setup_specific: residual -> EXACT;
- find_destination: tag 1264 -> 1248.
### 3.1 New lever: callee register pressure through a late-bound position pointer (find_destination)
January `action_vehicle_find_destination` is a private register-ABI function. Every caller loads
`ecx = actor_index` and `eax = vehicle_index`, then pops 0x18.
**How January's body uses the actor pointer.** The actor datum pointer is stored once at entry, to `[ebp-0x14]`.
It is loaded exactly once, at +0x113, immediately after the anchor-point selection, into `edi`:
mov edi,[ebp-0x14]; fsub [edi+0x12c]; add edi,0x12c; ...
All later body-position reads go through that one pointer, including the `debug->field_C8` struct copy
(`mov edx,edi`). In the base source the actor pointer was used at every site, so it took `ebx`, and
`vehicle_index` lost its register parameter.
**Source change.** `real_point3d const *actor_position;` is declared with the block locals. It is assigned
`&actor->input.position.body_position` immediately before its first use, which is the entry-direction computation after
the anchor selection. Every body-position read goes through it.
**Why the assignment point matters (measured):**
| Placement of the pointer | Result |
| --- | --- |
| Initialized at the top of the if-block (f2) | ABI unchanged |
| Initialized at the top of the function (f5) | ABI unchanged |
| Assigned just before the anchor selection (f8) | ABI unchanged |
| Assigned just after the anchor selection (f3 / landed m1) | January ABI |
| Declared and initialized in a nested block after the anchor selection (f4) | January ABI (not used, because it needs a bare block) |
With the pointer assigned after the anchor selection, the pointer's live range begins where January's `edi` derivation begins.
Only the minimal routing change was landed. The residual rule does not allow more changes to this function.
find_destination stays residual at 1248/34 with frame 0x468 (January 0x464). Its first 0x55 bytes are now identical to January.
### 3.2 Non-redundant desirable chain (required prerequisite)
The landed desirable has no initializer. It reads:
if (dead) desirable = FALSE;
else if (scripted) desirable = TRUE;
else { ...; desirable = FALSE; if (already_inside || distance_squared3d(body, origin) < max*max) { desirable = TRUE; if (moving) desirable = FALSE; } }
The upright veto follows unchanged. The TU-local `REAL_MATH_EXTERNAL_MAGNITUDE_SQUARED3D` define/undef pair is removed, as the 100K lane
proved necessary. Semantics are identical to the base on all inputs, and no store restates an earlier value. This avoids the
redundant dead-arm re-store that got the 100K `vd` form rejected.
This body gives desirable January's private ABI (`eax = actor_index`, `esi = vehicle_index`) and January's branch layout.
- **perform:** the new ABI removes perform's actor/vehicle register swap.
- **setup_specific:** its inline copy gets January's branchless dead/scripted if-conversion.
Measured prerequisites:
| Variant | Result |
| --- | --- |
| Base desirable + m1 | perform, setup_specific residual |
| d1 keeping the magnitude define | perform grows to 880 bytes |
| d3 (d1 with named dx/dy/dz) | setup_specific exact, perform not |
| d2 (boolean-expression form) | desirable no longer inlined into setup_impromptu |
desirable itself stays residual at 288/7. The only remaining difference is that the not-scripted `desirable = FALSE` store is not hoisted
into January's entry `xor bl,bl`.
### 3.3 Checks
- Point guard passed.
- parkcheck: no parks.
- Owner census against `build/base`: one added select-any COMDAT, `_magnitude_squared3d`. January references it undefined from desirable
and setup_impromptu; it is the systemic header-inline class. There is no data, BSS, or COMMON change, and nothing is dropped.
- Fake scan: 0 leads (base 0).
- `/W3` warnings: identical to the base.
## 4. Not landed
| Function | Final | Classification / reason |
| --- | --- | --- |
| `_action_vehicle_evaluate_seat` | 960/31 in the real file; **strict EXACT scratch candidate** `E1.c` | Blocked only by the earlier orchestrator decline of 2D-helper casts; see section 5 |
| `_action_vehicle_find_destination` | 1248/34 | Offset-vector representation cast, plus x87 operand-order ties (below) |
| `_action_vehicle_desirable` | 288/7 | Owner ruling on the vd dead-arm restatement |
| `_action_vehicle_setup_impromptu` | 464/18 sha | Depends on vd. With vd + m1 only the inlined x,z,y square order differs |
| `_distance3d` | 64 sha | Exhausted per the 2026-08-27 ledger |
**find_destination cast diagnostic (x2/x5).** Size 1232, frame 0x464, and January's success/failure epilogue layout are reached with a
single-exit `found` flag. The remaining x87 operand-order differences were measured inert:
- destination `fld down.i; fmul t`: operand swap, `test.x + down.i*t`, and the inline point_from_line3d body (diagnostic, guard failure)
all still load `t` first;
- the refine dot operand order;
- the offset_distance temp lifetime;
- the chosen-to-actor square order.
**distance3d via a genuine caller (g1).** Using the header-inline copy through `distance3d(&marker, hint_point)` emits the header body with
sha 01d7809ae796fc6e, which is the wave-one z,x,y schedule. That is a measured negative: the COMDAT body does not depend on its callers.
## 5. evaluate_seat: new levers and the proposal
The 100K lane stopped at 992/31: its cast form still inlined the second `normalize2d`. Three new measured VC7 levers close
that gap. All three are cast-independent mechanisms.
1. **Loop spelling controls post-loop inline frequency.**
- `for (prop = next; prop; prop = next)` keeps `normalize2d(&to_seat_direction)` inline.
- `while (prop = prop_iterator_next(&iterator))`, or `prop = next; while (prop) {...; prop = next;}`, makes it an out-of-line call
as in January.
- A diagnostic with the loop deleted also puts the call out of line. The rotated for-loop inflates the estimated frequency of the code
after it.
2. **Sibling scope shares a frame slot.** Putting the 8-byte `prop_iterator` in its own block around the loop lets it share `[ebp-0x18]`
with `to_seat_direction`. The frame goes from 0x5c to 0x54.
3. **Single exit (HCEA topology).** The body sets `result = TRUE` after the entrance point is found. A conflicting actor does
`result = FALSE; break;`. The tail sits in `if (result)` with one `return result;`. This reproduces January's three epilogues:
the threaded `xor al,al`, the tail-duplicated `mov al,1`, and `mov al,bl`.
The candidate `scratch/workers/action_vehicle/E1.c` (the current real file plus `E1.txt`) makes evaluate_seat **strict EXACT at 976/30/9f67690555e799a6**.
- Whole-TU gate: exact 13, with all 12 real-file exact rows kept.
- Point guard passed; fake scan 0 leads.
- Census: adds a header-inline `_dot_product2d` COMDAT (systemic class).
- It also adds the legal `#include "units/vehicle_definitions.h"`. This removes the base's C4013 implicit
`vehicle_specific_definition_get` call to a nonexistent `_vehicle_specific_definition_get`.
**Casts in the candidate:**
- `normalize2d((real_vector2d *)&facing)`;
- `MIN(distance2d((real_point2d *)&body, (real_point2d *)&entrance_point), distance2d(... &seat_point))`;
- `vector_from_points2d((real_point2d *)&body, (real_point2d *)&seat_point, &to_seat_direction)`;
- `dot_product2d(&to_seat_direction, (real_vector2d *)&actor->input.facing_vector)`.
**New January-side evidence for these casts:**
- January `actors.obj` `.rdata` contains the assert literals `(real_vector2d *) &actor->input.facing_vector` and
`(real_vector2d *) &control_data.facing_vector`. These are stringified by `assert_valid_real_normal2d`, and canonical
`source/ai/actors.c:1946-1950` and `4167-4171` already reproduce them. So Bungie's AI code applied 2D helpers to 3D vectors through
exactly this cast.
- The canonical tree already carries 31 such casts in admitted exact code. Examples:
- `action_obey.c:2449` `normalize2d((real_vector2d *)&facing) == 0.0f`;
- `action_obey.c:2615-2622`;
- `action_charge.c:769/834`;
- `actor_combat.c:2310`;
- `actor_firing_position.c:1548`.
- January's own action_vehicle COMDAT set is `point_from_line2d`, `distance_squared2d`, `distance2d`, `negate_vector2d`, `distance3d`,
in `real_math.h` order. It shows that this TU instantiated the 2D helpers.
**ORCHESTRATOR-PROPOSAL:** admit these January-attested casts for `action_vehicle_evaluate_seat`, and apply `E1.txt` (the function body plus the
include) to `source/ai/action_vehicle.c`. Expected gain: 976 padded bytes. No header or config change; the change has no external consumers.
## 6. Do-not-repeat (this wave)
- desirable:
- d1/d2/va/d3 and d1 with the magnitude define (all measured above);
- the dead-arm restatement (vd) is the only exact form.
- find_destination pointer placement: f2, f5 and f8 do not change the ABI. Accessor binding on the first use only (f1) does not change it either.
- The destination x87 load order is inert to operand order, sum order, and the inline point_from_line3d body.
- setup_impromptu: d2 stops the desirable inline.
- `_distance3d`: the header copy via a genuine caller gives the z,x,y body.
- evaluate_seat, still inline: nested `normalize2d(vector_from_points2d(...))` (e4), splitting the first condition into nested ifs (e5),
and a no-op cast on the site-2 argument (ed1).
## 7. Reopen criteria
- **evaluate_seat:** the cast ruling in section 5 (candidate ready).
- **find_destination:** the cast ruling, plus a source spelling that controls the destination/refine x87 operand order. Start from
`scratch/workers/action_vehicle/x5.c`. The closer cast-free body is `f7.c`.
- **desirable, setup_impromptu:** an owner ruling on vd, or a non-redundant source whose FALSE store VC7 hoists to the entry.
After that, the inlined x,z,y square order in setup_impromptu remains.
- **`_distance3d`:** the 2026-08-27 criteria.
@@ -1,166 +0,0 @@
# action_vehicle.obj - opus5 250K house-clean lane, wave w3d (2026-09-15)
Unit: `source/ai/action_vehicle.c`. Worker notes: `scratch/workers/w3d_action_vehicle.md`.
Scratch: `scratch/workers/w3d_action_vehicle/`. Predecessors: the 11 earlier
`action_vehicle_obj_*` ledgers, in particular `_obj_opus5_100k_20260914.md` and `_obj_opus5_150k_w1_20260914.md`.
## Baseline and result
| | exact | residual | unwritten |
| --- | ---: | ---: | ---: |
| HEAD (baseline, guard on) | 12 | 5 | 0 |
| after w3d | 13 | 4 | 0 |
Newly strict EXACT: `_action_vehicle_evaluate_seat` - 976 padded bytes, 30 relocations, sha 9f67690555e799a6.
No parks for this unit; no baseline-exact row changed; `_point_from_line3d` emitted-symbol guard passed.
Remaining non-exact rows: `_action_vehicle_desirable` 272 [size 288, sha], `_action_vehicle_find_destination`
1232 [size 1248, sha], `_action_vehicle_setup_impromptu` 464 [sha], `_distance3d` 64 [sha].
## 1. What unblocked the object
`scratch/w3/laws_w3.md` section B lists, under "Newly ADMITTED by verifiers (do not avoid)":
"(real_point2d const *)&point3d casts into 2D helpers (32 source sites, HCEA); A15 role order for distance2d",
landed in wave w3a as `_actor_move_avoidance_setup` (see `actor_moving_obj_opus5_150k_w3_20260914.md` line 77).
Wave w1 had already produced a strict-EXACT `_action_vehicle_evaluate_seat` body and withheld it for exactly that
policy. The 2026-09-15 owner rulings (brief sections 9b/9c) do not touch the cast class, so no new ruling was needed.
## 2. `_action_vehicle_evaluate_seat` - EXACT, landed
Target 976 / 30 / 9f67690555e799a6; HEAD was 960 / 31 / sha.
Structural census before the work:
- One relocation too many (31 vs 30). January calls `_normalize2d` twice (@0x00c1 for the entry facing, @0x0283
for the to-seat direction) and `_tag_get` twice; ours inlined the second normalize2d and emitted an extra
`_tag_get` produced by the C4013 implicit declaration of `vehicle_specific_definition_get` at line 471
(`scratch/gate4/c4013.txt`).
- 16 bytes short (960 vs 976) from the same two effects plus the frame.
The landed body combines five measured levers:
1. **A7 missing prototype.** `#include "units/vehicle_definitions.h"` - the genuine owner, which defines
`vehicle_specific_definition_get` as a macro over `tag_get` at vehicle_definitions.h:30. No consumer-local
prototype (R8). This also removes the paired C4047 "differs in levels of indirection" warning.
2. **Admitted 2D-helper casts.** `normalize2d((real_vector2d *)&facing)`,
`distance2d((real_point2d const *)&actor->input.position.body_position, (real_point2d const *)&entrance_point)`,
the MIN of that and the seat distance, `vector_from_points2d(... , &to_seat_direction)` and
`dot_product2d(&to_seat_direction, (real_vector2d const *)&actor->input.facing_vector)`. `const` is used wherever
the real_math.h parameter is `const`, matching the admitted w3a idiom.
3. **A23-class loop spelling.** `while (prop = prop_iterator_next(&iterator))`. A rotated
`for (prop = next; prop; prop = next)` inflates the estimated frequency of the code after the loop and inlines
the second `normalize2d`; the while form leaves it out of line exactly like January. The idiom is already in the
tree at actions.c:4454, action_obey.c:1826 and actor_looking.c:734.
4. **Sibling scope.** The prop iterator and `prop` live in their own block around the loop, so VC7 shares their
8-byte slot with `to_seat_direction` ([ebp-0x18]) and the frame drops 0x5c -> 0x54.
5. **A3/A29 single exit.** `result = TRUE` after the entrance-point success, `result = FALSE; break;` for the
conflicting actor, the whole publish tail under `if (result)`, one `return result;`. This reproduces January's
three epilogues: the break threads to `xor al,al`, the success path tail-duplicates `mov al,1`, the failures
share `mov al,bl`.
6. **A17 discarded validation call.** The `if (!allow_any_seat)` block is the bare statement
`vehicle_specific_definition_get(vehicle->definition_index);` followed by
`unit_seat_is_driver(vehicle_index, seat_index);`. The earlier candidate used an unused local plus a `(void)`
cast, which A17 rejects.
Shapes: E1 (the w1 body, re-gated at this HEAD) EXACT; v1 = E1 plus the const casts and the A17 bare statement,
EXACT - landed.
Checks: all 12 baseline-EXACT rows preserved (row-by-row `--all` diff); guard passed; `scratch/parkcheck.py` reports
no parks for the unit; owner census versus `build/base` adds only `_dot_product2d`, a real_math.h select-any
header-inline COMDAT of the class laws_w2 A4 accepts as systemic (`_vector_from_points3d`, `_magnitude_squared3d`,
`_normalize2d`, `_square_root`, `_scale_vector2d`, `_vector_from_points2d`, `_magnitude2d`, `_magnitude_squared2d`,
`_distance_squared3d` are already candidate-only owners at HEAD, so R14 is not newly triggered); no data, BSS or
COMMON change; `tools/fake_match_scan.py` 0 leads (HEAD 0); `CL /Zs /W3` removes C4013 and C4047 and adds nothing.
Authenticity: the routine computes the seat entry facing from entrance to seat in the XY plane, falls back to the
actor own facing vector when that degenerates, takes the nearer of the entrance and seat 2D distances, rejects the
seat when a friendly actor is already closer to the same seat, and publishes weight / range / facing / fake flags
only for an accepted seat. Every helper used is the real_math.h routine the arithmetic is named after.
## 3. `_action_vehicle_find_destination` - structural gap closed in scratch, not landed
Target 1232 / 34 / 698d66d07e83af5b; HEAD 1248 / 34, frame 0x468 against the January 0x464.
Census (recorded before any shape):
- **Relocation census by target**: 34 versus 34, every symbol count equal, with ONE real difference -
January references `__real@3dfae147` where we emit `__real@3dfae148`. `0x3dfae147` is `(float)(0.35f*0.35f)`,
the square of the float 0.35f; `0x3dfae148` is the decimal literal `0.1225f`. The HEAD source writes the folded
literal; January wrote the product. This is an emitted-constant fidelity defect, not a tie.
- **Relocation ORDER**: January calls `_global_collision_bsp_get` at @0x40f, BEFORE the `_global_up3d` read at
@0x418; ours calls it at @0x47e just before `_collision_bsp_test_vector`. By the store/call-order law the BSP
pointer is obtained before the test_point and down_vector statements, i.e. it is a block-top declaration
`struct collision_bsp *collision_bsp = global_collision_bsp_get();`, not an inline argument.
- **Frame census** (`slots.py` over `alndiff --include-equal`): January `sub esp,0x464`, ours `sub esp,0x468`.
Ours owns one extra dword `[ebp-0x50]` (3 references, all in the trailing collision block) which pushes the
collision result buffer from `[ebp-0x464]` to `[ebp-0x468]`. January packs BOTH trailing aggregates into slots
the earlier block already owns (test_point -> [ebp-0x10..-8], down_vector -> [ebp-0x24..-0x1c]); ours packs only
down_vector. laws_w2 A6 side law: nested-block locals may pack.
- **Statement order from the aligned stream**: January computes `marker.x - actor_position->x` and
`marker.y - actor_position->y` at @0x116/@0x130, i.e. in the OUTER block before `anchor_point` is assigned, and
reuses those two cells in the `t` numerator and the perpendicular sign test - a named outer-block 2D vector is
missing from HEAD. The tail is single-exit (`test al,al; je <fail>` with the success block inline, `mov al,1`
and a separate `xor al,al` epilogue). `*surface_index_reference` is stored FIRST, its load interleaved into the
x destination product.
All five defects are implemented by the w1 diagnostic body `x5.c`, which the cast admission now makes usable. On
top of it this wave spent five evidence-led shapes:
| shape | change | result |
| --- | --- | --- |
| s1 | the two hand-written 2D sign dots become `dot_product2d(...)` calls (A35(1)+(2)) | 411 vs 412 insns; also closed the point_from_line2d x87 CSE pop and the magnitude_squared3d k,j,i sum order. KEPT |
| s2 | `collision.t*down_vector.i + test_point.x` (t-first) | byte identical, inert. REVERTED |
| s3 | `scale_vector3d(global_down3d, 4.0f, &down_vector);` for the three component multiplies (A35(1) leaf age) | 412 vs 412 instructions at IDENTICAL addresses; closed the stray `fxch st(1)` and the Y/Z destination products. KEPT |
| s4 | surface_index store moved after the destination stores | worse. REVERTED |
| s5 | dot-2 argument swap | byte identical - confirms laws_w3 A44. REVERTED |
Fixed point `scratch/workers/w3d_action_vehicle/fd_cand.c` (the current real file with only find_destination
replaced): 1232 / 34, `[sha]` only - the `[size]` tag is gone - 412/412 instructions at identical addresses,
whole-TU `== exact 13 residual 4`, guard passed, fakescan 0 leads, no new warnings, owner census +`_dot_product2d`
and +`_scale_vector3d` (both A30/A4 accepted class) and the `.rdata` literal corrected to `__real@3dfae147`.
Eleven instructions remain, in four x87 leaf-age / scheduling families: the i-term of the first inlined
`dot_product2d` (@0x284), both terms of the second (@0x36e, @0x377), the interleave of the
`debug->field_F8 = chosen_point` loads against the `debug->field_F4 = TRUE` byte store (@0x3dd), and the X
destination product (@0x483/@0x48c; Y and Z now match).
NOT LANDED under brief section 6: an already-written residual may be landed only when it becomes strict EXACT, and
this candidate is not below the January size or relocation count, so the fuzzy allowance does not apply.
Reopen criterion: a source lever that flips x87 leaf age inside an inlined `dot_product2d` or a named-local product
without an R3 operand swap (A35 is diagnostic only today); or an orchestrator decision to take the strictly-closer
zero-credit body for its proven fidelity content - notably the emitted constant, where HEAD writes `0.1225f`
(`__real@3dfae148`) and January writes `0.35f*0.35f` (`__real@3dfae147`).
## 4. `_action_vehicle_desirable` - initializer proven, blocked by the pending A42 ruling
Target 272 / 7 / e3b1ecf64be4fb82; HEAD 288 / 7 (100 versus 112 instructions).
Census: relocations 7/7 with identical targets; frame slots identical apart from one extra `[ebp-8]` read. The
16-byte gap is therefore not a frame gap: it is our two extra reloads of the datum homed at `[ebp-8]` plus the
position of `xor bl,bl`.
New evidence this wave: laws_w3 A38/A42 fire. January zeroes BL at 0x2b, before the `test al,4` dead-bit compare at
0x2d, which A42 reads as proof of a declaration initializer. Shape `d4` - `boolean desirable = FALSE;` at the
declaration plus `if (!TEST_FLAG(vehicle->object.damage_flags, _object_dead_bit)) { if (scripted_request) ... else
... }`, with no redundant store anywhere - reproduces that hoisted `xor bl,bl` exactly and brings the row to
**272 bytes, the January size**, at 101 versus 100 instructions. So the initializer, not the w1 `vd` redundant
dead-arm store, is what the January entry proves.
Two families still differ in `d4`, and they are coupled:
- January keeps `vehicle_index` in its incoming ESI and the vehicle datum in EDI (pushing only ebx and edi);
`d4` moves `vehicle_index` into EDI and the vehicle datum into ESI, which flips the private ABI of the static.
`_action_vehicle_perform` (768) and `_action_vehicle_setup_specific` (336), both exact at HEAD, then fall back to
`[sha]` (`== exact 11 residual 6`). Losing an exact row is a regression, so `d4` is not landable.
- January lays the not-dead block out of line (`je 0x58`, `jmp 0x31` back); the `d4` `if (!dead) { ... }` body stays
inline. HEAD's `if (dead) X; else if ... else ...` chain reproduces the January layout, but its first arm cannot
be empty, so the only spelling carrying BOTH the initializer and the chain is the w1 `vd` redundant store.
Reopen criterion: the pending A42 owner ruling on byte-proven redundant stores (laws_w3 C3 groups
`_encounter_test_rule`, `_extract_sequence`, `_action_alert_next_position` and `_action_obey_command_begin` here).
If granted, `vd` plus this wave's initializer evidence closes desirable at 272, and the w1 ledger records that
`_action_vehicle_setup_impromptu` then has only the inlined x,z,y square order left.
## 5. Not targets this wave
`_action_vehicle_setup_impromptu` (464, [sha]) and `_distance3d` (64, [sha]) were not in the w3d TARGET list and no
shapes were spent. w1 classified setup_impromptu as desirable-dependent (it inlines desirable) plus one x87
square-order tie, and `_distance3d` as exhausted - a caller-independent header COMDAT body whose two frozen waves
and the w1 `g1` rename-removal probe all produce z,x,y or z,y,x against the January x,y,z.
Loaded 100 of 7399 files, more files were not shown because too many files have changed in this diff. Show more