Crusader_Decomp/docs/raw-0008-000c.md
MaddoScientisto 4d3c8cd81b Add detailed class event processing and family comparison tools
- Enhance `extract_eusecode_flx.py` to derive class event rows with additional metadata including derived body windows and repeated template statuses.
- Introduce `usecode_family_compare.py` for comparing event families, analyzing commonalities in event bodies, and generating reports on identical groups and differences.
- Implement new data structures for managing class event rows and family artifact specifications.
- Update output formats to include derived body information and repeated family regression checks.
- Ensure robust validation of repeated family expectations against actual extracted data.
2026-03-22 23:24:46 +01:00

55 KiB
Raw Blame History

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 entity_state_check_field49_and_call_vfunc3c Checks field [ptr+0x49]: 1→reset to 0 return 1; 2→call vtable[0x3c] return 0; else thunk dispatch
000c:b153 entity_state_animation_done_tick Checks [param_2+0x14+0xa] animation-complete flag; if zero increments field49 and calls entity_state_check_field49_and_call_vfunc3c; if set calls vtable[0x3c]
000c:b199 entity_state_input_key_handler Full input dispatcher: ESC/x/X → vtable[0x3c] (cancel); Left/Right arrows 0x14b/0x148 → prev state; n/N/0x14d/0x150 → next state; e/E → set field47=1; - with counter → trigger at 4. Manages field47 and field49
000c:b2c3 stub_noop_000c_b2c3 Empty stub; returns immediately
000c:b2c8 entity_state_dispatch_if_field49_eq4 Fires thunk only when [ptr+0x49]==4
000c:b349 entity_state_dispatch_if_far_ptr_nonzero_a Fires thunk if far-pointer args non-zero
000c:b383 entity_state_set_field3f_and_dispatch If non-NULL: writes &DAT_0000_2d18 to [ptr+0x3f], then dispatches
000c:b3d8 entity_state_dispatch_if_far_ptr_nonzero_b Same null-guard pattern as b349, variant b

Patterns confirmed:

  • field49 = state-sequence index; 0=reset, 2=vtable callback, 4=triggered end
  • field47 = keystroke-combo counter
  • field3f = linked data pointer (event/record reference)
  • [0x6054] = current entity handle; [0x6828] = 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 (Roffset, Goffset, Boffset) clamped to 0 to VGA I/O 0x3c8/0x3c9; decrements [0x630d] by step [0x6316]; clears active at [0x630a] when black
000c:ce57 palette_fade_step_up Same loop, adds offset, clamps at 63 (0x3f full VGA). Clears [0x630a] when fully bright

Globals used: [0x6312]=start index, [0x6314]=count, [0x630e]=palette src ptr, [0x630d]=brightness offset, [0x6316]=step, [0x630a]=active flag.

Entity Mini-VM / Record-Player Context

Address Name Evidence
000c:f6b8 record_table_get_by_index Bounds check param < [0x8c88]; return word at [0x8c84 + param*4]. Table at 0x8c84
000c:f6e8 entity_vm_stack_init_with_data Init stack ptrs at [ptr+0xcc..+0xd4] pointing to self; max depth 199; copies optional initial data
000c:f772 entity_vm_state_copy Copies 200 bytes (100 words from [src+4] to [dst+4]), then copies 4 words at +0xcc..+0xd2
000c:f7c7 entity_vm_stack_push_frame Push call-frame: saves ret offset at [ptr+0xd4], decrements [ptr+0xcc] by param_size, zeroes new frame

