Crusader_Decomp/docs/raw-0008-000c.md
MaddoScientisto 3daffbf113 Add extractor for Crusader's EUSECODE.FLX container
- Implemented a Python script to extract data from the EUSECODE.FLX file format.
- Defined data structures for candidate entries and extracted chunks using dataclasses.
- Added functions to read and parse the FLX table, extract candidate data, and generate human-readable output files.
- Included functionality for analyzing extracted data, including generating summaries, descriptors, and event family reports.
- Implemented utilities for calculating printable ratios, zero ratios, and identifying text-like data.
- Added support for writing various output formats, including JSON, TSV, and Markdown.
2026-03-22 14:27:38 +01:00

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

Patterns confirmed:

  • field49 = state-sequence index; 0=reset, 2=vtable callback, 4=triggered end
  • field47 = keystroke-combo counter
  • field3f = linked data pointer (event/record reference)
  • [0x6054] = current entity handle; [0x6828] = another global entity far pointer
  • Bits in [ptr+0x5b]: 0x1=init, 0x2=active/event, 0x40=pending dispatch, 0x100=flag100, 0x180=skip-all mask

Raw 000c Palette Fade + Entity VM Cluster

VGA Palette Fade

Address Name Evidence
000c:cdde palette_fade_step_down Writes (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), and entity_vm_slot_load_value contains a verified PUSH 0x410 path at 000d:5290 before calling the unresolved seg091 event/abort lane at 000a:44fd.
  • This is not enough yet to say that entity_vm_set_value_from_slot_plus_offset is the immortality trigger, but it does show that the 000c mini-VM / record-player cluster can hand work directly into a 000d helper that emits event 0x410.
  • 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.
  • 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.
  • 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. | 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