605 lines
84 KiB
Markdown
605 lines
84 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 `[g_active_dispatch_entry_farptr + 0x40] = 1` then calls thunk unconditionally; current evidence treats this as raising the shared active-entry transition/display hold byte rather than toggling an unrelated global |
|
||
| `000c:ac55` | `entity_state_fire_if_handle_valid` | Guard: fires thunk dispatch only when `[0x6054] != -1`; no-op otherwise |
|
||
| `000c:ac6d` | `entity_state_fire_with_args_if_handle_valid` | 3-arg variant: pushes `[BP+0xe]` (byte), `[BP+0xc]`, `[BP+0xa]`, handle `[0x6054]`, then `CALLF 0000:ffff` |
|
||
| `000c:afa5` | `transition_file_family_select_and_refresh` | Local startup/display selector: `field49==-1` normalizes to `0`; `field49==2` dispatches `vtable[0x3c]`; `field49==0/1/4` composes one of three sibling filenames from inherited base `0x6aa:0x6ac` plus stem/suffix buffers `0x621c/0x6223`, `0x621c/0x622d`, or `0x621c/0x6237`, loads the result into object `+0x520`, then runs the shared redraw/palette/input refresh path |
|
||
| `000c:b153` | `transition_file_family_advance_on_anim_tick` | Polls `[param_2+0x14+0xa]`; when clear increments `field49` and re-enters `transition_file_family_select_and_refresh`, otherwise exits through `vtable[0x3c]` |
|
||
| `000c:b199` | `transition_file_family_input_key_handler` | Local selector key handler: ESC/x/X → `vtable[0x3c]`; Left/Right arrows `0x14b/0x148` → previous file-family state; n/N/`0x14d/0x150` → next state; e/E arms `field47`; `-` after arming counts up to forced state `4`; selector moves drain the event queue and clear `0x8a94/0x8a96/0x8a98` |
|
||
| `000c:b2c3` | `stub_noop_000c_b2c3` | Empty stub; returns immediately |
|
||
| `000c:b2c8` | `entity_state_dispatch_if_field49_eq4` | Fires thunk only when `[ptr+0x49]==4` |
|
||
| `000c:b349` | `entity_state_dispatch_if_far_ptr_nonzero_a` | Fires thunk if far-pointer args non-zero |
|
||
| `000c:b383` | `entity_state_set_field3f_and_dispatch` | If non-NULL: writes `&DAT_0000_2d18` to `[ptr+0x3f]`, then dispatches |
|
||
| `000c:b3d8` | `entity_state_dispatch_if_far_ptr_nonzero_b` | Same null-guard pattern as `b349`, variant b |
|
||
|
||
**Patterns confirmed:**
|
||
- `field49` = local transition file-family selector state in this startup/display family; `0/1/4` choose sibling filenames under shared base `0x6aa:0x6ac` plus stem `0x621c`, `2` dispatches `vtable[0x3c]`, and `-1` normalizes back to `0`
|
||
- `field47` = keystroke arm/counter for the local `e/E` then `-` path into selector state `4`
|
||
- `field3f` = linked data pointer (event/record reference)
|
||
- `[0x6054]` = current entity handle; `[0x6828]` = `g_active_dispatch_entry_farptr`, the shared active-dispatch entry owner whose byte `+0x40` is reused across the startup/display lane as a hold/busy token
|
||
- Bits in `[ptr+0x5b]`: `0x1=init`, `0x2=active/event`, `0x40=pending dispatch`, `0x100=flag100`, `0x180=skip-all mask`
|
||
|
||
---
|
||
|
||
## Raw 000c Palette Fade + Entity VM Cluster
|
||
|
||
### VGA Palette Fade
|
||
|
||
| Address | Name | Evidence |
|
||
|---------|------|---------|
|
||
| `000c:cdde` | `palette_fade_step_down` | Writes (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_opcode_sequence_run` (`000d:ebe3`) is now named conservatively in Ghidra: it seeds the stage chain from object `+0xfe`, runs `000d:177c -> 000d:1acb -> 000d:0988 -> 000d:22bc -> optional 000d:1d4a -> 000d:2104`, then finishes with tracked-handle cleanup plus the `0008:ebe7` gate on object `+0xc0` and byte `+0x4b`
|
||
- `entity_vm_context_sync_global_value_and_dispatch` (`000d:48da`) is the current context-side runner/sync point: it marks the context busy at `+0x123`, calls `entity_vm_set_field_da_to_global`, optionally writes the current value through `+0x11b/+0x11d`, and dispatches through the context vtable on success
|
||
- `entity_vm_context_save` / `entity_vm_context_load` / `entity_vm_context_destroy` / `entity_vm_context_free_buffer` (`000d:498f`, `000d:4a78`, `000d:4962`, `000d:48b6`) now pin down the lifecycle of this object family rather than leaving the whole `000d:45xx..4exx` island anonymous
|
||
- `entity_vm_context_try_create_masked_for_entity` is now better constrained at the return-value level too: after the runtime-disable check at `0x6610` and the owner-side slot-mask test succeed, it reports two distinct success shapes. Immediate-flagged contexts (`+0x16 & 0x0008`) clear the caller output word, while object-backed contexts return the created object's low word. That makes the helper a typed bridge from gameplay entities into VM-backed object results, not only a yes/no mask probe.
|
||
- `entity_vm_runtime_owner_resource_create` (`000d:7000`) is now one step tighter too: the embedded seg069/070 helper is file-backed rather than abstract. Construction starts with `dos_file_handle_init` (`0009:1c00`), then uses helper vtable slot `+0x04` as the size query that drives the child `+0x10/+0x12` allocation and helper vtable slot `+0x0c` as the table-population callback for the `0x0d`-stride owner table.
|
||
- That file-backed helper is now tighter one step deeper as well. The seg070 loops rooted at raw windows `0009:67b6` and `0009:6916` walk helper-owned record arrays at object `+0x10/+0x18`, format per-entry paths through the seg001 string helpers (`0003:e4d3` / `0003:e590`), then open, read, and close each file through `file_handle_alloc_init_and_open` (`0009:1c3a`), `dos_file_seek` (`0009:2034`), and `dos_file_close` (`0009:1e61`). The paired `+0x18` entries are consumed as 16-bit ids passed into those path-format loops beside the far-pointer path table at `+0x10`; no object-1 or `classid + 2` arithmetic appears there, so the safest current read is slot-local file ids rather than exposed original class/object indices. That is strong evidence that `000d:7000` seeds the owner table from an indexed external file set rather than by copying one monolithic in-memory descriptor blob.
|
||
- A final loader-side tightening from the current pass is that `0009:67b6` and `0009:6916` now read as paired file-family walkers rather than one isolated path-format callback. Both windows iterate the helper-owned count at `+0x14`, index the far-pointer path table at `+0x10` and paired 16-bit id table at `+0x18`, check the source path through `0003:e669`, build formatted paths with distinct local format strings (`DS:3f2d` vs `DS:3f40`), and then reach the same file open/read/close lane. Each loop also writes into its own independently allocated output far buffer before the shared trailer runs, so the best current reading is two parallel file families or record banks loaded by the same helper rather than two phases over one shared buffer. The remaining open question is the exact per-family record schema and higher-level resource role, not whether the helper is file-backed.
|
||
- The caller-side bootstrap for that helper is now anchored too: `entity_vm_runtime_init_from_path_if_configured` (`000d:44df`) first checks the configured byte/string global at `0x65a`, builds a path through seg072 helper `0009:3600` using globals `0x6d6:0x6d8` plus `0x65a`, validates that path through `000a:500a`, then calls `entity_vm_runtime_create(0,0,path)`. This is the first verified source-argument path for `entity_vm_runtime_owner_resource_create`, and it strongly suggests the owner/resource table is loaded from an external configured file rather than from a purely in-memory descriptor blob.
|
||
- Seg072 helper `0009:3600` is now classified more tightly as a rotating slash-aware path composer rather than a generic buffer advance helper. Its prologue cycles through five `0x50`-byte temp buffers, and its inner cases append optional string parts while inserting `\` only when adjacent path components need a separator. That narrows the two globals used by `000d:44df`: `0x65a` behaves as the configured relative runtime-owner filename/path component, while `0x6d6:0x6d8` behaves as the mutable base/resource-root path buffer that gets joined with `0x65a` before `000a:500a` validation.
|
||
- The two still-xref-dark wrappers `0005:2c35` and `0005:2c68` are also narrower now. Their signed extra word does not participate in owner-mask selection inside `entity_vm_context_try_create_masked_for_entity`; it is forwarded into `entity_vm_context_create_from_slot_index`, stored in context field `+0x34`, and passed on to `entity_vm_slot_load_value_plus_offset`. The best current reading is therefore `offset-specialized masked context creation`, not a separate direct selector lane.
|
||
- Ghidra now records that signed-offset contract directly in the wrapper names too: `0005:2c35` = `entity_vm_context_try_create_mask_0400_slot0a_with_offset` and `0005:2c68` = `entity_vm_context_try_create_mask_0800_slot0b_with_offset`. That still stops short of real caller-role recovery, but it removes the last ambiguity about whether the extra stack word is semantically live.
|
||
- The first opcode-level behavior split inside that runtime is now visible in the `000d:0988` family:
|
||
- one branch calls `entity_vm_referent_chain_append_unique_from`, which looks like an attach/union operation on the current referent payload chain
|
||
- the `0x1a/0x1b` branch instead calls `entity_vm_referent_chain_remove_matching_from`, which looks like the inverse operation and makes the opcode family materially closer to a graph-editing script VM than a flat event list
|
||
- both paths return through `entity_vm_opcode_finish`, so the referent-global write to `0x8c94` is now better understood as a shared interpreter epilogue rather than a unique quirk of one helper
|
||
- One additional runtime layer is now named under that context family: the referent registry at `0x8c8c/0x8c8e/0x8c90/0x8c94`.
|
||
- `entity_vm_referent_registry_init` / `entity_vm_referent_registry_destroy` (`000d:6000`, `000d:60bf`) allocate and free the registry buffer, seed the free-list/root metadata, and clear the current referent id at `0x8c94`
|
||
- `entity_vm_referent_registry_alloc` (`000d:613e`) allocates one registry node from the free list and stores the current referent id from `0x8c94` into node field `+0x04`
|
||
- `entity_vm_referent_registry_release_by_id` / `entity_vm_referent_registry_free_node` (`000d:6251`, `000d:62ac`) release all live nodes for one referent id and coalesce adjacent free nodes
|
||
- this makes `entity_vm_set_field_da_to_global` more important than it first looked: it writes `0x8c94` from the current context `+0xda` lane and then immediately enters the still-misaligned `000c:3350` body, so the referent selected by the context is now visibly feeding runtime registry state
|
||
- the registry nodes are not flat scalars only; the surrounding container helpers are now named too:
|
||
- `entity_vm_referent_chain_copy` / `entity_vm_referent_chain_append_unique_from` (`000d:6694`, `000d:68c3`) build deep-copied or deduplicated chains of referent-linked payloads
|
||
- `entity_vm_referent_chain_destroy`, `entity_vm_referent_chain_next`, and `entity_vm_referent_chain_append_node` (`000d:6602`, `000d:6651`, `000d:687b`) provide the list management shell
|
||
- `entity_vm_referent_chain_contains_entry`, `entity_vm_referent_chain_get_entry_data_at`, and `entity_vm_referent_chain_get_indirect_data` (`000d:6c31`, `000d:67f2`, `000d:6860`) show that some chains carry fixed-size inline payloads while others carry indirect string-like payload nodes
|
||
- This matters for script readability: the current runtime model is no longer only "one referent id hits one event." It now supports a more useful intermediate representation where one referent anchor can own one or more payload chains, and neighboring event-bearing descriptors can attach behavior to that anchor without duplicating the anchor's own record.
|
||
- Nearby descriptor work on `EUSECODE.FLX` is consistent with that model: event-bearing classes (`EVENT`, `NPCTRIG`, `SFXTRIG`, several `*_BOOT` records) use a stable `69:0A00 -> event` tag, while `JELYHACK` / `JELYH2` remain referent-only descriptors in a neighborhood that includes `TRIGPAD`, `SPECIAL`, `REE_BOOT`, `SURCAMEW`, and `SFXTRIG`.
|
||
- The strongest current callsites into this context-construction path are the large `000d:208b` and `000d:21ed` bodies, which both feed per-object stream/data state from `+0xcc/+0xce` into `entity_vm_context_create_from_slot_index` before continuing bytecode-style reads from the newly seeded `+0xd6/+0xd8` lane. That makes the `000d` interpreter/object lane a better current immortality target than the older `000e` text-parser hypothesis.
|
||
- The immediate producer chain for that `+0xcc/+0xce` stream state is now one layer tighter:
|
||
- `entity_vm_context_create_from_slot_index` (`000d:46ec`) allocates the `0x6714` context, then calls `entity_vm_context_setup` (`000c:f844`) on the embedded mini-VM object at context `+0x36`.
|
||
- `entity_vm_context_setup` delegates to `entity_vm_stack_init_with_data` (`000c:f6e8`), which seeds `[mini_vm+0xcc..+0xd2]` to point into the object's own payload area and optionally prepends caller-owned inline bytes by moving the stack pointer backward.
|
||
- `entity_vm_state_copy` (`000c:f772`) copies that same `+0xcc..+0xd2` stream/base quartet verbatim when one mini-VM object is cloned.
|
||
- Upstream of the setup helper, `000d:46ec` derives the source payload from the runtime owner table behind `0x6611 -> +0x1315/+0x1317`: with slot index `SI`, it walks owner table `*(owner+0x10/+0x12) + 0x0d*SI + 4`, passes that far pointer into `000c:f844`, and mirrors the resulting per-slot source into `0x39ca[slot]`.
|
||
- This sharpens the current JELYHACK-side model rather than overturning it: the code-side producer recovered in this batch is still a generic slot-backed VM source object keyed by gameplay-entity slot selection and owner-side mask bits, not a direct hard-coded descriptor-class switch on `JELYHACK` or `JELYH2`. Combined with the extractor evidence that `JELYHACK` / `JELYH2` remain referent-only while `REE_BOOT` / `SFXTRIG` keep active `event` tags and `SURCAMEW` keeps `eventTrigger`, the better fit is still `referent anchor -> slot-backed payload chain -> neighboring event-bearing attachment`.
|
||
- The `0x39ca` mirror question is now split more cleanly. Fresh windows at `0008:709c/70cb`, `0008:7309/7338`, and `0008:85f9/8617` still show only global base-pointer save/restore and allocation/zeroing of the `0x39ca:0x39cc` table itself, but two additional per-slot row writers are now verified in `000d`: `FUN_000d_7299` writes static source `DS:67f2` to `0x39ca[obj+2]` after creating a `0x44`-byte object, and `active_dispatch_entry_create_default` (`000d:761c`) writes static source `DS:6872` to `0x39ca[obj+2]` for the default active dispatch entry. `entity_vm_context_create_from_slot_index` (`000d:46ec`) remains the only confirmed owner-table-derived writer, but it is no longer the only concrete row writer overall.
|
||
- The current pass narrows that split one step further. `entity_vm_context_create_from_slot_index` (`000d:46ec`) still derives its row from runtime owner table `(+0x10/+0x12) + 0x0d*slot + 4` before mirroring it into `0x39ca[slot]`, while `000d:7299` and `000d:761c` never touch the owner table at all in the verified windows. Instead they allocate local dispatch-entry-style objects, derive the row index from object field `+0x2`, and seed `0x39ca[row]` from fixed static sources `DS:67f2` and `DS:6872`. The safest current interpretation is therefore `owner-backed VM source mirror` versus `dispatch-entry-local static seed rows`, not three competing writers to the same semantic lane.
|
||
- One exact numeric collision is now ruled out as unrelated noise rather than a second VM source: `000e:0953` in the animation/audio lane pushes literal `0x410` into imported `ASYLUM.27` immediately after setting the local audio-completion byte at `+0xef1`. Because `ASYLUM.DLL` is the `ASS_*` audio/media library, this does not weaken the attribution of gameplay event `0x410` to the `000d` VM/USECODE lane.
|
||
- Current best JELYHACK reading after this pass: `JELYHACK` itself still looks like a referent-only map/object descriptor, but that no longer makes it inert. A referent-only record can still matter by supplying the referent id that populates the VM referent registry, while neighboring classes such as `REE_BOOT`, `SURCAMEW`, and `SFXTRIG` supply the event-bearing logic attached to the same local object island.
|
||
|
||
### 000d:21ed/22bc id-correlation table (runtime lane vs descriptor families)
|
||
|
||
| Runtime element | Code anchors | Observed width/shape | Correlation status |
|
||
|---|---|---|---|
|
||
| Metadata byte A | `000d:22d2` after context from `000d:46ec` | 1-byte signed (`CBW`), used as first loop dimension/count input | Not a descriptor id. Behaves as compact shape/count metadata for matrix construction. |
|
||
| Metadata byte B | `000d:22ee` | 1-byte signed (`CBW`), paired with byte A and summed to derive loop bounds | Not a descriptor id. Same shape/count role as byte A. |
|
||
| Streamed words feeding matrix | `000d:2324`, `000d:2372`, `000d:237b -> 0008:7d27` | 16-bit words consumed from caller stream and passed to `entity_link` | Best fit: runtime entity/link ids, not descriptor-class selectors. |
|
||
| Matrix output writeback filter | `000d:23da..2421` | tests `0x0400`; only non-`0x0400` words are pushed back | Matches `entity_word_list` style link-flag semantics, not event opcode tagging. |
|
||
| Source stream provenance | `000d:4732..4751`, `000d:47a3..47d4` | source pointer = owner table `(+0x10/+0x12) + 0x0d*slot + 4`; mirrored to `0x39ca[slot]` | Slot-indexed runtime source table, generic across gameplay entity lanes. |
|
||
|
||
Conservative interpretation after this pass:
|
||
|
||
- The `000d:21ed -> 000d:22bc` lane is strongly supported as a slot-backed payload to entity-link closure path, where two signed byte-sized metadata fields shape an exact `A x B` matrix walk: byte A is the lead-word row count, byte B is the shared target-list width, and the word entries passed to `entity_link` are runtime link/entity ids rather than descriptor selectors.
|
||
- Descriptor-family alignment is therefore stronger with generic active event ecosystems (`EVENT`/`NPCTRIG`/`*_BOOT`/`SFXTRIG`) than with `SURCAM*` callback holders, because no direct `eventTrigger`-specific discriminator is read in this lane.
|
||
- Direct descriptor-id attribution is still rejected for now: no code evidence ties the consumed bytes/words here to explicit EUSECODE class indices or to a hard `JELYHACK`/`SURCAM*` switch.
|
||
- The new extractor-side structure pass tightens the descriptor-side fit inside that generic active-event ecosystem. `USECODE/EUSECODE_extracted/immortality_body_structure.md` shows `EVENT` slot `0x0a` as a broad hub clause stream with `90` internal `0x53 0x5c <u16> EVENT` subheaders and the widest field trailer, while `NPCTRIG` slot `0x0a` stays compact at `5` subheaders and a narrow `referent/event/item/item2` tail. That does not prove a direct class-id bridge into `000d:21ed -> 000d:22bc`, but it does make `NPCTRIG slot 0x0a` the strongest remaining compact descriptor-side candidate for the offset-specialized slot-`0x0a` runtime wrapper `entity_vm_context_try_create_mask_0400_slot0a_with_offset` (`0005:2c35`) instead of the older undifferentiated `EVENT or NPCTRIG` frontier.
|
||
- The next focused extractor pass sharpens that fit again. `USECODE/EUSECODE_extracted/immortality_npctrig_clauses.md` now shows `NPCTRIG` slot `0x0a` as a fixed-width five-clause ladder: subheaders at `0x0064/0x0093/0x00c2/0x00f1/0x0120`, uniform `0x2f` stride, backward-walking targets, and one `branch_3f_0a` + `push_24_51` + `writeback_57_02` triple in each full clause. The new runtime-fit section also matters: `000d:5572` proves the extra word from `0005:2c35` is additive (`entity_vm_slot_load_value(...) + offset`), so slot `0x0a` now exposes the only surviving compact five-row selector family that plausibly matches byte A in `000d:21ed`, while slot `0x20` remains a one-clause typeNpc-heavy body with no comparable writeback/push motif or stride family.
|
||
- The downstream-use follow-up weakens that direct selector fit. Instruction windows at `000d:47ef..47f3` show `entity_vm_context_create_from_slot_index` storing slot index `SI` at `+0x32` and the dynamic additive word `DI` at `+0x34`, but the live sequencer lane `000d:21ed -> 000d:22bc` never rereads either field: after the create call it only touches the copied blob at `+0x102`, the seeded byte lane at `+0xd6/+0xd8`, and the caller stream at `+0xcc/+0xce`. The persistent uses of `+0x34` are instead the object save/load path: `000d:49e9..4a27` serializes `+0x10c` then `+0x34`, and `000d:4c2d..4c4d` reloads `(+0x32,+0x34)` through `entity_vm_slot_load_value_plus_offset` before storing the returned pair at `+0x10c/+0x10e`. The safest current read is therefore `persisted source offset feeding a later slot-value reload`, not `direct clause selector consumed by the matrix stage`, which weakens the `NPCTRIG slot 0x0a` alignment unless the derived reload value itself can still be tied back to that ladder.
|
||
|
||
### entity_vm_opcode_sequence_run opcode-to-payload-shape matrix (sequencer-local)
|
||
|
||
| Sequencer stage | Code anchors | Opcode / lane status | Payload shape class | Verified behavior |
|
||
|---|---|---|---|---|
|
||
| `000d:0988` (`entity_vm_opcode_mutate_referent_chain`) | `000d:ec1d`, `000d:0988` body | Known `0x18..0x1b` family | Inline/indirect chain payloads | `0x18/0x19` append-unique and `0x1a/0x1b` remove-matching over referent chains, with indirect-vs-inline mode split and shared epilogue. |
|
||
| `000d:177c` | `000d:ebf5`, `000d:178b..17aa` | Numeric opcode unresolved in this dispatcher lane | Word scalar (frame-local -> stream) | Does not read `+0xd6/+0xd8`; subtracts `2` from `[context+0xcc]` and pushes one frame-local word (`BP-0x1c6`) onto the stream stack. |
|
||
| `000d:1acb` | `000d:ec09`, `000d:1acb..1b22` | Numeric opcode unresolved in this dispatcher lane | Word-pair/list consumer + boolean output | Reads one 32-bit pair from stream (`[context+0xcc]`, then `+4`), compares against `AX:DX`, and pushes a 16-bit predicate result back to stream. |
|
||
| `000d:21ed -> 000d:22bc` | `000d:21ed`, `000d:22d2`, `000d:22ee`, `000d:2324..237b`, `000d:23da..2421` | Caller block + internal stage | Mixed: byte metadata + word id matrix | Consumes two signed bytes from seeded `+0xd6/+0xd8` as shape/count metadata, then consumes streamed words as entity/link ids for `entity_link`; only non-`0x0400` words are pushed back. |
|
||
| `000d:1d4a` | `000d:ec48`, `000d:1d4a` | Conditional substage when `[obj+0xba]==0` | Control/sentinel (no payload shape proven) | Current body is `INT3`-only (boundary suspect); treated as a control gate/trap island, not a verified payload transformer. |
|
||
| `000d:2104` | `000d:ec54`, `000d:2104..212b` | Numeric opcode unresolved in this dispatcher lane | Mixed scalar/handle return | Writes result to caller out-ptr: path A stores frame-local dword (`BP+0xfdaa/fdac`), path B stores object word (`[obj+2]`) with high word cleared; then returns via opcode epilogue. |
|
||
|
||
### Pass-4 dispatcher lane update (opcode selector evidence)
|
||
|
||
What is now hard evidence in code:
|
||
|
||
- `000d:0988` compares one opcode-local word at `[BP-0x32]` against concrete values `0x19`, `0x1a`, and `0x1b` (`000d:099b`, `000d:09a1`, `000d:0a07`, `000d:0a0d`).
|
||
- `entity_vm_opcode_sequence_run` (`000d:ebe3`) calls `000d:177c -> 000d:1acb -> 000d:0988 -> 000d:22bc -> optional 000d:1d4a -> 000d:2104` (`000d:ebf5`, `000d:ec09`, `000d:ec1d`, `000d:ec31`, `000d:ec48`, `000d:ec54`).
|
||
- `000d:177c`, `000d:1acb`, and `000d:2104` do not contain their own opcode compares in recovered body ranges; they behave as wrapper stages around the opcode-local family tested in `000d:0988`.
|
||
- The entry/exit contract is one step tighter too. `000d:ebe9` seeds the first stage from object field `+0xfe`, while the success tail at `000d:ec62..ec79` runs `tracked_entity_handle_mark_remove_all_if_enabled` and then gates `FUN_0008_ebe7` on object field `+0xc0` plus byte `+0x4b`. So the sequencer is not just an isolated opcode cluster; it also participates in outer runtime cleanup and follow-up dispatch state.
|
||
|
||
Conservative case identity mapping from this pass:
|
||
|
||
- `000d:177c` = pre-mutate stack push stage for the same `[BP-0x32]` family.
|
||
- `000d:1acb` = comparator stage (stream dword pair -> boolean word) for that family.
|
||
- `000d:0988` = concrete opcode discriminator for `0x19/0x1a/0x1b` (with `0x18` still implied by sibling path behavior).
|
||
- `000d:2104` = family finalizer writing mixed immediate/object output to caller out-ptr.
|
||
|
||
Still unresolved after this pass:
|
||
|
||
- The animation constructor near calls at `000e:283e`, `000e:2931`, and `000e:29e4` land on a separate mis-split `000e:ebe3` region, not on `entity_vm_opcode_sequence_run`. They therefore no longer count as direct xref evidence for the `000d` dispatcher.
|
||
- The true upstream selector/write path for `[BP-0x32]` in `entity_vm_opcode_sequence_run` is still unresolved, and no additional opcode id can yet be assigned uniquely beyond the internal `0x19/0x1a/0x1b` family already proven inside `000d:0988`.
|
||
- Repeated MCP-visible instruction and data-use searches still do not produce a real direct caller edge for `entity_vm_opcode_sequence_run`, `0005:2c35`, or `0005:2c68`. For now that makes the next defensible route `caller-frame / shared-consumer recovery`, not more recycled raw call searches or the retired `000a:44fd` and `000e:ebe3` hypotheses.
|
||
|
||
### First readable VM IR sketch (verified-only)
|
||
|
||
From direct decompile/disassembly in `000d:0988`, `000d:208b`, `000d:21ed`, `000d:22bc`, and `0008:7d27`, the current script-readable IR shape is:
|
||
|
||
- `APPEND_UNIQUE_INLINE` (`opcode 0x18`, implied sibling in `000d:0988`)
|
||
- `APPEND_UNIQUE_INDIRECT` (`opcode 0x19`)
|
||
- `REMOVE_MATCHING_INDIRECT` (`opcode 0x1a`)
|
||
- `REMOVE_MATCHING_INLINE` (`opcode 0x1b`)
|
||
- `MATERIALIZE_OR_FORWARD_VALUE` (`000d:208b` path after `entity_vm_context_create_from_slot_index`)
|
||
- `PUSH_FRAME_WORD_LITERAL` (`000d:177c`: pushes one frame-local word to stream stack)
|
||
- `COMPARE_STREAM_DWORD_AND_PUSH_BOOL` (`000d:1acb`: consumes one stream dword pair and pushes predicate word)
|
||
- `PREPEND_INLINE_PAYLOAD` (`000d:21ed`: subtracts from context `+0x102` then copies caller bytes)
|
||
- `BUILD_ENTITY_LINK_MATRIX` (`000d:22bc`: two streamed dimension bytes, streamed id table, repeated `entity_link` calls)
|
||
- `FINALIZE_MIXED_VALUE_TO_OUTPTR` (`000d:2104`: emits either immediate frame dword or object-word-derived value)
|
||
- `EMIT_OR_PUSHBACK_RESULT` (`000d:22bc` tail: values without `0x0400` marker are pushed back to caller stream before `entity_vm_opcode_finish`)
|
||
|
||
Minimal pseudocode-style sketch:
|
||
|
||
`referent = active_referent_id()`
|
||
`chain = referent.payload_chain`
|
||
`chain = mutate(chain, opcode_0x18_to_0x1b, payload_mode)`
|
||
`value = materialize_or_forward(context_from_slot(stream_state))`
|
||
`if opcode_lane == inline_payload: value = prepend_inline_payload_and_build_link_matrix(stream_ids)`
|
||
`emit(value)`
|
||
|
||
This remains consistent with descriptor-side evidence: referent-only anchors (`JELYHACK`/`JELYH2`) can still drive behavior once neighboring event-capable descriptors attach payload/event semantics to the same referent island.
|
||
|
||
### First readable pseudo-script renderings (verified-only)
|
||
|
||
`entity_vm_context_create_from_slot_index` adds one more readable anchor for this IR: after it seeds the embedded mini-VM from the runtime owner table at `0x6611 -> +0x1315/+0x1317 -> (+0x10/+0x12) + 0x0d*slot + 4`, it also writes the same far source pair into the per-slot mirror row addressed through `0x39ca[context_slot]`. That keeps the current readable model honest: the mirror is part of context creation for slot-backed VM state, not yet a proven standalone descriptor-dispatch cache.
|
||
|
||
The best verified human-readable form right now is therefore a small family of templates rather than a one-record-equals-one-opcode script dump.
|
||
|
||
Readable template A: referent anchor with event-bearing attachment (JELYHACK island)
|
||
|
||
```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`.
|
||
- The two direct `0005:2d30` callers are now role-shaped as well: `0005:5370` reaches slot `0x0f` only after `entity_class_has_flag2000` succeeds and class-word bit `0x8000` is clear, while `0005:6f47` reaches the same gate from the complementary branch where class-word bit `0x2000` is still clear before the caller continues into its larger state/update flow.
|
||
- `0005:2c68` is no longer usable as indirect selector evidence. The `0007:e521` and `0007:e73c` instruction windows do push `0x2c68` immediately before `CALLF 000a:44fd`, but decompile now shows that value is the caller-local data pointer `DAT_0000_2c68` passed into a fatal-report helper, not an indirect call to wrapper `0005:2c68`.
|
||
- `0005:2c35` and `0005:2c68` therefore both remain unresolved in direct caller/xref evidence, and the real selector work stays centered on the still-xref-dark upstream edge into `entity_vm_opcode_sequence_run` rather than the disproven `000a:44fd` hypothesis.
|
||
- Net effect: the active-event ecosystem fit is reinforced by direct caller behavior and payload shapes, but final slot-to-descriptor ownership still requires real caller-role recovery for the remaining xref-dark entry points.
|
||
|
||
#### Current batch: masked-context hub and sequencer-internal consumer recovery
|
||
|
||
- The generic masked VM-context hub is now instruction-verified at `000d:463a`. That body maps the incoming entity through `entity_vm_slot_index_from_entity`, rejects the path when runtime global `0x6610` is active or the owner/resource table at `0x6611 + 0x1315/+0x1317` is absent, tests the per-slot `0x0d`-stride owner mask pair against the caller-supplied high/low mask words, and only then falls into `entity_vm_context_create_from_slot_index` (`000d:46ec`).
|
||
- `search_instructions` on `000d:463a` now confirms this hub is not isolated to the `0005` wrapper island. In addition to the known seg021 wrappers, live direct callers now include `0004:f047` (mask `0x8000:0x0007`), `0004:f076` (mask `0x2000:0x0015`), and larger callers at `0006:0bbc` / `0006:10e7`. That is new caller-side evidence for the wider owner-slot taxonomy even though the offset-specialized wrappers `0005:2c35` and `0005:2c68` themselves still have no direct caller edges.
|
||
- The xref-dark offset wrappers are now tighter structurally too. Disassembly of `0005:2c35` and `0005:2c68` confirms they do nothing beyond sign-extending one extra word, passing mask pairs `0x0400:0x000a` and `0x0800:0x000b`, forwarding the entity pointer to `000d:463a`, and returning the out-word on success. That keeps their best current reading at `offset-specialized masked context creation`, not a separate selector lane.
|
||
- The offset word is now behaviorally tighter too. `entity_vm_slot_load_value_plus_offset` (`000d:5572`) is a straight `entity_vm_slot_load_value(...) + offset` wrapper, so the extra word passed by `0005:2c35` is not a second mask or opaque cookie; it is an additive selector/value adjustment that can plausibly choose one of the evenly spaced slot-`0x0a` clause starts once a real caller is recovered.
|
||
- The next caller-path pass tightens why `0005:2c35` stays dark. MCP xrefs now show only three entries into `entity_vm_context_create_from_slot_index` (`000d:46ac` from the generic masked hub, plus direct internal sequencer islands `000d:208b` and `000d:21ed`), while `0005:2c35` itself still has no recovered code or data xrefs. Stack setup at `000d:208b` hardcodes the `000d:5572` additive slot-load parameter to `0`, which does not match the `NPCTRIG` slot-`0x0a` clause starts (`0x0064/0x0093/0x00c2/0x00f1/0x0120`) or backward targets (`0x00db/0x00ac/0x007d/0x004e/0x001f`). The remaining live selector frontier is therefore the still-overlapped `000d:21ed` caller frame, not a normal visible caller of `0005:2c35`.
|
||
- The sequencer lane also gained two concrete internal consumer shapes. `000d:208b` is now the instruction-verified `create one slot-backed context and materialize or forward its result` path: it builds a `0x6714` context from the caller stream state, writes immediate-flagged results straight to the out pointer, and otherwise forwards the created object through `entity_vm_opcode_finish`. `000d:21ed` is the matching `prepend inline payload and build entity-link matrix` path: it creates a context, prepends caller-owned bytes into `+0x102`, consumes the seeded `+0xd6/+0xd8` bytes as shape/count metadata, and builds repeated `entity_link` closures from the following streamed ids before the same finish path.
|
||
- A new downstream-use pass narrows the extra-word role further. The stored offset field at context `+0x34` is now confirmed as durable object state rather than an immediate sequencer input: `000d:21ed -> 000d:22bc` does not reread it at all, `000d:498f`/`000d:4a78` serialize and reload it, and `000d:4c2d..4c4d` recomputes a slot-backed value from `(+0x32,+0x34)` into `+0x10c/+0x10e`. That shifts the remaining immortality question one step downstream: if `NPCTRIG slot 0x0a` still fits this runtime lane, it is more likely through the value reloaded from the slot-plus-offset pair than through `+0x34` as a direct clause selector.
|
||
- The hidden pre-call span in the `000d:21ed` lane is now recovered from direct program-memory bytes as well. Window `000d:2131..21ed` reads the seeded `+0xd6/+0xd8` stream as three successive words followed by two signed bytes: word0 becomes the slot index pushed at `000d:21d4`, word1 and word2 are added at `000d:21d0` before being pushed as the dynamic additive arg at `000d:21d3`, byte3 is forwarded as the setup-data length byte, and byte4 becomes the inline-blob length used for the later prepend copy. That makes the source classification explicit: context `+0x34` is not loaded from the owner table or from the caller object at `+0xd4`; it is a computed sum of two consecutive words inside the seeded stream itself.
|
||
- The same recovered window also tightens the upstream source layout feeding `entity_vm_context_setup`. The current caller frame base is `caller + [caller+0xd4]`, where `+0xd4` matches the saved frame offset written by `entity_vm_stack_push_frame` (`000c:f7c7`) rather than a descriptor-local field. From that frame base, `000d:21db..21e0` pushes `[frame+0x0a/+0x0c]` as a far pointer passed into `entity_vm_context_setup`, and `000d:21bd..21c8` separately derives `[frame+0x0e]` as the inline payload tail copied after context creation. So this consumer is now better modeled as one generic VM frame-record shape with two payload sources: a frame-stored far pointer plus byte-sized setup length for the initial `+0xcc` stack seed, followed by an adjacent inline tail blob with its own byte-sized length.
|
||
- The next frame-producer pass recovers the closest non-overlapped writer feeding that lane too. Raw bytes at `000c:fbf7..fc47` (`caseD_0`) show a generic frame-record producer reading one signed placement byte from the same seeded `+0xd6/+0xd8` stream, popping a far-pointer dword from the caller stream at `[caller+0xcc/+0xce]`, computing `frame_base = caller + [caller+0xd4]`, and storing the dword at `[frame_base + placement + 0x4/+0x6]`. That means the immediate source far pointer consumed later by `000d:21ed` is already stream-backed rather than owner-row-backed; if the `000d:21ed` record uses this exact producer family for its `[frame+0x0a/+0x0c]` lane, the relevant placement byte is `0x0006`, which is the only value that lands the written dword at `+0x0a/+0x0c` and leaves the inline tail starting at `+0x0e`.
|
||
- That stronger runtime shape weakens any claim that `000d:21ed` is already reading a descriptor-family-specific record. `NPCTRIG` slot `0x0a` still remains the best surviving descriptor-side candidate because its five-clause ladder is the only compact body that fits the row-count frontier, but the code evidence now shows the immediate input to `000d:21ed` is a generic frame-local record containing a source far pointer, a seeded slot/additive pair, and an inline tail. The remaining descriptor-side question is therefore one level earlier again: where the caller frame receives its `[frame+0x0a/+0x0c]` far pointer and whether the summed `add_a + add_b` still corresponds to a clause-base/delta pair inside `NPCTRIG` slot `0x0a` rather than to a more generic descriptor-relative offset.
|
||
- That changes the `NPCTRIG` cross-check in one important way. `NPCTRIG` slot `0x0a` remains the strongest surviving descriptor-side hypothesis only as an upstream source for a predecoded caller-stream record, because the recovered writer consumes a caller-stream dword plus a seeded placement byte instead of indexing owner rows or descriptor tables directly. `NPCTRIG` slot `0x20` still reads as the typed/setup companion body, but neither slot is now a good fit for the immediate write into `[frame+0x0a/+0x0c]` itself.
|
||
- One more layer of the producer path is now instruction-verified too. The setup call at `000d:4788 -> 000c:f844 -> 000c:f6e8` does not seed the new context's `+0xcc/+0xce` caller stream directly from the owner table row. Instead `entity_vm_context_setup` first allocates or reuses the object-local stream buffer at `context+0x36+0xcc`, then copies a caller-supplied setup blob from the parent frame using the far pointer/length arguments passed through `000d:46ec`. The slot/additive record returned by `entity_vm_slot_load_value_plus_offset` becomes the separate seeded `+0xd6/+0xd8` stream, while the owner-table row at `(+0x10/+0x12) + 0x0d*slot + 4` is mirrored to `0x39ca[slot]` and preserved separately in the context state.
|
||
- The closest sibling template to `caseD_0` also sharpens the placement-byte reading. `000c:ff9f..000d:000d` reads one signed placement byte and one length byte from the same seeded `+0xd6/+0xd8` stream, then copies `len` bytes from `[frame_base + placement + 0x4]` back onto the caller stream. Together with the recovered `000d:21ed` consumer layout (`[frame+0x0a/+0x0c]` far ptr, `[frame+0x0e..]` inline tail), that makes the strongest current fit a fixed two-slot family for this record shape: `caseD_0` uses placement `0x0006` for the far-pointer dword, and the sibling blob-copier uses placement `0x000a` for the inline tail starting at `frame+0x0e`.
|
||
- The producer side of that same record family is now tighter too. Linear raw-byte recovery across `000c:f98b..000d:000d` shows `000c:fc4b..fcbb` as the forward blob producer matching the reverse `000c:ff9f..000d:000d` case: it reads placement and length from the seeded `+0xd6/+0xd8` lane, computes `frame_base = caller + [caller+0xd4]`, and copies `len` bytes from the caller stream at `[caller+0xcc/+0xce]` into `[frame_base + placement + 0x4]`. For the `000d:21ed` record shape, that makes placement `0x000a` the best fit for the inline tail now consumed from `[frame+0x0e..]`.
|
||
- The dword lane now has a matching reverse case as well. Raw bytes at `000c:ff1f..ff83` show the same recursive family in the opposite direction: it reads one signed placement byte from the seeded `+0xd6/+0xd8` lane, computes `frame_base = caller + [caller+0xd4]`, loads a dword from `[frame_base + placement + 0x4/+0x6]`, subtracts `4` from `[caller+0xcc]`, and writes that dword back onto the caller stream. In other words, the immediate upstream producer for the `000c:fbf7..fc47` far-pointer write can already be another frame-record copier, not a direct owner-row or descriptor-table lookup.
|
||
- That narrows the remaining source classification again. The setup far pointer consumed by `000d:21ed` is now best modeled as a recursively propagated pointer into another VM-side byte buffer or predecoded descriptor workspace, not as the owner/resource row source mirrored separately through `0x39ca`. The owner row still matters for slot-backed state reloads, but the `entity_vm_context_setup` blob pointer itself is traveling through the frame-record family independently of that owner-row mirror.
|
||
- That also weakens the full-tuple `NPCTRIG` fit one more notch without killing it. The surviving tuple is now better read as `(slot, add_a, add_b, setup_len, inline_len, placement=0x0006/0x000a)` feeding a generic recursive frame-record contract. `NPCTRIG` slot `0x0a` remains the strongest descriptor-side candidate only as an earlier decoder that could have produced this predecoded record family, while slot `0x20` still reads as the typed/setup companion body. No recovered instruction in the immediate `000c:f98b..000d:000d` family yet ties the setup far pointer directly back to either slot.
|
||
- Net effect on source classification: the `000d:21ed`-relevant frame record is still not best modeled as generic VM scratch. Its immediate setup bytes are recursively copied from a parent frame record, and the wider context-build path is still anchored in descriptor-derived VM state (`+0xd6/+0xd8` from `entity_vm_slot_load_value_plus_offset`, owner-row source mirrored via `0x39ca`). What remains open is not whether this lane is scratch-backed, but which earlier decoder materializes the parent-frame far pointer before `000c:fbf7` consumes the next dword.
|
||
- After the new reverse-case recovery, that blocker can be stated more tightly: the missing piece is no longer a generic parent-frame materializer somewhere above `000c:fbf7`, but the first non-recursive decoder that originates the far pointer before the `ff1f/ff9f -> fbf7/fc4b -> 000d:21ed` propagation chain repeats it.
|
||
- The next pass closes that specific source-classification gap inside the same hidden interpreter body. Raw bytes at `000c:fa2f..fa5b` recover an inner opcode dispatcher that reads one opcode byte from the seeded `+0xd6/+0xd8` lane, bounds-checks it against `0x79`, and jumps through `CS:[0x3d9f + opcode * 2]`. That matters because the same local case family now exposes both the recursive frame-record replay stages and a separate set of direct caller-stream seed cases.
|
||
- Those non-recursive seed cases are now concrete. `000c:fd51` writes one inline byte from the `+0xd6/+0xd8` control stream onto the caller stream after decrementing `[caller+0xcc]` by `1`, `000c:fd91` and `000c:fdd1` do the same for inline words, and `000c:fe11..fe59` does it for an inline dword. In the dword case the interpreter advances through four literal bytes in the control stream, subtracts `4` from `[caller+0xcc]`, and writes the literal dword directly onto the caller stream before any frame replay logic runs.
|
||
- That makes `000c:fe11` the strongest current first non-recursive origin for the far-pointer lane later consumed by `000c:fbf7..fc47` and then by `000d:21ed`. The immediate setup far pointer is therefore no longer best modeled as coming from the owner/resource row, the mirrored `0x39ca` lane, or a generic VM scratch buffer. Its immediate compiled-side source is an inline dword literal embedded in the interpreter/control stream itself; `000c:ff1f..ff83` and `000c:fbf7..fc47` are replay stages layered on top of that literal-seeding path.
|
||
- That retunes the `NPCTRIG` cross-check again without killing it. `NPCTRIG` slot `0x0a` still remains the best upstream descriptor-side candidate because it is still the only compact active-event body that fits the surviving slot/additive shape, and slot `0x20` still reads as the typed/setup companion. But any direct immortality mapping now has to explain how the upstream decoder turns that descriptor family into a literal-bearing VM control stream before `000c:fe11`, not how `000d:21ed` or `000c:fbf7` index descriptor rows directly.
|
||
- One more pass tightens the creator/consumer split enough to rule out the owner row as the immediate control-stream builder. Direct instruction recovery at `000d:46ec` shows `entity_vm_context_create_from_slot_index` using the owner-table row `(+0x10/+0x12) + 0x0d*slot + 4` only for the separate `0x39ca[slot]` mirror, while the live `+0xd6/+0xd8` lane passed into `entity_vm_context_setup` still comes from `entity_vm_slot_load_value_plus_offset`. In the recovered `000d:21ed` pre-call span, that seeded lane is consumed as `word slot_index`, `word add_a`, `word add_b`, `byte setup_len`, `byte inline_len`, with `add_a + add_b` forwarded as the dynamic word stored at context `+0x34`.
|
||
- The same pass also clarifies the setup-payload contract that feeds the later link-matrix stage. `000d:21ed` passes `[frame+0x0a/+0x0c]` as the setup far pointer into `entity_vm_context_setup`, copies `[frame+0x0e..]` as a separate inline tail, and then `000d:22bc` consumes two signed metadata bytes plus a streamed word matrix to drive repeated `entity_link` calls. The immediate source is therefore `decoded per-slot VM stream + frame replay`, not `owner-row lookup + direct descriptor row`.
|
||
- That changes the opcode-family reading around `000c:fa2f` in a useful way even though the exact opcode indices remain unresolved in the current overlapped table view. The hidden dispatcher now has a verified immediate-literal family: `000c:fd51` pushes one inline byte to the caller stream, `000c:fd91` pushes a sign-extended byte as a word, `000c:fdd1` pushes an inline word, and `000c:fe11` pushes an inline dword. Together with the recursive replay cases `000c:ff1f` and `000c:ff9f`, that is enough to classify the upstream builder as a generic literal-bearing interpreter/control stream rather than a direct `NPCTRIG` clause reader.
|
||
- The descriptor-side fit therefore weakens from `specific direct NPCTRIG selector` to `broader descriptor-derived VM workspace` while staying narrow enough to keep `NPCTRIG` slot `0x0a` alive as the best upstream candidate. Slot `0x0a` still matches the event-bearing compact body and its five-clause ladder remains the only surviving compact source family with a plausible row-count/additive shape, but slot `0x20` still looks like the typed/setup companion and neither slot is now a good fit for the immediate control-stream seeding logic itself.
|
||
- The slot-load miss path now closes the workspace-materialization side of that question. Inside `entity_vm_slot_load_value` (`000d:51fd`), a cache miss triggers `000d:5066`, which first reads a slot header and then a `count * 6 + 0xc0` subentry table through the owner-resource wrapper `000d:714c`. When one subentry is still unloaded, `000d:5305..53d4` allocates a value object through `000d:3800`, then calls `000d:714c` again with the subentry source range and the new object's buffer at `+0x0a/+0x0c`; the function returns that same buffer pointer as the final `DX:AX` result. The immediate `+0xd6/+0xd8` workspace is therefore first materialized as a file-backed slot-value buffer during the slot-load miss path itself, not synthesized later from the owner-row mirror or from generic scratch state.
|
||
- The inline-tail source is not as tightly closed yet. The same hidden case family contains several immediate scalar caller-stream seed cases, so the `000d:21ed` tail at `[frame+0x0e..]` can now plausibly be assembled from control-stream literals or from another nearby non-recursive payload case rather than from a direct owner-row read. No instruction recovered in `000c:f98b..000d:000d` performs a matching direct descriptor-row lookup for that tail.
|
||
- Net effect from this pass: the missing outer selector into `entity_vm_opcode_sequence_run` is still unresolved, but the lane is no longer just one opaque dispatcher plus dark wrappers. It now has a verified generic masked-context creation hub, wider caller-family anchors for that hub, and two internally differentiated sequencer consumer blocks built directly on `entity_vm_context_create_from_slot_index`.
|
||
|
||
#### Follow-up: four newly surfaced direct `000d:463a` callers
|
||
|
||
- `0004:f033` (`0x8000:0x0007`) now reads as a generic gameplay-side materialization lane rather than a state-transition helper. When the local seg021 class-nibble query returns `8`, the wrapper bypasses the VM path and returns object word `+0x02` directly from the locally produced object. Otherwise it forwards through `entity_vm_context_try_create_masked_for_entity` and returns the created object's word `+0x02` on success.
|
||
- `0004:f05c` (`0x2000:0x0015`) stays on the gameplay-state side too, but with a stronger caller role. The only current direct caller window at `0004:f2b3` reaches it after overlap/proximity tests and entity byte `+0x32` toggling, so the safest reading is still `stateful gameplay materialization lane`, not `descriptor selector`.
|
||
- `entity_vm_context_try_create_mask_0008_slot30_with_offset` (`0006:0ba4`) adds the first strong non-`0005` extra-payload lane. It passes mask `0x0008:0x0030` plus one caller word into `000d:463a`; on failure it drops into `0006:0cfa`, which copies class-detail word `+0x02` to `+0x04`, derives a replacement selector from class-detail words `+0x06/+0x08/+0x0a` or the caller value, may clear flag `0x08` through `entity_class_clear_flag8_and_dispatch`, and then continues into the local state-transition/dispatch table. That is concrete evidence that at least one extra-word masked lane is feeding class-state transition materialization rather than a free-standing VM selector root.
|
||
- `entity_vm_context_try_create_mask_0010_slot08_with_offset_if_ready` (`0006:108c`) provides the second strong extra-payload lane. It passes mask `0x0010:0x0008` plus one caller word into `000d:463a`, but only after local readiness gates through `0006:ffed` plus the seg021 availability/flag8-clear path. Unlike the earlier looser reading, the helper itself does not fall back to `0006:13b0` or `0006:13e4`; on miss it simply returns `0`. That makes the function a guarded masked-materialization attempt, while the neighboring `0006:13b0/13e4 -> 0006:07c0` class-linked lookups remain adjacent family evidence rather than a direct local fallback inside `0006:108c`.
|
||
- Taken together, the new seg004 and seg006 callers strengthen the existing read of the still-dark wrappers `0005:2c35` (`0x0400:0x000a`) and `0005:2c68` (`0x0800:0x000b`). Those wrappers still have no direct caller evidence, but they now sit inside a larger verified subfamily of `extra-word masked materializers` whose known members feed state selectors, class-linked values, or other gameplay-side payload resolution instead of acting as the real upstream selector into `entity_vm_opcode_sequence_run`.
|
||
- MCP-native function xrefs now reinforce that stopping point rather than changing it: `entity_vm_context_try_create_masked_for_entity` reports the expected direct callers through `0004:f047`, `0004:f076`, the named `0005` wrapper island, and the two seg006 callsites `0006:0bbc` / `0006:10e7`, while `entity_vm_opcode_sequence_run` plus the dark `0x0400/0x000a` and `0x0800/0x000b` wrappers still surface no direct function-xref callers in the current database. The best next path therefore remains caller-frame recovery or nearby unnamed-function repair, not another generic masked-hub sweep.
|
||
|
||
#### Latest verified NE pass: collision producer and local storage-process queue
|
||
|
||
- The next earlier compiled-side producer for the already-named `StorageDataProcess_Create` / `StorageDataProcess_Run` pair is now closed in the live `CRUSADER.EXE` session. `AreaSearch_CollideMove` at `10e0:123a` allocates a local queue, then emits paired `0x236` processes in both the first-collision lane and the linked-list collision lane.
|
||
- The subtype assignment is now explicit at the caller, not just inferred from `StorageDataProcess_Run`: `0x20b` is the local `hit` notifier from the moving item to the collided item, and the reciprocal `0x20c` process is the `got-hit` notifier from the collided item back to the moving item. The first-collision lane uses the precomputed collision magnitude `local_4` as the damage word; the later linked-list lane uses `0`.
|
||
- The same pass also closes the local queue-helper trio in seg031. `10f0:046d` is now `storage_process_ref_list_create`, allocating the small queue header plus a counted far-pointer array; `10f0:0502` is now `storage_process_ref_list_append`, storing one `StorageDataProcess` far pointer and recording the assigned slot index in process field `+0x3a`; and `10f0:06b5` is now `storage_process_ref_list_destroy`, freeing the array and optionally the header object.
|
||
- The same live pass also widens the producer surface around that queue without breaking the earlier read. Direct callers into `AreaSearch_CollideMove` are now confirmed as movement/collision heavy: `Item_LegalMoveToPoint`, `Item_LegalMoveToPointWithCollisionInfo`, `GravityProcess_Run`, `AnimPrimitive_CheckToStartNewAnimation`, `AnimPrimitiveProcess_Run`, `SuperSprite_AdvanceFrame`, and `GravityProcess_FastAreaCleanup` (`1038:11fd`).
|
||
- Two more structural names now anchor that caller set in the live NE database. `10a0:1841` is `Item_LegalMoveToPointWithCollisionInfo`, the legal-move wrapper variant that preserves blocked/collision outputs around the same area-search commit path, and `1138:0ee8` is `SuperSprite_SweepTestAdvance`, the supersprite-side sweep probe that stores the first collision before `SuperSprite_AdvanceFrame` commits movement.
|
||
- The same movement lane is now tighter at the helper level too. `10e0:11c5` is now `AreaSearch_SweepShapeBetweenPoints`, the thin wrapper that seeds the search struct and forwards one shape/path sweep into `AreaSearch_SweepTestPt`; `10e0:15b4` is `AreaSearch_SweepItemToPointWithStepUp`, the item-based bridge from current item position and shape into that sweep path; and `10e0:162f` is `AreaSearch_SweepShapeBetweenPointsWithStepUp`, the step-aware wrapper that retries same-z sweeps with vertical offsets and optional `+8` / `+9` step-up probes before returning the resolved point in `srch->pt`.
|
||
- The seg031 queue now has its release-side cleanup pair named as well. `10f0:03ff` is `StorageDataProcess_Release`, a release path that terminates queued peer processes referencing the same item before unlinking both MList hooks, and `10f0:0542` is `storage_process_ref_list_terminate_item_matches`, the counted-array helper that clears matching queue slots and forces termination for processes whose `itemno` or `otheritem` matches the requested item.
|
||
- One adjacent seg090 helper is now anchored structurally too: `10a0:196f` is `ItemCache_PushAndPopToDirectionalOffset`, which pushes the current item into the cache and repositions the cache pop target to the current point plus one direction-offset lookup from the local `0x0ffe` / `0x100e` tables.
|
||
- This moves the VM/caller frontier one step earlier without overclaiming the selector. The closed producer family is still a gameplay collision queue, not an owner-loaded class-family chooser, and no direct non-collision caller currently reaches `StorageDataProcess_Create` or `StorageDataProcess_RunAndTerminateProcs`. The remaining gap is therefore the earlier policy layer that decides when those movement lanes call `AreaSearch_CollideMove`, or the first non-collision producer if one exists elsewhere.
|
||
|
||
| `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` |
|