Current EUSECODE / event bridge notes

  • entity_vm_set_value_from_slot_plus_offset (000c:f95f) now provides a concrete bridge from the 000c mini-VM cluster into the 000d event/countdown lane:
    • it calls FUN_000d_5572(*(word *)0x6611, *(word *)0x6613, param_3, param_4, 0, 0)
    • then stores the returned far pair into target object fields +0xd6/+0xd8
  • entity_vm_slot_load_value_plus_offset (000d:5572) is a thin wrapper over entity_vm_slot_load_value (000d:51fd), but the previously suspicious PUSH 0x410 path at 000d:5290 is now reclassified: it pushes 0x410, DS, and 0x6616 into the seg091 fatal-report helper at 000a:44fd, so this is an error/assert path rather than a live gameplay event dispatch.
  • This closes the earlier compiled-code immortality bridge from 000c:f95f into 000d:51fd. The verified bridge that remains is the data/value handoff into the context +0xd6/+0xd8 lane, not a direct event 0x410 producer.
  • Supporting renamed helpers in the same lane now include:
    • entity_vm_slot_find_or_select (000d:4e7c): scans 0x26-byte slot records, returns a matching slot id when present, and tracks one fallback slot for reuse/eviction
    • entity_vm_slot_decrement_use_count (000d:558d): decrements one slot-use counter and traps on underflow
    • entity_vm_slot_release_value (000d:5617): releases one slot value, restores the owner's 0x1300/0x1302 budget pair, writes the slot state back to -1, and notifies through 000a:2b9d
    • entity_vm_opcode_finish (000d:3350): shared VM opcode epilogue used by the 000d:039f, 000d:08a2, 000d:0988, 000d:177c, and 000d:1acb handlers; if the local result slot is non-zero it writes the current referent id to 0x8c94, optionally pops one slot_array frame through 0x659c/0x659e, and returns the opcode result from local state
    • entity_vm_referent_chain_remove_matching_from (000d:6a9a): destructive chain-difference helper used by the 0x1a/0x1b opcode path in 000d:0988; it walks one source chain against a destination chain, removes matching entries in place, and frees removed registry nodes / indirect payloads
    • entity_vm_referent_chain_set_entry_data_at (000d:6cf6): finds one chain entry by index and overwrites its payload in place, including indirect/string cleanup when the chain uses indirect storage
  • The surrounding runtime/context family is now materially clearer too:
    • entity_vm_runtime_create / entity_vm_runtime_init_slots / entity_vm_runtime_release_slots / entity_vm_runtime_destroy (000d:4c99, 000d:4d36, 000d:4d75, 000d:4e01) are the global 0x6611 owner for this lane; they allocate the 0x2040-byte runtime body, clear the 0x80-entry slot table, manage the runtime budget/default fields at +0x1300..+0x1314, and retain one owner/resource object at +0x1315/+0x1317 returned by 000d:7000
    • entity_vm_slot_index_from_entity (000d:45c5) computes one slot index from a gameplay entity by branching on seg021 class/type helpers and then adding one of the current runtime base offsets 0x8c7c/0x8c7e/0x8c80
    • entity_vm_context_try_create_masked_for_entity (000d:463a) uses that slot index to test one owner-side mask entry before it creates a context, which is the strongest current bridge from gameplay entities into this VM lane
    • entity_vm_context_create_from_slot_index (000d:46ec) allocates one 0x6714 context object, seeds its +0xd6/+0xd8 lane through entity_vm_slot_load_value_plus_offset, initializes the local mini-VM state, and can prepend caller data into the backward-growing buffer at +0x102
    • entity_vm_context_sync_global_value_and_dispatch (000d:48da) is the current context-side runner/sync point: it marks the context busy at +0x123, calls entity_vm_set_field_da_to_global, optionally writes the current value through +0x11b/+0x11d, and dispatches through the context vtable on success
    • entity_vm_context_save / entity_vm_context_load / entity_vm_context_destroy / entity_vm_context_free_buffer (000d:498f, 000d:4a78, 000d:4962, 000d:48b6) now pin down the lifecycle of this object family rather than leaving the whole 000d:45xx..4exx island anonymous
  • entity_vm_context_try_create_masked_for_entity is now better constrained at the return-value level too: after the runtime-disable check at 0x6610 and the owner-side slot-mask test succeed, it reports two distinct success shapes. Immediate-flagged contexts (+0x16 & 0x0008) clear the caller output word, while object-backed contexts return the created object's low word. That makes the helper a typed bridge from gameplay entities into VM-backed object results, not only a yes/no mask probe.
  • entity_vm_runtime_owner_resource_create (000d:7000) is now one step tighter too: the embedded seg069/070 helper is file-backed rather than abstract. Construction starts with dos_file_handle_init (0009:1c00), then uses helper vtable slot +0x04 as the size query that drives the child +0x10/+0x12 allocation and helper vtable slot +0x0c as the table-population callback for the 0x0d-stride owner table.
  • That file-backed helper is now tighter one step deeper as well. The seg070 loops rooted at raw windows 0009:67b6 and 0009:6916 walk helper-owned record arrays at object +0x10/+0x18, format per-entry paths through the seg001 string helpers (0003:e4d3 / 0003:e590), then open, read, and close each file through file_handle_alloc_init_and_open (0009:1c3a), dos_file_seek (0009:2034), and dos_file_close (0009:1e61). 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.
  • The caller-side bootstrap for that helper is now anchored too: entity_vm_runtime_init_from_path_if_configured (000d:44df) first checks the configured byte/string global at 0x65a, builds a path through seg072 helper 0009:3600 using globals 0x6d6:0x6d8 plus 0x65a, validates that path through 000a:500a, then calls entity_vm_runtime_create(0,0,path). This is the first verified source-argument path for entity_vm_runtime_owner_resource_create, and it strongly suggests the owner/resource table is loaded from an external configured file rather than from a purely in-memory descriptor blob.
  • Seg072 helper 0009:3600 is now classified more tightly as a rotating slash-aware path composer rather than a generic buffer advance helper. Its prologue cycles through five 0x50-byte temp buffers, and its inner cases append optional string parts while inserting \ only when adjacent path components need a separator. That narrows the two globals used by 000d:44df: 0x65a behaves as the configured relative runtime-owner filename/path component, while 0x6d6:0x6d8 behaves as the mutable base/resource-root path buffer that gets joined with 0x65a before 000a:500a validation.
  • The two still-xref-dark wrappers 0005:2c35 and 0005:2c68 are also narrower now. Their signed extra word does not participate in owner-mask selection inside entity_vm_context_try_create_masked_for_entity; it is forwarded into entity_vm_context_create_from_slot_index, stored in context field +0x34, and passed on to entity_vm_slot_load_value_plus_offset. The best current reading is therefore offset-specialized masked context creation, not a separate direct selector lane.
  • The first opcode-level behavior split inside that runtime is now visible in the 000d:0988 family:
    • one branch calls entity_vm_referent_chain_append_unique_from, which looks like an attach/union operation on the current referent payload chain
    • the 0x1a/0x1b branch instead calls entity_vm_referent_chain_remove_matching_from, which looks like the inverse operation and makes the opcode family materially closer to a graph-editing script VM than a flat event list
    • both paths return through entity_vm_opcode_finish, so the referent-global write to 0x8c94 is now better understood as a shared interpreter epilogue rather than a unique quirk of one helper
  • One additional runtime layer is now named under that context family: the referent registry at 0x8c8c/0x8c8e/0x8c90/0x8c94.
    • entity_vm_referent_registry_init / entity_vm_referent_registry_destroy (000d:6000, 000d:60bf) allocate and free the registry buffer, seed the free-list/root metadata, and clear the current referent id at 0x8c94
    • entity_vm_referent_registry_alloc (000d:613e) allocates one registry node from the free list and stores the current referent id from 0x8c94 into node field +0x04
    • entity_vm_referent_registry_release_by_id / entity_vm_referent_registry_free_node (000d:6251, 000d:62ac) release all live nodes for one referent id and coalesce adjacent free nodes
    • this makes entity_vm_set_field_da_to_global more important than it first looked: it writes 0x8c94 from the current context +0xda lane and then immediately enters the still-misaligned 000c:3350 body, so the referent selected by the context is now visibly feeding runtime registry state
    • the registry nodes are not flat scalars only; the surrounding container helpers are now named too:
      • entity_vm_referent_chain_copy / entity_vm_referent_chain_append_unique_from (000d:6694, 000d:68c3) build deep-copied or deduplicated chains of referent-linked payloads
      • entity_vm_referent_chain_destroy, entity_vm_referent_chain_next, and entity_vm_referent_chain_append_node (000d:6602, 000d:6651, 000d:687b) provide the list management shell
      • entity_vm_referent_chain_contains_entry, entity_vm_referent_chain_get_entry_data_at, and entity_vm_referent_chain_get_indirect_data (000d:6c31, 000d:67f2, 000d:6860) show that some chains carry fixed-size inline payloads while others carry indirect string-like payload nodes
  • This matters for script readability: the current runtime model is no longer only "one referent id hits one event." It now supports a more useful intermediate representation where one referent anchor can own one or more payload chains, and neighboring event-bearing descriptors can attach behavior to that anchor without duplicating the anchor's own record.
  • Nearby descriptor work on EUSECODE.FLX is consistent with that model: event-bearing classes (EVENT, NPCTRIG, SFXTRIG, several *_BOOT records) use a stable 69:0A00 -> event tag, while JELYHACK / JELYH2 remain referent-only descriptors in a neighborhood that includes TRIGPAD, SPECIAL, REE_BOOT, SURCAMEW, and SFXTRIG.
  • The strongest current callsites into this context-construction path are the large 000d:208b and 000d:21ed bodies, which both feed per-object stream/data state from +0xcc/+0xce into entity_vm_context_create_from_slot_index before continuing bytecode-style reads from the newly seeded +0xd6/+0xd8 lane. That makes the 000d interpreter/object lane a better current immortality target than the older 000e text-parser hypothesis.
  • The immediate producer chain for that +0xcc/+0xce stream state is now one layer tighter:
    • entity_vm_context_create_from_slot_index (000d:46ec) allocates the 0x6714 context, then calls entity_vm_context_setup (000c:f844) on the embedded mini-VM object at context +0x36.
    • entity_vm_context_setup delegates to entity_vm_stack_init_with_data (000c:f6e8), which seeds [mini_vm+0xcc..+0xd2] to point into the object's own payload area and optionally prepends caller-owned inline bytes by moving the stack pointer backward.
    • entity_vm_state_copy (000c:f772) copies that same +0xcc..+0xd2 stream/base quartet verbatim when one mini-VM object is cloned.
    • Upstream of the setup helper, 000d:46ec derives the source payload from the runtime owner table behind 0x6611 -> +0x1315/+0x1317: with slot index SI, it walks owner table *(owner+0x10/+0x12) + 0x0d*SI + 4, passes that far pointer into 000c:f844, and mirrors the resulting per-slot source into 0x39ca[slot].
  • This sharpens the current JELYHACK-side model rather than overturning it: the code-side producer recovered in this batch is still a generic slot-backed VM source object keyed by gameplay-entity slot selection and owner-side mask bits, not a direct hard-coded descriptor-class switch on JELYHACK or JELYH2. Combined with the extractor evidence that JELYHACK / JELYH2 remain referent-only while REE_BOOT / SFXTRIG keep active event tags and SURCAMEW keeps eventTrigger, the better fit is still referent anchor -> slot-backed payload chain -> neighboring event-bearing attachment.
  • The 0x39ca mirror question is 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 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:

  • 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.

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)

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)

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

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 FUN_000d_ebe3 rather than the disproven 000a:44fd hypothesis.
  • Net effect: the active-event ecosystem fit is reinforced by direct caller behavior and payload shapes, but final slot-to-descriptor ownership still requires real caller-role recovery for the remaining xref-dark entry points.

