Add 'annotate-usecode' command to import USECODE IR JSON annotations
- Introduced a new command 'annotate-usecode' to import USECODE IR JSON annotation hints as Ghidra comments on compiled anchors. - Added argument parsing for multiple IR JSON files, comment type selection, and a dry-run option. - Implemented logic to read annotation records from the provided IR files and set comments on the corresponding addresses in Ghidra. - Enhanced JSON schema to include response structure for the new command.
This commit is contained in:
parent
4d3c8cd81b
commit
daa363c3d2
39 changed files with 41450 additions and 871 deletions
|
|
@ -201,9 +201,9 @@ Second sweep through `000c` adjacent helpers — gated thunk wrappers and input/
|
|||
| `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` | `entity_state_check_field49_and_call_vfunc3c` | Checks field `[ptr+0x49]`: −1→reset to 0 return 1; 2→call `vtable[0x3c]` return 0; else thunk dispatch |
|
||||
| `000c:b153` | `entity_state_animation_done_tick` | Checks `[param_2+0x14+0xa]` animation-complete flag; if zero increments `field49` and calls `entity_state_check_field49_and_call_vfunc3c`; if set calls `vtable[0x3c]` |
|
||||
| `000c:b199` | `entity_state_input_key_handler` | Full input dispatcher: ESC/x/X → `vtable[0x3c]` (cancel); Left/Right arrows `0x14b/0x148` → prev state; n/N/`0x14d/0x150` → next state; e/E → set `field47=1`; `-` with counter → trigger at 4. Manages `field47` and `field49` |
|
||||
| `000c: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 |
|
||||
|
|
@ -211,8 +211,8 @@ Second sweep through `000c` adjacent helpers — gated thunk wrappers and input/
|
|||
| `000c:b3d8` | `entity_state_dispatch_if_far_ptr_nonzero_b` | Same null-guard pattern as `b349`, variant b |
|
||||
|
||||
**Patterns confirmed:**
|
||||
- `field49` = state-sequence index; 0=reset, 2=vtable callback, 4=triggered end
|
||||
- `field47` = keystroke-combo counter
|
||||
- `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`
|
||||
|
|
@ -259,15 +259,17 @@ Globals used: `[0x6312]`=start index, `[0x6314]`=count, `[0x630e]`=palette src p
|
|||
- `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 twin entry 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. The remaining open question is not whether they are file-backed, but whether they represent two file families, two record templates, or two load phases inside the same helper class.
|
||||
- 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
|
||||
|
|
@ -307,11 +309,14 @@ Globals used: `[0x6312]`=start index, `[0x6314]`=count, `[0x630e]`=palette src p
|
|||
|
||||
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.
|
||||
- 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.
|
||||
|
||||
### FUN_000d_ebe3 opcode-to-payload-shape matrix (sequencer-local)
|
||||
### entity_vm_opcode_sequence_run opcode-to-payload-shape matrix (sequencer-local)
|
||||
|
||||
| Sequencer stage | Code anchors | Opcode / lane status | Payload shape class | Verified behavior |
|
||||
|---|---|---|---|---|
|
||||
|
|
@ -327,8 +332,9 @@ Conservative interpretation after this pass:
|
|||
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`).
|
||||
- `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:
|
||||
|
||||
|
|
@ -339,9 +345,9 @@ Conservative case identity mapping from this pass:
|
|||
|
||||
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 `FUN_000d_ebe3`. They therefore no longer count as direct xref evidence for the `000d` dispatcher.
|
||||
- The true upstream selector/write path for `[BP-0x32]` in `FUN_000d_ebe3` 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 `FUN_000d_ebe3`, `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.
|
||||
- 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)
|
||||
|
||||
|
|
@ -466,9 +472,52 @@ The next gameplay-side wrapper pass now extends well past the three earlier seed
|
|||
- 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 `FUN_000d_ebe3` rather than the disproven `000a:44fd` hypothesis.
|
||||
- `0005:2c35` and `0005:2c68` therefore both remain unresolved in direct caller/xref evidence, and the real selector work stays centered on the still-xref-dark upstream edge into `entity_vm_opcode_sequence_run` rather than the disproven `000a:44fd` hypothesis.
|
||||
- Net effect: the active-event ecosystem fit is reinforced by direct caller behavior and payload shapes, but final slot-to-descriptor ownership still requires real caller-role recovery for the remaining xref-dark entry points.
|
||||
|
||||
#### Current batch: masked-context hub and sequencer-internal consumer recovery
|
||||
|
||||
- The generic masked VM-context hub is now instruction-verified at `000d:463a`. That body maps the incoming entity through `entity_vm_slot_index_from_entity`, rejects the path when runtime global `0x6610` is active or the owner/resource table at `0x6611 + 0x1315/+0x1317` is absent, tests the per-slot `0x0d`-stride owner mask pair against the caller-supplied high/low mask words, and only then falls into `entity_vm_context_create_from_slot_index` (`000d:46ec`).
|
||||
- `search_instructions` on `000d:463a` now confirms this hub is not isolated to the `0005` wrapper island. In addition to the known seg021 wrappers, live direct callers now include `0004:f047` (mask `0x8000:0x0007`), `0004:f076` (mask `0x2000:0x0015`), and larger callers at `0006:0bbc` / `0006:10e7`. That is new caller-side evidence for the wider owner-slot taxonomy even though the offset-specialized wrappers `0005:2c35` and `0005:2c68` themselves still have no direct caller edges.
|
||||
- The xref-dark offset wrappers are now tighter structurally too. Disassembly of `0005:2c35` and `0005:2c68` confirms they do nothing beyond sign-extending one extra word, passing mask pairs `0x0400:0x000a` and `0x0800:0x000b`, forwarding the entity pointer to `000d:463a`, and returning the out-word on success. That keeps their best current reading at `offset-specialized masked context creation`, not a separate selector lane.
|
||||
- The offset word is now behaviorally tighter too. `entity_vm_slot_load_value_plus_offset` (`000d:5572`) is a straight `entity_vm_slot_load_value(...) + offset` wrapper, so the extra word passed by `0005:2c35` is not a second mask or opaque cookie; it is an additive selector/value adjustment that can plausibly choose one of the evenly spaced slot-`0x0a` clause starts once a real caller is recovered.
|
||||
- The next caller-path pass tightens why `0005:2c35` stays dark. MCP xrefs now show only three entries into `entity_vm_context_create_from_slot_index` (`000d:46ac` from the generic masked hub, plus direct internal sequencer islands `000d:208b` and `000d:21ed`), while `0005:2c35` itself still has no recovered code or data xrefs. Stack setup at `000d:208b` hardcodes the `000d:5572` additive slot-load parameter to `0`, which does not match the `NPCTRIG` slot-`0x0a` clause starts (`0x0064/0x0093/0x00c2/0x00f1/0x0120`) or backward targets (`0x00db/0x00ac/0x007d/0x004e/0x001f`). The remaining live selector frontier is therefore the still-overlapped `000d:21ed` caller frame, not a normal visible caller of `0005:2c35`.
|
||||
- The sequencer lane also gained two concrete internal consumer shapes. `000d:208b` is now the instruction-verified `create one slot-backed context and materialize or forward its result` path: it builds a `0x6714` context from the caller stream state, writes immediate-flagged results straight to the out pointer, and otherwise forwards the created object through `entity_vm_opcode_finish`. `000d:21ed` is the matching `prepend inline payload and build entity-link matrix` path: it creates a context, prepends caller-owned bytes into `+0x102`, consumes the seeded `+0xd6/+0xd8` bytes as shape/count metadata, and builds repeated `entity_link` closures from the following streamed ids before the same finish path.
|
||||
- A new downstream-use pass narrows the extra-word role further. The stored offset field at context `+0x34` is now confirmed as durable object state rather than an immediate sequencer input: `000d:21ed -> 000d:22bc` does not reread it at all, `000d:498f`/`000d:4a78` serialize and reload it, and `000d:4c2d..4c4d` recomputes a slot-backed value from `(+0x32,+0x34)` into `+0x10c/+0x10e`. That shifts the remaining immortality question one step downstream: if `NPCTRIG slot 0x0a` still fits this runtime lane, it is more likely through the value reloaded from the slot-plus-offset pair than through `+0x34` as a direct clause selector.
|
||||
- The hidden pre-call span in the `000d:21ed` lane is now recovered from direct program-memory bytes as well. Window `000d:2131..21ed` reads the seeded `+0xd6/+0xd8` stream as three successive words followed by two signed bytes: word0 becomes the slot index pushed at `000d:21d4`, word1 and word2 are added at `000d:21d0` before being pushed as the dynamic additive arg at `000d:21d3`, byte3 is forwarded as the setup-data length byte, and byte4 becomes the inline-blob length used for the later prepend copy. That makes the source classification explicit: context `+0x34` is not loaded from the owner table or from the caller object at `+0xd4`; it is a computed sum of two consecutive words inside the seeded stream itself.
|
||||
- The same recovered window also tightens the upstream source layout feeding `entity_vm_context_setup`. The current caller frame base is `caller + [caller+0xd4]`, where `+0xd4` matches the saved frame offset written by `entity_vm_stack_push_frame` (`000c:f7c7`) rather than a descriptor-local field. From that frame base, `000d:21db..21e0` pushes `[frame+0x0a/+0x0c]` as a far pointer passed into `entity_vm_context_setup`, and `000d:21bd..21c8` separately derives `[frame+0x0e]` as the inline payload tail copied after context creation. So this consumer is now better modeled as one generic VM frame-record shape with two payload sources: a frame-stored far pointer plus byte-sized setup length for the initial `+0xcc` stack seed, followed by an adjacent inline tail blob with its own byte-sized length.
|
||||
- The next frame-producer pass recovers the closest non-overlapped writer feeding that lane too. Raw bytes at `000c:fbf7..fc47` (`caseD_0`) show a generic frame-record producer reading one signed placement byte from the same seeded `+0xd6/+0xd8` stream, popping a far-pointer dword from the caller stream at `[caller+0xcc/+0xce]`, computing `frame_base = caller + [caller+0xd4]`, and storing the dword at `[frame_base + placement + 0x4/+0x6]`. That means the immediate source far pointer consumed later by `000d:21ed` is already stream-backed rather than owner-row-backed; if the `000d:21ed` record uses this exact producer family for its `[frame+0x0a/+0x0c]` lane, the relevant placement byte is `0x0006`, which is the only value that lands the written dword at `+0x0a/+0x0c` and leaves the inline tail starting at `+0x0e`.
|
||||
- That stronger runtime shape weakens any claim that `000d:21ed` is already reading a descriptor-family-specific record. `NPCTRIG` slot `0x0a` still remains the best surviving descriptor-side candidate because its five-clause ladder is the only compact body that fits the row-count frontier, but the code evidence now shows the immediate input to `000d:21ed` is a generic frame-local record containing a source far pointer, a seeded slot/additive pair, and an inline tail. The remaining descriptor-side question is therefore one level earlier again: where the caller frame receives its `[frame+0x0a/+0x0c]` far pointer and whether the summed `add_a + add_b` still corresponds to a clause-base/delta pair inside `NPCTRIG` slot `0x0a` rather than to a more generic descriptor-relative offset.
|
||||
- That changes the `NPCTRIG` cross-check in one important way. `NPCTRIG` slot `0x0a` remains the strongest surviving descriptor-side hypothesis only as an upstream source for a predecoded caller-stream record, because the recovered writer consumes a caller-stream dword plus a seeded placement byte instead of indexing owner rows or descriptor tables directly. `NPCTRIG` slot `0x20` still reads as the typed/setup companion body, but neither slot is now a good fit for the immediate write into `[frame+0x0a/+0x0c]` itself.
|
||||
- One more layer of the producer path is now instruction-verified too. The setup call at `000d:4788 -> 000c:f844 -> 000c:f6e8` does not seed the new context's `+0xcc/+0xce` caller stream directly from the owner table row. Instead `entity_vm_context_setup` first allocates or reuses the object-local stream buffer at `context+0x36+0xcc`, then copies a caller-supplied setup blob from the parent frame using the far pointer/length arguments passed through `000d:46ec`. The slot/additive record returned by `entity_vm_slot_load_value_plus_offset` becomes the separate seeded `+0xd6/+0xd8` stream, while the owner-table row at `(+0x10/+0x12) + 0x0d*slot + 4` is mirrored to `0x39ca[slot]` and preserved separately in the context state.
|
||||
- The closest sibling template to `caseD_0` also sharpens the placement-byte reading. `000c:ff9f..000d:000d` reads one signed placement byte and one length byte from the same seeded `+0xd6/+0xd8` stream, then copies `len` bytes from `[frame_base + placement + 0x4]` back onto the caller stream. Together with the recovered `000d:21ed` consumer layout (`[frame+0x0a/+0x0c]` far ptr, `[frame+0x0e..]` inline tail), that makes the strongest current fit a fixed two-slot family for this record shape: `caseD_0` uses placement `0x0006` for the far-pointer dword, and the sibling blob-copier uses placement `0x000a` for the inline tail starting at `frame+0x0e`.
|
||||
- The producer side of that same record family is now tighter too. Linear raw-byte recovery across `000c:f98b..000d:000d` shows `000c:fc4b..fcbb` as the forward blob producer matching the reverse `000c:ff9f..000d:000d` case: it reads placement and length from the seeded `+0xd6/+0xd8` lane, computes `frame_base = caller + [caller+0xd4]`, and copies `len` bytes from the caller stream at `[caller+0xcc/+0xce]` into `[frame_base + placement + 0x4]`. For the `000d:21ed` record shape, that makes placement `0x000a` the best fit for the inline tail now consumed from `[frame+0x0e..]`.
|
||||
- The dword lane now has a matching reverse case as well. Raw bytes at `000c:ff1f..ff83` show the same recursive family in the opposite direction: it reads one signed placement byte from the seeded `+0xd6/+0xd8` lane, computes `frame_base = caller + [caller+0xd4]`, loads a dword from `[frame_base + placement + 0x4/+0x6]`, subtracts `4` from `[caller+0xcc]`, and writes that dword back onto the caller stream. In other words, the immediate upstream producer for the `000c:fbf7..fc47` far-pointer write can already be another frame-record copier, not a direct owner-row or descriptor-table lookup.
|
||||
- That narrows the remaining source classification again. The setup far pointer consumed by `000d:21ed` is now best modeled as a recursively propagated pointer into another VM-side byte buffer or predecoded descriptor workspace, not as the owner/resource row source mirrored separately through `0x39ca`. The owner row still matters for slot-backed state reloads, but the `entity_vm_context_setup` blob pointer itself is traveling through the frame-record family independently of that owner-row mirror.
|
||||
- That also weakens the full-tuple `NPCTRIG` fit one more notch without killing it. The surviving tuple is now better read as `(slot, add_a, add_b, setup_len, inline_len, placement=0x0006/0x000a)` feeding a generic recursive frame-record contract. `NPCTRIG` slot `0x0a` remains the strongest descriptor-side candidate only as an earlier decoder that could have produced this predecoded record family, while slot `0x20` still reads as the typed/setup companion body. No recovered instruction in the immediate `000c:f98b..000d:000d` family yet ties the setup far pointer directly back to either slot.
|
||||
- Net effect on source classification: the `000d:21ed`-relevant frame record is still not best modeled as generic VM scratch. Its immediate setup bytes are recursively copied from a parent frame record, and the wider context-build path is still anchored in descriptor-derived VM state (`+0xd6/+0xd8` from `entity_vm_slot_load_value_plus_offset`, owner-row source mirrored via `0x39ca`). What remains open is not whether this lane is scratch-backed, but which earlier decoder materializes the parent-frame far pointer before `000c:fbf7` consumes the next dword.
|
||||
- After the new reverse-case recovery, that blocker can be stated more tightly: the missing piece is no longer a generic parent-frame materializer somewhere above `000c:fbf7`, but the first non-recursive decoder that originates the far pointer before the `ff1f/ff9f -> fbf7/fc4b -> 000d:21ed` propagation chain repeats it.
|
||||
- The next pass closes that specific source-classification gap inside the same hidden interpreter body. Raw bytes at `000c:fa2f..fa5b` recover an inner opcode dispatcher that reads one opcode byte from the seeded `+0xd6/+0xd8` lane, bounds-checks it against `0x79`, and jumps through `CS:[0x3d9f + opcode * 2]`. That matters because the same local case family now exposes both the recursive frame-record replay stages and a separate set of direct caller-stream seed cases.
|
||||
- Those non-recursive seed cases are now concrete. `000c:fd51` writes one inline byte from the `+0xd6/+0xd8` control stream onto the caller stream after decrementing `[caller+0xcc]` by `1`, `000c:fd91` and `000c:fdd1` do the same for inline words, and `000c:fe11..fe59` does it for an inline dword. In the dword case the interpreter advances through four literal bytes in the control stream, subtracts `4` from `[caller+0xcc]`, and writes the literal dword directly onto the caller stream before any frame replay logic runs.
|
||||
- That makes `000c:fe11` the strongest current first non-recursive origin for the far-pointer lane later consumed by `000c:fbf7..fc47` and then by `000d:21ed`. The immediate setup far pointer is therefore no longer best modeled as coming from the owner/resource row, the mirrored `0x39ca` lane, or a generic VM scratch buffer. Its immediate compiled-side source is an inline dword literal embedded in the interpreter/control stream itself; `000c:ff1f..ff83` and `000c:fbf7..fc47` are replay stages layered on top of that literal-seeding path.
|
||||
- That retunes the `NPCTRIG` cross-check again without killing it. `NPCTRIG` slot `0x0a` still remains the best upstream descriptor-side candidate because it is still the only compact active-event body that fits the surviving slot/additive shape, and slot `0x20` still reads as the typed/setup companion. But any direct immortality mapping now has to explain how the upstream decoder turns that descriptor family into a literal-bearing VM control stream before `000c:fe11`, not how `000d:21ed` or `000c:fbf7` index descriptor rows directly.
|
||||
- One more pass tightens the creator/consumer split enough to rule out the owner row as the immediate control-stream builder. Direct instruction recovery at `000d:46ec` shows `entity_vm_context_create_from_slot_index` using the owner-table row `(+0x10/+0x12) + 0x0d*slot + 4` only for the separate `0x39ca[slot]` mirror, while the live `+0xd6/+0xd8` lane passed into `entity_vm_context_setup` still comes from `entity_vm_slot_load_value_plus_offset`. In the recovered `000d:21ed` pre-call span, that seeded lane is consumed as `word slot_index`, `word add_a`, `word add_b`, `byte setup_len`, `byte inline_len`, with `add_a + add_b` forwarded as the dynamic word stored at context `+0x34`.
|
||||
- The same pass also clarifies the setup-payload contract that feeds the later link-matrix stage. `000d:21ed` passes `[frame+0x0a/+0x0c]` as the setup far pointer into `entity_vm_context_setup`, copies `[frame+0x0e..]` as a separate inline tail, and then `000d:22bc` consumes two signed metadata bytes plus a streamed word matrix to drive repeated `entity_link` calls. The immediate source is therefore `decoded per-slot VM stream + frame replay`, not `owner-row lookup + direct descriptor row`.
|
||||
- That changes the opcode-family reading around `000c:fa2f` in a useful way even though the exact opcode indices remain unresolved in the current overlapped table view. The hidden dispatcher now has a verified immediate-literal family: `000c:fd51` pushes one inline byte to the caller stream, `000c:fd91` pushes a sign-extended byte as a word, `000c:fdd1` pushes an inline word, and `000c:fe11` pushes an inline dword. Together with the recursive replay cases `000c:ff1f` and `000c:ff9f`, that is enough to classify the upstream builder as a generic literal-bearing interpreter/control stream rather than a direct `NPCTRIG` clause reader.
|
||||
- The descriptor-side fit therefore weakens from `specific direct NPCTRIG selector` to `broader descriptor-derived VM workspace` while staying narrow enough to keep `NPCTRIG` slot `0x0a` alive as the best upstream candidate. Slot `0x0a` still matches the event-bearing compact body and its five-clause ladder remains the only surviving compact source family with a plausible row-count/additive shape, but slot `0x20` still looks like the typed/setup companion and neither slot is now a good fit for the immediate control-stream seeding logic itself.
|
||||
- The slot-load miss path now closes the workspace-materialization side of that question. Inside `entity_vm_slot_load_value` (`000d:51fd`), a cache miss triggers `000d:5066`, which first reads a slot header and then a `count * 6 + 0xc0` subentry table through the owner-resource wrapper `000d:714c`. When one subentry is still unloaded, `000d:5305..53d4` allocates a value object through `000d:3800`, then calls `000d:714c` again with the subentry source range and the new object's buffer at `+0x0a/+0x0c`; the function returns that same buffer pointer as the final `DX:AX` result. The immediate `+0xd6/+0xd8` workspace is therefore first materialized as a file-backed slot-value buffer during the slot-load miss path itself, not synthesized later from the owner-row mirror or from generic scratch state.
|
||||
- The inline-tail source is not as tightly closed yet. The same hidden case family contains several immediate scalar caller-stream seed cases, so the `000d:21ed` tail at `[frame+0x0e..]` can now plausibly be assembled from control-stream literals or from another nearby non-recursive payload case rather than from a direct owner-row read. No instruction recovered in `000c:f98b..000d:000d` performs a matching direct descriptor-row lookup for that tail.
|
||||
- Net effect from this pass: the missing outer selector into `entity_vm_opcode_sequence_run` is still unresolved, but the lane is no longer just one opaque dispatcher plus dark wrappers. It now has a verified generic masked-context creation hub, wider caller-family anchors for that hub, and two internally differentiated sequencer consumer blocks built directly on `entity_vm_context_create_from_slot_index`.
|
||||
|
||||
#### Follow-up: four newly surfaced direct `000d:463a` callers
|
||||
|
||||
- `0004:f033` (`0x8000:0x0007`) now reads as a generic gameplay-side materialization lane rather than a state-transition helper. When the local seg021 class-nibble query returns `8`, the wrapper bypasses the VM path and returns object word `+0x02` directly from the locally produced object. Otherwise it forwards through `entity_vm_context_try_create_masked_for_entity` and returns the created object's word `+0x02` on success.
|
||||
- `0004:f05c` (`0x2000:0x0015`) stays on the gameplay-state side too, but with a stronger caller role. The only current direct caller window at `0004:f2b3` reaches it after overlap/proximity tests and entity byte `+0x32` toggling, so the safest reading is still `stateful gameplay materialization lane`, not `descriptor selector`.
|
||||
- `entity_vm_context_try_create_mask_0008_slot30_with_offset` (`0006:0ba4`) adds the first strong non-`0005` extra-payload lane. It passes mask `0x0008:0x0030` plus one caller word into `000d:463a`; on failure it drops into `0006:0cfa`, which copies class-detail word `+0x02` to `+0x04`, derives a replacement selector from class-detail words `+0x06/+0x08/+0x0a` or the caller value, may clear flag `0x08` through `entity_class_clear_flag8_and_dispatch`, and then continues into the local state-transition/dispatch table. That is concrete evidence that at least one extra-word masked lane is feeding class-state transition materialization rather than a free-standing VM selector root.
|
||||
- `entity_vm_context_try_create_mask_0010_slot08_with_offset_if_ready` (`0006:108c`) provides the second strong extra-payload lane. It passes mask `0x0010:0x0008` plus one caller word into `000d:463a`, but only after local readiness gates through `0006:ffed` plus the seg021 availability/flag8-clear path. Unlike the earlier looser reading, the helper itself does not fall back to `0006:13b0` or `0006:13e4`; on miss it simply returns `0`. That makes the function a guarded masked-materialization attempt, while the neighboring `0006:13b0/13e4 -> 0006:07c0` class-linked lookups remain adjacent family evidence rather than a direct local fallback inside `0006:108c`.
|
||||
- Taken together, the new seg004 and seg006 callers strengthen the existing read of the still-dark wrappers `0005:2c35` (`0x0400:0x000a`) and `0005:2c68` (`0x0800:0x000b`). Those wrappers still have no direct caller evidence, but they now sit inside a larger verified subfamily of `extra-word masked materializers` whose known members feed state selectors, class-linked values, or other gameplay-side payload resolution instead of acting as the real upstream selector into `entity_vm_opcode_sequence_run`.
|
||||
- MCP-native function xrefs now reinforce that stopping point rather than changing it: `entity_vm_context_try_create_masked_for_entity` reports the expected direct callers through `0004:f047`, `0004:f076`, the named `0005` wrapper island, and the two seg006 callsites `0006:0bbc` / `0006:10e7`, while `entity_vm_opcode_sequence_run` plus the dark `0x0400/0x000a` and `0x0800/0x000b` wrappers still surface no direct function-xref callers in the current database. The best next path therefore remains caller-frame recovery or nearby unnamed-function repair, not another generic masked-hub sweep.
|
||||
|
||||
| `000c:f844` | `entity_vm_context_setup` | Calls `entity_vm_stack_init_with_data`, then sets `+0xd6..+0xe3` with position/dimension/state params |
|
||||
| `000c:f600` | `entity_vm_pair_stack_push` | Push (word_a, word_b) onto 31-entry array at `[ptr+0x80]` (count); error if full |
|
||||
| `000c:f63c` | `entity_vm_pair_stack_pop` | Pop and return word from pair stack; error if empty |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue