Crusader_Decomp/docs/raw-0008-000c.md
MaddoScientisto daa363c3d2 Add 'annotate-usecode' command to import USECODE IR JSON annotations
- 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.
2026-03-24 18:14:20 +01:00

80 KiB
Raw Blame History

Raw 0008 & 000c: Dispatch Helpers & State Machine

Content extracted from crusader_decompilation_notes.md. Covers the 0008 gameplay dispatch helper cluster and all 000c state machine helpers.


Raw 0008 Gameplay Dispatch Helper Batch

Small conservative rename batch from direct field-write behavior in the 0008:ba00-0008:be05 cluster.

Newly renamed functions

Address Name Evidence
0008:ba00 entity_dispatch_entry_init Constructor-style init: optional alloc (0x32 bytes), vtable/list-link setup (0x3b06, 0x2d10, 0x3afe), zeroes state fields, seeds group from global active layer 0x39c9 via entity_set_group_id
0008:bbb6 entity_set_source_type Writes entry word field +0x04 from incoming parameter, then dispatches through FAR thunk path
0008:bc27 entity_set_event_type_checked Writes entry word field +0x06; when source field +0x04 is non-zero, validates old/new event transition, including special checks for 0xF0-0xF7 and upper bound <= 0x0FFF
0008:bca8 entity_set_group_id Validates group id range 1..31, writes low 5-bit group in byte +0x08, decrements old per-group counter and increments new one via counter table pointed to by 0x39c5
0008:bd53 entity_dispatch_entry_unlink Clears bit 0x1000 in flags2 at +0x18 and zeroes the four link/state words at +0x0a..+0x10; used as the common unlink/reset tail in the local dispatch-entry pruning path
0008:be05 entity_increment_group_id Computes ((entry+0x08)&0x1F)+1, validates against active-layer assumptions (0x39c9), then applies through entity_set_group_id

Verified call/xref notes

  • entity_set_group_id is called from entity_dispatch_entry_init (0008:bae4) and entity_increment_group_id (0008:be57).
  • entity_set_source_type is used from FUN_0008_c92f (0008:c94d, 0008:c96d) and FUN_0008_ca18 (0008:ca36, 0008:ca56).
  • 0008:bd79 remains positional, but current evidence shows it compares an entry extent/position tuple against the player world position (g_player_entity_farptr + 0x40/+0x42), optionally fires the vtable callback at +0x28 when flag 0x100 is armed, then calls entity_dispatch_entry_unlink.

Gameplay relevance

This cluster manages core dispatch-entry metadata (source_type, event_type, group/layer byte and counters) that feeds the seg021 scheduler/event system. The field offsets match the current seg021 entity/dispatch layout notes (+0x04, +0x06, +0x08).


Raw 0008 Pair-Sync Helper Batch

Conservative directional rename batch from the 0008:c7f1-0008:cad7 cluster. These functions are clearly paired and structurally symmetric, but final gameplay semantics are still partial due to FAR-thunk heavy internals.

Newly renamed functions

Address Name Evidence
0008:c7f1 entity_pair_update_link_slot_a Guards on entry flags (+0x16 must not include 0x4000), then dispatches through FAR thunk using entry local struct at +0x28 and partner-side key/id input
0008:c890 entity_pair_update_link_slot_b Twin of entity_pair_update_link_slot_a with identical call shape and guard behavior; used in opposite order by pair-sync wrappers
0008:c92f entity_pair_sync_a If either side has unset source_type (+0x04), copies from partner via entity_set_source_type; then calls link-slot helpers in A-order and ends in FAR thunk using first side +0x1e data
0008:ca18 entity_pair_sync_b Mirror of entity_pair_sync_a with reversed side/order for helper calls and final thunk argument ordering
0008:c9ee entity_pair_mark_and_sync_a Sets bit 0x10 in entry flags at +0x16, then calls entity_pair_sync_a
0008:cad7 entity_pair_mark_and_sync_b Sets bit 0x10 in entry flags at +0x16, then calls entity_pair_sync_b

Raw 0008 Flag-0x20 Target-State Helpers

Two complementary helpers near the pair-sync cluster.

Address Name Evidence
0008:cb2c entity_flag20_clear_and_update_target Clears bit 0x20 at entry flags +0x16; if non-null target args are provided, writes far-pointer target fields +0x12/+0x14; then calls shared refresh helper 0008:c01d
0008:cb5c entity_flag20_set_and_init_target Sets bit 0x20 at entry flags +0x16; initializes target far-pointer fields +0x12/+0x14 only when currently zero; then calls shared refresh helper 0008:c01d

Both helpers share the same post-update refresh path (0008:c01d), suggesting they are two state transitions in one target/link-management subsystem.


Raw 0008 Dispatch Refresh Pipeline

Follow-up rename batch for the shared refresh node used by the flag-0x20 helpers.

Address Name Evidence
0008:c01d entity_refresh_dispatch_state Early-exit when flags at +0x16 indicate dead (0x8) or already refreshed (0x4000); otherwise runs pre-clear, sets 0x4000, calls update vfunc path, then runs flag-conditioned handlers
0008:bfb2 entity_clear_status_bits_from_flags Clears specific bits in status word at +0x32 based on state flags (+0x16:0x400, +0x18:0x40/0x80)
0008:bf8e entity_call_update_vfunc14 Calls helper 0008:be6b, then dispatches entity vtable call at offset +0x14
0008:beee entity_run_flagged_handlers Executes handler calls gated by flags (+0x16:0x400/0x4, +0x18:0x40/0x80) and then dispatches via FAR thunk using entry slot/index (+0x2)

State pipeline after target/link changes: flag-gated status clear → mark refreshed (0x4000) → vtable update callback → flag-conditioned subsystem handlers.


Raw Import Note: 0000:ffff Thunk Target

FUN_0000_ffff renamed to unresolved_far_thunk_dispatch. Current raw-import evidence indicates this is not valid local executable logic in this program view:

  • Decompiler emits overlapping-instruction warnings and bad-control-flow warnings.
  • Disassembly from 0000:ffff into 0001:xxxx is nonsensical/misaligned (mixed data/code artifacts).
  • The body is heavily shared as a call sink from many segments, consistent with unresolved inter-segment thunking in this import mode.

Treat calls to unresolved_far_thunk_dispatch as unresolved external/indirect dispatch edges. Semantic recovery should continue from call-site argument setup and local field effects.


Raw 0008 Flag-0x100 and Constructor-Variant Batch

Address Name Evidence
0008:d1a4 entity_set_flag100_in_flags2 Gate-checked setter: ORs bit 0x100 into entry word at +0x18
0008:d1dc entity_clear_flag100_in_flags2 Gate-checked clearer: ANDs entry word at +0x18 with 0xFEFF (clears bit 0x100)
0008:cefb entity_dispatch_entry_ctor_vtbl_3ad2 Constructor variant: allocates if null, reinitializes via entity_dispatch_entry_init, sets vtable 0x3ad2, sets flag 0x100 at +0x16, and zeroes the extension words at +0x32/+0x34
0008:d214 entity_dispatch_entry_ctor_vtbl_3aa6 Constructor variant: allocates 0x40 bytes if null, reinitializes via 0008:cefb, sets vtable to 0x3aa6, sets flag 0x200 at +0x16, zeroes fields +0x38..+0x3e

Raw 0008 Periodic/Counter Helpers

Follow-up renames from the 0008:d313-0008:d47d cluster tied to the 0x3aa6 constructor branch.

Address Name Evidence
0008:d313 entity_periodic_accumulate_and_dispatch Adds global delta (0x39d0/0x39d2) into entry accumulator (+0x3c/+0x3e), wraps against period (+0x38/+0x3a), and on wrap invokes entry vtable callback at +0x28 with reentrancy guard bit 0x400 in +0x18
0008:d3e6 entity_set_flag2000_and_update_active_counters Atomic (CLI/PUSHF) set of bit 0x2000 in +0x16; if bit 0x400 is set, decrements global counter 0x39f6 and increments 0x39f4
0008:d433 entity_clear_flag2000_and_update_active_counters Atomic clear of bit 0x2000 in +0x16; if bit 0x400 is set, decrements 0x39f4 and increments 0x39f6

The 0x39f4/0x39f6 counter swap implies global bookkeeping for a scheduler subset associated with these entries.


Raw 0008 Word-List Management Batch

Verified helper cluster for entry-owned word-list storage (sentinel-terminated with 0x0408).

Address Name Evidence
0008:da00 entity_word_list_set_0408_terminated Rebuilds/replaces entry list from stack-provided words terminated by 0x0408; frees prior list pointer at +0x06/+0x08; allocates and populates new list
0008:dba3 entity_word_list_free_existing Validates list pointer exists, then frees old list buffer referenced by +0x06/+0x08
0008:dbec entity_word_list_destroy Resets vtable to 0x2d10, frees list if present via entity_word_list_free_existing, and optionally frees object when destroy flag bit 1 is set
0008:dc38 entity_word_list_ensure_contains Scans existing list for a given word; if missing, appends through entity_word_list_append_unique
0008:dcab entity_word_list_append_unique Allocates larger list, copies existing words, appends new word plus 0x0408 terminator, frees old list, then rebuilds via entity_word_list_set_0408_terminated

Entry fields used by this subsystem: count at +0x02, list far pointer at +0x06/+0x08. The explicit 0x0408 terminator appears in both scanner/build logic and append path.


Raw 0008 Word-List Access/Mutation Batch

Follow-up renames extending the same list subsystem.

Address Name Evidence
0008:deea entity_word_list_get_at Bounds-checks index against count (+0x02) and returns word from list pointer (+0x06/+0x08, stride 2)
0008:df1b entity_word_list_set_at Bounds-checks index then writes value into list element (+0x06/+0x08, stride 2)
0008:dfa1 entity_word_list_find_unflagged_by_id10 Scans list and returns first value satisfying (value & 0x400)==0 and (value & 0x3ff)==requested_id; writes 0 when not found
0008:ddaf entity_word_list_remove_value Removes matching value(s) by counting survivors, rebuilding compact storage for non-matching entries, freeing old list storage, and updating list state

List entries pack a 10-bit id plus flag bits (0x400 observed).


Raw 0008 Gate-Callback Wrapper Batch

Conservative renames for callback wrappers sharing the same global gate condition.

Address Name Evidence
0008:d00e entity_gate_callback_wrapper_a Gate check on globals 0x39a8/0x39f9/0x3991; on pass dispatches callback through unresolved thunk using entry +0x2 and [0x3b32:0x3b34] + 0x32
0008:d05f entity_gate_callback_wrapper_b Same gate pattern; callback wrapper variant via unresolved thunk
0008:d0b0 entity_gate_callback_wrapper_c Same gate pattern; passthrough-style callback wrapper
0008:d0ed entity_gate_callback_wrapper_d Same gate pattern; passthrough-style callback wrapper
0008:d12a entity_gate_callback_wrapper_e Same gate pattern; passthrough-style callback wrapper
0008:d167 entity_gate_callback_wrapper_f Same gate pattern; passthrough-style callback wrapper
0008:d3d2 entity_slot_callback_wrapper Thin wrapper: pushes entry slot/index (+0x2) and dispatches through unresolved thunk

Additional Unresolved Thunk Stubs

Follow-up thunk census after inspecting 0000:ffff behavior. All of the following are single-instruction wrappers (CALLF 0000:ffff):

Address New Name Observed Caller(s)
0004:2592 thunk_callf_0000_ffff_0004_2592 0004:262d (FUN_0004_2620)
000b:f924 thunk_callf_0000_ffff_000b_f924 000b:0144 (FUN_000b_010b)
000c:827d thunk_callf_0000_ffff_000c_827d 000c:8985, 000c:8f96 (FUN_000c_88b4)
000c:82f9 thunk_callf_0000_ffff_000c_82f9 000c:8a10, 000c:8f79, 000c:9052
000c:8356 thunk_callf_0000_ffff_000c_8356 000c:84a9 (FUN_000c_84a5)
000c:e4f9 thunk_callf_0000_ffff_000c_e4f9 000c:e4f5 (FUN_000c_e4e0)

unresolved_far_thunk_dispatch is represented by multiple local trampoline copies in different segment regions. Separating them by address improves call-graph navigation.


Raw 000c State-Dispatch Helper Cluster

After separating thunk stubs, a coherent local state/chain management cluster was lifted in 000c:ab32-000c:ac8f.

Address Name Evidence
000c:ab32 entity_state_tick_dispatch Core state tick helper using fields +0x38/+0x39/+0x3b/+0x3d/+0x5b; clears mode bit 0x100 when +0x38==0, may call cleanup helper 000c:ac55, calls 000c:7730(state,1), and conditionally advances chain
000c:ab96 entity_state_reset_and_tick_dispatch Reset wrapper: zeroes +0x38 and +0x39 then calls entity_state_tick_dispatch
000c:abb4 entity_state_advance_next_or_fallback_a Advance path A: when +0x49!=0, follows node-next pointers from +0x3b/+0x3d using offsets +2/+4; when exhausted, either clears active flag and re-dispatches, or falls back to backup pointer +0x41/+0x43
000c:ac8f entity_state_advance_next_or_fallback_b Advance path B: same structure as A but follows alternate node offsets +6/+8 and fallback pointer +0x45/+0x47

Raw 000c State-Flag Guard / Input Handler Batch

Second sweep through 000c adjacent helpers — gated thunk wrappers and input/animation tick handlers.

Address Name Evidence
000c:9f74 entity_state_flag100_check_and_dispatch Init latch guard at [0x6053]; clears [0x8c55] on first call; checks [ptr+0x5b] bits 0x100 and 0x40; three-path thunk dispatch
000c:a1ad entity_state_clear_flag40_and_dispatch Skips if [ptr+0x5b] has 0x180 bits set; if 0x40 set, clears it and calls far ptr at [0x5e82/0x5e84]; then dispatches twice with args (0x0b,0x10,0x1,0x0) (record/state-key pattern)
000c:a74e entity_state_dispatch_if_flag_bit2 Tests [ptr+0x5b] bit 0x2; if set pushes extra arg + ptr and dispatches via thunk
000c:84c3 entity_state_set_byte40_at_global_ptr Sets byte [g_active_dispatch_entry_farptr + 0x40] = 1 then calls thunk unconditionally; current evidence treats this as raising the shared active-entry transition/display hold byte rather than toggling an unrelated global
000c:ac55 entity_state_fire_if_handle_valid Guard: fires thunk dispatch only when [0x6054] != -1; no-op otherwise
000c:ac6d entity_state_fire_with_args_if_handle_valid 3-arg variant: pushes [BP+0xe] (byte), [BP+0xc], [BP+0xa], handle [0x6054], then CALLF 0000:ffff
000c:afa5 transition_file_family_select_and_refresh Local startup/display selector: field49==-1 normalizes to 0; field49==2 dispatches vtable[0x3c]; field49==0/1/4 composes one of three sibling filenames from inherited base 0x6aa:0x6ac plus stem/suffix buffers 0x621c/0x6223, 0x621c/0x622d, or 0x621c/0x6237, loads the result into object +0x520, then runs the shared redraw/palette/input refresh path
000c:b153 transition_file_family_advance_on_anim_tick Polls [param_2+0x14+0xa]; when clear increments field49 and re-enters transition_file_family_select_and_refresh, otherwise exits through vtable[0x3c]
000c:b199 transition_file_family_input_key_handler Local selector key handler: ESC/x/X → vtable[0x3c]; Left/Right arrows 0x14b/0x148 → previous file-family state; n/N/0x14d/0x150 → next state; e/E arms field47; - after arming counts up to forced state 4; selector moves drain the event queue and clear 0x8a94/0x8a96/0x8a98
000c:b2c3 stub_noop_000c_b2c3 Empty stub; returns immediately
000c:b2c8 entity_state_dispatch_if_field49_eq4 Fires thunk only when [ptr+0x49]==4
000c:b349 entity_state_dispatch_if_far_ptr_nonzero_a Fires thunk if far-pointer args non-zero
000c:b383 entity_state_set_field3f_and_dispatch If non-NULL: writes &DAT_0000_2d18 to [ptr+0x3f], then dispatches
000c:b3d8 entity_state_dispatch_if_far_ptr_nonzero_b Same null-guard pattern as b349, variant b

Patterns confirmed:

  • field49 = local transition file-family selector state in this startup/display family; 0/1/4 choose sibling filenames under shared base 0x6aa:0x6ac plus stem 0x621c, 2 dispatches vtable[0x3c], and -1 normalizes back to 0
  • field47 = keystroke arm/counter for the local e/E then - path into selector state 4
  • field3f = linked data pointer (event/record reference)
  • [0x6054] = current entity handle; [0x6828] = g_active_dispatch_entry_farptr, the shared active-dispatch entry owner whose byte +0x40 is reused across the startup/display lane as a hold/busy token
  • Bits in [ptr+0x5b]: 0x1=init, 0x2=active/event, 0x40=pending dispatch, 0x100=flag100, 0x180=skip-all mask

Raw 000c Palette Fade + Entity VM Cluster

VGA Palette Fade

Address Name Evidence
000c:cdde palette_fade_step_down Writes (Roffset, Goffset, Boffset) clamped to 0 to VGA I/O 0x3c8/0x3c9; decrements [0x630d] by step [0x6316]; clears active at [0x630a] when black
000c:ce57 palette_fade_step_up Same loop, adds offset, clamps at 63 (0x3f full VGA). Clears [0x630a] when fully bright

Globals used: [0x6312]=start index, [0x6314]=count, [0x630e]=palette src ptr, [0x630d]=brightness offset, [0x6316]=step, [0x630a]=active flag.

Entity Mini-VM / Record-Player Context

Address Name Evidence
000c:f6b8 record_table_get_by_index Bounds check param < [0x8c88]; return word at [0x8c84 + param*4]. Table at 0x8c84
000c:f6e8 entity_vm_stack_init_with_data Init stack ptrs at [ptr+0xcc..+0xd4] pointing to self; max depth 199; copies optional initial data
000c:f772 entity_vm_state_copy Copies 200 bytes (100 words from [src+4] to [dst+4]), then copies 4 words at +0xcc..+0xd2
000c:f7c7 entity_vm_stack_push_frame Push call-frame: saves ret offset at [ptr+0xd4], decrements [ptr+0xcc] by param_size, zeroes new frame

