Crusader_Decomp/docs/raw-0008-000c.md
MaddoScientisto de42fd1ea1 Add Crusader-specific USECODE data and documentation
- Introduced new file `vm_mask_ladder.tsv` containing detailed mappings for Crusader USECODE VM masks and their associated descriptors.
- Added comprehensive documentation in `scummvm-crusader-reference.md` outlining the structure, findings, and implications for reverse-engineering the Crusader engine within ScummVM.
- Created `usecode-roundtrip-ir.md` to document the plan for converting Crusader USECODE bytes into a human-readable format, detailing the container layout, event names, and intrinsic tables.
- Implemented a PowerShell script `temp_usecode_sample.ps1` for extracting and analyzing USECODE data from the Crusader FLX files, providing insights into class and event structures.
2026-03-22 17:26:39 +01:00

540 lines
52 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 `[DAT_0000_6828 ptr + 0x40] = 1` then calls thunk unconditionally; enables global entity flag |
| `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` | `entity_state_check_field49_and_call_vfunc3c` | Checks field `[ptr+0x49]`: 1→reset to 0 return 1; 2→call `vtable[0x3c]` return 0; else thunk dispatch |
| `000c:b153` | `entity_state_animation_done_tick` | Checks `[param_2+0x14+0xa]` animation-complete flag; if zero increments `field49` and calls `entity_state_check_field49_and_call_vfunc3c`; if set calls `vtable[0x3c]` |
| `000c:b199` | `entity_state_input_key_handler` | Full input dispatcher: ESC/x/X → `vtable[0x3c]` (cancel); Left/Right arrows `0x14b/0x148` → prev state; n/N/`0x14d/0x150` → next state; e/E → set `field47=1`; `-` with counter → trigger at 4. Manages `field47` and `field49` |
| `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` = state-sequence index; 0=reset, 2=vtable callback, 4=triggered end
- `field47` = keystroke-combo counter
- `field3f` = linked data pointer (event/record reference)
- `[0x6054]` = current entity handle; `[0x6828]` = another global entity far pointer
- 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_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`). 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.
- 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.
- 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 narrower now too. Fresh windows at `0008:709c/70cb`, `0008:7309/7338`, and `0008:85f9/8617` show only global base-pointer save/restore and allocation/zeroing of the `0x39ca:0x39cc` table itself. The only verified per-slot row writer in this lane remains `entity_vm_context_create_from_slot_index` (`000d:46ec`), which writes `0x39ca[context_slot] = {source_off, source_seg}` after it derives the slot-backed payload source.
- 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 byte-sized metadata fields shape the matrix walk and word entries are link/entity ids.
- 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.
### FUN_000d_ebe3 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`).
- `FUN_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`.
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:
- Direct CALL xrefs into `FUN_000d_ebe3` are now confirmed from `animation_ctor_variant_a/b/c` at `000e:283e`, `000e:2931`, and `000e:29e4`, so the entry is no longer globally xref-dark.
- Those constructor callsites still do not expose a new concrete wrapper-level opcode number or the direct write/read path for `[BP-0x32]`; no additional opcode id can yet be assigned uniquely beyond the internal `0x19/0x1a/0x1b` family already proven inside `000d:0988`.
### 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)
```text
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)
```text
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 | 0x2000)`, checks class-record bits through `FUN_0005_32a8` / `FUN_0005_32d2` (byte `+0` or `+6` bit `0x10` in the `0x7e46` class table), rejects some seg030 classes unless ids `0x576/0x596/0x59c/0x58f` match, branches on `FUN_0005_11c4` class nibble values `4`, `7`, and `8`, may emit dispatch entry `0x0f16` / event type `0x20f` through `FUN_0004_f08b`, and only then attempts the masked VM context. Current direct callers are `0005:5370` and `0005:6f47`. |
#### 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`.
- `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 `FUN_000d_ebe3` 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.
| `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` |