| 000c:f844 | entity_vm_context_setup | Calls entity_vm_stack_init_with_data, then sets +0xd6..+0xe3 with position/dimension/state params | | 000c:f600 | entity_vm_pair_stack_push | Push (word_a, word_b) onto 31-entry array at [ptr+0x80] (count); error if full | | 000c:f63c | entity_vm_pair_stack_pop | Pop and return word from pair stack; error if empty | | 000c:f94f | entity_vm_counter_add | [ptr+0xd6] += param_2; simple accumulator | | 000c:f95f | entity_vm_set_value_from_slot_plus_offset | Calls entity_vm_slot_load_value_plus_offset and writes the resulting 32-bit value into [ptr+0xd6/+0xd8] | | 000c:f98b | entity_vm_set_field_da_to_global | Writes [param_2+0xda far-ptr + 2] into [0x8c94] |

VM field offsets: +0xcc=VM stack ptr, +0xce/+0xd0=segment regs, +0xd2=base, +0xd4=frame depth, +0xd6/+0xd8=32-bit VM value/counter lane, +0xda/+0xdc=additional VM pointer/bounds lane. The 200-byte body region at [ptr+4..+0xcc] holds 100 words of script/state payload. The pair-stack (field +0x80) is separate — likely pass/return value stack for the mini-script.


Raw 000c Cursor Zone / Slot Array / String Queue Batch

Cursor / Directional Zone Classifier

Address Name Evidence
000c:e6d9 cursor_zone_quadrant_classify Splits screen by [0x63d6]/2 and [0x63d8]/2 vs bounds [0x8c6c..0x8c72]; returns directional code from 9-word table at 0x6401

Zone table layout (9 entries): NW/N/NE / W/Center/E / SW/S/SE based on horizontal threshold at 0x8c6c/0x8c70 and vertical at 0x8c6e/0x8c72.

Slot Array System

A complete 29-slot menu/choice array with fixed stride 0x15 bytes, base at [ptr+0x67], count at [ptr+0x7a]:

Address Name Evidence
000c:ea53 entity_slot_count_update_and_notify Sets [ptr+0x72]=param-1; calls slot_array_get_current_entry and slot_array_find_and_dispatch; calls vtable[0]() when +0x75 flags set
000c:eba5 slot_array_dispatch_matching Walks 0xb-stride array from [ptr+4]; calls thunk for each entry where [entry+9]==param_4
000c:ec30 slot_array_dispatch_if_nonempty Returns 0xffff if count < 1; else dispatches
000c:ec9e slot_array_find_and_dispatch Searches 0xb-stride array for [entry+9]==param_4; calls thunk on first match
000c:ecf5 slot_array_push_entry Copies named string to [base+0xc]; writes 6 param words at +0x12..0x20; increments count
000c:edb0 slot_array_push_raw Copies 0x15-byte raw entry from param_2; increments count
000c:edf7 slot_array_pop Decrements [ptr+0x7a]; asserts >= 0
000c:ee19 slot_array_init Sets [ptr+0x78]=0, [ptr+0x76]=0, [ptr+0x75]=1 (active flag)
000c:ee32 slot_array_clear_flags Clears [ptr+0x74]=0, [ptr+0x75]=0
000c:ee44 slot_array_get_current_entry Returns ptr + [ptr+0x7a]*0x15 + 0x67 (current entry ptr); 0 if count <= 0

String Queue

Address Name Evidence
000c:eadd string_queue_push Appends string to 10-entry queue at [ptr+4]; count at [ptr+2]; sets [ptr+0xd]=param_4

Additional VM-Adjacent Helpers

Address Name Evidence
000c:f2e7 entity_call_vtable_entry_10_if_valid Null-guard: calls (*[ptr+8+0x10])() if param_1 non-null
000c:f39f string_table_lookup Searches [0x65bc/0x65be] table by key string; returns matching words to out-params

Raw 000c Cursor Nav Dispatcher / State Reset Batch

Cursor navigation subsystem in 000c:d3e9000c:db68. Manages directional zone changes, mouse button reads, keyboard scancodes, and entity vtable dispatch for interactive UI cursors.

Cursor Navigation Fields (entity object offsets)

Offset Purpose
+0x32 Current zone code (08)
+0x33 Previous zone code
+0x37+0x3a Directional booleans: N/S/W/E
+0x3f+0x42 Mouse button flags
+0x45 Last keyboard scancode
+0x47 Navigation index

Globals: [0x63da] = mouse button state, [0x63d6]/[0x63d8] = cursor X/Y, [0x638e] and [0x6346] = reference data tables.

Address Name Evidence
000c:dac1 cursor_nav_state_reset Zeros all directional/button flags; sets [+0x32/+0x33]=0xff, [+0x47]=0xffff