Current EUSECODE / event bridge notes

  • entity_vm_set_value_from_slot_plus_offset (000c:f95f) now provides a concrete bridge from the 000c mini-VM cluster into the 000d event/countdown lane:
    • it calls FUN_000d_5572(*(word *)0x6611, *(word *)0x6613, param_3, param_4, 0, 0)
    • then stores the returned far pair into target object fields +0xd6/+0xd8
  • entity_vm_slot_load_value_plus_offset (000d:5572) is a thin wrapper over entity_vm_slot_load_value (000d:51fd), but the previously suspicious PUSH 0x410 path at 000d:5290 is now reclassified: it pushes 0x410, DS, and 0x6616 into the seg091 fatal-report helper at 000a:44fd, so this is an error/assert path rather than a live gameplay event dispatch.
  • This closes the earlier compiled-code immortality bridge from 000c:f95f into 000d:51fd. The verified bridge that remains is the data/value handoff into the context +0xd6/+0xd8 lane, not a direct event 0x410 producer.
  • Supporting renamed helpers in the same lane now include:
    • entity_vm_slot_find_or_select (000d:4e7c): scans 0x26-byte slot records, returns a matching slot id when present, and tracks one fallback slot for reuse/eviction
    • entity_vm_slot_decrement_use_count (000d:558d): decrements one slot-use counter and traps on underflow
    • entity_vm_slot_release_value (000d:5617): releases one slot value, restores the owner's 0x1300/0x1302 budget pair, writes the slot state back to -1, and notifies through 000a:2b9d
    • entity_vm_opcode_finish (000d:3350): shared VM opcode epilogue used by the 000d:039f, 000d:08a2, 000d:0988, 000d:177c, and 000d:1acb handlers; if the local result slot is non-zero it writes the current referent id to 0x8c94, optionally pops one slot_array frame through 0x659c/0x659e, and returns the opcode result from local state
    • entity_vm_referent_chain_remove_matching_from (000d:6a9a): destructive chain-difference helper used by the 0x1a/0x1b opcode path in 000d:0988; it walks one source chain against a destination chain, removes matching entries in place, and frees removed registry nodes / indirect payloads
    • entity_vm_referent_chain_set_entry_data_at (000d:6cf6): finds one chain entry by index and overwrites its payload in place, including indirect/string cleanup when the chain uses indirect storage
  • The surrounding runtime/context family is now materially clearer too:
    • entity_vm_runtime_create / entity_vm_runtime_init_slots / entity_vm_runtime_release_slots / entity_vm_runtime_destroy (000d:4c99, 000d:4d36, 000d:4d75, 000d:4e01) are the global 0x6611 owner for this lane; they allocate the 0x2040-byte runtime body, clear the 0x80-entry slot table, manage the runtime budget/default fields at +0x1300..+0x1314, and retain one owner/resource object at +0x1315/+0x1317 returned by 000d:7000
    • entity_vm_slot_index_from_entity (000d:45c5) computes one slot index from a gameplay entity by branching on seg021 class/type helpers and then adding one of the current runtime base offsets 0x8c7c/0x8c7e/0x8c80
    • entity_vm_context_try_create_masked_for_entity (000d:463a) uses that slot index to test one owner-side mask entry before it creates a context, which is the strongest current bridge from gameplay entities into this VM lane
    • entity_vm_context_create_from_slot_index (000d:46ec) allocates one 0x6714 context object, seeds its +0xd6/+0xd8 lane through entity_vm_slot_load_value_plus_offset, initializes the local mini-VM state, and can prepend caller data into the backward-growing buffer at +0x102
    • entity_vm_opcode_sequence_run (000d:ebe3) is now named conservatively in Ghidra: it seeds the stage chain from object +0xfe, runs 000d:177c -> 000d:1acb -> 000d:0988 -> 000d:22bc -> optional 000d:1d4a -> 000d:2104, then finishes with tracked-handle cleanup plus the 0008:ebe7 gate on object +0xc0 and byte +0x4b
    • entity_vm_context_sync_global_value_and_dispatch (000d:48da) is the current context-side runner/sync point: it marks the context busy at +0x123, calls entity_vm_set_field_da_to_global, optionally writes the current value through +0x11b/+0x11d, and dispatches through the context vtable on success
    • entity_vm_context_save / entity_vm_context_load / entity_vm_context_destroy / entity_vm_context_free_buffer (000d:498f, 000d:4a78, 000d:4962, 000d:48b6) now pin down the lifecycle of this object family rather than leaving the whole 000d:45xx..4exx island anonymous
  • entity_vm_context_try_create_masked_for_entity is now better constrained at the return-value level too: after the runtime-disable check at 0x6610 and the owner-side slot-mask test succeed, it reports two distinct success shapes. Immediate-flagged contexts (+0x16 & 0x0008) clear the caller output word, while object-backed contexts return the created object's low word. That makes the helper a typed bridge from gameplay entities into VM-backed object results, not only a yes/no mask probe.
  • entity_vm_runtime_owner_resource_create (000d:7000) is now one step tighter too: the embedded seg069/070 helper is file-backed rather than abstract. Construction starts with dos_file_handle_init (0009:1c00), then uses helper vtable slot +0x04 as the size query that drives the child +0x10/+0x12 allocation and helper vtable slot +0x0c as the table-population callback for the 0x0d-stride owner table.
  • That file-backed helper is now tighter one step deeper as well. The seg070 loops rooted at raw windows 0009:67b6 and 0009:6916 walk helper-owned record arrays at object +0x10/+0x18, format per-entry paths through the seg001 string helpers (0003:e4d3 / 0003:e590), then open, read, and close each file through file_handle_alloc_init_and_open (0009:1c3a), dos_file_seek (0009:2034), and dos_file_close (0009:1e61). The paired +0x18 entries are consumed as 16-bit ids passed into those path-format loops beside the far-pointer path table at +0x10; no object-1 or classid + 2 arithmetic appears there, so the safest current read is slot-local file ids rather than exposed original class/object indices. That is strong evidence that 000d:7000 seeds the owner table from an indexed external file set rather than by copying one monolithic in-memory descriptor blob.
  • A final loader-side tightening from the current pass is that 0009:67b6 and 0009:6916 now read as paired file-family walkers rather than one isolated path-format callback. Both windows iterate the helper-owned count at +0x14, index the far-pointer path table at +0x10 and paired 16-bit id table at +0x18, check the source path through 0003:e669, build formatted paths with distinct local format strings (DS:3f2d vs DS:3f40), and then reach the same file open/read/close lane. Each loop also writes into its own independently allocated output far buffer before the shared trailer runs, so the best current reading is two parallel file families or record banks loaded by the same helper rather than two phases over one shared buffer. The remaining open question is the exact per-family record schema and higher-level resource role, not whether the helper is file-backed.
  • The caller-side bootstrap for that helper is now anchored too: entity_vm_runtime_init_from_path_if_configured (000d:44df) first checks the configured byte/string global at 0x65a, builds a path through seg072 helper 0009:3600 using globals 0x6d6:0x6d8 plus 0x65a, validates that path through 000a:500a, then calls entity_vm_runtime_create(0,0,path). This is the first verified source-argument path for entity_vm_runtime_owner_resource_create, and it strongly suggests the owner/resource table is loaded from an external configured file rather than from a purely in-memory descriptor blob.
  • Seg072 helper 0009:3600 is now classified more tightly as a rotating slash-aware path composer rather than a generic buffer advance helper. Its prologue cycles through five 0x50-byte temp buffers, and its inner cases append optional string parts while inserting \ only when adjacent path components need a separator. That narrows the two globals used by 000d:44df: 0x65a behaves as the configured relative runtime-owner filename/path component, while 0x6d6:0x6d8 behaves as the mutable base/resource-root path buffer that gets joined with 0x65a before 000a:500a validation.
  • The two still-xref-dark wrappers 0005:2c35 and 0005:2c68 are also narrower now. Their signed extra word does not participate in owner-mask selection inside entity_vm_context_try_create_masked_for_entity; it is forwarded into entity_vm_context_create_from_slot_index, stored in context field +0x34, and passed on to entity_vm_slot_load_value_plus_offset. The best current reading is therefore offset-specialized masked context creation, not a separate direct selector lane.
  • Ghidra now records that signed-offset contract directly in the wrapper names too: 0005:2c35 = entity_vm_context_try_create_mask_0400_slot0a_with_offset and 0005:2c68 = entity_vm_context_try_create_mask_0800_slot0b_with_offset. That still stops short of real caller-role recovery, but it removes the last ambiguity about whether the extra stack word is semantically live.
  • The first opcode-level behavior split inside that runtime is now visible in the 000d:0988 family:
    • one branch calls entity_vm_referent_chain_append_unique_from, which looks like an attach/union operation on the current referent payload chain
    • the 0x1a/0x1b branch instead calls entity_vm_referent_chain_remove_matching_from, which looks like the inverse operation and makes the opcode family materially closer to a graph-editing script VM than a flat event list
    • both paths return through entity_vm_opcode_finish, so the referent-global write to 0x8c94 is now better understood as a shared interpreter epilogue rather than a unique quirk of one helper
  • One additional runtime layer is now named under that context family: the referent registry at 0x8c8c/0x8c8e/0x8c90/0x8c94.
    • entity_vm_referent_registry_init / entity_vm_referent_registry_destroy (000d:6000, 000d:60bf) allocate and free the registry buffer, seed the free-list/root metadata, and clear the current referent id at 0x8c94
    • entity_vm_referent_registry_alloc (000d:613e) allocates one registry node from the free list and stores the current referent id from 0x8c94 into node field +0x04
    • entity_vm_referent_registry_release_by_id / entity_vm_referent_registry_free_node (000d:6251, 000d:62ac) release all live nodes for one referent id and coalesce adjacent free nodes
    • this makes entity_vm_set_field_da_to_global more important than it first looked: it writes 0x8c94 from the current context +0xda lane and then immediately enters the still-misaligned 000c:3350 body, so the referent selected by the context is now visibly feeding runtime registry state
    • the registry nodes are not flat scalars only; the surrounding container helpers are now named too:
      • entity_vm_referent_chain_copy / entity_vm_referent_chain_append_unique_from (000d:6694, 000d:68c3) build deep-copied or deduplicated chains of referent-linked payloads
      • entity_vm_referent_chain_destroy, entity_vm_referent_chain_next, and entity_vm_referent_chain_append_node (000d:6602, 000d:6651, 000d:687b) provide the list management shell
      • entity_vm_referent_chain_contains_entry, entity_vm_referent_chain_get_entry_data_at, and entity_vm_referent_chain_get_indirect_data (000d:6c31, 000d:67f2, 000d:6860) show that some chains carry fixed-size inline payloads while others carry indirect string-like payload nodes
  • This matters for script readability: the current runtime model is no longer only "one referent id hits one event." It now supports a more useful intermediate representation where one referent anchor can own one or more payload chains, and neighboring event-bearing descriptors can attach behavior to that anchor without duplicating the anchor's own record.
  • Nearby descriptor work on EUSECODE.FLX is consistent with that model: event-bearing classes (EVENT, NPCTRIG, SFXTRIG, several *_BOOT records) use a stable 69:0A00 -> event tag, while JELYHACK / JELYH2 remain referent-only descriptors in a neighborhood that includes TRIGPAD, SPECIAL, REE_BOOT, SURCAMEW, and SFXTRIG.
  • The strongest current callsites into this context-construction path are the large 000d:208b and 000d:21ed bodies, which both feed per-object stream/data state from +0xcc/+0xce into entity_vm_context_create_from_slot_index before continuing bytecode-style reads from the newly seeded +0xd6/+0xd8 lane. That makes the 000d interpreter/object lane a better current immortality target than the older 000e text-parser hypothesis.
  • The immediate producer chain for that +0xcc/+0xce stream state is now one layer tighter:
    • entity_vm_context_create_from_slot_index (000d:46ec) allocates the 0x6714 context, then calls entity_vm_context_setup (000c:f844) on the embedded mini-VM object at context +0x36.
    • entity_vm_context_setup delegates to entity_vm_stack_init_with_data (000c:f6e8), which seeds [mini_vm+0xcc..+0xd2] to point into the object's own payload area and optionally prepends caller-owned inline bytes by moving the stack pointer backward.
    • entity_vm_state_copy (000c:f772) copies that same +0xcc..+0xd2 stream/base quartet verbatim when one mini-VM object is cloned.
    • Upstream of the setup helper, 000d:46ec derives the source payload from the runtime owner table behind 0x6611 -> +0x1315/+0x1317: with slot index SI, it walks owner table *(owner+0x10/+0x12) + 0x0d*SI + 4, passes that far pointer into 000c:f844, and mirrors the resulting per-slot source into 0x39ca[slot].
  • This sharpens the current JELYHACK-side model rather than overturning it: the code-side producer recovered in this batch is still a generic slot-backed VM source object keyed by gameplay-entity slot selection and owner-side mask bits, not a direct hard-coded descriptor-class switch on JELYHACK or JELYH2. Combined with the extractor evidence that JELYHACK / JELYH2 remain referent-only while REE_BOOT / SFXTRIG keep active event tags and SURCAMEW keeps eventTrigger, the better fit is still referent anchor -> slot-backed payload chain -> neighboring event-bearing attachment.
  • The 0x39ca mirror question is now split more cleanly. Fresh windows at 0008:709c/70cb, 0008:7309/7338, and 0008:85f9/8617 still show only global base-pointer save/restore and allocation/zeroing of the 0x39ca:0x39cc table itself, but two additional per-slot row writers are now verified in 000d: FUN_000d_7299 writes static source DS:67f2 to 0x39ca[obj+2] after creating a 0x44-byte object, and active_dispatch_entry_create_default (000d:761c) writes static source DS:6872 to 0x39ca[obj+2] for the default active dispatch entry. entity_vm_context_create_from_slot_index (000d:46ec) remains the only confirmed owner-table-derived writer, but it is no longer the only concrete row writer overall.
  • The current pass narrows that split one step further. entity_vm_context_create_from_slot_index (000d:46ec) still derives its row from runtime owner table (+0x10/+0x12) + 0x0d*slot + 4 before mirroring it into 0x39ca[slot], while 000d:7299 and 000d:761c never touch the owner table at all in the verified windows. Instead they allocate local dispatch-entry-style objects, derive the row index from object field +0x2, and seed 0x39ca[row] from fixed static sources DS:67f2 and DS:6872. The safest current interpretation is therefore owner-backed VM source mirror versus dispatch-entry-local static seed rows, not three competing writers to the same semantic lane.
  • One exact numeric collision is now ruled out as unrelated noise rather than a second VM source: 000e:0953 in the animation/audio lane pushes literal 0x410 into imported ASYLUM.27 immediately after setting the local audio-completion byte at +0xef1. Because ASYLUM.DLL is the ASS_* audio/media library, this does not weaken the attribution of gameplay event 0x410 to the 000d VM/USECODE lane.
  • Current best JELYHACK reading after this pass: JELYHACK itself still looks like a referent-only map/object descriptor, but that no longer makes it inert. A referent-only record can still matter by supplying the referent id that populates the VM referent registry, while neighboring classes such as REE_BOOT, SURCAMEW, and SFXTRIG supply the event-bearing logic attached to the same local object island.

