diff --git a/docs/object_matching_logs/recorded_animations_obj_jonas_playback_wave_20260827.md b/docs/object_matching_logs/recorded_animations_obj_jonas_playback_wave_20260827.md new file mode 100644 index 00000000..46d4ebab --- /dev/null +++ b/docs/object_matching_logs/recorded_animations_obj_jonas_playback_wave_20260827.md @@ -0,0 +1,87 @@ +# `recorded_animations.obj` playback worker and wrapper recovery (2026-08-27) + +## Result + +This wave advances `recorded_animations.obj` from 2/16 to 6/16 strict-exact +functions. It adds the private thread lookup and all three public playback +wrappers while preserving both accepted disposal functions. + +| Strict function | Padded bytes | Relocations | Normalized SHA-256 | +| --- | ---: | ---: | --- | +| `_recorded_animations_dispose` | 48 | 4 | `b9b64bb030325c73231a51ab101f24b1d8ce3ec37789c76d2686751f0cc07ed0` | +| `_recorded_animations_dispose_from_old_map` | 16 | 2 | `9dedd614496101c84c04e338b0fb2167843b37380b3f0e2a50007ce9c2bc7edc` | +| `_code_000836c0` | 96 | 4 | `dfe89944f5a5133e3b05901bba5076f4bcda484539e5373342c7a97452e745b7` | +| `_recorded_animation_play` | 32 | 1 | `ef585acc1d2013cd265d49eaa84ecdaf199a4240338ab525e2e5415f55138fa2` | +| `_recorded_animation_play_and_delete` | 32 | 1 | `35ec6175e1878120e0ea7fdb6a81827fd940b345897631bc2401ac44c32740ea` | +| `_recorded_animation_play_and_hover` | 32 | 1 | `c11e67670227db5072be50fc141406af4628cdaabce20cdbd8b1377aed52d2a3` | + +The four new owners contribute 151 meaningful bytes: 85 bytes for the lookup +and 22 bytes for each wrapper. Including the two disposal owners, the object +now has 200/2,684 meaningful code bytes exact. + +The recovered `_code_000839a0` playback worker is a real typed implementation, +not a wrapper-enabling stub. It is 640/640 padded bytes, has the target's 38 +relocations and the same relocation destinations in the same semantic order, +and emits the exact target assertion expression and error strings. Its +normalized instruction bytes remain residual because the compiler assigns the +long-lived unit and animation-index values to the opposite ESI/EDI roles and +schedules several typed field loads differently. That residual is retained as +the lawful foundation for the remaining object. + +## Evidence and history audit + +Before editing production, the complete reachable source history for +`source/cutscene/recorded_animations.c` was swept through the scratch compiler. +There were three unique blobs; the cumulative source was already best at 2/16. +The historical `jonas/recorded-animation-wrappers-wave-20260820` tree contained +only the symbol skeleton, not hidden implementations. + +The prior accepted ledgers were read in full: + +- `recorded_animations_obj_jonas_dispose_old_map_20260821.md`; +- `recorded_animations_obj_jonas_dispose_first_shot_20260826.md`. + +Their private-EAX warning was decisive: compiling the public donor wrappers as +ordinary external forwards is not target-authentic. The worker and lookup had +to exist in the same translation unit so VC7 could derive the private register +ABI naturally. + +Semantic source came from the pinned local donor mirrors: + +- `build/audit/refs/halocea/src/blam/cutscene/recorded_animation_play_internal.c`; +- `build/audit/refs/halocea/src/blam/memory/get_controlling_thread.c`; +- the three HCEA playback wrapper sources under + `build/audit/refs/halocea/src/blam/cutscene/`; +- `build/audit/refs/stian-halo/src/halo/cutscene/recorded_animations.c`. + +January split COFF and relocation-annotated disassembly remained the authority +for control-flow order, field widths, private ABI, exact strings, calls, +relocations, and padding. In particular, the January helper's loop is a +`while (thread)` scan with an equality break. Expressing that ordinary source +topology made `_code_000836c0` strict exact and simultaneously gave all three +wrappers their exact EAX-plus-stack call shape. + +Claude's active worktrees were read only. No file, index, object, or build +artifact in those worktrees was modified. + +## Policy and validation boundary + +The retained implementation uses typed structures for the animation thread, +debug entry, playback codec table, scenario block, and data-array iterator. It +contains no assembly, volatile/register steering, pragma, intrinsic, +attribute, barrier, raw address, raw byte-offset access, pointer/integer +reconstruction, punning, undefined behavior, synthetic anchor, payload, or +object-byte forcing. All declarations are C89-compatible and every function +has an explicit return. + +The scratch gate reports 6 exact, one residual, and nine unwritten functions. +Direct production-object comparison proves all six accepted owners have equal +padded bytes plus ordered relocation address/type/destination/addend identity. +The final frozen source was rebuilt through one normal selected-object Ninja +edge for the helper/CFG wave. + +The campaign gate and branch sweep were also made concurrency-safe during this +audit: alternate branch blobs now compile through `gate.py --source` from a +PID-scoped scratch source. `branch_sweep.py` no longer overwrites or restores +the production source, so it cannot collide with Claude or another active +worker. diff --git a/docs/object_matching_logs/wind_obj_jonas_axis_countdown_20260827.md b/docs/object_matching_logs/wind_obj_jonas_axis_countdown_20260827.md new file mode 100644 index 00000000..e2468efb --- /dev/null +++ b/docs/object_matching_logs/wind_obj_jonas_axis_countdown_20260827.md @@ -0,0 +1,25 @@ +# `wind.obj` axis-countdown refinement (2026-08-27) + +## Result + +`wind.obj` remains 7/9 strict-exact functions, with `_code_0017f750` residual +and `_code_0017fbd0` unwritten. The retained ordinary-C change replaces the +three-axis index comparison with the target-backed two-role loop topology: +`axis_index` selects the variance row while `axes_remaining` is the countdown. + +The candidate remains 208 meaningful/padded bytes versus the target's 224 and +retains all eight target relocation destinations. Its relocation-normalized +differing-byte distance improves from 193 to 124. The target loop prologue, +`mov ebx,3`, countdown `dec ebx`, and back edge now agree. + +The remaining principal difference is the January compiler's full-width +sign-bit clearing sequence around the floating sample key. The lawful source +still canonicalizes `fabs` to the x87 `fabs` instruction. A split assignment +was scratch-tested and produced the same x87 form. The historical exact +experiment used inactive-union access and remains rejected; byte +representation writes are outside campaign policy. + +All current wind ledgers and reachable source-history blobs were audited before +retention. The final gate reports seven exact, one residual, and one unwritten +function, and the frozen source was rebuilt through one normal selected-object +Ninja edge. No accepted sibling changed. diff --git a/source/cutscene/recorded_animations.c b/source/cutscene/recorded_animations.c index cf840f9d..b92ad081 100644 --- a/source/cutscene/recorded_animations.c +++ b/source/cutscene/recorded_animations.c @@ -87,14 +87,72 @@ symbols in this file: /* ---------- headers */ #include "cseries/cseries.h" +#include "cseries/errors.h" +#include "cutscene/recorded_animation_definitions.h" +#include "game/players.h" #include "memory/data.h" +#include "objects/objects.h" +#include "scenario/scenario.h" +#include "scenario/scenario_definitions.h" +#include "units/units.h" /* ---------- constants */ -/* ---------- macros */ +enum +{ + _recording_thread_finished_bit = 0, + _recording_thread_unit_was_controlled_bit = 2, + RECORDED_ANIMATION_VERSION = 4, +}; /* ---------- structures */ +struct animation_thread +{ + short identifier; + short pad2; + long unit_index; + word ticks_left; + word flags; + long relative_ticks; + byte *event_stream; + byte controller[0x40]; + byte animation_state[0xC]; + short version; + short pad62; +}; + +struct animation_thread_debug +{ + boolean active; + byte pad1[3]; + byte *event_stream; + long event_stream_size; + short animation_index; + short padE; +}; + +struct animation_playback +{ + void (*initialize_event_stream)( + void *animation_state, + void *controller, + byte **event_stream, + byte unit_control_data_version); +}; + +struct recorded_animation_playback_data +{ + void *unused[4]; + struct animation_playback *playback_codec[RECORDED_ANIMATION_VERSION]; +}; + +/* ---------- macros */ + +#define animation_threads_debug_get(index) \ + (animation_threads_debug + ((index) & 0xFFFF)) +#define playback_codec (data_002dd160.playback_codec) + struct recorded_animations_globals_prefix { struct data_array *animation_threads; @@ -102,10 +160,22 @@ struct recorded_animations_globals_prefix /* ---------- prototypes */ +static struct animation_thread *code_000836c0( + long unit_index, + long *thread_index_reference); +static boolean code_000839a0( + long unit_index, + short animation_index, + word extra_flags); + +extern boolean recorded_animation_controlling_unit( + long unit_index); + /* ---------- globals */ -extern void *animation_threads_debug; +extern struct animation_thread_debug *animation_threads_debug; extern struct recorded_animations_globals_prefix bss_00435ca4; +extern struct recorded_animation_playback_data data_002dd160; /* ---------- public code */ @@ -132,4 +202,172 @@ void recorded_animations_dispose_from_old_map( return; } +boolean recorded_animation_play( + long unit_index, + short animation_index) +{ + return code_000839a0(unit_index, animation_index, 0); +} + +boolean recorded_animation_play_and_delete( + long unit_index, + short animation_index) +{ + return code_000839a0(unit_index, animation_index, 1 << 3); +} + +boolean recorded_animation_play_and_hover( + long unit_index, + short animation_index) +{ + return code_000839a0(unit_index, animation_index, 1 << 4); +} + /* ---------- private code */ + +static struct animation_thread *code_000836c0( + long unit_index, + long *thread_index_reference) +{ + long thread_index = NONE; + struct data_iterator iterator; + struct animation_thread *thread; + + data_iterator_new(&iterator, bss_00435ca4.animation_threads); + + thread = data_iterator_next(&iterator); + while (thread) + { + if (thread->unit_index == unit_index) + { + thread_index = iterator.datum_index; + break; + } + + thread = data_iterator_next(&iterator); + } + + if (thread_index_reference) + *thread_index_reference = thread_index; + + return thread; +} + +static boolean code_000839a0( + long unit_index, + short animation_index, + word extra_flags) +{ + boolean result = FALSE; + long thread_index; + struct animation_thread *thread; + struct recorded_animation_definition const *animation; + struct animation_thread_debug *debug; + struct animation_playback *playback; + + if (unit_index != NONE) + { + if (animation_index != NONE && animation_index < global_scenario_get()->recorded_animations.count) + { + object_get_and_verify_type(unit_index, _object_mask_unit); + player_index_from_unit_index(unit_index); + + thread = code_000836c0(unit_index, &thread_index); + animation = TAG_BLOCK_GET_ELEMENT( + &global_scenario_get()->recorded_animations, + animation_index, + struct recorded_animation_definition); + + if (!recorded_animation_controlling_unit(unit_index)) + { + if (!thread) + { + thread_index = datum_new(bss_00435ca4.animation_threads); + if (thread_index != NONE) + thread = datum_get(bss_00435ca4.animation_threads, thread_index); + } + + if (thread) + { + thread->unit_index = unit_index; + thread->relative_ticks = 0; + thread->ticks_left = animation->length_in_ticks; + thread->event_stream = tag_data_get_pointer( + &animation->event_stream, + 0, + animation->event_stream.size); + + debug = animation_threads_debug_get(thread_index); + debug->active = TRUE; + debug->event_stream = thread->event_stream; + debug->event_stream_size = animation->event_stream.size; + debug->animation_index = animation_index; + + thread->version = animation->version - 1; + thread->flags &= ~(1 << _recording_thread_finished_bit); + match_assert( + "c:\\halo\\SOURCE\\cutscene\\recorded_animations.c", + 233, + animation->version>0&&animation->version<=RECORDED_ANIMATION_VERSION&&playback_codec[animation->version-1]); + + playback = playback_codec[thread->version]; + playback->initialize_event_stream( + thread->animation_state, + thread->controller, + &thread->event_stream, + animation->unit_control_data_version); + + unit_set_actively_controlled(unit_index, TRUE); + if (unit_controllable(unit_index)) + thread->flags |= 1 << _recording_thread_unit_was_controlled_bit; + else + thread->flags &= ~(1 << _recording_thread_unit_was_controlled_bit); + + unit_set_controllable(unit_index, FALSE); + unit_set_possessed(unit_index, TRUE); + object_set_automatic_deactivation(unit_index, FALSE); + thread->flags |= extra_flags; + result = TRUE; + } + else + { + error(_error_silent, "Could not allocate space for a new animation"); + } + } + else if (thread) + { + char const *playing_name; + + debug = animation_threads_debug_get(thread_index); + playing_name = ""; + if (debug->active) + { + playing_name = TAG_BLOCK_GET_ELEMENT( + &global_scenario_get()->recorded_animations, + debug->animation_index, + struct recorded_animation_definition)->name; + } + + error( + _error_silent, + "trying to play %s while %s is playing", + animation->name, + playing_name); + } + else + { + error(_error_silent, "can't play animation on unit"); + } + } + else + { + error(_error_silent, "this animation doesn't exist"); + } + } + else + { + error(_error_silent, "unit doesn't exist"); + } + + return result; +} diff --git a/source/scenario/wind.c b/source/scenario/wind.c index 9854753d..2184f40f 100644 --- a/source/scenario/wind.c +++ b/source/scenario/wind.c @@ -137,9 +137,11 @@ static void code_0017f750( real const axis_scale[3] = { 0.1f, 0.2f, 0.07f }; real magnitude = maximum_magnitude * (1.f / 3.f); long axis_index; + long axes_remaining = 3; *wind = *global_zero_vector3d; - for (axis_index = 0; axis_index < 3; axis_index++) + axis_index = 0; + do { real sample_key = (real)fabs( ((axis_scale[axis_index] * wind_globals.time * local_variation_rate) @@ -151,7 +153,9 @@ static void code_0017f750( wind->i += wind_globals.variance[axis_index][(short)sample_index].i; wind->j += wind_globals.variance[axis_index][(short)sample_index].j; wind->k += wind_globals.variance[axis_index][(short)sample_index].k; + axis_index++; } + while (--axes_remaining); wind->i *= magnitude; wind->j *= magnitude; diff --git a/tools/campaign/branch_sweep.py b/tools/campaign/branch_sweep.py index 338cf641..6f671dec 100644 --- a/tools/campaign/branch_sweep.py +++ b/tools/campaign/branch_sweep.py @@ -1,5 +1,5 @@ -"""For each named unit, try every branch's version of its .c file and report -any version that gates strictly better than the one on this branch. +"""For each named unit, compile every branch's version of its .c file from +scratch and report any version that gates strictly better than the current one. Four objects were advanced this session by finding work already done in another lane; this automates the search. @@ -23,8 +23,11 @@ def unique_blobs(path): return blobs -def gate(unit): - r = subprocess.run(GATE + [unit], capture_output=True, text=True) +def gate(unit, source=None): + command = GATE + [unit] + if source: + command += ['--source', source] + r = subprocess.run(command, capture_output=True, text=True) if 'COMPILE FAILED' in r.stdout: return None, 'compile-fail' m = re.search(r'== exact (\d+)\s+residual (\d+)\s+unwritten (\d+)', r.stdout) @@ -49,27 +52,27 @@ def sweep(unit): return cur = subprocess.run(['git', 'rev-parse', f'HEAD:{path}'], capture_output=True, text=True).stdout.strip() blobs = unique_blobs(path) - orig = open(path, 'rb').read() base_score, base_desc = gate(unit) print(f'\n=== {unit} ({len(blobs)} unique blobs) current: {base_desc}', flush=True) best = (base_score, 'CURRENT', base_desc) + scratch_source = f'scratch/_branch_sweep_{os.getpid()}.c' try: for blob, brs in blobs.items(): if blob == cur: continue content = subprocess.run(['git', 'cat-file', 'blob', blob], capture_output=True).stdout - open(path, 'wb').write(content) - score, desc = gate(unit) + open(scratch_source, 'wb').write(content) + score, desc = gate(unit, scratch_source) tag = brs[0] + (f' (+{len(brs)-1})' if len(brs) > 1 else '') better = score is not None and base_score is not None and score > base_score print(f' {blob[:8]} {tag:52s} {desc}{" <== BETTER" if better else ""}', flush=True) if score is not None and (best[0] is None or score > best[0]): best = (score, tag, desc) finally: - open(path, 'wb').write(orig) - # no ninja here: gate.py compiles to its own scratch object, so the sweep - # never reads build/base. Skipping it keeps concurrent sweeps safe (one - # ninja per build dir at a time) - run a full ninja when the sweep ends. + if os.path.exists(scratch_source): + os.remove(scratch_source) + # The sweep never edits the production source or reads build/base. + # gate.py compiles each branch blob to its own scratch object. if best[1] != 'CURRENT': print(f' >>> BEST: {best[1]} {best[2]}', flush=True) diff --git a/tools/campaign/gate.py b/tools/campaign/gate.py index e1d649e4..37880af1 100644 --- a/tools/campaign/gate.py +++ b/tools/campaign/gate.py @@ -9,6 +9,7 @@ import coff_compare as cc ap = argparse.ArgumentParser() ap.add_argument('unit') +ap.add_argument('--source') ap.add_argument('--edits') ap.add_argument('--fn', action='append', default=[]) ap.add_argument('--disas') @@ -16,7 +17,8 @@ ap.add_argument('--all', action='store_true') a = ap.parse_args() unit = a.unit.replace('\\', '/') -src = unit + '.c' +unit_src = unit + '.c' +src = a.source or unit_src bn = open('build.ninja').read() # the "build : cl " line may be wrapped with a $ continuation, so match # on the object path alone rather than requiring the leading "build " on that line @@ -42,7 +44,7 @@ obj = f'scratch/_gate_{os.getpid()}.obj' if os.path.exists(obj): os.remove(obj) cl = r'C:\halo-worktrees\claude-finish-hs-20260816\xbox\bin\vc7\CL.Exe' -incdir = os.path.dirname(src) +incdir = os.path.dirname(unit_src) cmd = [cl, '/nologo', '/c'] + toks + ['/I' + incdir, '/Fo' + obj, scratch_src] r = subprocess.run(cmd, capture_output=True, text=True) if not os.path.exists(obj) or r.returncode != 0: