- 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.
540 lines
52 KiB
Markdown
540 lines
52 KiB
Markdown
# 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 (R−offset, G−offset, B−offset) 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:d3e9–000c: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 (0–8) |
|
||
| `+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` |
|