Add Crusader-specific USECODE data and documentation

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

View file

@ -245,8 +245,8 @@ Globals used: `[0x6312]`=start index, `[0x6314]`=count, `[0x630e]`=palette src p
- `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`), and `entity_vm_slot_load_value` contains a verified `PUSH 0x410` path at `000d:5290` before calling the unresolved seg091 event/abort lane at `000a:44fd`.
- This is not enough yet to say that `entity_vm_set_value_from_slot_plus_offset` is the immortality trigger, but it does show that the `000c` mini-VM / record-player cluster can hand work directly into a `000d` helper that emits event `0x410`.
- `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
@ -262,6 +262,11 @@ Globals used: `[0x6312]`=start index, `[0x6314]`=count, `[0x630e]`=palette src p
- `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
@ -284,8 +289,182 @@ Globals used: `[0x6312]`=start index, `[0x6314]`=count, `[0x630e]`=palette src p
- `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 |