17 KiB
actions.obj exact-match log
Scope and standard
This lane reconstructs source/ai/actions.obj from the January 14, 2002 Xbox
object. The HCEX decompilation and Fable correspondence packets are source
topology hints only. January code bytes, relocation identities, data bytes,
and symbol ownership decide every result. The lane uses the established XDK
3911 compiler and unchanged /O2 /Oy- /DDEBUG /Dxbox flags. Assembly,
volatile, inline-control keywords, optimizer barriers, undefined behavior,
byte forcing, flag changes, and comparator exceptions are out of scope.
Fresh baseline (fc8e1b07)
The target contains 63 functions and 19,172 meaningful padded code bytes.
The fresh candidate has zero exact functions: 62 functions are absent and
actor_action_debug_color is 160 bytes with 15 relocations versus January's
176 bytes and 15 relocations. January also owns the 888-byte/96-relocation
global_action_functions constant block and the 16-byte/3-relocation
global_actor_mode_names data block; neither is emitted by the baseline.
Fable's exact-unique name map covers 55 of the 63 functions (17,484 January bytes), and the local HCEX tree has readable C for those names. That coverage does not grant match credit. It only gives this lane a much better starting shape than decompiling every body from raw instructions.
Strategy
The first wave is deliberately small: callback dispatchers, names, class and default-state accessors, plus the existing debug-color function. These are the low-risk functions that establish the action table's exact layout and common dispatcher shapes. Header-inline helpers are not duplicated in this translation unit; they should be emitted naturally once real callers require them. The entire object is rebuilt and compared after every coherent wave so an apparent local win cannot hide a sibling or ownership regression.
Experiments
Wave 1, natural HCEX/January topology
The six dispatcher bodies were written as ordinary callback-table lookups with
the January action-index assertion restored at its original file and line.
The two name getters and default-state getter use a single initialized result
and one range guard. actor_action_debug_color retained the existing body and
restored the missing signed lower-bound check. The first compile made nine of
the eleven functions exact. The only two misses were otherwise-identical
conditional branches: jae in the candidate versus January's jge in
actor_mode_name and actor_action_get_default_state.
That difference came from using NUMBEROF(array), whose unsigned result
converted the short index before the upper-bound comparison. Replacing the
two unsigned counts with named enum constants kept the code readable and
restored the original signed comparison. The second compile made all eleven
functions exact. _global_actor_mode_names also matches as a 16-byte data
owner with the exact three string relocations and null final entry.
Strict result after Wave 1: 11/63 functions, 1,088/19,172 code bytes, and 16/16 bytes of the actor-mode-name data owner are exact. No existing exact function regressed and no shared header was changed.
Waves 2-4, panic and berserk leaves
The next twelve leaf functions were recovered from the exact-unique HCEX map, then checked individually against January. The HCEX bodies supplied names and high-level intent, while January supplied the missing Xbox-only details. Two general source-shape findings mattered:
- The redundant sign extension in the attached-melee panic handler comes from
applying the ordinary
MAXmacro directly to a short field and an enum. An explicitly typed temporary removes that promotion and does not match. - January has a local-game debug bypass in the panic-from-damage threshold
which is absent from HCEX. Naming byte +8 in
ai_debug_staterecovered the condition without changing that structure's layout. January only applies the bypass to a local connection; spelling the connection test in the other direction leaves one branch different.
The projectile and burning handlers also proved the responsible-unit and active-prop call chains. January's relocation identities, not just the call count, were used to distinguish those helpers. At the end of these waves the object was 23/63 functions exact, 2,688/19,172 code bytes; all earlier matches remained exact.
Wave 5, action stopping and transition denial
actor_action_can_stop_guarding first compiled to the correct logic but used
four-byte integer result materialization. Casting the two comparison
expressions to the real one-byte boolean type produced January's setge al,
xor al,al, and shared one-byte true exit exactly. This is a type recovery,
not a code-forcing cast: the API genuinely returns the project's byte-sized
boolean.
actor_action_deny_transition initially nested encounter_get inside
encounter_get_squad. That changed argument evaluation and cleanup order.
Binding the encounter datum first, as a readable named local, reproduced
January's call order and naturally emitted the 144-byte
encounter_get_squad header helper. Spelling the timer branch as
combat_status >= 5 followed by expiration (with denial in the else) fixed
the remaining branch direction. Both functions and the helper are strict
exact.
For actor_action_can_stop_conversing, an initialized TRUE result preserves
the no-conversation exit. A compound logical expression and three explicit
early returns both placed the shared true block at the end. The readable
if/else if/else result chain places the first true block where January does
and makes later true branches jump backward to it. The result is strict exact,
including the scenario tag-block access and all six relocation destinations.
Strict result after Wave 5: 27/63 functions, 3,280/19,172 code bytes, plus the exact actor-mode-name data owner. No exact sibling regressed.
actor_action_allow_cover_seeking live residual
The semantic body is recovered and January proves that the damage comparison
uses recent_body_damage (+0x1c0), despite the HCEX reconstruction naming it
as shield damage. Direct definition-field accesses recover January's x87
loads and avoid a spurious float local. Four declaration/scope variants for
the short visibility duration and the long-lived allow boolean have been
measured. They preserve 8/8 relocation destinations but our current source
keeps the duration in BX and spills allow; January keeps allow in BL
and reuses the dead second-parameter home for the duration. Declaration order
alone did not move this allocation. The readable best body remains in source
as a nonexact reconstruction; no credit is granted, and register-forcing or
volatile tricks are barred.
Waves 6-7, setup leaves and transition handlers
actor_action_try_to_seek_cover and actor_action_try_to_panic use a complete
action_state_data union, not a member-sized scratch buffer. The full union
is part of the original readable contract even though each setup helper only
writes its own member; it also reproduces January's stack frame and argument
layout. actor_action_handle_pending_command_list likewise needed the full
union and the original ordering: perform the immediate action first, then
clear the command-list reference. All three compiled strict-exact on the
first measured natural shape.
actor_action_handle_done_fleeing, actor_action_handle_combat_failure, and
actor_action_handle_berserk_transition were recovered from the mapped source
and January control flow. The fleeing handler retains the original success
assert and source line 2681. actor_action_handle_combat_transition also has
an Xbox-only tail absent from HCEX: when already in combat with status zero it
calls actor_action_class and deliberately discards the value. That call is
not dead trivia--it preserves the original range assertion. These four
handlers are all strict-exact.
actor_action_handle_exit_pursuit exposed a useful source-shape rule. Assigning
each action's done flag directly to the return variable made VC7 reuse AL for
the flag tests instead of January's DL. Calling lost-contact directly from
each successful case fixed the registers but allowed the compiler to combine
the preceding cdecl cleanup in the wait arm. The exact readable topology is:
test each action flag directly, set a one-byte reported = TRUE marker after
any perception notification, then make one common lost-contact call when the
marker is set. This preserves separate cleanup, keeps flag tests in DL, and
leaves the called result naturally in AL.
Strict result after Wave 7: 33/63 functions, 4,208/19,172 code bytes, plus the exact actor-mode-name data owner. Every previously exact sibling remained exact.
actor_action_handle_grenade_throwing live residual
The January semantics and all seven relocation destinations are recovered.
Readiness requires a target type of at least five and rejects a fleeing actor
whose panic type is positive; the variant definition is fetched before those
checks. The two grenade-stimulus cases use combat status or the prop's swarm
state, and an already-armed throw is attempted after consideration. The best
readable body is 224 bytes versus January's 208. January keeps the result in
BL, reloads the actor index for calls, and emits a sign-extended short switch;
the current compiler keeps the actor index in EBX, spills the result byte,
and retains word comparisons. Moving the result and short declarations did
not change that allocation. The function remains nonexact and uncredited;
broader semantic work continues before any tie classification.
Wave 8, vehicle and grenade leaves
actor_action_try_to_enter_vehicle initially gave VC7 a separate working
pointer for either the caller's seat list or the local fallback array. That
extra lifetime changed the January register assignment. Reusing the optional
parameter itself as the working pointer is both ordinary C and the natural
contract: when the pointer is null it is assigned the local array, otherwise
the caller-supplied count is used. Keeping the loop count short and sharing
the final false return then reproduced all 208 bytes and six relocations.
actor_action_try_to_throw_grenade required four semantic details that the
later HCEX body obscured. The damage rejection is spelled !(damage > 0) so
the original NaN path is preserved; the normalized direction must be positive;
the facing rejection is !(dot < cosine); and the throw order sets the combat
flag before clearing the pending-plan flag. The minimum cosine is named from
its January float bits rather than replaced by a decimal approximation. The
result is strict-exact at 288 bytes and twelve relocations.
The neighboring actor_action_consider_grenade is reconstructed and has the
exact 288-byte size and all eleven relocations. Its remaining bytes are only
three references to one stack slot: January colors the base-chance temporary
at [ebp-4], while this build colors it at [ebp-8]. Tested shapes include a
single combined expression, three named float temporaries, moving those
declarations outside the check, moving them before the last-check local, and
scoping the last-check local inside a nested block. The best readable shape
retains the named base/difficulty/result calculation and is not credited.
Wave 9, exact vehicle-exit cluster
The mapped HCEX vehicle-exit topology incorrectly returned immediately when
the actor had no vehicle. January always clears stimuli.vehicle_eviction,
including that path, so the search is a guarded body followed by unconditional
cleanup. The first corrected shape reached exact size and relocation count,
but kept the successful exited value live in BL across game_time_get.
January calls game_time_get, stores the re-entry deadline, and only then sets
exited = TRUE. That source statement order lets VC7 return constant AL=1
on the successful path while retaining the false result slot for both failure
paths. The complete handler is strict-exact at 336 bytes and seven
relocations.
The adjacent private code_0000b140 is the vehicle re-entry cooldown check.
It takes the actor index in the compiler-selected EAX private convention and
the vehicle index on the stack, returning false only while the requested
vehicle equals vehicle_ignore_index and the deadline is still in the future.
The direct readable predicate compiled strict-exact on its first build: 64
bytes and three relocations.
actor_action_handle_vehicle_entry checkpoint residual
The full 960-byte January behavior is reconstructed, including both candidate
sources: friendly player-driven vehicle props and the 32-entry scripted
enterable-vehicle registry. The source-local registry layout is proven as
0x28 bytes per entry at ai_globals+0x3B8; no shared AI header was expanded.
The helper calls, object types, distances, team/type masks, packed AI-index
matching, and action setup give the exact 22 relocation destinations.
The first natural form was 960 bytes but mirrored the long-lived actor pointer
and actor index between EBX and EDI. Making the result a real named
success/failure value restored January's register roles. Further measured
changes were semantic rather than register forcing:
- spell the best-distance test as
!(distance < best)to reject NaNs exactly; - compare the packed encounter component through a dword xor plus
& 0xFFFF, rather than letting VC7 narrow the xor to a word; - express AI scope selection as a switch, producing January's decrement chain;
- increment the long loop variables normally while comparing their short views, avoiding extra sign extension; and
- assign the prop-derived squared distance before its vehicle index, allowing the independent integer store to fill the x87 multiply window.
Those changes make the body instruction-identical over its substantive search
windows. The best checkpoint form is 944 bytes with all 22 relocations and
differs in the false-return topology: January materializes a zero byte in
DL for the three early guards and retains a separate late false epilogue;
this compiler folds both blocks despite equivalent named-result and explicit
label spellings. No barrier, volatile, register forcing, or control-flow byte
trick was attempted. The readable body stays as a nonexact reconstruction
and receives no credit.
Checkpoint status before canonical reconciliation
The independent wave ends at 37/63 strict-exact functions and
4,760/19,172 meaningful January code bytes (24.83%). Those functions
occupy 5,104 padded section bytes; keeping the meaningful and padded measures
separate avoids overstating progress. The 16-byte/3-relocation
actor-mode-name data owner is also strict-exact. The 888-byte action dispatch
table remains deliberately disabled because callbacks in other unfinished
action translation units are not yet linkable; it receives no data credit.
The only shared-header changes are prototypes in actions.h and a
layout-neutral name for the already-existing byte at ai_debug_state+8.
actors.h, ai.h, ai_communication.h, encounters.h, path.h, and the
other headers concurrently used by the AI-debug lane were not modified.
Canonical reconciliation and final checkpoint gates
The checkpoint was rebased onto canonical a0bad121 before final validation.
The Actions object was then forcibly deleted and rebuilt with the unchanged
January campaign flags. The hardened semantic audit still finds exactly
37 strict functions (4,760 meaningful bytes), while direct coff_compare
confirms _global_actor_mode_names at 16 bytes, three relocations, and
normalized SHA-256
374708fff7719dd5979ec875d56cd2286f6d3cf7ec317a3b25632aab28ec37bb.
The object remains NonMatching; no function, data, semantic, or parked
exception was added to configuration.
The fail-closed regression gate took a fresh Actions snapshot, deleted and
rebuilt the object again, and reported all 37 functions still_exact, with
zero changed nonexact functions, newly exact warnings, failures, ownership
warnings, or other warnings. Full halobetacache_build and libcmt_build
both passed after the rebase, so the layout-neutral header name and Actions
prototypes compile across every consumer. progress and semantic_progress
completed with 470 units scanned, 3,473 semantic-exact functions, 93 hidden
exact functions, and zero unit errors. The admission audit produced no new
completion candidate; this checkpoint intentionally receives no object-level
credit.
All 179/179 tool tests passed. git diff --check is clean, the config
diff is empty, and the added production code contains no assembly, volatile,
force/no-inline annotation, optimizer barrier, undefined-behavior trick,
byte forcing, compiler-flag change, or comparator change. Original behavior
that may look surprising is retained and explained above rather than silently
"fixed". This is therefore an honest readable-C foundation checkpoint, not
a claim that actions.obj is finished.