diff --git a/docs/object_matching_logs/recorded_animations_obj_jonas_continuation_20260827.md b/docs/object_matching_logs/recorded_animations_obj_jonas_continuation_20260827.md new file mode 100644 index 00000000..8e06b6c7 --- /dev/null +++ b/docs/object_matching_logs/recorded_animations_obj_jonas_continuation_20260827.md @@ -0,0 +1,103 @@ +# `recorded_animations.obj` continuation wave (2026-08-27) + +## Result + +This wave advances `recorded_animations.obj` from 6/16 to 12/16 +strict-exact functions. The object now has no unwritten function owners: +12 are strict exact and the remaining four are typed, target-sized residuals +with the target relocation counts. + +The strict-exact owners account for 1,648/2,800 padded target bytes. + +| Strict function | Padded bytes | Relocations | Normalized SHA-256 | +| --- | ---: | ---: | --- | +| `_code_000836c0` | 96 | 4 | `dfe89944f5a5133e3b05901bba5076f4bcda484539e5373342c7a97452e745b7` | +| `_recorded_animation_controlling_unit` | 96 | 4 | `f21618f605218178bd0239d32d5361e014e232c079b62c3efde6d64fecdcfef0` | +| `_recorded_animation_play` | 32 | 1 | `ef585acc1d2013cd265d49eaa84ecdaf199a4240338ab525e2e5415f55138fa2` | +| `_recorded_animation_play_and_delete` | 32 | 1 | `35ec6175e1878120e0ea7fdb6a81827fd940b345897631bc2401ac44c32740ea` | +| `_recorded_animation_play_and_hover` | 32 | 1 | `c11e67670227db5072be50fc141406af4628cdaabce20cdbd8b1377aed52d2a3` | +| `_recorded_animations_clear_debug_storage` | 64 | 7 | `1211ce37dc4823572de1d952fab7bec9bfd63c39ac60cad7e77b3c10d3603e2e` | +| `_recorded_animations_dispose` | 48 | 4 | `b9b64bb030325c73231a51ab101f24b1d8ce3ec37789c76d2686751f0cc07ed0` | +| `_recorded_animations_dispose_from_old_map` | 16 | 2 | `9dedd614496101c84c04e338b0fb2167843b37380b3f0e2a50007ce9c2bc7edc` | +| `_recorded_animations_initialize` | 128 | 14 | `62d7955bddb2ac2c835aad58824e6de31cd285e0c65afd49c7325dd4cf8ed262` | +| `_recorded_animations_initialize_for_new_map` | 80 | 9 | `974d8b0cddba4e676cb8649ac9b1efbfa2e238ef8e1fe20f223dc475581781e8` | +| `_recorded_animations_update` | 560 | 34 | `d0606972324bba256754b94bd47bef61e499de94bc830df2dcbb5cff364c5f05` | +| `_render_debug_recording` | 464 | 28 | `a888551deefc05cfa73b658369687feeb95c6c0fe75561a53f9122ef938ea885` | + +The retained residual owners are: + +| Residual function | Target/candidate padded bytes | Target/candidate relocations | Remaining difference | +| --- | ---: | ---: | --- | +| `_code_000839a0` | 640/640 | 38/38 | normalized instruction SHA | +| `_recorded_animation_get_time_left` | 144/144 | 8/8 | normalized instruction SHA | +| `_recorded_animation_kill` | 96/96 | 4/4 | normalized instruction SHA | +| `_recorded_animation_verify` | 272/272 | 14/14 | normalized instruction SHA | + +## Recovered behavior + +The update owner is now a complete typed implementation. It advances each +playback codec, validates the relative tick count and debug stream cursor, +applies unit control, detects corrupt early termination, restores unit state, +performs the optional destroy/hover completion actions, clears the debug slot, +and retires the datum. Its 192 decoded instructions match the target exactly. + +The debug renderer is also complete and strict exact. It builds the original +tabbed debug report in the 0x2800-byte local buffer, resolves scenario object +and recording names through typed tag blocks, applies the 200/300 tab stops, +renders the string, and restores the tab-stop state. Its 150 decoded +instructions and all 28 relocation identities match the target. + +This wave also closes initialization, new-map debug clearing, debug storage +clearing, and the controlling-unit query. `recorded_animation_verify`, the +time-left query, the kill operation, and the private playback worker are kept +as lawful semantic residuals rather than forced through compiler artifacts. + +## Evidence and experiments + +The January split COFF remained the byte, relocation, string, line-number, +field-width, and control-flow authority. Semantic recovery used the pinned +local donors under: + +- `build/audit/refs/stian-halo/src/halo/cutscene/recorded_animations.c`; +- `build/audit/refs/halocea/src/blam/cutscene/recorded_animations_update.c`; +- the recorded-animation playback and lookup donors documented by the prior + playback wave. + +The update target's relocation order exposed the two omitted stream checks and +the dynamic `animation %s appears corrupt` assertion. Restoring those ordinary +source blocks first produced an equal 560-byte body; matching the natural +positive-valid-object branch, branch-local lifetimes, direct flag fields, and +the original stringized expression then made the owner strict exact. + +The renderer donor produced an instruction-for-instruction match on its first +typed implementation apart from the two independent zero assignments. Reversing +their source order recovered the target schedule. Naming the two standalone +globals directly recovered the target relocation identities without raw +addresses or synthetic aliases. + +For `recorded_animation_verify`, a typed 0x40-byte `unit_control_data` aggregate +recovered the target size and relocation count. Its remaining difference is +the compiler's choice of two interchangeable local stack slots. Natural loop, +break, nested-if, shared-result, and early-return forms were evaluated for the +two small query/kill residuals; the retained forms preserve target sizes and +relocation counts without steering constructs. + +Claude's active work was treated read-only throughout. No Claude worktree, +index, object, build output, or session state was modified. + +## Policy and validation + +The retained source uses typed animation-thread, codec, debug-entry, scenario, +object, unit-control, tag-block, and data-iterator access. It contains no +assembly, volatile/register steering, pragmas, intrinsics, barriers, raw +addresses, byte-offset access, pointer reconstruction, punning, undefined +behavior, synthetic anchors, byte payloads, or object patching. Declarations +remain C89-compatible and every function returns explicitly. + +The frozen whole-object scratch gate reports `12 exact / 4 residual / 0 +unwritten`. One selected-object production Ninja build completed successfully, +and direct post-build comparison reproduced `12 / 4 / 0`. `git diff --check` +passed; Git emitted only its line-ending notice. The production compiler also +reported one pointer-level warning at the `game_state_data_new` assignment, +caused by this translation unit's missing visible allocator prototype; the +32-bit emitted owner nevertheless matches the target strictly. diff --git a/source/cutscene/recorded_animations.c b/source/cutscene/recorded_animations.c index b92ad081..a5572cac 100644 --- a/source/cutscene/recorded_animations.c +++ b/source/cutscene/recorded_animations.c @@ -92,8 +92,10 @@ symbols in this file: #include "game/players.h" #include "memory/data.h" #include "objects/objects.h" +#include "render/render_debug.h" #include "scenario/scenario.h" #include "scenario/scenario_definitions.h" +#include "text/draw_string.h" #include "units/units.h" /* ---------- constants */ @@ -101,12 +103,31 @@ symbols in this file: enum { _recording_thread_finished_bit = 0, + _recording_thread_killed_bit = 1, _recording_thread_unit_was_controlled_bit = 2, + _recording_thread_delete_unit_on_complete_bit = 3, + _recording_thread_hover_vehicle_on_complete_bit = 4, RECORDED_ANIMATION_VERSION = 4, }; /* ---------- structures */ +struct unit_control_data +{ + char animation_state; + char aiming_speed; + word control_flags; + short weapon_index; + short grenade_index; + short zoom_level; + short pad; + real_vector3d throttle; + real primary_trigger; + real_vector3d facing_vector; + real_vector3d aiming_vector; + real_vector3d looking_vector; +}; + struct animation_thread { short identifier; @@ -116,7 +137,7 @@ struct animation_thread word flags; long relative_ticks; byte *event_stream; - byte controller[0x40]; + struct unit_control_data controller; byte animation_state[0xC]; short version; short pad62; @@ -126,8 +147,8 @@ struct animation_thread_debug { boolean active; byte pad1[3]; - byte *event_stream; - long event_stream_size; + byte *event_stream_start; + long stream_length; short animation_index; short padE; }; @@ -139,6 +160,11 @@ struct animation_playback void *controller, byte **event_stream, byte unit_control_data_version); + boolean (*apply_event_stream)( + void *animation_state, + struct unit_control_data *controller, + long *relative_ticks, + byte **event_stream); }; struct recorded_animation_playback_data @@ -168,6 +194,15 @@ static boolean code_000839a0( short animation_index, word extra_flags); +void unit_control( + long unit_index, + struct unit_control_data const *control_data); +void hs_object_destroy( + long object_index); +void vehicle_hover( + long vehicle_index, + boolean hover); + extern boolean recorded_animation_controlling_unit( long unit_index); @@ -176,9 +211,39 @@ extern boolean recorded_animation_controlling_unit( extern struct animation_thread_debug *animation_threads_debug; extern struct recorded_animations_globals_prefix bss_00435ca4; extern struct recorded_animation_playback_data data_002dd160; +extern boolean debug_recording; +extern short debug_recording_newlines; /* ---------- public code */ +#define animation_threads bss_00435ca4.animation_threads + +void recorded_animations_initialize( + void) +{ + animation_threads = game_state_data_new( + "recorded animations", + 64, + sizeof(struct animation_thread)); + match_assert( + "c:\\halo\\SOURCE\\cutscene\\recorded_animations.c", + 108, + animation_threads); + + animation_threads_debug = match_malloc( + "c:\\halo\\SOURCE\\cutscene\\recorded_animations.c", + 111, + 0x400); + match_assert( + "c:\\halo\\SOURCE\\cutscene\\recorded_animations.c", + 112, + animation_threads_debug); + + return; +} + +#undef animation_threads + void recorded_animations_dispose( void) { @@ -202,6 +267,346 @@ void recorded_animations_dispose_from_old_map( return; } +void recorded_animations_clear_debug_storage( + void) +{ + match_assert( + "c:\\halo\\SOURCE\\cutscene\\recorded_animations.c", + 0x99, + animation_threads_debug); + memset(animation_threads_debug, 0, 0x400); + + return; +} + +void recorded_animations_update( + void) +{ + struct data_iterator iterator; + struct animation_thread *thread; + + data_iterator_new(&iterator, bss_00435ca4.animation_threads); + thread = data_iterator_next(&iterator); + while (thread) + { + if (object_try_and_get_and_verify_type(thread->unit_index, _object_mask_unit)) + { + if (!TEST_FLAG(thread->flags, _recording_thread_finished_bit)) + { + struct animation_thread_debug *thread_debug; + long *relative_ticks; + boolean finished; + + thread->ticks_left--; + relative_ticks = &thread->relative_ticks; + finished = !playback_codec[thread->version]->apply_event_stream( + thread->animation_state, + &thread->controller, + relative_ticks, + &thread->event_stream); + + match_assert( + "c:\\halo\\SOURCE\\cutscene\\recorded_animations.c", + 347, + thread->relative_ticks>=0); + + thread_debug = animation_threads_debug_get(iterator.datum_index); + if (thread_debug->active) + { + match_assert( + "c:\\halo\\SOURCE\\cutscene\\recorded_animations.c", + 354, + thread->event_stream-thread_debug->event_stream_startstream_length||(thread->event_stream-thread_debug->event_stream_start==thread_debug->stream_length&&finished)); + } + + thread->relative_ticks++; + unit_control(thread->unit_index, &thread->controller); + SET_FLAG( + thread->flags, + _recording_thread_finished_bit, + finished); + } + else + { + struct animation_thread_debug *thread_debug; + + thread_debug = animation_threads_debug_get(iterator.datum_index); + if (thread_debug->active && + !TEST_FLAG(thread->flags, _recording_thread_killed_bit) && + thread->ticks_left != 0) + { + match_vassert( + "c:\\halo\\SOURCE\\cutscene\\recorded_animations.c", + 373, + FALSE, + csprintf( + temporary, + "animation %s appears corrupt", + TAG_BLOCK_GET_ELEMENT( + &global_scenario_get()->recorded_animations, + thread_debug->animation_index, + struct recorded_animation_definition)->name)); + } + + thread_debug->active = FALSE; + unit_set_controllable( + thread->unit_index, + TEST_FLAG(thread->flags, _recording_thread_unit_was_controlled_bit)); + unit_set_possessed(thread->unit_index, FALSE); + unit_set_actively_controlled(thread->unit_index, FALSE); + object_set_automatic_deactivation(thread->unit_index, TRUE); + if (TEST_FLAG( + thread->flags, + _recording_thread_delete_unit_on_complete_bit)) + { + hs_object_destroy(thread->unit_index); + } + if (TEST_FLAG( + thread->flags, + _recording_thread_hover_vehicle_on_complete_bit)) + { + vehicle_hover(thread->unit_index, TRUE); + } + + datum_delete(bss_00435ca4.animation_threads, iterator.datum_index); + } + } + else + { + datum_delete(bss_00435ca4.animation_threads, iterator.datum_index); + } + + thread = data_iterator_next(&iterator); + } + + return; +} + +void recorded_animations_initialize_for_new_map( + void) +{ + data_make_valid(bss_00435ca4.animation_threads); + recorded_animations_clear_debug_storage(); + + return; +} + +void render_debug_recording( + void) +{ + char string[0x2800]; + struct data_iterator iterator; + struct scenario_object_name *object_name; + short tab_stops[2]; + short newline_index; + long string_length; + struct animation_thread *thread; + struct object_datum *object; + struct animation_thread_debug *thread_debug; + char const *recording_name; + + if (debug_recording) + { + string_length = 0; + newline_index = 0; + tab_stops[0] = 200; + tab_stops[1] = 300; + while (newline_index < debug_recording_newlines) + { + string_length += sprintf(string + (short)string_length, "|n"); + newline_index++; + } + + string_length += sprintf( + string + (short)string_length, + "recording name|tticks left|tobject name"); + + data_iterator_new(&iterator, bss_00435ca4.animation_threads); + thread = data_iterator_next(&iterator); + while (thread) + { + object = object_try_and_get_and_verify_type( + thread->unit_index, + _object_mask_all); + thread_debug = animation_threads_debug_get(iterator.datum_index); + if (!TEST_FLAG(thread->flags, _recording_thread_finished_bit) && + object && + object->object.name_index != NONE) + { + object_name = TAG_BLOCK_GET_ELEMENT( + &global_scenario_get()->object_names, + object->object.name_index, + struct scenario_object_name); + + recording_name = ""; + if (thread_debug->active) + { + recording_name = TAG_BLOCK_GET_ELEMENT( + &global_scenario_get()->recorded_animations, + thread_debug->animation_index, + struct recorded_animation_definition)->name; + } + + string_length += sprintf( + string + (short)string_length, + "|n%s|t", + recording_name); + string_length += sprintf( + string + (short)string_length, + "%d|t", + thread->ticks_left); + string_length += sprintf( + string + (short)string_length, + "%s", + object_name->name); + } + + thread = data_iterator_next(&iterator); + } + + string[0x400] = 0; + draw_string_set_tab_stops(tab_stops, NUMBEROF(tab_stops)); + render_debug_string(TRUE, string); + draw_string_set_tab_stops(tab_stops, 0); + } + + return; +} + +void recorded_animation_verify( + struct recorded_animation_definition const *animation) +{ + struct unit_control_data controller; + byte animation_state[0xC]; + byte *stream; + byte *playback_stream; + long size; + long relative_ticks = 0; + long ticks_left; + boolean finished; + + stream = animation->event_stream.address; + playback_stream = stream; + size = animation->event_stream.size; + ticks_left = (word)animation->length_in_ticks; + + playback_codec[animation->version-1]->initialize_event_stream( + animation_state, + &controller, + &playback_stream, + animation->unit_control_data_version); + + do + { + ticks_left--; + finished = !playback_codec[animation->version-1]->apply_event_stream( + animation_state, + &controller, + &relative_ticks, + &playback_stream); + + match_assert( + "c:\\halo\\SOURCE\\cutscene\\recorded_animations.c", + 428, + ticks_left>=0); + match_assert( + "c:\\halo\\SOURCE\\cutscene\\recorded_animations.c", + 429, + relative_ticks>=0); + match_assert( + "c:\\halo\\SOURCE\\cutscene\\recorded_animations.c", + 430, + playback_stream-streamunit_index != unit_index || + TEST_FLAG(thread->flags, _recording_thread_finished_bit)) + { + thread = data_iterator_next(&iterator); + if (!thread) + break; + } + + if (thread) + result = TRUE; + } + + return result; +} + +void recorded_animation_kill( + long unit_index) +{ + 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) + break; + + thread = data_iterator_next(&iterator); + } + + if (thread) + { + thread->flags |= + (1 << _recording_thread_finished_bit) | + (1 << _recording_thread_killed_bit); + } + + return; +} + +long recorded_animation_get_time_left( + long unit_index) +{ + long result = 0; + struct data_iterator iterator; + struct animation_thread *thread; + + data_iterator_new(&iterator, bss_00435ca4.animation_threads); + thread = data_iterator_next(&iterator); + if (thread) + { + while (thread->unit_index != unit_index) + { + thread = data_iterator_next(&iterator); + if (!thread) + break; + } + } + + match_assert( + "c:\\halo\\SOURCE\\cutscene\\recorded_animations.c", + 312, + !thread||thread->unit_index==unit_index); + + if (thread) + result = thread->ticks_left; + + return result; +} + boolean recorded_animation_play( long unit_index, short animation_index) @@ -299,8 +704,8 @@ static boolean code_000839a0( 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->event_stream_start = thread->event_stream; + debug->stream_length = animation->event_stream.size; debug->animation_index = animation_index; thread->version = animation->version - 1; @@ -313,7 +718,7 @@ static boolean code_000839a0( playback = playback_codec[thread->version]; playback->initialize_event_stream( thread->animation_state, - thread->controller, + &thread->controller, &thread->event_stream, animation->unit_control_data_version);