- Introduced a new command 'annotate-usecode' to import USECODE IR JSON annotation hints as Ghidra comments on compiled anchors. - Added argument parsing for multiple IR JSON files, comment type selection, and a dry-run option. - Implemented logic to read annotation records from the provided IR files and set comments on the corresponding addresses in Ghidra. - Enhanced JSON schema to include response structure for the new command.
72 KiB
Raw 000a & 000d: Tracked Handles, Cache Manager & Proximity Buckets
Content extracted from crusader_decompilation_notes.md. Covers the 000d proximity/visibility bucket cluster, 000a tracked-handle table, generic cache manager, seg082 allocator, seg137/138 palette helpers, and seg004/seg005 startup paths.
Raw 000d Proximity/Visibility Bucket Cluster
Small conservative rename batch from the 000d:cc00-d413 region.
| Address | Name | Evidence |
|---|---|---|
000d:cc00 |
entity_compute_proximity_or_visibility_bucket |
Returns bucket 0x40 for null or on-screen entities (entity_projected_bbox_overlaps_viewport), else computes a distance bucket from the current reference entity at 0x7e22 with thresholds 0x17d, 0x281, 0x3c1 mapping to 0x32, 0x20, 0x10, 0x08 |
000d:d413 |
entity_refresh_recent_proximity_or_visibility_buckets |
Walks the last four active records in the 0x69ac array, recomputes the same bucket, stores it back to each entry, and calls 000a:6343 when the bucket changes |
000d:cdd0 |
tracked_entity_bucket_prune_invalid_entries |
Walks the 0x69ac array, validates backing handles through 000a:637a, and clears entry handles to 0xffff when the backing object is gone |
000d:cd62 |
tracked_entity_bucket_find_free_main_slot |
Finds the first free entry in the main portion of the 0x69ac array (0 .. count-4) |
000d:cd9a |
tracked_entity_bucket_find_free_aux_slot |
Finds the first free entry in the auxiliary tail portion of the 0x69ac array (count-4 .. count-1) |
Supporting caller notes
000d:ce1epopulates one0x69acentry by reserving a free slot, computing the initial bucket throughentity_compute_proximity_or_visibility_bucket, storing both current and previous bucket fields, then allocating/linking the backing handle through000a:5f36.000d:d409is a thin wrapper that only callsentity_refresh_recent_proximity_or_visibility_buckets.000d:cfadis an update-or-allocate helper for(param_1,param_2)pairs: it tries to update an existing tracked entry through000a:606a, clears dead entries, and falls back to000d:ce1eallocation when no live match remains.000d:cec5is the auxiliary-slot allocator: it prunes invalid entries, usestracked_entity_bucket_find_free_aux_slot, tags the new entry with byte+0x0a = 1, and seeds its handle via000a:5f36(..., flag=1).000a:606a=tracked_entity_bucket_handle_update_or_alloc— updates the backing handle for an existing tracked bucket entry when possible, or falls back to allocation via000a:5f36if the handle has gone stale.000d:d350=tracked_entity_bucket_set_value— finds a tracked(entity_id, entity_ref)entry and pushes a new bucket value into its backing handle through000a:6343.000d:d10b=tracked_entity_bucket_clear_ref_field— clears only the+0x02reference field for all matching entries.000d:d151=tracked_entity_bucket_remove_by_ref— marks matching entries' backing handles for removal and clears the local entry handle/reference fields.000d:d1b1=tracked_entity_bucket_remove_tagged_by_ref— same removal path, but only for entries whose byte+0x0atag is set.
Raw 000a Tracked-Handle Table
The 0x4673 table is the backing handle registry for the 0x69ac tracked-entry bucket subsystem. That client layer sits on top of a separate generic cache manager rooted at 0x4688..0x46b7.
| Address | Name | Evidence |
|---|---|---|
000a:5f02 |
tracked_entity_handle_find_slot |
Linear scan over 12 entries in the 0x4673 table for a matching 32-bit handle id |
000a:602b |
tracked_entity_handle_is_live |
Returns true only when a handle exists in 0x4673 and its flag word at +0x0a does not have bit 0x0002 set |
000a:60eb |
tracked_entity_handle_mark_remove |
Sets bit 0x0002 in the handle-table flag word and dispatches through the unresolved cleanup path |
000a:612e |
tracked_entity_handle_mark_remove_all |
Iterates all 12 handle-table entries and marks each live handle for removal |
000a:6167 |
tracked_entity_handle_alloc_slot |
Allocates a slot in one of two ranges (0..7 or 8..11) depending on the aux flag; when full, wraps in a ring and evicts via tracked_entity_handle_mark_remove before reusing the slot |
000a:6228 |
tracked_entity_handle_prune_removed |
Reaps entries previously marked with bit 0x0002, clears dead slots, and refreshes high-index entries through 000a:6b2d |
000a:63bc |
tracked_entity_handle_find_by_entity |
Finds the first live handle-table entry whose key/entity word at +0x04 matches the requested entity id |
Handle entry layout (stride 0x0c)
| Offset | Field |
|---|---|
+0x00 |
32-bit handle id |
+0x04 |
key/entity id |
+0x06 |
class/group/source-style selector |
+0x08 |
current bucket/value |
+0x0a |
flags (bit0 = aux-slot allocation, bit1 = pending removal) |
Thin public wrappers
| Address | Name |
|---|---|
000a:5276 |
entity_bucket_track_default_main — gated by 0x45aa; creates or refreshes a main-slot tracked handle with bucket 0x40 and selector 0xff |
000a:5294 |
entity_bucket_track_main — same path, but takes the bucket value as an argument for the main-slot range |
000a:52d0 |
entity_bucket_track_default_aux — aux-slot variant with default bucket 0x40 |
000a:52ee |
entity_bucket_track_aux — aux-slot variant with explicit bucket argument |
Raw 000a Generic Cache Manager
Follow-up analysis of 000a:6b2d and the 0x4688..0x46b7 globals shows that this region is a generic cache manager used by the tracked-handle layer, not part of the tracked-entity subsystem itself.
| Address | Name | Evidence |
|---|---|---|
000a:6b2d |
cache_lookup_or_load_entry_by_id |
Fast-paths the last id via 0x46af/0x46b1, otherwise searches 0x469d, evicts older cache slots until there is room under byte budget 0x46a5, allocates a block from the free-list, clears/initializes the payload, records the id, and dispatches through the loader interface at 0x468c |
000a:6a95 |
cache_release_entry_by_slot |
Releases a cached slot by index, clears any client references through 000a:62d8, frees its backing block through cache_free_block_by_slot, and marks the slot id in 0x469d as unused (0xffff) |
000a:6d07 |
cache_alloc_block_for_slot |
Allocates or splits a block from the free-list anchored at 0x4688, tags it with the owning cache slot index, and updates the in-use byte count at 0x46a9 |
000a:6f4d |
cache_free_block_by_slot |
Finds the free-list node for a cache slot, marks it free, subtracts its size from 0x46a9, and coalesces adjacent free blocks |
000a:67d9 |
cache_shutdown |
Tears down the generic cache manager: flushes/reset state, frees slot arrays at 0x4699/0x469d/0x46b3, frees the free-list container at 0x4688, and closes backing state at 0x4691 |
000a:6898 |
cache_set_loader_interface |
Installs the backend loader/callback interface pointer at 0x468c |
Cache globals
| Address | Name | Notes |
|---|---|---|
0x4688 |
free-list/block-list head | Used by cache_alloc_block_for_slot and cache_free_block_by_slot |
0x468c |
cache_loader_interface | Backend callback table; +0x34 = size query, +0x0c = load/bind callback |
0x4695 |
arena base pointer | Base for the raw cache payload arena |
0x4699 |
per-slot payload-pointer table | |
0x469d |
per-slot cached id table | 0xffff = unused |
0x46a5 |
byte budget / arena capacity | |
0x46a9 |
bytes currently in use | |
0x46af/0x46b1 |
fast-path cache | Last requested id and slot index |
0x46b3 |
per-slot block metadata mirror | Used when releasing or refreshing slots |
Follow-up: Cache Init and Runtime State
| Address | Name | Notes |
|---|---|---|
000a:6600 |
cache_init |
Stores slot count in 0x46ad; allocates per-slot payload-pointer table; seeds each slot; queries/derives arena size; allocates arena backing object at 0x4691; allocates per-slot metadata mirrors; initializes free-list head at 0x4688; calls cache_reset_runtime_state |
000a:68aa |
cache_reset_runtime_state |
Shared cache reset/bootstrap helper called from cache_init, cache_shutdown, and external reset paths. Allocates per-slot arena-header nodes, rebinds slot pointers to arena base, clears the cached-id table, seeds the free-list head, and resets 0x46a9 (bytes in use) plus 0x46af (last-id fast path) |
000a:703e |
cache_compact_arena_blocks |
Compacts live cache arena blocks into earlier free holes when allocation would fail, updates per-slot payload pointers, and merges adjacent free-list headers afterward |
Follow-up: Tracked-Handle Table Init/Shutdown
| Address | Name | Notes |
|---|---|---|
000a:5e00 |
tracked_entity_handle_table_init |
If 0x4672 is clear, allocates 0x90 bytes at 0x4673/0x4675, aborts through runtime_init_or_abort on failure, calls 000a:577d and local helper 000a:5e95, then sets 0x4672 = 1 |
000a:5e59 |
tracked_entity_handle_table_shutdown |
Matching teardown for tracked_entity_handle_table_init |
000a:5e95 |
tracked_entity_handle_table_clear_and_dispatch |
When tracked_entity_handle_table_active is set, zeroes the full 0x90-byte handle table at 0x4673, resets adjacent local state at 0x4677/0x4679/0x467b, then dispatches through the remaining thunked follow-up path |
000a:5339 |
tracked_entity_handle_mark_remove_all_if_enabled |
Thin gate wrapper that only forwards to tracked_entity_handle_mark_remove_all when tracked_entity_bucket_system_enabled is set |
Table globals: 0x4672 = tracked_entity_handle_table_active, 0x4673/0x4675 = tracked_entity_handle_table (12 entries × 0x0c = 0x90 bytes).
Follow-up: Tracked Bucket System Init/Shutdown
| Address | Name | Notes |
|---|---|---|
000a:5186 |
tracked_entity_bucket_system_init |
Allocates a rotating buffer via 0009:3600, lazily creates tracked_entity_bucket_backend_object through 0009:5600 when absent, installs that object into cache_loader_interface, allocates the tracked handle table via 000a:5e00, allocates the 32-entry 0x69ac bucket array via 000d:cca3(0x20), then sets tracked_entity_bucket_system_enabled |
000a:538e |
tracked_entity_bucket_system_init_if_configured |
Only calls the init routine when config/feature gate 0x89f4 is non-zero |
000a:5223 |
tracked_entity_bucket_system_shutdown |
Tears down the tracked handle table, frees the 0x69ac bucket array, calls backend-object vtable slot +0x38 with (3, backend_object), clears tracked_entity_bucket_backend_object; called from the wider engine teardown routine at 0004:621b |
System globals: 0x45aa = tracked_entity_bucket_system_enabled, 0x45ab/0x45ad = tracked_entity_bucket_backend_object.
Public thin gate wrappers that feed the 0x69ac tracked-entry layer:
0005:3b34=tracked_entity_bucket_alloc_main_if_enabled0005:3b53=tracked_entity_bucket_alloc_aux_if_enabled0005:3b72=tracked_entity_bucket_remove_by_entity_and_ref_if_enabled→ forwards into000d:d086 = tracked_entity_bucket_remove_by_entity_and_refwhen0x45aais set.
Follow-up: Backend Object Constructor
| Address | Name | Notes |
|---|---|---|
0009:5600 |
cache_backend_object_init |
Allocates a 0x20-byte object when caller passes null; initializes embedded DOS file-handle state via dos_file_handle_init; seeds internal method-table / state fields at object offsets +0x08, +0x0c, +0x10, +0x14, +0x16, +0x18, and +0x1c; dispatches through the object method table during construction; returns the object pointer cached at 0x45ab/0x45ad |
Verified callback roles inside cache_lookup_or_load_entry_by_id:
- backend vtable
+0x34= size query callback for a cache entry id (used before allocation/eviction) - backend vtable
+0x0c= load/bind callback that populates the newly allocated slot buffer for the requested id
Follow-up: External Reset Paths
- The path around
0004:25a9classifies as an external reset sequence: it callscache_reset_runtime_state, thentracked_entity_handle_table_clear_and_dispatch, then continues through additional tracked-entry/cache-side refresh helpers (000d:cd22,000d:44b3,0006:ae66,0006:ae00, etc.). - The path around
0004:eb80is a conditional tracked-bucket reset/update sequence: whentracked_entity_bucket_system_enabledis set, it callstracked_entity_handle_mark_remove_all_if_enabled, thentracked_entity_handle_table_clear_and_dispatch, thencache_compact_arena_blocks, before resuming its outer flow.
Follow-up: Repaired seg004 Reset-Path Function Objects
| Address | Name | Notes |
|---|---|---|
0004:2592 |
runtime_cache_reset_sequence |
Calls 0008:7bfe; calls game_mode_init(*(0x27c4)); calls import-resolved site 0004:25a4, now verified from the separately imported ASYLUM.DLL as ordinal 24 = _ASS_StopAllSFX; then resets cache runtime state, clears tracked handles, refreshes tracked-entry/cache helpers. Known caller: 0004:262d inside the tiny wrapper at 0004:2620, which sets byte +0x40 on the object at 0x6828 before invoking the reset sequence. |
0004:eb1f |
entity_dispatch_entry_ctor_0f3a_with_cache_reset |
Allocates/initializes an entity dispatch entry; stamps entry type 0x0f3a; stores its two word payload fields; runs local setup through embedded helper at 0004:ebf4 (which dispatches entity_dispatch_reset_all(*0x7e22, 0x00f0) and — when the local flag plus global 0x0ee1 allow it — allocates a type 0x0f5e dispatch entry and passes it to entity_pair_sync_b); when tracked_entity_bucket_system_enabled is set, performs the tracked-handle removal / clear / cache-compaction sequence before finalizing through 0009:b1c3 in phase 0. |
0004:ea00 |
entity_dispatch_entry_alloc_type_0f5e |
Reuses the incoming FAR pointer when non-null; otherwise allocates 0x33 bytes through mem_alloc_far; initializes the entry through entity_dispatch_entry_init; stamps the entry type word at +0x00 to 0x0f5e. |
Follow-up: seg082 Allocator Cluster
| Address | Name | Notes |
|---|---|---|
0009:a229 |
(size-only wrapper) | Public size-only wrapper around the seg082 allocator. Lazily initializes the allocator on first use through 0009:bcb9, then calls allocator_try_alloc_from_head_table(size, default_tag, 0xff). |
0009:bcb9 |
(lazy initializer) | One-time lazy initializer. Parses an optional -x tuning value from the PSP command line, clamps derived percentage into 0x14..0x50, seeds local seg082 helpers, then sets init flag 0x4096 = 1. |
0009:b06b |
allocator_try_alloc_from_head_table |
Validates requested size, reserves a temporary work token through 0009:e15f, scans the 0x8724 allocator head table in 0x0c-byte entries via allocator_head_try_alloc_block. On success, commits the result through 0009:e2b4, clears failure flag 0x4098. When a pass does not find a fit, interleaves up to two finalize phases through allocator_phase_finalize_pass(phase) before the final retry. |
0009:a336 |
allocator_head_try_alloc_block |
Per-head first-fit allocator. Normalizes requested size (rounds odd small requests up, page-aligns large non-page-aligned requests), adds 0x0a node header overhead, enforces minimum of 0x10 bytes. Walks the node chain for one allocator head until it finds a free span large enough. On success, unlinks the chosen free node, either consumes it whole or splits off a remainder when >= 0x10 bytes remain. On failure, returns 0. |
0009:a5d1 |
allocator_head_free_block |
Per-head free paired with allocator_head_try_alloc_block. Rebuilds the node header from a payload pointer (payload - 0x0a), validates the owner/tag word, reinserts the block into one allocator head, and coalesces with adjacent free neighbors when possible. |
0009:b224 |
allocator_free_block_by_ptr |
Converts the payload pointer back through local header helpers, scans the 0x8724 head table for the owning range, dispatches to allocator_head_free_block, and aborts if no owning head is found. Known wrappers 0009:a24f and 0009:a27a are small checked entry points into this path. |
0009:b1c3 |
allocator_phase_finalize_pass |
Accepts phase bytes 0 or 1. Forwards that byte twice to the object rooted at 0x4588 through vtable slot +0x08. Then sweeps the allocator head table at 0x8724 up to the active head count at 0x879c, calling allocator_head_finalize_sweep on each entry. |
0009:af87 |
(free-space probe) | Walks the node chain rooted at 0x8724. For each node, accumulates node_size - 9 into a running total and tracks the largest single free block. Used by cache_init and seg013 path at 0004:833b. |
0009:a961 |
(per-head finalize sweep) | Walks one 0x8724 head's node chain, skips odd-tagged spans, coalesces or rewrites eligible spans, and updates head/back-pointer links when deferred space needs to be merged back into the chain. |
Allocator head table structure:
0x8724= array of0x0c-byte allocator heads0x879c= active head count / table limit- Per-node size/value encoding manipulated through
0009:c628and0009:c6ae, which read/write a packed 32-bit quantity split acrossword + byte + bytefields
Follow-up: seg137 Palette and Dispatch-Entry Helper Family
A coherent palette-write and palette-backed dispatch-entry emission family tied to the same runtime-state constructor lane.
| Address | Name | Evidence |
|---|---|---|
000d:85da |
vga_palette_set_all_black |
Allocates a 0x100-entry palette buffer filled with zero RGB triplets, writes it to VGA, frees the scratch buffer. (Previously mis-named map_object_set_dirty_flag.) |
000d:8653 |
vga_palette_set_all_white |
Same shape as black — all three RGB components initialized to 0x3f, then written through vga_palette_write. |
000d:86cc |
vga_palette_set_all_rgb |
Takes caller-supplied RGB bytes, replicates them across a 0x100-entry palette buffer, writes the result to VGA, frees the scratch palette. |
000d:82ea |
dispatch_entry_create_black_palette_state_active |
Builds a runtime-state dispatch entry of type 0x051e from a black 0x100-entry palette buffer; first sets g_active_dispatch_entry_farptr[+0x40] = 1. |
000d:8a47 |
dispatch_entry_create_black_palette_state |
Same as above without marking the active dispatch entry. |
000d:83be |
dispatch_entry_create_grayscale_palette_state_active |
Reads the current VGA palette, normalizes each triplet by copying the first channel across all three RGB bytes, then builds a runtime-state dispatch entry from that grayscale palette while marking the active dispatch entry. |
000d:875d |
dispatch_entry_create_solid_palette_state_active |
Validates 0..0x3f RGB inputs, fills a scratch 0x100-entry palette buffer with that solid color, builds the same 0x051e runtime-state dispatch entry, marks the active entry. |
000d:88b2 |
dispatch_entry_create_solid_palette_state |
Same as above without marking the active dispatch entry. |
Additional caller-side comments (not renamed) added on:
000d:84f4— current-palette dispatch entry paired with a second object of type0x68bfthroughentity_pair_sync_b000d:89c6— parameterized current-palette runtime-state wrapper with active-state flags
Follow-up: seg138 Caller-Side Dispatch-Entry Emission Helper
FUN_000d_938c (000d:938c-000d:9583) — a real caller-side helper with an evidence-preserving decompiler comment added in Ghidra instead of forcing a speculative rename.
Current verified behavior:
- When the mode/global gate is not already in the
0x13:0x0008state and entity byte+0x33is clear, it allocates a scratch palette buffer, constructs a dispatch entry, sets type0x051e, and initializes runtime state throughentity_dispatch_entry_init_runtime_statewith entry kind0x3c. - Later in the same helper it constructs a second dispatch entry from the current palette globals at
0x4e4:0x4e6, again sets type0x051e, and initializes runtime state with entry kind0x14and active-state parameters(1,0,1). - Both created entries are polled until their runtime flag word clears bit
0x0002, after which the helper redraws the global sprite path, syncs display-state byte0x58efrom the entity when the global display object exists, callsFUN_0006_16e1, clearsg_active_dispatch_entry_farptr[+0x40], and finally dispatches through the input object's vtable slot+0x08.
Follow-up: seg005 Startup/Display Orchestration
| Address | Name | Notes |
|---|---|---|
0004:60c0 |
startup_display_transition_prepare |
Startup/display transition prepare step: broad setup calls, reads the live VGA palette, validates a caller-provided object through vtable slot +0x0c, drives the sprite/object lane through 0x4f38, creates the default active dispatch entry through active_dispatch_entry_create_default, programs mouse interrupt state via seg056 INT 33h wrappers, then hands off into startup_display_transition_driver. |
0004:1e00 |
startup_display_transition_driver |
Non-return startup/display transition driver: raises the shared active-dispatch hold byte around the seg049 watch/controller lane, then clears it before the seg080 redraw and seg126 follow-up path. |
000d:7600 |
active_dispatch_entry_mark_enabled |
Marks the active dispatch entry enabled |
000d:760e |
active_dispatch_entry_mark_disabled |
Marks the active dispatch entry disabled |
000d:761c |
active_dispatch_entry_create_default |
Creates the default active dispatch entry |
Current verified caller-side detail:
startup_display_transition_preparenow has enough exact instruction evidence to pin the seg108 lane down more tightly. Window0004:618e..620ccalls the0x4f38sprite/object helpers in a stable sequence around the shared active-entry creation: seg108000b:1e39,000b:2492, and000b:26bdrun beforeactive_dispatch_entry_create_default, and seg108000b:2706runs again immediately before the handoff intostartup_display_transition_driver.- The same seg108 window now shows a local bounded counter/stack contract instead of reuse of the validated caller object.
000b:26bdincrements object word+0x196up to7before calling the common local helper at000b:2592, and000b:2706reads one prior slot from+0x186, decrements+0x196, and replays the same helper beforestartup_display_transition_drivertakes over. - The seg108 helper pair is now named too:
sprite_object_push_state_word(000b:26bd) increments the bounded local stack depth at+0x196, stores the incoming word into the per-object stack at+0x186, refreshes local sprite state through000b:2592, and replays redraw when the object was already marked dirty;sprite_object_pop_state_word(000b:2706) returns the previous top word, decrements the same bounded depth, and reapplies the new top through that same helper. This makes the0x4f38lane read more like a self-contained sprite/object state stack than a reused validated caller object. startup_display_transition_drivernow has exact hold-token ordering too. Window0004:2013..212craisesg_active_dispatch_entry_farptr[+0x40], dispatches the seg049 watch/controller object at0x2bd8through vtable slot+0x2c, runs the intervening transition call at0004:eece, and then clears the same shared hold byte again just before the seg080 redraw pair and seg126 follow-up.- Upstream caller tracing now shows that the four
0004:eececall shapes are chosen from one startup switch-parser lane rather than from a named local phase enum. Window0004:63c1..66fbwalks an argv-like table against a local dispatch/jump table, and the0004:64ff..65c1case loads globals0x84a/0x84c/0x84e/0x850plus scalar0x856;startup_display_transition_driverlater fans those values into the0004:2049,0004:20b3,0004:20c6, and0004:20fecall variants. The other direct caller anchors at0004:2657,000c:8786, and000c:742call remain inside the same startup/display presentation-handoff family, so the safest output is still tighter caller-family semantics rather than a new neutral state label. - The seg049 and seg108 globals are now better separated by direct decompile evidence rather than only call-window correlation.
watch_entity_controller_dispatch_if_presentconfirms that0x2bd8is a real controller object with active vtable dispatch at slots+0x2cand+0x30, whilesprite_object_set_flag40_if_presentandsprite_object_clear_flag40_if_presentshow that0x4f38is a separate global object lane whose immediate local contract is only bit0x40in object word+0x32. - The seg127 fade-controller ownership is also one step tighter in the same lane.
transition_preentry_setup_resourcesresets0x630aat000c:c855,transition_preentry_step_scriptnow has a verified early gate at000c:ca25that yields to the fade controller whenever0x630ais active, andtransition_palette_fade_beginat000c:cdcaexplicitly installs palette source/range/step state into0x630e..0x6316, asserts0x630a, and kicks one immediate fade tick. - Fade direction is now pinned to seg126 script-control bytes rather than the outer seg005 wrappers. Inside
transition_preentry_step_script, control byte0x5ereachespalette_fade_begin_full_downat000c:cb06, while control byte0x26reachespalette_fade_begin_full_upat000c:cd1a; control byte0x2ashares the same post-fade bookkeeping path after the full-up call. - The upstream producer path for the remaining seg126 control bytes is now tighter too.
transition_preentry_setup_resourcescomposes one path from the mutable base at0x6aa:0x6acplus local name buffers (0x631c,0x6335) through the seg072 slash-aware path helper0009:3600, opens that file throughfile_handle_alloc_init_and_open, allocates a buffer of the returned size, reads the full payload into0x6301:0x6303, and seeds0x62fa/0x62fc/0x62ff/0x6305/0x630a/0x6318before the loop starts. Current best reading is thereforefile-backed transition script/control buffer, not locally synthesized opcodes. - The adjacent seg126 selector lane is now classified tightly enough for conservative renames.
transition_file_family_select_and_refresh(000c:afa5) keys object field+0x49through values0,1, and4, composes three sibling filenames from the inherited base0x6aa:0x6acplus shared stem0x621cwith suffix buffers0x6223,0x622d, and0x6237, loads the chosen file into object+0x520, and then runs the same redraw/palette/input refresh path. The same helper usesfield49==2as a directvtable[0x3c]callback branch andfield49==-1as a normalize-back-to-zero state. - The local wrappers around that selector now sharpen the caller model without forcing a stronger UI label.
transition_file_family_advance_on_anim_tick(000c:b153) increments+0x49when the polled byte at[param_2+0x14+0xa]is clear and then re-enters the selector, whiletransition_file_family_input_key_handler(000c:b199) maps Left/Right andn/Ninto previous/next selector steps, usese/Eplus repeated-to force selector state4, and otherwise exits throughvtable[0x3c]. - This closes the narrow
+0x49question as a local three-way file-family selector lane, but it still does not justify a stronger UI label for the paired0x8c5c/0x8c60renderer presets or the sibling seg127 fade inputs. - The remaining
transition_preentry_step_scriptopcodes now have stable local mechanics even though the higher-level text semantics are still open. Control byte0x21consumes the next script word intoSIand advances0x62ffby two, which makes it the current baseline/start-position loader for later text draws. Control byte0x40renders one null-terminated entry from the same script buffer through renderer object0x8c5c:0x8c5e, while control byte0x24mirrors that behavior through0x8c60:0x8c62; both paths measure width through the renderer vtable, draw through seg088000a:30d7, blit through seg0800009:943a, advanceSIby rendered width plus four, and then scan forward to the next opcode byte. Control byte0x23sets local completion byte0x62fe = 1and returns, so the outer shell exits on the next loop test instead of iterating further. - Secondary renderer-factory sampling keeps the
0x8c5c/0x8c60split conservative. Other sampled000a:9748xrefs use different adjacent preset pairs such as0x0d/0x0cat0007:df30/df3fand0x0c/0x0fat0008:47c9/4851, while no sampled caller reproduced the exact0x10/0x11startup pair outsidetransition_preentry_setup_resources. That supports keeping these as paired preset text renderers without forcing a title/body or normal/highlight label. - The missing seg126 step body at
000c:ca1dstill cannot be split out safely becausecreate_function_by_addresscollides with the existing oversized overlap namespace, so this pass preserved the recovery as a decompiler comment instead of forcing a destructive boundary repair. Current best reading is still that000c:ca1d..cd34is the realtransition_preentry_step_scriptbody and that000c:cd35starts the fade-tick helper.
Follow-up: 0x31a2 Break/Hold Depth and Active Dispatch Ownership
This pass tightened the shared startup/display transition lane enough to preserve the gate semantics directly in Ghidra and to promote the active-dispatch helpers out of an isolated foothold.
Verified 0x31a2 role
0008:a283increments0x31a2while installing one live far-pointer slot record into the seg008 per-index table, and the paired path at0008:a314decrements0x31a2while clearing that same record.0005:453ais now commented in Ghidra as a plain getter for the shared0x31a2depth word.transition_preentry_run_until_complete_or_abort(000c:c9f4) now decompiles cleanly and confirms the main transition loop runs until either local completion byte0x62febecomes non-zero or0x31a2becomes positive.- The shell around
transition_preentry_step_scriptis now tighter too:000c:ca11is the direct second exit test in the outer seg126 loop, so a positive0x31a2falls straight intotransition_preentry_release_resourceseven when local completion byte0x62feis still clear. 0004:c24dand000c:e4d8are now tightened as pure busy-wait edge loops on0x31a2, while000c:e546and000c:e5c6are the same break-depth check embedded in local presentation/cleanup loops rather than plain one-shot flag tests.- The blocking/waiting readers around
000c:e4d8,000c:e546, and000c:e5c6treat0x31a2as an asynchronous break condition rather than a local state bit: they either busy-wait for a positive edge or abort their local presentation loop early when the depth is already positive. - Additional caller-side reads at
000d:9304,000d:b6b1, and000d:c0eeall use0x31a2 > 0to short-circuit or advance local dispatch-entry state, which fits a shared break/hold depth better than a one-shot acknowledge flag.dispatch_entry_kind2_tick_hold_and_maybe_dispatch(000d:92eb) is still the clearest recovered dispatch-entry example: when a kind-0x0002entry is pending, a positive0x31a2lets the helper dispatch vtable slot+0x08even if the local+0x40hold byte is still asserted, after which it decrements that local byte. The newly checked000d:b6b1reader is narrower: it advances one local state-5branch only when entity byte+0x78is set and class/state word+0x16carries bit0x4000, then optionally runs the seg092 follow-up when the0x45aagate and entity byte+0x74aare both active.
Current best neutral description: 0x31a2 is a shared asynchronous break/hold depth maintained by the seg008 install/remove path and consumed by transition/presentation code as a positive-count modal break condition.
0x6341 to 0x6828 relationship
- The missing seg126 function object at
000c:c63ais now created and namedtransition_preentry_setup_resources; its body allocates the paired temporary text-renderer objects at0x8c5c/0x8c60, draws the preset0x10and0x11text variants, loads the file-backed buffer into0x6301:0x6303, and seeds the pre-entry state bytes before the main loop starts. - The paired helper
transition_preentry_release_resources(000c:c890) still handles teardown, but its late branch at000c:c958also constructs the transition-local animation object atDS:0x6341throughanimation_ctor_variant_a, then immediately setsg_active_dispatch_entry_farptr[+0x40] = 1at000c:c963. active_dispatch_entry_create_default(000d:761c) still owns the canonicalg_active_dispatch_entry_farptrinstallation.dispatch_entry_kind2_tick_hold_and_maybe_dispatch(000d:92eb) now makes that paired readback explicit: when a kind-0x0002dispatch entry is pending and either entry byte+0x40is already non-zero or the shared break depth0x31a2is positive, it dispatches vtable slot+0x08and then decrements+0x40.FUN_000d_938candentity_cleanup_resources_and_dispatchboth clearg_active_dispatch_entry_farptr[+0x40]after their palette/presentation handoff work, which ties the active entry to the same transition-owned presentation lane rather than to an isolated constructor helper.entity_cleanup_resources_and_dispatchis now tighter on the caller side too. Its first0x4588callback emit at000d:9d5eis confirmed as theentity +0x12d/+0x12fpayload-pair path, while the later emit at000d:a3b7uses the separateentity +0x74f/+0x751pair. Both still sit inside the same palette/watch-controller cleanup body rather than a separate callback-only helper.- Caller-role alignment is now tighter across the remaining startup/display cleanup bodies. The mis-split seg126 window
000c:6176/619c -> 000c:6226constructs only a temporary local animation payload, frees it, dispatches the seg049 watch/controller object at0x2bd8through vtable slot+0x2c, and then clears the shared owner byte+0x40;FUN_000d_938csimilarly waits on two temporary palette/state entries, redraws, clears the same shared hold byte at000d:958d, and only then dispatches its caller object through vtable slot+0x08;entity_cleanup_resources_and_dispatchclearsg_active_dispatch_entry_farptr[+0x40]at000d:a1cfonly on the branch where entity byte+0x737is set and no temporary object remains, before falling into the same watch/controller dispatch at000d:a1ed. That is enough to align them as consumers of one shared presentation hold token around the seg049 lane, but still not enough to justify a single higher-level subsystem rename forFUN_000d_938cor the mis-split seg126 body.
Follow-up: shared owner versus borrowed hold-token model
active_dispatch_entry_mark_enabled(000d:7600) andactive_dispatch_entry_mark_disabled(000d:760e) are now verified as tiny wrappers that only write the shared owner byteg_active_dispatch_entry_farptr[+0x40] = 1/0; they do not install or replace the owner object.entity_dispatch_entry_init_runtime_state(000d:7e00) now tightens that ownership split further. When it builds a runtime-state dispatch entry, it copies the current shared owner byte atg_active_dispatch_entry_farptr[+0x40]into the new entry's local byte+0x40; if the new entry stays inactive while a shared owner exists, it raises the shared owner's+0x40byte to1instead of replacing the owner pointer.- The paired destructor
entity_dispatch_entry_release_runtime_state(000d:8078) clearsg_active_dispatch_entry_farptr[+0x40]when the runtime-state entry was marked as owner-propagating (+0x41 != 0) or when the entry's local hold byte was never asserted. This matches borrowed hold-state propagation, not separate owner creation. startup_display_transition_drivernow provides the clearest caller-side proof in seg005: it raisesg_active_dispatch_entry_farptr[+0x40]at0004:2013before the seg049 watch/camera path and the0004:eecetransition call, and clears that same byte again at0004:2128before the seg080 display update pair, sprite redraw, and seg126 follow-up thunk000c:82f9.- The still-mis-split seg126 window around
000c:6176/619calso fits the same model. It makes one mode-dependentanimation_ctor_variant_acall on a temporary local object, frees that temporary object, reloads the palette, dispatches the0x2bd8watch/camera controller through vtable slot+0x2c, and later clearsg_active_dispatch_entry_farptr[+0x40]at000c:6226. No canonical owner installation is visible in that body. - The thin seg005 wrappers at
0005:3c36and0005:3c5bare now confirmed as pureanimation_ctor_variant_apreset shims. Combined with the seg126 windows above, current best reading is:DS:0x6341and the other constructor callsites build transition-local or display-local animation payloads, while0x6828remains the shared active-dispatch owner installed elsewhere.
Current best model: g_active_dispatch_entry_farptr is a shared owner installed by active_dispatch_entry_create_default, and the startup/display transition lane mostly borrows and propagates the owner's byte +0x40 as a hold/busy token while palette/runtime-state helpers run. The remaining open problem is the exact state/object label behind the seg049 watch/camera path, the seg108 sprite/object lane, and the cleanup branches that consume the same token.
Current batch: presentation handoff family versus single-owner hypothesis
- The exact late sequencing now supports one stricter neutral read: these bodies behave like a shared startup/display presentation handoff family, but not like one single owner-object family. In
startup_display_transition_prepare, the validated caller object (vtable+0x0c), the seg1080x4f38lane, and the seg0490x2bd8watch/controller lane stay separated by direct instruction windows rather than collapsing into one reused object path. transition_preentry_setup_resourcesalso tightened the paired renderer role one step further. Window000c:c659..c6aballocates renderer presets0x10and0x11through seg099000a:9748, stores them at0x8c5c:0x8c5eand0x8c60:0x8c62, and immediately draws the same seed text bufferDS:0x631aat(0x0a,0x0a)through both objects. That makes the pair look like two preset text-render variants inside the same temporary presentation lane, not two separate owner objects.- The shared hold-token consumers now line up more exactly than before.
startup_display_transition_driverraisesg_active_dispatch_entry_farptr[+0x40]before the seg049+0x2cdispatch and clears it again before the redraw/follow-up path;transition_preentry_release_resourcestears down the paired renderers and script buffer, and only on its late completion branch builds localDS:0x6341then raises the same shared byte; the mis-split000c:6176/619c -> 000c:6226body frees its temporary local animation object, reloads the palette, dispatches0x2bd8, and only then clears the shared byte;FUN_000d_938cwaits on two temporary palette/state entries, redraws, clears the shared byte, and only then dispatches caller vtable+0x08;entity_cleanup_resources_and_dispatchclears the shared byte only on the late+0x737cleanup branch immediately before the same0x2bd8dispatch. - The seg049 controller lane is also slightly tighter locally.
watch_entity_controller_create_global(0007:ba00) delegates towatch_entity_controller_create(0007:ba45), which stamps type0x2c2b, stores the global object at0x2bd8, and seeds static rowDS:0x2be4into0x39ca[obj+2]; the commonwatch_entity_controller_dispatch_if_presentwrapper (0007:ba13) then runs both vtable slots+0x2cand+0x30. That still supports a real controller object, but not a strong enough state label to rename the wider family. - The seg108 lane is one step tighter in the same pass.
sprite_redraw_if_needed(000b:2492) remains the redraw-facing helper, while the newly namedsprite_object_push_state_word/sprite_object_pop_state_wordpair show that prepare-time use of0x4f38is bracketed by a bounded per-object state-word stack at+0x186/+0x196rather than by reuse of the validated caller object or the seg049 controller.
Current safest naming conclusion from this batch: keep the existing concrete function names, treat FUN_000d_938c and the related seg126/seg138 callers as one shared startup/display presentation handoff family, and defer any stronger single-state or single-owner rename until a caller-side state discriminator appears.
This is enough to treat seg136 as a shared active-dispatch owner/hold-state controller and seg138 as a real cleanup/presentation caller family, even though the final subsystem name is still open.
Current batch: exact edge waits, interleaved handoff, and broader sprite-stack reuse
- The remaining pure
0x31a2edge waits are now exact rather than inferred.0004:c24dis a two-phase wait that first spins while the shared break/hold depth is non-zero and then spins until it becomes positive again before continuing, while000c:e4d8is the simpler positive-edge gate that only waits for0x31a2 > 0and immediately returns into the local presentation path. FUN_000d_938cis now slightly tighter on sequencing even though it still does not justify a rename. The first scratch-palette runtime-state entry (kind 0x3c) is only built on the branch where global0x68e6is not already in the0x13:0x0008mode or entity byte+0x33is clear; after that wait completes, the helper clears the seg049 controller bit and dispatches0x2bd8through vtable slot+0x2c, performs one rectangle/display sync, and only then conditionally builds the current-palette runtime-state entry (kind 0x14) before the final redraw, shared hold-byte clear, and caller-object vtable+0x08dispatch. That keeps the body inside the same presentation-handoff family while making it less plausible as a single-owner constructor.- Additional seg108 push/pop callsites now show that the
0x4f38lane is reused outside the startup prepare shell. Windows000b:9bb8/9bdaand000b:9c3c/9c6ebracket transient seg101 presentation helpers with the samesprite_object_push_state_word/sprite_object_pop_state_wordpair on global sprite object0x5e82:0x5e84, while000c:831f,000c:8845,000c:8909, and000c:a05fpop that same state stack during later UI or object-cleanup flows. This strengthens the neutral reading that0x4f38is a generic sprite-object state stack, not the validated prepare-time caller object and not the0x2bd8watch/controller object.
Current batch: shared hold-token ownership closure
- The highest-value remaining ownership question in the startup/display lane is now narrow enough to close without a speculative rename.
active_dispatch_entry_create_defaultremains the canonical installer forg_active_dispatch_entry_farptr, while the later seg005/seg126/seg138 bodies only borrow or propagate the shared owner byte+0x40as a transition/presentation hold token. - The set/clear sites now line up as one borrowed-hold family instead of competing owner installs.
startup_display_transition_driverraises the shared byte at0004:2013before the seg049 controller dispatch and clears it at0004:2128;transition_preentry_release_resourcesraises it only on the late completion branch at000c:c963after building temporaryDS:0x6341;FUN_000d_938cclears it at000d:958dafter both temporary palette/state waits and redraw;entity_cleanup_resources_and_dispatchclears it at000d:a1cfonly on the late cleanup branch immediately before the same seg049 controller dispatch. - The seg049 and seg108 lanes stay separate in the exact places that matter for ownership.
watch_entity_controller_dispatch_if_present(0007:ba13) confirms0x2bd8is a real controller object dispatched through vtable slots+0x2c/+0x30, whilesprite_object_clear_flag40_if_present/sprite_object_set_flag40_if_present(000b:2b08/000b:2b20) only toggle bit0x40in the separate global sprite/object at0x4f38 + 0x32. - The owner/borrow split also remains visible inside the dispatch-entry helpers.
entity_dispatch_entry_init_runtime_statecopies the shared owner byte into new runtime-state entries and re-raises the owner's+0x40byte when needed, which matches propagation of borrowed hold state rather than transfer of owner identity.
Current best neutral conclusion from this pass: the shared g_active_dispatch_entry_farptr[+0x40] byte is a startup/display presentation hold token borrowed across the seg049 controller lane and later cleanup/handoff bodies; the seg108 0x4f38 lane is a separate local sprite-object state stack with its own bit-0x40 contract, not the owner of the shared active-dispatch token.
Current batch: seg126 control-stream producer tightening and completed 0x31a2 read classes
- The higher-level seg126 control-byte producer is now tighter without breaking the conservative file-backed model.
transition_preentry_run_until_complete_or_abort(000c:c9f4) still has no data/bytecode arguments and is only reached from the local wrappers at000c:7427and000c:0d0d; both wrappers only stage the surrounding presentation lane before entering the seg126 loop, and neither injects script bytes or a script pointer. transition_preentry_setup_resources(000c:c63a) remains the only verified source of the consumed bytes. It copies the shared mutable base path from0x6aa:0x6ac, composes local filenames through the slash-aware seg072 helper0009:3600using local buffers0x631c,0x6323,0x632c, and0x6335, opens the resulting file through the seg070 file-handle lane, allocates a buffer of the returned size, and reads the full payload into0x6301:0x6303before seeding0x62fa/0x62fc/0x62ff/0x6305/0x630a/0x6318.- Neighboring seg126 code now supports the same selector-path reading. Window
000c:b018..b03dalso reloads the same shared base path from0x6aa:0x6acand composes a sibling local filename through0009:3600using0x621c/0x6223, which makes the startup/display lane look more like a family of file-selected transition assets than a local script-byte emitter. - The upstream
0x6aa:0x6acquestion is now narrow enough to close as an earlier inherited path lane rather than an in-scope seg126 producer. Literal-address instruction search still finds no store into0x6aaor0x6ac; the seg004 parser window only mutates the first byte of the pointed buffer at0004:0ccd/0004:0cd8, while the same parser explicitly installs the sibling root0x6ae:0x6b0from parsed input at0004:0d28..0d2c. Current best read is therefore:0x6aa:0x6acalready points at a mutable external/default base-path buffer before the seg126 startup/display family begins composing filenames on top of it. - The neighboring seg126 helper family sharpens that close further. Window
000c:afa5..b152keys object field+0x49through local values0,1, and4, composes three sibling filenames from the shared stem buffer0x621cplus suffix buffers0x6223,0x622d, and0x6237, loads the selected file into object+0x520through seg0020004:0098, then runs the same display/update chain; wrapper000c:b153..b25fincrements or decrements+0x49on selected event codes and re-enters that same loader. This makes the nearby seg126 lane look like a local three-way transition-asset family selector layered on top of the inherited shared base path. - The overlap check does not force repair yet.
analyze_function_boundariesstill reports000c:ca1das a plausible function entry and000c:cd53as the next clean function, while the oversized overlap rooted at000c:db68still pollutes the namespace. That overlap no longer blocks byte-behavior or read-site classification, but it still blocks clean function recovery fortransition_preentry_step_script. - The in-scope
0x31a2readers are now classed cleanly by role.0004:c24dand000c:e4d8are edge waits;000c:ca11is the seg126 modal-break exit;000c:e546,000c:e5c6, and000d:c0eeare cleanup-abort exits;000d:9304and000d:b6b1are deferred dispatch/state-advance gates. - Two remaining
0x31a2reads stay outside that presentation classification set.0005:453dis only a plain getter wrapper for the shared depth word, and0008:5149is a seg008 internal/accounting-side read that adds the current depth to another local count before tripping a>= 0x10capacity flag.
Current batch: renderer preset contract and seg127 fade-input closure
transition_preentry_setup_resourcesis now exact on the paired renderer setup path. Instruction window000c:c659..c6abshows thatFUN_000a_9748is called only with preset ids0x10and0x11, storing the resulting temporary renderer objects at0x8c5c:0x8c5eand0x8c60:0x8c62, then immediately drawing the same seed text bufferDS:0x631aat(0x0a,0x0a)through both. This closes the structural question aspaired preset text lanesinside one temporary transition presentation path, but still does not justify a stronger title/body or highlight/shadow label.- The recovered
transition_preentry_step_scriptbody is also slightly tighter on the two text opcodes.0x40and0x24both measure their string through renderer vtable slot+0x0c, center it inside a0x280-wide lane, fetch rendered width through slot+0x08, draw through seg088000a:30d7, blit through seg0800009:943a, and advanceSIbyrendered_width + 4; only the selected preset lane differs (0x8c5cfor0x40,0x8c60for0x24). - The seg127 fade-controller inputs are now exact rather than only role-level.
transition_palette_fade_beginstores palette source at0x630e:0x6310, start index at0x6312, count at0x6314, step at0x6316, brightness at0x630d, active flag at0x630a, and direction/state at0x630b, then immediately ticks the local fade controller.transition_palette_fade_tickdispatches0x630b==1totransition_palette_fade_out_stepand0x630b==2totransition_palette_fade_in_step. - The two default script-selected fade wrappers are now instruction-verified too.
palette_fade_begin_full_downat000c:c616pushes direction1, step4, count0x80, start0, and palette bufferDS:0x8c64;palette_fade_begin_full_upat000c:c600is the same wrapper with direction2. Combined with the0x5e,0x26, and0x2ascript-byte sites intransition_preentry_step_script, this closes the neighboring seg127 fade-input contract for the startup/display lane. - The late presentation-handoff family is now direct-decompile confirmed rather than only caller-window inferred.
FUN_000d_938ccreates up to two temporary runtime-state palette entries (kind 0x3c, thenkind 0x14), waits for them to clear, redraws, clearsg_active_dispatch_entry_farptr[+0x40], and only then dispatches caller vtable+0x08;entity_cleanup_resources_and_dispatchshows the same late shared-hold clear on theentity +0x737branch immediately before the shared0x2bd8controller dispatch. That is enough to treat the startup/display major section as materially complete, with only low-impact residual ambiguity around the exact UI label of preset pair0x10/0x11and the optional overlap hygiene at000c:db68.
Follow-up: 0x4588 Object-Role Evidence
The 0x4588 FAR object is a runtime-installed callback/dispatch object that participates in conditional render or presentation-side flow. It has an explicit install, clear, callback, and teardown lifecycle.
Verified lifecycle
- Install:
000a:4932and000a:4936store the incoming dword into0x4590and0x458c, then000a:493estores the incoming FAR object pointer into0x4588. - Clear:
0004:5b8cand0004:5bbfboth clear0x4588immediately before the fatal/reporting-style seg091 call through000a:454d;0004:5ea7and0004:6430both clear0x4588and then immediately run the one-shot teardown path000a:4a56(1). - Teardown:
000a:4a56checks a once-flag at0x4595, clears0x4588when non-null, optionally performs a vtable+0x0ccallback when0x4590 != 0x458c, then calls vtable slot+0x04followed byFUN_0009_0d30(). - Callbacks:
000a:b9e5,000a:ba66,000d:9d5e, and000d:a3b7all push a two-word value pair followed by the0x4588FAR pointer and call vtable slot+0x0c.entity_conditional_render_dispatchcalls the same vtable slot with a single literal0x0101argument.
Current batch note:
runtime_callback_object_init_once,runtime_callback_object_teardown_once, andentity_conditional_render_dispatchnow line up even more strongly as a video or presentation-state callback lane rather than a generic allocator client. The object is installed only after BIOS video-state snapshot, teardown emits a final callback only when recorded mode/state changed, and one live caller uses the literal mode-like pair0x0101through the same vtable+0x0cslot. That is enough to keep pushing the role towardpresentation/video-state callback broker, but still not enough for a fully behavioral subsystem rename.
Payload pairs from payload sync callsites
000d:9d5e→ vtable+0x0cpayload from object fields+0x12d/+0x12f000d:a3b7→ vtable+0x0cpayload from object fields+0x74f/+0x751000a:b9e5,000a:ba66→ emitting only when the candidate two-word pair differs from the current pair, then mirroring that pair through000b:1e39using global sprite/object pointer0x4f38/0x4f3a
Globals
| Address | Name |
|---|---|
0x4588 |
runtime FAR object pointer (nullable) |
0x458c |
callback sync field (compared against 0x4590 in teardown) |
0x4590 |
paired sync field |
0x4594/0x4595 |
state flags |
0x45a6 |
clock/cookie global used by assert_buffer_valid |
0x39ca |
dispatch callback-table pointer |
0x6828 |
g_active_dispatch_entry_farptr |
Follow-up: VM Owner/Resource Loader and Owner-Loaded Class Validation
The next ScummVM-guided validation step now confirms that the sampled owner-loaded EUSECODE classes are compatible with the ScummVM indexing model even though one header detail remains open.
Sampled class-record findings
- Using the extracted chunks plus the live raw path
000d:44df -> 000d:4c99 -> 000d:7000, the large chunk at table offset0x88behaves as object1. - For representative class bodies, deriving
object_index = (table_offset - 0x80) / 8, thenclass_id = object_index - 2, and then reading object1at4 + 13 * class_idyields the expected names:EVENT,NPCTRIG,SURCAMNS,JELYHACK,REE_BOOT,SURCAMEW, andSFXTRIG. - This is the first direct local confirmation that the owner-loaded records match the ScummVM
object 1name-table plusclassid + 2body lookup at the indexing level.
Header and event-table shape
- The loader-side count field is now tighter too. The first dword in the sampled owner-loaded class header is not the total slot count;
000d:5066uses it as the extra-slot count beyond a fixed0x20base table, which is why the cached table allocation isextra_count * 6 + 0xc0and the refcount array isextra_count * 2 + 0x40. - That reading matches the extracted class-family shapes exactly:
EVENTkeeps first dword0x00000000,NPCTRIGmoves to0x00000001, andROLL_NSto0x00000002, while the already-validated owner-loaded event counts remain0x20,0x21, and0x23respectively. - The sampled class records do contain a stable 4-byte header field at bytes
8..11. - The observed values are small boundaries:
0x00d4,0x00da, and0x00e6in the current sample set. - Treating that dword directly as the first post-event-table offset makes the layout line up cleanly:
(dword_at_8 - 20) / 6yields valid tables of 32, 33, or 35 slots before inline payload/name data begins. - The region at
class + 0x14is therefore now directly confirmed as repeated 6-byte slots withu16 unknown_word + u32 code_or_payload_fieldlayout. - Representative low-slot examples are
JELYHACKslot1={word=0x002a, dword=0x00000001},SURCAMNSslot1={word=0x0051, dword=0x000000d2},SURCAMEWslot1={word=0x00f7, dword=0x000000d2},EVENTslot10={word=0x1fd6, dword=0x00000001}, andREE_BOOTslots10/15/16={0x034b,1},{0x025c,0x034c},{0x003b,0x05a8}. - The runtime-side selector arithmetic is now exact as well: the owner-resource callbacks operate on
class_id + 2, which matches the extractedobject_indexcolumn directly.EVENTtherefore lands on child0x363from class id0x361, andNPCTRIGon child0x365from class id0x363. - The leading event word is still not decoded semantically.
What remains open
- Scanning with the previously noted ScummVM-style
(base_offset + 19) / 6interpretation overruns into inline payload/name bytes on these owner-loaded records, so the local sample set does not support that exact event-count formula as written. - The best current arithmetic fit is now tighter: ScummVM's decremented
base_offsetis also used as the live code-stream base inuc_machine.cpp, so the local owner-loaded records fit best if bytes8..11are the first code-byte offset and event-count derivation is(base_offset - 19) / 6, which is exactly equivalent here to(raw_u32_at_8_11 - 20) / 6. - Current
000dloader evidence does not point to a header rewrite before VM consumption.entity_vm_runtime_init_from_path_if_configured(000d:44df) only builds the external path and creates the runtime,entity_vm_runtime_create(000d:4c99) only installs the helper returned by000d:7000,entity_vm_runtime_owner_resource_create(000d:7000) only allocates the child owner table and fills it through helper vtable+0x0c, andentity_vm_context_create_from_slot_index(000d:46ec) directly reads slot-backed source data from that owner table. No local step is yet verified as rewriting the sampled class headers. - The slot-value miss path is now exact enough to align against the extractor rather than only against motifs.
entity_vm_slot_load_value(000d:51fd) does not build the returned workspace out of owner-row fields or late interpreter scratch: on a miss it uses000d:5066plus the same owner-resource wrapper000d:714cto read a0x14-byte class header, then a cached6 * (0x20 + extra_count)subentry table, and finally the selected subentry's byte range straight into a newly allocated value-object buffer at+0x0a/+0x0c. - The final body read at
000d:53b4now matches the extracted row arithmetic exactly. The 6-byte row contributesword body_lenplusdword raw_code_offset, the class header contributesdword code_base, and the reader fetchesbody_lenbytes fromcode_base + raw_code_offset - 1throughcode_base + raw_code_offset + body_len - 2. - That gives a direct owner-loaded fit for the two surviving
NPCTRIGbodies. For classNPCTRIG(class_id = 0x363,object_index = 0x365), slot0x0auses{len = 0x0175, raw_code_offset = 0x00000001, code_base = 0x00da}and therefore materializes range0x00da..0x024e(373bytes), while slot0x20uses{len = 0x0159, raw_code_offset = 0x00000176, code_base = 0x00da}and therefore materializes range0x024f..0x03a7(345bytes).EVENTslot0x0afits the same runtime arithmetic with{len = 0x1fd6, raw_code_offset = 0x00000001, code_base = 0x00d4}->0x00d4..0x20a9. - Because
000d:5066/51fd/53b4now line up with the extracted class headers and event rows byte-for-byte, the remaining immortality blocker is no longer header math or slot-number translation. The open step is upstream class selection into this now-verified loader path: whether the live slot0x0arequest really namesNPCTRIG,EVENT, or another descriptor family sharing the same owner-loaded format. entity_vm_runtime_owner_resource_create(000d:7000) still does not expose a direct binary-side class-name lookup or explicitclassid + 2arithmetic. What it does expose is an indexed file-set loader contract: helper-owned count at+0x14, far-pointer table at+0x10, paired per-entry word table at+0x18, vtable+0x04size query, and vtable+0x0cmaterialization of the0x0d-stride owner records later consumed byentity_vm_context_create_from_slot_index. The current pass also makes the helper shape slightly more concrete: the two raw seg070 windows at0009:67b6and0009:6916are twin per-entry path/read loops with distinct format strings (DS:3f2dandDS:3f40) but the same+0x10/+0x18indexing and file open/read/close lane, which is better evidence for a multi-table or multi-phase external loader than for direct in-memory descriptor iteration.- The signed slot-offset lane used by the still-xref-dark wrappers
0005:2c35/0005:2c68is also no longer confined toentity_vm_context_create_from_slot_index(000d:46ec). Ghidra now reflects that contract in the conservative wrapper namesentity_vm_context_try_create_mask_0400_slot0a_with_offsetandentity_vm_context_try_create_mask_0800_slot0b_with_offset. Insideentity_vm_runtime_create, the pre-entry body at000d:4c25..4c90reloads object fields+0x32/+0x34throughentity_vm_slot_load_value_plus_offset(000d:5572), stores the reconstructedDX:AXpair into object fields+0x10c/+0x10e, and also caches the owner-source far pointer at+0x117/+0x119. The paired save path at000d:49ecis narrower than it first looked: it serializes only the low word at+0x10cthrough seg0700009:2034, while the high word is recomputed on load from the freshentity_vm_slot_load_value()result plus the saved additive word. - Current disassembly closes the exact low-slot wrapper contracts too.
0005:2c35sign-extends caller word[BP+0x0a], then callsentity_vm_context_try_create_masked_for_entitywith slot0x0aand packed mask0x00000400;0005:2c68is the same signed-additive shim for slot0x0band packed mask0x00000800. Neither wrapper has a recovered outward code/data xref yet, so the best current provenance remainsextra-word masked materializer family member, not a gameplay event label. - The newly recovered post-load consumers of
+0x10c/+0x10eare weak and do not behave like a recovered event-dispatch selector. PredicateFUN_0001_a772returns true only when the pair is exactly0000:0001, while normalization blockFUN_0002_1860checkssegment == 0and clampsoffset < 0x0080up to0x0080. No recovered downstream comparison or dispatch branch matches the five verifiedNPCTRIGslot0x0aclause starts (0x0064/0x0093/0x00c2/0x00f1/0x0120) or backward targets (0x001f/0x004e/0x007d/0x00ac/0x00db); if anything, the0x0080floor cuts across that family instead of confirming it. - The masked-create hub in front of that lane is now explicit too. Window
000d:463a..46e8maps one gameplay entity throughentity_vm_slot_index_from_entity, tests the owner/resource table row mask at0x6611 -> +0x1315/+0x1317 -> (+0x10/+0x12) + 0x0d*slot, and only then callsentity_vm_context_create_from_slot_index. That matters because the offset-specialized wrappers0005:2c35/0005:2c68are now instruction-verified as nothing more than sign-extended extra-word shims over this generic masked-context hub, rather than separate selector logic. - The upstream slot selector is now exact enough to rule out one remaining binary-side shortcut.
entity_vm_slot_index_from_entity(000d:45c5) does not expose a class-family choice likeNPCTRIGversusEVENT; it only chooses one of three generic category spans before the owner row is consulted:(a)entity ids1..255with class-word bit0x0002clear map toentity_id + base_0x8c7e,(b)class-nibble4objects map toclass_byte_0x7e05 + base_0x8c80, and(c)everything else maps totype_word_0x7df9 + base_0x8c7c. - The runtime init path now shows where those bases come from too. After
entity_vm_runtime_createsucceeds,entity_vm_runtime_init_from_path_if_configured(000d:44df) seeds0x8c7c/0x8c7e/0x8c80/0x8c82as cumulative category bases by looping over four word counts at0x6608..0x660e. Because the compiled side only sees those category-base spans and the later owner-row mask words, it still does not reveal a direct descriptor-class discriminator before the slot body is loaded. - One direct non-hub consumer reinforces that read.
FUN_0005_295fis the only currently recovered caller ofentity_vm_slot_index_from_entityoutside the masked hub; it recomputes the same slot index, directly tests owner-row bit0x0040, and then branches into gameplay handling before optionally callingentity_vm_context_try_create_masked_for_entitywith mask0x0040:0x0006. Together with the still-empty xref results for0005:2c35and the stable0005:2c35..2c57function boundary, the safest current interpretation is that these owner-row words are generic capability masks, not explicitNPCTRIG/EVENTfamily tags. - The next immortality pass separates that owner-row path from the live control-stream path even more sharply. Inside
entity_vm_context_create_from_slot_index(000d:46ec), the owner-table row still feeds only the preserved0x39ca[slot]mirror, while the actual+0xd6/+0xd8control stream handed toentity_vm_context_setupcomes fromentity_vm_slot_load_value_plus_offsetand the caller-supplied setup/tail pointers come from the current VM frame record. That makes the immediate builder for the000d:21edlaneslot-backed decoded stream plus frame-local replay, notowner-row decode. - That is the current hard wall for the immortality frontier. The strongest verified answer remains that
NPCTRIGslot0x0ais the best upstream descriptor-side fit andEVENTslot0x0aremains the generic-hub baseline, but the binary selector path now bottoms out at category spans plus row-capability bits rather than at a provable class-family discriminator. - The open descriptor question therefore moves one step earlier again. Current
000dloader/runtime evidence still supports a descriptor-derived upstream workspace, but not a direct owner-row-to-opcode path for the immortality trigger. The closest verified compiled-side seeding now happens later inside the hidden dispatcher at000c:fa2f, where immediate literal cases can push byte/word/dword payloads straight onto the caller stream before the frame replay family re-materializes them into the child frame. - The seg070 twin-file-family helper is now tighter at the buffer/schema level as well. The paired loops at
0009:67b6and0009:6916do not reuse one ambiguous scratch object: each loop performs its own size query/allocation sequence, builds paths from the same+0x10/+0x18/+0x14table trio with its own format string (DS:3f2dversusDS:3f40), feeds a dedicated temporary far buffer through the sharedfile_handle_alloc_init_and_open/dos_file_seek/dos_file_closetrailer, and then frees that loop-local buffer before returning. Current safest read is thereforetwo distinct temporary file-family materialization passes inside one owner-resource helper, not one callback shard reused for both families. - Additional
0x39caconsumers are now classified more cleanly. Beyond the already-known static seeds at000d:7299 -> DS:67f2and000d:761c -> DS:6872, the constructor-like windows at000d:929aand000d:963cseed rowsDS:68ecandDS:68f5respectively before enabling local timer/dispatch behavior. Those writes behave like dispatch-entry-local static seed rows, not owner-table mirrors. Separately,FUN_000d_938creads temporary dispatch-entry fields+0x32/+0x34at000d:9449..9468and000d:9547..9566only as a wait/poll condition on the scratch-palette (kind 0x3c) and current-palette (kind 0x14) entries it creates, which further separates active dispatch-entry state from the owner-backed0x39ca[slot] = {source_off, source_seg}rows written by000d:46ec. - Safe event-label correlation remains intentionally narrow after this pass. The sampled low slot ids are now concrete, but none of them yet have a verified binary-side behavior match strong enough to promote a ScummVM label like
look,use, orcachein.
Current batch: higher-slot masked wrapper ladder (0x10..0x14)
- The gameplay-side masked-wrapper island now extends one verified step past the older
0x0ffrontier. Raw call setup around0005:3115..322dshows five higher-slot entries feedingentity_vm_context_try_create_masked_for_entitywith slot ids0x10,0x11,0x12,0x13, and0x14. - The slot
0x10lane is not yet a clean standalone function object, but the containing body at0005:3115..3129is exact enough to classify its call shape: it pushes zero extra word, slot0x10, packed mask0x00010000, and the live entity pointer before the far call to000d:463a. The preceding guard at0005:30f2..3113restricts that path to one class-nibble-4lane. - Four neighboring helpers are now renamed directly in Ghidra from stable function objects:
0005:313e=entity_vm_context_try_create_mask_00020000_slot11_with_offset0005:3171=entity_vm_context_try_create_mask_00040000_slot120005:31da=entity_vm_context_try_create_mask_00080000_slot13_with_offset_if_valid_entity0005:31a0=entity_vm_context_try_create_mask_00100000_slot14_with_offset
- Their payload shapes are now exact from disassembly, not only inferred from decompile:
- slot
0x11pushes one caller-supplied extra word (MOVZX EAX,[BP+0xa] ; PUSH EAX) - slot
0x12pushes a fixed zero extra word - slot
0x13pushes one sign-extended caller word after the same0005:2686/0005:ffedentity-validity gate used by the older slot-0x01helper - slot
0x14pushes one caller-supplied extra word
- slot
- This widens the verified owner-slot taxonomy in a USECODE-relevant way: the binary is no longer only distinguishing compact low-slot wrappers like
0x0a/0x0b; it also separates a higher-slot family with mixedno extra wordversussigned extra wordcall contracts. - The first outward callers in this higher-slot family are now explicit too.
entity_vm_context_try_create_mask_00040000_slot12(0005:3171) is called at0005:1776and0005:1945; both callsites are currently trapped in non-function windows, but they are real direct edges into the slot-0x12zero-extra-word lane. By contrast, current MCP xrefs still show no direct outward callers for the slot0x11,0x13, or0x14wrappers and still none for the dark slot0x0a/0x0bpair. - The persisted-context side of the same lane is now tighter at the field level.
entity_vm_context_save(000d:498f) serializes+0x11f,+0x121, the derived low word at+0x10c, the additive word at+0x34, and the0x80-byte local buffer at+0x36/+0x38;entity_vm_context_load(000d:4a78) rebuilds the frame pointers, reloads the saved low word as the additive argument toentity_vm_slot_load_value_plus_offset, restores+0x10c/+0x10e, and refreshes the owner-linked source pair at+0x117/+0x119. That strengthens the current read that persistence preserves(slot, additive_word, derived_low_word)after selector choice, not the upstream class-family selector itself. - The external event-name correlation can now be tightened slightly but still stays hint-level only:
- slot
0x12having no extra word is compatible with the externaljustMoved()zero-argument event label - slot
0x13carrying one extra word is compatible with Pentagram'sAvatarStoleSomething(uword)signature - slot
0x11carrying one extra word is compatible with Pentagram's placeholderfunc11(sint16)signature and with ScummVM's unresolvedcast-side slot only at the broadone scalar argumentlevel - slot
0x14currently does not fit Pentagram's older zero-argumentanimGetHit()signature, so that ordinal should remain slot-numbered on the binary side for now
- slot
- Operational consequence for the current VM lane: there is now stronger binary evidence that the masked-context family is organized around slot ordinals with distinct payload shapes, not only around one low-slot trigger subset. That helps the current round-trip IR because it justifies keeping higher ordinals as slot-stable records with payload-shape metadata even when their event labels remain external hints.
- The sequencer-side consumer model is also now preserved directly in Ghidra. Address
000d:22bccarries a decompiler comment recording it as a sequencer-internal matrix stage: it reads two signed metadata bytes from+0xd6/+0xd8, consumes caller-stream words as entity/link ids, repeatedly calls0008:7d27, and only pushes back words without bit0x0400before jumping toentity_vm_opcode_finish.
Conservative parser rule from this batch
- For current owner-loaded/raw EUSECODE work, keep bytes
8..11raw and derive event count only with(raw_u32_at_8_11 - 20) / 6when divisibility and object-size bounds checks succeed. - Keep the decremented
code_base_minus_one = raw_u32_at_8_11 - 1as a separate code-addressing field rather than collapsing it into the event-count rule. - Preserve the 6-byte event rows and their leading word verbatim until the event-entry word semantics are verified.