000d:21ed/22bc id-correlation table (runtime lane vs descriptor families)

Runtime element Code anchors Observed width/shape Correlation status
Metadata byte A 000d:22d2 after context from 000d:46ec 1-byte signed (CBW), used as first loop dimension/count input Not a descriptor id. Behaves as compact shape/count metadata for matrix construction.
Metadata byte B 000d:22ee 1-byte signed (CBW), paired with byte A and summed to derive loop bounds Not a descriptor id. Same shape/count role as byte A.
Streamed words feeding matrix 000d:2324, 000d:2372, 000d:237b -> 0008:7d27 16-bit words consumed from caller stream and passed to entity_link Best fit: runtime entity/link ids, not descriptor-class selectors.
Matrix output writeback filter 000d:23da..2421 tests 0x0400; only non-0x0400 words are pushed back Matches entity_word_list style link-flag semantics, not event opcode tagging.
Source stream provenance 000d:4732..4751, 000d:47a3..47d4 source pointer = owner table (+0x10/+0x12) + 0x0d*slot + 4; mirrored to 0x39ca[slot] Slot-indexed runtime source table, generic across gameplay entity lanes.

Conservative interpretation after this pass:

  • The 000d:21ed -> 000d:22bc lane is strongly supported as a slot-backed payload to entity-link closure path, where two signed byte-sized metadata fields shape an exact A x B matrix walk: byte A is the lead-word row count, byte B is the shared target-list width, and the word entries passed to entity_link are runtime link/entity ids rather than descriptor selectors.
  • Descriptor-family alignment is therefore stronger with generic active event ecosystems (EVENT/NPCTRIG/*_BOOT/SFXTRIG) than with SURCAM* callback holders, because no direct eventTrigger-specific discriminator is read in this lane.
  • Direct descriptor-id attribution is still rejected for now: no code evidence ties the consumed bytes/words here to explicit EUSECODE class indices or to a hard JELYHACK/SURCAM* switch.
  • The new extractor-side structure pass tightens the descriptor-side fit inside that generic active-event ecosystem. USECODE/EUSECODE_extracted/immortality_body_structure.md shows EVENT slot 0x0a as a broad hub clause stream with 90 internal 0x53 0x5c <u16> EVENT subheaders and the widest field trailer, while NPCTRIG slot 0x0a stays compact at 5 subheaders and a narrow referent/event/item/item2 tail. That does not prove a direct class-id bridge into 000d:21ed -> 000d:22bc, but it does make NPCTRIG slot 0x0a the strongest remaining compact descriptor-side candidate for the offset-specialized slot-0x0a runtime wrapper entity_vm_context_try_create_mask_0400_slot0a_with_offset (0005:2c35) instead of the older undifferentiated EVENT or NPCTRIG frontier.
  • The next focused extractor pass sharpens that fit again. USECODE/EUSECODE_extracted/immortality_npctrig_clauses.md now shows NPCTRIG slot 0x0a as a fixed-width five-clause ladder: subheaders at 0x0064/0x0093/0x00c2/0x00f1/0x0120, uniform 0x2f stride, backward-walking targets, and one branch_3f_0a + push_24_51 + writeback_57_02 triple in each full clause. The new runtime-fit section also matters: 000d:5572 proves the extra word from 0005:2c35 is additive (entity_vm_slot_load_value(...) + offset), so slot 0x0a now exposes the only surviving compact five-row selector family that plausibly matches byte A in 000d:21ed, while slot 0x20 remains a one-clause typeNpc-heavy body with no comparable writeback/push motif or stride family.
  • The downstream-use follow-up weakens that direct selector fit. Instruction windows at 000d:47ef..47f3 show entity_vm_context_create_from_slot_index storing slot index SI at +0x32 and the dynamic additive word DI at +0x34, but the live sequencer lane 000d:21ed -> 000d:22bc never rereads either field: after the create call it only touches the copied blob at +0x102, the seeded byte lane at +0xd6/+0xd8, and the caller stream at +0xcc/+0xce. The persistent uses of +0x34 are instead the object save/load path: 000d:49e9..4a27 serializes +0x10c then +0x34, and 000d:4c2d..4c4d reloads (+0x32,+0x34) through entity_vm_slot_load_value_plus_offset before storing the returned pair at +0x10c/+0x10e. The safest current read is therefore persisted source offset feeding a later slot-value reload, not direct clause selector consumed by the matrix stage, which weakens the NPCTRIG slot 0x0a alignment unless the derived reload value itself can still be tied back to that ladder.

entity_vm_opcode_sequence_run opcode-to-payload-shape matrix (sequencer-local)

Sequencer stage Code anchors Opcode / lane status Payload shape class Verified behavior
000d:0988 (entity_vm_opcode_mutate_referent_chain) 000d:ec1d, 000d:0988 body Known 0x18..0x1b family Inline/indirect chain payloads 0x18/0x19 append-unique and 0x1a/0x1b remove-matching over referent chains, with indirect-vs-inline mode split and shared epilogue.
000d:177c 000d:ebf5, 000d:178b..17aa Numeric opcode unresolved in this dispatcher lane Word scalar (frame-local -> stream) Does not read +0xd6/+0xd8; subtracts 2 from [context+0xcc] and pushes one frame-local word (BP-0x1c6) onto the stream stack.
000d:1acb 000d:ec09, 000d:1acb..1b22 Numeric opcode unresolved in this dispatcher lane Word-pair/list consumer + boolean output Reads one 32-bit pair from stream ([context+0xcc], then +4), compares against AX:DX, and pushes a 16-bit predicate result back to stream.
000d:21ed -> 000d:22bc 000d:21ed, 000d:22d2, 000d:22ee, 000d:2324..237b, 000d:23da..2421 Caller block + internal stage Mixed: byte metadata + word id matrix Consumes two signed bytes from seeded +0xd6/+0xd8 as shape/count metadata, then consumes streamed words as entity/link ids for entity_link; only non-0x0400 words are pushed back.
000d:1d4a 000d:ec48, 000d:1d4a Conditional substage when [obj+0xba]==0 Control/sentinel (no payload shape proven) Current body is INT3-only (boundary suspect); treated as a control gate/trap island, not a verified payload transformer.
000d:2104 000d:ec54, 000d:2104..212b Numeric opcode unresolved in this dispatcher lane Mixed scalar/handle return Writes result to caller out-ptr: path A stores frame-local dword (BP+0xfdaa/fdac), path B stores object word ([obj+2]) with high word cleared; then returns via opcode epilogue.

Pass-4 dispatcher lane update (opcode selector evidence)

What is now hard evidence in code:

  • 000d:0988 compares one opcode-local word at [BP-0x32] against concrete values 0x19, 0x1a, and 0x1b (000d:099b, 000d:09a1, 000d:0a07, 000d:0a0d).
  • entity_vm_opcode_sequence_run (000d:ebe3) calls 000d:177c -> 000d:1acb -> 000d:0988 -> 000d:22bc -> optional 000d:1d4a -> 000d:2104 (000d:ebf5, 000d:ec09, 000d:ec1d, 000d:ec31, 000d:ec48, 000d:ec54).
  • 000d:177c, 000d:1acb, and 000d:2104 do not contain their own opcode compares in recovered body ranges; they behave as wrapper stages around the opcode-local family tested in 000d:0988.
  • The entry/exit contract is one step tighter too. 000d:ebe9 seeds the first stage from object field +0xfe, while the success tail at 000d:ec62..ec79 runs tracked_entity_handle_mark_remove_all_if_enabled and then gates FUN_0008_ebe7 on object field +0xc0 plus byte +0x4b. So the sequencer is not just an isolated opcode cluster; it also participates in outer runtime cleanup and follow-up dispatch state.

Conservative case identity mapping from this pass:

  • 000d:177c = pre-mutate stack push stage for the same [BP-0x32] family.
  • 000d:1acb = comparator stage (stream dword pair -> boolean word) for that family.
  • 000d:0988 = concrete opcode discriminator for 0x19/0x1a/0x1b (with 0x18 still implied by sibling path behavior).
  • 000d:2104 = family finalizer writing mixed immediate/object output to caller out-ptr.

Still unresolved after this pass:

  • The animation constructor near calls at 000e:283e, 000e:2931, and 000e:29e4 land on a separate mis-split 000e:ebe3 region, not on entity_vm_opcode_sequence_run. They therefore no longer count as direct xref evidence for the 000d dispatcher.
  • The true upstream selector/write path for [BP-0x32] in entity_vm_opcode_sequence_run is still unresolved, and no additional opcode id can yet be assigned uniquely beyond the internal 0x19/0x1a/0x1b family already proven inside 000d:0988.
  • Repeated MCP-visible instruction and data-use searches still do not produce a real direct caller edge for entity_vm_opcode_sequence_run, 0005:2c35, or 0005:2c68. For now that makes the next defensible route caller-frame / shared-consumer recovery, not more recycled raw call searches or the retired 000a:44fd and 000e:ebe3 hypotheses.

First readable VM IR sketch (verified-only)

From direct decompile/disassembly in 000d:0988, 000d:208b, 000d:21ed, 000d:22bc, and 0008:7d27, the current script-readable IR shape is:

  • APPEND_UNIQUE_INLINE (opcode 0x18, implied sibling in 000d:0988)
  • APPEND_UNIQUE_INDIRECT (opcode 0x19)
  • REMOVE_MATCHING_INDIRECT (opcode 0x1a)
  • REMOVE_MATCHING_INLINE (opcode 0x1b)
  • MATERIALIZE_OR_FORWARD_VALUE (000d:208b path after entity_vm_context_create_from_slot_index)
  • PUSH_FRAME_WORD_LITERAL (000d:177c: pushes one frame-local word to stream stack)
  • COMPARE_STREAM_DWORD_AND_PUSH_BOOL (000d:1acb: consumes one stream dword pair and pushes predicate word)
  • PREPEND_INLINE_PAYLOAD (000d:21ed: subtracts from context +0x102 then copies caller bytes)
  • BUILD_ENTITY_LINK_MATRIX (000d:22bc: two streamed dimension bytes, streamed id table, repeated entity_link calls)
  • FINALIZE_MIXED_VALUE_TO_OUTPTR (000d:2104: emits either immediate frame dword or object-word-derived value)
  • EMIT_OR_PUSHBACK_RESULT (000d:22bc tail: values without 0x0400 marker are pushed back to caller stream before entity_vm_opcode_finish)

Minimal pseudocode-style sketch:

referent = active_referent_id() chain = referent.payload_chain chain = mutate(chain, opcode_0x18_to_0x1b, payload_mode) value = materialize_or_forward(context_from_slot(stream_state)) if opcode_lane == inline_payload: value = prepend_inline_payload_and_build_link_matrix(stream_ids) emit(value)

This remains consistent with descriptor-side evidence: referent-only anchors (JELYHACK/JELYH2) can still drive behavior once neighboring event-capable descriptors attach payload/event semantics to the same referent island.

First readable pseudo-script renderings (verified-only)

entity_vm_context_create_from_slot_index adds one more readable anchor for this IR: after it seeds the embedded mini-VM from the runtime owner table at 0x6611 -> +0x1315/+0x1317 -> (+0x10/+0x12) + 0x0d*slot + 4, it also writes the same far source pair into the per-slot mirror row addressed through 0x39ca[context_slot]. That keeps the current readable model honest: the mirror is part of context creation for slot-backed VM state, not yet a proven standalone descriptor-dispatch cache.

The best verified human-readable form right now is therefore a small family of templates rather than a one-record-equals-one-opcode script dump.

Readable template A: referent anchor with event-bearing attachment (JELYHACK island)

anchor JELYHACK(referent)
anchor JELYH2(referent)

attach REE_BOOT(event, counter, item)
attach SFXTRIG(event)
optional_callback SURCAMEW(eventTrigger, link, code, screen, cameraEgg, trueRef, therma)

vm_effect:
	chain = APPEND_UNIQUE_INLINE(...) or APPEND_UNIQUE_INDIRECT(...)
	chain = REMOVE_MATCHING_INLINE(...) or REMOVE_MATCHING_INDIRECT(...)
	value = MATERIALIZE_OR_FORWARD_VALUE(slot_backed_context)
	if inline_payload_present:
		payload = PREPEND_INLINE_PAYLOAD(caller_blob)
		links = BUILD_ENTITY_LINK_MATRIX(shape_a, shape_b, entity_ids)
	FINALIZE_MIXED_VALUE_TO_OUTPTR(value)

Why this is the current best readable rendering:

  • JELYHACK and JELYH2 remain referent-only sibling descriptors with identical first-16-word header shape in jelyhack_descriptor_compare.tsv.
  • The nearest event-bearing neighbors in jelyhack_island_graph.md are REE_BOOT (event), SURCAMEW (eventTrigger), and SFXTRIG (event), so the readable unit is better modeled as anchor + attachment than as a self-contained JELYHACK event record.
  • The runtime side already supports exactly that shape: one referent anchor can own mutable payload chains, and the 000d:21ed -> 000d:22bc path can expand one inline payload into an entity-link closure before entity_vm_opcode_finish commits the result.

Readable template B: active event hub with trigger-side neighbors (EVENT island)

neighbor ROLL_NS(referent, item, item2, riderList, time, total, counter, oldz, cargo, zCheck, zMax)
attach COR_BOOT(event, counter, item)
attach EVENT(event, item, source, dest, door, link, time, counter, counter2, post1, post2, floor, flicMan)
attach NPCTRIG(event, item, item2, typeNpc)
neighbor CRUZTRIG(referent, item, elev)
neighbor NPC_ONLY(referent, item, link)
neighbor VMAIL(referent, textFile)

vm_effect:
	select referent-bearing neighborhood
	mutate referent payload chain via opcode 0x18..0x1b family
	materialize slot-backed value or inline payload
	if payload carries shape/count bytes:
		build entity-link closure matrix from streamed ids
	emit event-bearing result through shared opcode epilogue

Why this second template matters:

  • event_island_graph.md and event_descriptor_compare.tsv show a compact three-node event-bearing core (COR_BOOT, EVENT, NPCTRIG) embedded inside referent/link/text neighbors, which matches the same anchor/neighbor + attachment runtime model seen around JELYHACK.
  • EVENT is structurally richer than the _BOOT and NPCTRIG satellites, so it reads better as a hub descriptor whose fields parameterize the same VM-side payload-chain and link-matrix machinery rather than as a flat peer row.
  • This is the first point where the binary descriptor artifacts and the 000d VM IR can be rendered together as a readable pseudo-script target without claiming a direct descriptor-id switch that the code still does not prove.

Wrapper mask-family expansion around 0005:2867-2d30

The next gameplay-side wrapper pass now extends well past the three earlier seed wrappers and shows one coherent local mask ladder around entity_vm_context_try_create_masked_for_entity.

Verified wrapper ladder

Address Mask pair Extra pushed value Verified caller / guard notes
0005:27a4 0x0001:0000 none Existing seed. Called from 000c:a09e on the entity +0x5b bit-0x0004 branch.
0005:2867 0x0002:0001 none Calls FUN_0005_2686 first, so the local entity id must be 1..255 when that gate matters. If seg030 helper FUN_0005_ffed reports true, the wrapper only continues when entity_class_get_flag8(local_id) is true or local_id == 1. Called at 000c:8b5b, 000c:8be2, 000c:8d59, 000c:8dec, 000c:9536, 000c:95ed, 000c:9868, and 000c:a007; the 000c:8b5b / 000c:a007 callers then store the returned word into entity field +0x39 before entity_state_tick_dispatch.
0005:2918 0x0020:0005 CONCAT22(param_4,param_3) Sole current caller is 0006:43e5, reached only when caller object word +0x3c == 0x20b; it passes caller fields +0x36/+0x38 as one extra dword before the out pointer.
0005:2ae2 0x0004:0002 none Sole current caller is 0008:023d inside a dispatch-style loop body.
0005:2c06 0x0200:0009 none Adjacent simple wrapper in the same local family.
0005:2c35 0x0400:000a sign-extended word argument Adjacent simple wrapper; assembly pushes one extra sign-extended word before the out pointer.
0005:2c68 0x0800:000b sign-extended word argument Same pattern as 0005:2c35, with one extra sign-extended word operand.
0005:2c9b 0x0010:0004 none Global gate wrapper: returns early unless 0x1056 != 0.
0005:2cd2 0x1000:000c none Adjacent simple wrapper in the same family.
0005:2d01 0x4000:000e none Adjacent simple wrapper in the same family.
0005:2d30 0x8000:000f none Larger gameplay gate. Sets entity class-word bit 0x2000 via `FUN_0005_2745(entity, class_word

Shared preconditions and what they imply

  • This island is firmly gameplay-side, not a descriptor-id switch. The wrappers consume live entity/object far pointers, use the runtime slot mapper at 000d:45c5, and gate on entity-id range, entity class word bits, class-record bytes from 0x7e46, and state bytes such as entity +0x5b, +0x32, and +0x39.
  • The local ladder is not random. The mask pairs now cover 0x0001:0000, 0x0002:0001, 0x0004:0002, 0x0010:0004, 0x0020:0005, 0x0200:0009, 0x0400:000a, 0x0800:000b, 0x1000:000c, 0x4000:000e, and 0x8000:000f, which reads like one sparse owner-side slot taxonomy rather than one-off wrappers.
  • 0005:2918, 0005:2c35, and 0005:2c68 are especially useful because they push extra payload words before the out pointer. That shape fits the current VM model of slot-selected context + caller-provided payload data more naturally than a pure referent-anchor lookup.
  • 0005:2d30 is the strongest new caller-side anchor. Its branch structure is about class/state gating, dispatch-entry emission, and gameplay-object cleanup/state changes before the masked VM call, which is a better behavioral match for active-event or trigger-bearing descriptors than for a passive referent anchor.

Current attribution after the wrapper pass

  • The wrapper family now fits the readable active-event template better than the narrow JELYHACK referent-anchor template. The callers are dominated by gameplay state checks, class-table gating, dispatch-entry emission, and object-state writes; that is closer to EVENT / NPCTRIG / _BOOT style active-event ecosystems than to a record whose only verified descriptor-side field is referent.
  • This does not overturn the existing JELYHACK model. JELYHACK / JELYH2 still fit best as referent anchors that can feed the VM referent registry, while neighboring event-bearing descriptors can attach behavior to the same island.
  • The direct descriptor bridge is still unproven. No code path in this wrapper family reads an explicit EUSECODE class id or a 69:0A00 event versus 24:0A02 eventTrigger tag, so the result stays at ecosystem-level correlation rather than a hard descriptor-class rename.

Concrete caller/xref addendum from the next pass

  • Direct callsites are now pinned for the simpler wrappers: 0005:0292 -> 0005:2c06, 0005:0fee -> 0005:2cd2, 0005:5946/59e9 -> 0005:2c9b, and 0007:814e/822e -> 0005:2d01.
  • The two direct 0005:2d30 callers are now role-shaped as well: 0005:5370 reaches slot 0x0f only after entity_class_has_flag2000 succeeds and class-word bit 0x8000 is clear, while 0005:6f47 reaches the same gate from the complementary branch where class-word bit 0x2000 is still clear before the caller continues into its larger state/update flow.
  • 0005:2c68 is no longer usable as indirect selector evidence. The 0007:e521 and 0007:e73c instruction windows do push 0x2c68 immediately before CALLF 000a:44fd, but decompile now shows that value is the caller-local data pointer DAT_0000_2c68 passed into a fatal-report helper, not an indirect call to wrapper 0005:2c68.
  • 0005:2c35 and 0005:2c68 therefore both remain unresolved in direct caller/xref evidence, and the real selector work stays centered on the still-xref-dark upstream edge into entity_vm_opcode_sequence_run rather than the disproven 000a:44fd hypothesis.
  • Net effect: the active-event ecosystem fit is reinforced by direct caller behavior and payload shapes, but final slot-to-descriptor ownership still requires real caller-role recovery for the remaining xref-dark entry points.

Current batch: masked-context hub and sequencer-internal consumer recovery

  • The generic masked VM-context hub is now instruction-verified at 000d:463a. That body maps the incoming entity through entity_vm_slot_index_from_entity, rejects the path when runtime global 0x6610 is active or the owner/resource table at 0x6611 + 0x1315/+0x1317 is absent, tests the per-slot 0x0d-stride owner mask pair against the caller-supplied high/low mask words, and only then falls into entity_vm_context_create_from_slot_index (000d:46ec).
  • search_instructions on 000d:463a now confirms this hub is not isolated to the 0005 wrapper island. In addition to the known seg021 wrappers, live direct callers now include 0004:f047 (mask 0x8000:0x0007), 0004:f076 (mask 0x2000:0x0015), and larger callers at 0006:0bbc / 0006:10e7. That is new caller-side evidence for the wider owner-slot taxonomy even though the offset-specialized wrappers 0005:2c35 and 0005:2c68 themselves still have no direct caller edges.
  • The xref-dark offset wrappers are now tighter structurally too. Disassembly of 0005:2c35 and 0005:2c68 confirms they do nothing beyond sign-extending one extra word, passing mask pairs 0x0400:0x000a and 0x0800:0x000b, forwarding the entity pointer to 000d:463a, and returning the out-word on success. That keeps their best current reading at offset-specialized masked context creation, not a separate selector lane.
  • The offset word is now behaviorally tighter too. entity_vm_slot_load_value_plus_offset (000d:5572) is a straight entity_vm_slot_load_value(...) + offset wrapper, so the extra word passed by 0005:2c35 is not a second mask or opaque cookie; it is an additive selector/value adjustment that can plausibly choose one of the evenly spaced slot-0x0a clause starts once a real caller is recovered.
  • The next caller-path pass tightens why 0005:2c35 stays dark. MCP xrefs now show only three entries into entity_vm_context_create_from_slot_index (000d:46ac from the generic masked hub, plus direct internal sequencer islands 000d:208b and 000d:21ed), while 0005:2c35 itself still has no recovered code or data xrefs. Stack setup at 000d:208b hardcodes the 000d:5572 additive slot-load parameter to 0, which does not match the NPCTRIG slot-0x0a clause starts (0x0064/0x0093/0x00c2/0x00f1/0x0120) or backward targets (0x00db/0x00ac/0x007d/0x004e/0x001f). The remaining live selector frontier is therefore the still-overlapped 000d:21ed caller frame, not a normal visible caller of 0005:2c35.
  • The sequencer lane also gained two concrete internal consumer shapes. 000d:208b is now the instruction-verified create one slot-backed context and materialize or forward its result path: it builds a 0x6714 context from the caller stream state, writes immediate-flagged results straight to the out pointer, and otherwise forwards the created object through entity_vm_opcode_finish. 000d:21ed is the matching prepend inline payload and build entity-link matrix path: it creates a context, prepends caller-owned bytes into +0x102, consumes the seeded +0xd6/+0xd8 bytes as shape/count metadata, and builds repeated entity_link closures from the following streamed ids before the same finish path.
  • A new downstream-use pass narrows the extra-word role further. The stored offset field at context +0x34 is now confirmed as durable object state rather than an immediate sequencer input: 000d:21ed -> 000d:22bc does not reread it at all, 000d:498f/000d:4a78 serialize and reload it, and 000d:4c2d..4c4d recomputes a slot-backed value from (+0x32,+0x34) into +0x10c/+0x10e. That shifts the remaining immortality question one step downstream: if NPCTRIG slot 0x0a still fits this runtime lane, it is more likely through the value reloaded from the slot-plus-offset pair than through +0x34 as a direct clause selector.
  • The hidden pre-call span in the 000d:21ed lane is now recovered from direct program-memory bytes as well. Window 000d:2131..21ed reads the seeded +0xd6/+0xd8 stream as three successive words followed by two signed bytes: word0 becomes the slot index pushed at 000d:21d4, word1 and word2 are added at 000d:21d0 before being pushed as the dynamic additive arg at 000d:21d3, byte3 is forwarded as the setup-data length byte, and byte4 becomes the inline-blob length used for the later prepend copy. That makes the source classification explicit: context +0x34 is not loaded from the owner table or from the caller object at +0xd4; it is a computed sum of two consecutive words inside the seeded stream itself.
  • The same recovered window also tightens the upstream source layout feeding entity_vm_context_setup. The current caller frame base is caller + [caller+0xd4], where +0xd4 matches the saved frame offset written by entity_vm_stack_push_frame (000c:f7c7) rather than a descriptor-local field. From that frame base, 000d:21db..21e0 pushes [frame+0x0a/+0x0c] as a far pointer passed into entity_vm_context_setup, and 000d:21bd..21c8 separately derives [frame+0x0e] as the inline payload tail copied after context creation. So this consumer is now better modeled as one generic VM frame-record shape with two payload sources: a frame-stored far pointer plus byte-sized setup length for the initial +0xcc stack seed, followed by an adjacent inline tail blob with its own byte-sized length.
  • The next frame-producer pass recovers the closest non-overlapped writer feeding that lane too. Raw bytes at 000c:fbf7..fc47 (caseD_0) show a generic frame-record producer reading one signed placement byte from the same seeded +0xd6/+0xd8 stream, popping a far-pointer dword from the caller stream at [caller+0xcc/+0xce], computing frame_base = caller + [caller+0xd4], and storing the dword at [frame_base + placement + 0x4/+0x6]. That means the immediate source far pointer consumed later by 000d:21ed is already stream-backed rather than owner-row-backed; if the 000d:21ed record uses this exact producer family for its [frame+0x0a/+0x0c] lane, the relevant placement byte is 0x0006, which is the only value that lands the written dword at +0x0a/+0x0c and leaves the inline tail starting at +0x0e.
  • That stronger runtime shape weakens any claim that 000d:21ed is already reading a descriptor-family-specific record. NPCTRIG slot 0x0a still remains the best surviving descriptor-side candidate because its five-clause ladder is the only compact body that fits the row-count frontier, but the code evidence now shows the immediate input to 000d:21ed is a generic frame-local record containing a source far pointer, a seeded slot/additive pair, and an inline tail. The remaining descriptor-side question is therefore one level earlier again: where the caller frame receives its [frame+0x0a/+0x0c] far pointer and whether the summed add_a + add_b still corresponds to a clause-base/delta pair inside NPCTRIG slot 0x0a rather than to a more generic descriptor-relative offset.
  • That changes the NPCTRIG cross-check in one important way. NPCTRIG slot 0x0a remains the strongest surviving descriptor-side hypothesis only as an upstream source for a predecoded caller-stream record, because the recovered writer consumes a caller-stream dword plus a seeded placement byte instead of indexing owner rows or descriptor tables directly. NPCTRIG slot 0x20 still reads as the typed/setup companion body, but neither slot is now a good fit for the immediate write into [frame+0x0a/+0x0c] itself.
  • One more layer of the producer path is now instruction-verified too. The setup call at 000d:4788 -> 000c:f844 -> 000c:f6e8 does not seed the new context's +0xcc/+0xce caller stream directly from the owner table row. Instead entity_vm_context_setup first allocates or reuses the object-local stream buffer at context+0x36+0xcc, then copies a caller-supplied setup blob from the parent frame using the far pointer/length arguments passed through 000d:46ec. The slot/additive record returned by entity_vm_slot_load_value_plus_offset becomes the separate seeded +0xd6/+0xd8 stream, while the owner-table row at (+0x10/+0x12) + 0x0d*slot + 4 is mirrored to 0x39ca[slot] and preserved separately in the context state.
  • The closest sibling template to caseD_0 also sharpens the placement-byte reading. 000c:ff9f..000d:000d reads one signed placement byte and one length byte from the same seeded +0xd6/+0xd8 stream, then copies len bytes from [frame_base + placement + 0x4] back onto the caller stream. Together with the recovered 000d:21ed consumer layout ([frame+0x0a/+0x0c] far ptr, [frame+0x0e..] inline tail), that makes the strongest current fit a fixed two-slot family for this record shape: caseD_0 uses placement 0x0006 for the far-pointer dword, and the sibling blob-copier uses placement 0x000a for the inline tail starting at frame+0x0e.
  • The producer side of that same record family is now tighter too. Linear raw-byte recovery across 000c:f98b..000d:000d shows 000c:fc4b..fcbb as the forward blob producer matching the reverse 000c:ff9f..000d:000d case: it reads placement and length from the seeded +0xd6/+0xd8 lane, computes frame_base = caller + [caller+0xd4], and copies len bytes from the caller stream at [caller+0xcc/+0xce] into [frame_base + placement + 0x4]. For the 000d:21ed record shape, that makes placement 0x000a the best fit for the inline tail now consumed from [frame+0x0e..].
  • The dword lane now has a matching reverse case as well. Raw bytes at 000c:ff1f..ff83 show the same recursive family in the opposite direction: it reads one signed placement byte from the seeded +0xd6/+0xd8 lane, computes frame_base = caller + [caller+0xd4], loads a dword from [frame_base + placement + 0x4/+0x6], subtracts 4 from [caller+0xcc], and writes that dword back onto the caller stream. In other words, the immediate upstream producer for the 000c:fbf7..fc47 far-pointer write can already be another frame-record copier, not a direct owner-row or descriptor-table lookup.
  • That narrows the remaining source classification again. The setup far pointer consumed by 000d:21ed is now best modeled as a recursively propagated pointer into another VM-side byte buffer or predecoded descriptor workspace, not as the owner/resource row source mirrored separately through 0x39ca. The owner row still matters for slot-backed state reloads, but the entity_vm_context_setup blob pointer itself is traveling through the frame-record family independently of that owner-row mirror.
  • That also weakens the full-tuple NPCTRIG fit one more notch without killing it. The surviving tuple is now better read as (slot, add_a, add_b, setup_len, inline_len, placement=0x0006/0x000a) feeding a generic recursive frame-record contract. NPCTRIG slot 0x0a remains the strongest descriptor-side candidate only as an earlier decoder that could have produced this predecoded record family, while slot 0x20 still reads as the typed/setup companion body. No recovered instruction in the immediate 000c:f98b..000d:000d family yet ties the setup far pointer directly back to either slot.
  • Net effect on source classification: the 000d:21ed-relevant frame record is still not best modeled as generic VM scratch. Its immediate setup bytes are recursively copied from a parent frame record, and the wider context-build path is still anchored in descriptor-derived VM state (+0xd6/+0xd8 from entity_vm_slot_load_value_plus_offset, owner-row source mirrored via 0x39ca). What remains open is not whether this lane is scratch-backed, but which earlier decoder materializes the parent-frame far pointer before 000c:fbf7 consumes the next dword.
  • After the new reverse-case recovery, that blocker can be stated more tightly: the missing piece is no longer a generic parent-frame materializer somewhere above 000c:fbf7, but the first non-recursive decoder that originates the far pointer before the ff1f/ff9f -> fbf7/fc4b -> 000d:21ed propagation chain repeats it.
  • The next pass closes that specific source-classification gap inside the same hidden interpreter body. Raw bytes at 000c:fa2f..fa5b recover an inner opcode dispatcher that reads one opcode byte from the seeded +0xd6/+0xd8 lane, bounds-checks it against 0x79, and jumps through CS:[0x3d9f + opcode * 2]. That matters because the same local case family now exposes both the recursive frame-record replay stages and a separate set of direct caller-stream seed cases.
  • Those non-recursive seed cases are now concrete. 000c:fd51 writes one inline byte from the +0xd6/+0xd8 control stream onto the caller stream after decrementing [caller+0xcc] by 1, 000c:fd91 and 000c:fdd1 do the same for inline words, and 000c:fe11..fe59 does it for an inline dword. In the dword case the interpreter advances through four literal bytes in the control stream, subtracts 4 from [caller+0xcc], and writes the literal dword directly onto the caller stream before any frame replay logic runs.
  • That makes 000c:fe11 the strongest current first non-recursive origin for the far-pointer lane later consumed by 000c:fbf7..fc47 and then by 000d:21ed. The immediate setup far pointer is therefore no longer best modeled as coming from the owner/resource row, the mirrored 0x39ca lane, or a generic VM scratch buffer. Its immediate compiled-side source is an inline dword literal embedded in the interpreter/control stream itself; 000c:ff1f..ff83 and 000c:fbf7..fc47 are replay stages layered on top of that literal-seeding path.
  • That retunes the NPCTRIG cross-check again without killing it. NPCTRIG slot 0x0a still remains the best upstream descriptor-side candidate because it is still the only compact active-event body that fits the surviving slot/additive shape, and slot 0x20 still reads as the typed/setup companion. But any direct immortality mapping now has to explain how the upstream decoder turns that descriptor family into a literal-bearing VM control stream before 000c:fe11, not how 000d:21ed or 000c:fbf7 index descriptor rows directly.
  • One more pass tightens the creator/consumer split enough to rule out the owner row as the immediate control-stream builder. Direct instruction recovery at 000d:46ec shows entity_vm_context_create_from_slot_index using the owner-table row (+0x10/+0x12) + 0x0d*slot + 4 only for the separate 0x39ca[slot] mirror, while the live +0xd6/+0xd8 lane passed into entity_vm_context_setup still comes from entity_vm_slot_load_value_plus_offset. In the recovered 000d:21ed pre-call span, that seeded lane is consumed as word slot_index, word add_a, word add_b, byte setup_len, byte inline_len, with add_a + add_b forwarded as the dynamic word stored at context +0x34.
  • The same pass also clarifies the setup-payload contract that feeds the later link-matrix stage. 000d:21ed passes [frame+0x0a/+0x0c] as the setup far pointer into entity_vm_context_setup, copies [frame+0x0e..] as a separate inline tail, and then 000d:22bc consumes two signed metadata bytes plus a streamed word matrix to drive repeated entity_link calls. The immediate source is therefore decoded per-slot VM stream + frame replay, not owner-row lookup + direct descriptor row.
  • That changes the opcode-family reading around 000c:fa2f in a useful way even though the exact opcode indices remain unresolved in the current overlapped table view. The hidden dispatcher now has a verified immediate-literal family: 000c:fd51 pushes one inline byte to the caller stream, 000c:fd91 pushes a sign-extended byte as a word, 000c:fdd1 pushes an inline word, and 000c:fe11 pushes an inline dword. Together with the recursive replay cases 000c:ff1f and 000c:ff9f, that is enough to classify the upstream builder as a generic literal-bearing interpreter/control stream rather than a direct NPCTRIG clause reader.
  • The descriptor-side fit therefore weakens from specific direct NPCTRIG selector to broader descriptor-derived VM workspace while staying narrow enough to keep NPCTRIG slot 0x0a alive as the best upstream candidate. Slot 0x0a still matches the event-bearing compact body and its five-clause ladder remains the only surviving compact source family with a plausible row-count/additive shape, but slot 0x20 still looks like the typed/setup companion and neither slot is now a good fit for the immediate control-stream seeding logic itself.
  • The slot-load miss path now closes the workspace-materialization side of that question. Inside entity_vm_slot_load_value (000d:51fd), a cache miss triggers 000d:5066, which first reads a slot header and then a count * 6 + 0xc0 subentry table through the owner-resource wrapper 000d:714c. When one subentry is still unloaded, 000d:5305..53d4 allocates a value object through 000d:3800, then calls 000d:714c again with the subentry source range and the new object's buffer at +0x0a/+0x0c; the function returns that same buffer pointer as the final DX:AX result. The immediate +0xd6/+0xd8 workspace is therefore first materialized as a file-backed slot-value buffer during the slot-load miss path itself, not synthesized later from the owner-row mirror or from generic scratch state.
  • The inline-tail source is not as tightly closed yet. The same hidden case family contains several immediate scalar caller-stream seed cases, so the 000d:21ed tail at [frame+0x0e..] can now plausibly be assembled from control-stream literals or from another nearby non-recursive payload case rather than from a direct owner-row read. No instruction recovered in 000c:f98b..000d:000d performs a matching direct descriptor-row lookup for that tail.
  • Net effect from this pass: the missing outer selector into entity_vm_opcode_sequence_run is still unresolved, but the lane is no longer just one opaque dispatcher plus dark wrappers. It now has a verified generic masked-context creation hub, wider caller-family anchors for that hub, and two internally differentiated sequencer consumer blocks built directly on entity_vm_context_create_from_slot_index.

Follow-up: four newly surfaced direct 000d:463a callers

  • 0004:f033 (0x8000:0x0007) now reads as a generic gameplay-side materialization lane rather than a state-transition helper. When the local seg021 class-nibble query returns 8, the wrapper bypasses the VM path and returns object word +0x02 directly from the locally produced object. Otherwise it forwards through entity_vm_context_try_create_masked_for_entity and returns the created object's word +0x02 on success.
  • 0004:f05c (0x2000:0x0015) stays on the gameplay-state side too, but with a stronger caller role. The only current direct caller window at 0004:f2b3 reaches it after overlap/proximity tests and entity byte +0x32 toggling, so the safest reading is still stateful gameplay materialization lane, not descriptor selector.
  • entity_vm_context_try_create_mask_0008_slot30_with_offset (0006:0ba4) adds the first strong non-0005 extra-payload lane. It passes mask 0x0008:0x0030 plus one caller word into 000d:463a; on failure it drops into 0006:0cfa, which copies class-detail word +0x02 to +0x04, derives a replacement selector from class-detail words +0x06/+0x08/+0x0a or the caller value, may clear flag 0x08 through entity_class_clear_flag8_and_dispatch, and then continues into the local state-transition/dispatch table. That is concrete evidence that at least one extra-word masked lane is feeding class-state transition materialization rather than a free-standing VM selector root.
  • entity_vm_context_try_create_mask_0010_slot08_with_offset_if_ready (0006:108c) provides the second strong extra-payload lane. It passes mask 0x0010:0x0008 plus one caller word into 000d:463a, but only after local readiness gates through 0006:ffed plus the seg021 availability/flag8-clear path. Unlike the earlier looser reading, the helper itself does not fall back to 0006:13b0 or 0006:13e4; on miss it simply returns 0. That makes the function a guarded masked-materialization attempt, while the neighboring 0006:13b0/13e4 -> 0006:07c0 class-linked lookups remain adjacent family evidence rather than a direct local fallback inside 0006:108c.
  • Taken together, the new seg004 and seg006 callers strengthen the existing read of the still-dark wrappers 0005:2c35 (0x0400:0x000a) and 0005:2c68 (0x0800:0x000b). Those wrappers still have no direct caller evidence, but they now sit inside a larger verified subfamily of extra-word masked materializers whose known members feed state selectors, class-linked values, or other gameplay-side payload resolution instead of acting as the real upstream selector into entity_vm_opcode_sequence_run.
  • MCP-native function xrefs now reinforce that stopping point rather than changing it: entity_vm_context_try_create_masked_for_entity reports the expected direct callers through 0004:f047, 0004:f076, the named 0005 wrapper island, and the two seg006 callsites 0006:0bbc / 0006:10e7, while entity_vm_opcode_sequence_run plus the dark 0x0400/0x000a and 0x0800/0x000b wrappers still surface no direct function-xref callers in the current database. The best next path therefore remains caller-frame recovery or nearby unnamed-function repair, not another generic masked-hub sweep.

| 000c:f844 | entity_vm_context_setup | Calls entity_vm_stack_init_with_data, then sets +0xd6..+0xe3 with position/dimension/state params | | 000c:f600 | entity_vm_pair_stack_push | Push (word_a, word_b) onto 31-entry array at [ptr+0x80] (count); error if full | | 000c:f63c | entity_vm_pair_stack_pop | Pop and return word from pair stack; error if empty | | 000c:f94f | entity_vm_counter_add | [ptr+0xd6] += param_2; simple accumulator | | 000c:f95f | entity_vm_set_value_from_slot_plus_offset | Calls entity_vm_slot_load_value_plus_offset and writes the resulting 32-bit value into [ptr+0xd6/+0xd8] | | 000c:f98b | entity_vm_set_field_da_to_global | Writes [param_2+0xda far-ptr + 2] into [0x8c94] |

VM field offsets: +0xcc=VM stack ptr, +0xce/+0xd0=segment regs, +0xd2=base, +0xd4=frame depth, +0xd6/+0xd8=32-bit VM value/counter lane, +0xda/+0xdc=additional VM pointer/bounds lane. The 200-byte body region at [ptr+4..+0xcc] holds 100 words of script/state payload. The pair-stack (field +0x80) is separate — likely pass/return value stack for the mini-script.


Raw 000c Cursor Zone / Slot Array / String Queue Batch

Cursor / Directional Zone Classifier

Address Name Evidence
000c:e6d9 cursor_zone_quadrant_classify Splits screen by [0x63d6]/2 and [0x63d8]/2 vs bounds [0x8c6c..0x8c72]; returns directional code from 9-word table at 0x6401

Zone table layout (9 entries): NW/N/NE / W/Center/E / SW/S/SE based on horizontal threshold at 0x8c6c/0x8c70 and vertical at 0x8c6e/0x8c72.

Slot Array System

A complete 29-slot menu/choice array with fixed stride 0x15 bytes, base at [ptr+0x67], count at [ptr+0x7a]:

Address Name Evidence
000c:ea53 entity_slot_count_update_and_notify Sets [ptr+0x72]=param-1; calls slot_array_get_current_entry and slot_array_find_and_dispatch; calls vtable[0]() when +0x75 flags set
000c:eba5 slot_array_dispatch_matching Walks 0xb-stride array from [ptr+4]; calls thunk for each entry where [entry+9]==param_4
000c:ec30 slot_array_dispatch_if_nonempty Returns 0xffff if count < 1; else dispatches
000c:ec9e slot_array_find_and_dispatch Searches 0xb-stride array for [entry+9]==param_4; calls thunk on first match
000c:ecf5 slot_array_push_entry Copies named string to [base+0xc]; writes 6 param words at +0x12..0x20; increments count
000c:edb0 slot_array_push_raw Copies 0x15-byte raw entry from param_2; increments count
000c:edf7 slot_array_pop Decrements [ptr+0x7a]; asserts >= 0
000c:ee19 slot_array_init Sets [ptr+0x78]=0, [ptr+0x76]=0, [ptr+0x75]=1 (active flag)
000c:ee32 slot_array_clear_flags Clears [ptr+0x74]=0, [ptr+0x75]=0
000c:ee44 slot_array_get_current_entry Returns ptr + [ptr+0x7a]*0x15 + 0x67 (current entry ptr); 0 if count <= 0

String Queue

Address Name Evidence
000c:eadd string_queue_push Appends string to 10-entry queue at [ptr+4]; count at [ptr+2]; sets [ptr+0xd]=param_4

Additional VM-Adjacent Helpers

Address Name Evidence
000c:f2e7 entity_call_vtable_entry_10_if_valid Null-guard: calls (*[ptr+8+0x10])() if param_1 non-null
000c:f39f string_table_lookup Searches [0x65bc/0x65be] table by key string; returns matching words to out-params

Raw 000c Cursor Nav Dispatcher / State Reset Batch

Cursor navigation subsystem in 000c:d3e9000c:db68. Manages directional zone changes, mouse button reads, keyboard scancodes, and entity vtable dispatch for interactive UI cursors.

Cursor Navigation Fields (entity object offsets)

Offset Purpose
+0x32 Current zone code (08)
+0x33 Previous zone code
+0x37+0x3a Directional booleans: N/S/W/E
+0x3f+0x42 Mouse button flags
+0x45 Last keyboard scancode
+0x47 Navigation index

Globals: [0x63da] = mouse button state, [0x63d6]/[0x63d8] = cursor X/Y, [0x638e] and [0x6346] = reference data tables.

Address Name Evidence
000c:dac1 cursor_nav_state_reset Zeros all directional/button flags; sets [+0x32/+0x33]=0xff, [+0x47]=0xffff