Crusader_Decomp/docs/entity-vm-runtime-owner-resource-layout.md

21 KiB

Entity VM Runtime And Owner-Resource Layout

Purpose

This note gathers the current class-lift-relevant structure for the VM runtime lane into one place.

It focuses on four connected objects:

  • EntityVmRuntime
  • EntityVmOwnerResource
  • EntityVmContext
  • the slot/value helpers that connect gameplay entities to owner-loaded VM source data

The goal is not full opcode recovery. The goal is to make later class authoring and C++ skeleton emission faster by freezing the current ownership model.

High-Level Ownership Model

Current best model from docs/raw-0008-000c.md and docs/raw-000a-000d.md:

  1. startup path resolves a configured EUSECODE root/path
  2. entity_vm_runtime_create allocates the main runtime body
  3. runtime constructor attaches one file-backed helper created by entity_vm_runtime_owner_resource_create
  4. gameplay entities map to slot indices through entity_vm_slot_index_from_entity
  5. masked-create helpers test owner-side capability bits and then build per-entity or per-slot EntityVmContext objects
  6. contexts seed their local stream/value state from owner-loaded source rows and runtime slot caches

EntityVmRuntime

Strong anchors:

  • 000d:44df entity_vm_runtime_init_from_path_if_configured
  • 000d:4c99 entity_vm_runtime_create
  • 000d:4d36 entity_vm_runtime_init_slots
  • 000d:4d75 entity_vm_runtime_release_slots
  • 000d:4e01 entity_vm_runtime_destroy

Current strongest structural claims:

  • runtime body is the global owner behind 0x6611/0x6613
  • front region behaves like a 0x80 entry slot table with stride 0x26
  • tail region around +0x1300..+0x1318 holds runtime budget/default metadata plus the owner-resource helper pointer
  • helper attachment lives at +0x1315/+0x1317

Live runtime-helper classification added during the 2026-04-05 MCP-upgrade pass:

  • 1420:167c Remorse::EntityVmRuntime::AcquireSlotForEntity
    • scans the 0x80-entry slot table for an existing entity match or the first free slot
    • can evict the currently selected slot via the owner-row iterator when the runtime needs to recycle one
  • 1420:1866 Remorse::EntityVmRuntime::InitSlotOwnerBuffers
    • reads owner-resource metadata for one slot id
    • allocates the two slot-local working buffers later stored at +0x1e/+0x20 and +0x22/+0x24
    • seeds the sentinel-filled chunk-state array used by later lazy chunk loads
  • 1420:19fd Remorse::EntityVmRuntime::EnsureSlotChunkLoaded
    • lazily materializes one indexed owner chunk for a slot into runtime memory
    • marks the chunk present through the slot-local state arrays and updates the runtime-wide budget counters
  • 1420:1f24 entity_vm_runtime_apply_to_matching_owner_rows
    • runtime-owned iterator over the owner list, used both for broad cleanup and for filtered slot/category work
  • 1420:2040 entity_vm_slot_entry_create_or_clear
    • allocates or clears one 0x26-byte slot entry record
    • gives the current strongest live evidence for the stable slot-entry size and the buffer/cache lane at +0x1e..+0x24

Current safe class role:

  • long-lived VM root object that owns slot state, owner resource, category-base words, and runtime-wide value budgets

EntityVmOwnerResource

Strong anchors:

  • 000d:7000 entity_vm_runtime_owner_resource_create
  • 000d:70fd entity_vm_runtime_owner_resource_destroy

Best current helper shape:

  • compact file-backed helper object
  • helper-owned count at +0x14
  • far-pointer table at +0x10
  • paired 16-bit table at +0x18
  • helper vtable +0x04 acts as size query
  • helper vtable +0x0c materializes the 0x0d-stride owner rows later consumed by contexts

Current safest interpretation:

  • this is the most bounded class-lift target in the VM lane
  • it looks like a real helper object with a compact stable layout and a clear owner relationship to EntityVmRuntime

seg070 loader contract

The paired loops rooted at raw windows 0009:67b6 and 0009:6916 are current best evidence that the helper is file-backed rather than a pure in-memory descriptor copier.

Verified behavior already captured in the main notes:

  • iterate helper-owned count at +0x14
  • index path/id tables at +0x10 and +0x18
  • build formatted paths with two distinct format strings
  • open, seek/read, close, and free loop-local buffers through the DOS/file helper lane

Current caution:

  • exact per-family record schema is still open, so the helper should be modeled as a loader/index object first, not as a final descriptor-schema class.

EntityVmContext

Strong anchors:

  • 000d:463a entity_vm_context_try_create_masked_for_entity
  • 000d:46ec entity_vm_context_create_from_slot_index
  • 000d:48b6 entity_vm_context_free_buffer
  • 000d:48da entity_vm_context_sync_global_value_and_dispatch
  • 000d:4962 entity_vm_context_destroy
  • 000d:498f entity_vm_context_save
  • 000d:4a78 entity_vm_context_load

Current safe role:

  • per-entity or per-slot execution/context object built from runtime slot state plus owner-loaded source data

Current layout claims that matter for class lifting:

  • +0x32 stores slot index
  • +0x34 stores the additive offset word used by the slot_load_value_plus_offset lane
  • +0x36 embeds the mini-VM/state object
  • +0xd6/+0xd8 hold the seeded source/control stream lane
  • +0x102 is the backward-growing local payload/buffer lane
  • +0x10c/+0x10e store a derived low/high pair reused by save/load
  • +0x117/+0x119 cache the owner-linked source pair
  • +0x123 behaves as a busy or active flag in the sync/dispatch path

Current caution:

  • context dispatch semantics are still active work, so this object should be modeled around lifecycle and data ownership first, not around final method names for every opcode-facing helper.

Gameplay Entity To VM Bridge

Slot selection

entity_vm_slot_index_from_entity (000d:45c5) is the key bridge from gameplay entity identity into the VM lane.

Current safest summary:

  • it does not choose NPCTRIG versus EVENT directly
  • it maps gameplay entities into category spans using runtime base words such as 0x8c7c/0x8c7e/0x8c80
  • owner-row capability bits and later slot-value materialization do the next stage of filtering

Masked-create helpers

The masked-create family is already class-lift relevant even before final event labels are known.

What is safe now:

  • the hub at 000d:463a checks runtime-disable state and owner-side mask bits
  • low-slot and high-slot wrappers differ by slot id, mask, and whether they pass an extra signed/additive word
  • wrappers like slot 0x0a / 0x0b are offset-specialized context creators, not separate selector universes

This means future Ghidra or C++ modeling should treat them as helper factories around EntityVmContext, not as methods on unrelated gameplay classes.

Best Class-Lift Targets In This Lane

  1. EntityVmOwnerResource
  2. EntityVmRuntime
  3. EntityVmContext

Why this order:

  • owner-resource helper is compact and structurally bounded
  • runtime has clear ownership over the helper and slot table
  • context has the richest semantics but also the most unresolved dispatcher behavior

Live Ghidra Authoring Status

Verified first batch landed in the live CRUSADER.EXE session on 2026-04-05.

  • Created namespace Remorse and class owners Remorse::EntityVmOwnerResource, Remorse::EntityVmRuntime, and Remorse::EntityVmContext.
  • Created provisional datatype /Remorse/EntityVmOwnerResource with current stable anchors:
    • +0x10 owner_row_table
    • +0x14 entry_count
    • +0x18 entry_word_table
  • Created provisional datatype /Remorse/EntityVmRuntime with size 0x1319 and only the currently stable tail anchors around the owner-resource attachment lane.
  • Created provisional datatype /Remorse/EntityVmSlotEntry with size 0x26 and only the currently stable tail buffer fields named:
    • +0x1e owner_buffer_offset
    • +0x20 owner_buffer_segment
    • +0x22 chunk_state_offset
    • +0x24 chunk_state_segment
  • Moved 1430:0000 under Remorse::EntityVmOwnerResource::Create.
  • Moved 1430:00fd under Remorse::EntityVmOwnerResource::Destroy.
  • Moved 1420:1499 under Remorse::EntityVmRuntime::Create.
  • Moved 1420:1536 under Remorse::EntityVmRuntime::InitSlots.
  • Moved 1420:1575 under Remorse::EntityVmRuntime::ReleaseSlots.
  • Moved 1420:1601 under Remorse::EntityVmRuntime::Destroy.
  • Moved 1420:167c under Remorse::EntityVmRuntime::AcquireSlotForEntity after live decompilation showed a 0x80-entry scan over the runtime slot table with free-slot fallback and eviction of the currently selected slot.
  • Moved 1420:1866 under Remorse::EntityVmRuntime::InitSlotOwnerBuffers after live decompilation showed owner-resource reads plus the two slot-local buffer allocations and initial sentinel fill.
  • Moved 1420:19fd under Remorse::EntityVmRuntime::EnsureSlotChunkLoaded after live decompilation showed per-slot chunk materialization and cache-presence marking.
  • Renamed 1420:1cca to entity_vm_runtime_debug_dump_slot_memory after live decompilation showed a debug-gated walk of the runtime slot list with slot memory usage output.
  • Renamed 1420:1f24 to entity_vm_runtime_apply_to_matching_owner_rows after live decompilation showed filtered iteration over the runtime owner-row list.
  • Renamed 1420:2040 to entity_vm_slot_entry_create_or_clear after live decompilation showed allocation and zeroing of one 0x26-byte slot record.
  • Added short decompiler comments at 1430:0000 and 1430:00fd to preserve the raw 000d:7000 / 000d:70fd provenance.
  • Added short decompiler comments at 1420:1499, 1420:1536, 1420:1575, and 1420:1601 to preserve the runtime-lifecycle provenance and current layout claims.
  • Added short decompiler comments at 1420:167c, 1420:1866, 1420:19fd, 1420:1cca, 1420:1f24, and 1420:2040 so the slot-table evidence stays visible in the live database.
  • Repaired the decompiler health of 1420:1499 Remorse::EntityVmRuntime::Create after the delete/recreate cycle left it throwing Low-level Error: Symbol $$undef00000006 extends beyond the end of the address space; the root cause was the shared allocator helper at 1000:42e2, whose pointer-return signature decompiled with a hidden __return_storage_ptr__ and poisoned the caller stack model until it was normalized to an explicit dword return plus explicit stack storage.
  • Verified second batch landed in the live CRUSADER.EXE session on 2026-04-06.
  • Moved 1420:0eec under Remorse::EntityVmContext::CreateFromSlotIndex.
  • Moved 1420:10b6 under Remorse::EntityVmContext::FreeBuffer.
  • Moved 1420:10da under Remorse::EntityVmContext::SyncGlobalValueAndDispatch.
  • Moved 1420:1162 under Remorse::EntityVmContext::Destroy.
  • Moved 1420:118f under Remorse::EntityVmContext::Save.
  • Moved 1420:1278 under Remorse::EntityVmContext::Load.
  • Added short decompiler comments at 1420:0eec, 1420:10b6, 1420:10da, 1420:1162, 1420:118f, and 1420:1278 to preserve the raw 000d:46ec, 000d:48b6, 000d:48da, 000d:4962, 000d:498f, and 000d:4a78 provenance after the class-owner move.
  • Verified third batch landed through local PyGhidra on 2026-04-06 after the live run_write_script(...) route still returned 404 No context found for request against the active GUI session.
  • Created provisional datatype /Remorse/EntityVmContext with size 0x124 and the currently safest stable anchors:
    • +0x32 slot_index
    • +0x34 value_add_offset
    • +0xd6/+0xd8 source_stream_offset/source_stream_segment
    • +0x10c/+0x10e derived_pair_lo/derived_pair_hi
    • +0x117/+0x119 owner_source_offset/owner_source_segment
    • +0x123 busy_flag
  • Updated 1420:2040 entity_vm_slot_entry_create_or_clear to EntityVmSlotEntry * __cdecl16far entity_vm_slot_entry_create_or_clear(EntityVmSlotEntry * slot_entry) so the slot-record helper no longer falls back to anonymous byte * parameters.
  • Updated 1420:167c Remorse::EntityVmRuntime::AcquireSlotForEntity to return EntityVmSlotEntry *, while leaving the third param_3 entity-like pointer conservative until caller-side role recovery is tighter.
  • Updated 1420:1866 Remorse::EntityVmRuntime::InitSlotOwnerBuffers so the third parameter is now EntityVmSlotEntry * slot_entry.
  • Updated 1420:1536 Remorse::EntityVmRuntime::InitSlots to void __cdecl16far InitSlots(EntityVmRuntime * this).
  • Updated 1420:1575 Remorse::EntityVmRuntime::ReleaseSlots to void __cdecl16far ReleaseSlots(EntityVmRuntime * this).
  • Tried the same typed-this collapse on 1420:1499 Remorse::EntityVmRuntime::Create, but the pointer-sized this variant reintroduced a hidden __return_storage_ptr__; the function was restored immediately to the known-good split-word custom-storage signature dword __cdecl16far Create(word this, word runtime_segment, word owner_type, word owner_id).
  • Verified fourth live batch landed on 2026-04-06.
  • Updated the local_a decompiler local in 1420:19fd Remorse::EntityVmRuntime::EnsureSlotChunkLoaded to EntityVmSlotEntry *, so the slot-entry cache path now renders the stable owner_buffer_offset and chunk_state_offset fields directly instead of anonymous undefined4 pairs.
  • Retried the EntityVmContext lifecycle typing pass through live MCP. apply_class_layout dry-run for /Remorse/EntityVmContext now returns a normal structured preview instead of the earlier null failure, but the real apply path still fails with Failed to apply this type: Storage size does not match data type size: 2, and direct live set_function_this_type calls on FreeBuffer, SyncGlobalValueAndDispatch, Destroy, Save, and Load hit the same storage-size mismatch.
  • Retried live run_write_script(...) with and without explicit target selectors on CRUSADER.EXE, but the route still returned 404 No context found for request, so there is still no live in-session fallback for forcing the dynamic-storage rewrite on the context methods.
  • Verified fifth batch landed through local PyGhidra on 2026-04-06 after the live write-side routes still blocked the context pass.
  • Updated 1420:0eec Remorse::EntityVmContext::CreateFromSlotIndex so the first parameter is now EntityVmContext * this while preserving the existing UsecodeProcess * return type until the constructor/factory semantics are tighter.
  • Updated 1420:10b6 FreeBuffer, 1420:10da SyncGlobalValueAndDispatch, 1420:1162 Destroy, 1420:118f Save, and 1420:1278 Load so the first parameter is now EntityVmContext * this instead of UsecodeProcess *.
  • That local fallback confirms the newer dynamic-storage rewrite is sufficient for the context lifecycle cluster when applied outside the live GUI session. The remaining MCP issue is deployment/session parity, not whether the typing model itself works.
  • Verified sixth analysis-only live batch on 2026-04-06.
  • Exercised the new storage-aware prototype route against the two known 16-bit repair cases (1000:42e2 and 1420:1499) through the active MCP session. The checked-in source has the new route wiring, but the live GUI plugin still answered with legacy behavior: /set_function_prototype_storage returned the old set_function_prototype failure body, and /set_storage_aware_prototype returned 404 No context found for request. That confirms the remaining issue is live deployment parity, not endpoint design.
  • Rechecked the direct callers of CreateFromSlotIndex: Usecode_ItemCallEvent plus two Interpreter_NextUsecodeOp call sites. The Usecode_ItemCallEvent path explicitly calls CreateFromSlotIndex((EntityVmContext *)0x0,0,...) as an allocate-and-return factory, and the current caller-side uses immediately consume only base Process-style fields such as procid and termination flags. The two interpreter call sites likewise just store the returned far pointer in DX:AX scratch pairs before later base-process handling.
  • That caller evidence is enough to keep the current conservative return type for now: CreateFromSlotIndex is clearly manufacturing an EntityVmContext, but promoting the return to EntityVmContext * before the inheritance/base-process datatype story is explicit would probably make current caller decompilation less clear rather than more clear.

Current live datatype state:

  • /Remorse/EntityVmOwnerResource is the cleanest landed class in this lane so far.
  • /Remorse/EntityVmRuntime currently only freezes the stable tail fields and helper pointer, not the full slot-entry schema.
  • /Remorse/EntityVmSlotEntry now exists as a bounded helper datatype, but only the stable tail buffer-pair fields are named so far.
  • /Remorse/EntityVmContext now exists and matches the current owned lifecycle cluster, but it still only records the safest field anchors rather than the full embedded mini-VM layout.
  • apply_class_layout succeeded for Remorse::EntityVmOwnerResource but failed for Remorse::EntityVmRuntime when the binder tried to apply a this type, even though plain ownership moves worked.
  • The old apply_class_layout dry-run null failure for Remorse::EntityVmContext no longer reproduces on the current live server, but the actual write-side this typing path is still effectively old-build behavior: the real apply and direct set_function_this_type calls still fail on the existing UsecodeProcess * lifecycle signatures with Storage size does not match data type size: 2.
  • The EntityVmContext lifecycle signatures are now locally repaired through PyGhidra: CreateFromSlotIndex plus FreeBuffer / SyncGlobalValueAndDispatch / Destroy / Save / Load all carry EntityVmContext * this as their first parameter.
  • CreateFromSlotIndex should still keep its conservative UsecodeProcess * return type for the moment. The allocate-and-return behavior is clear, but the known callers currently consume it through base-process fields, and the repo does not yet have an inheritance-aware EntityVmContext : UsecodeProcess datatype model that would make a promoted return cleaner across the call sites.
  • The runtime lane is now split more accurately: InitSlots and ReleaseSlots can carry a direct EntityVmRuntime * this, while Create still needs the split-word custom-storage form to avoid hidden return-storage breakage.
  • The first slot-entry prototype batch is tighter now that EnsureSlotChunkLoaded carries a real EntityVmSlotEntry * local on the acquired-slot path, but the wider slot-entry model is still improved rather than finished.

Current scope of that batch stayed intentionally conservative:

  • no final source-format schema naming for the owner rows
  • no speculative promotion of additional seg069/070 helper callbacks into owned methods yet
  • no speculative promotion of the masked-create wrapper ladder into EntityVmContext methods
  • no speculative typing yet for the entity-like pointer parameter on AcquireSlotForEntity
  • no attempt yet to force slot-entry field names beyond the stable +0x1e..+0x24 tail region and the current conservative helper prototypes

Best immediate next moves after this landed:

  • inspect EnsureSlotChunkLoaded and adjacent 1420: helpers again now that AcquireSlotForEntity returns EntityVmSlotEntry *, and push the slot-entry type one step deeper only where the resulting local/object read is genuinely clearer
  • decide whether CreateFromSlotIndex can safely promote its return type from UsecodeProcess * to EntityVmContext *, or whether it should stay a factory-style bridge that only types this
  • if the context/base-process inheritance story becomes explicit in datatypes, revisit CreateFromSlotIndex return typing then; until that point, keep the current UsecodeProcess * return even though the body itself clearly builds an EntityVmContext
  • recover a storage-aware this-typing path for Create specifically; InitSlots and ReleaseSlots no longer need to stay in the unresolved set
  • redeploy or otherwise verify the live storage-fallback set_function_this_type / apply_class_layout build, then retry the EntityVmContext lifecycle typing pass in-session before dropping back to local PyGhidra
  • identify one or two additional strongly owned runtime or owner-resource helpers if the live session exposes them cleanly
  • keep the masked-create hub and offset-specialized wrapper ladder outside the class until caller-side role recovery is tighter

Source-Emission Guidance

If emitted as provisional C++ later, safest early skeleton is:

  • EntityVmOwnerResource with explicit loader/index fields and placeholder virtual/helper methods
  • EntityVmRuntime with fixed-size slot table, owner pointer, category-base fields, and create/destroy methods
  • EntityVmContext with exact saved-field placeholders and a distinct embedded mini-VM state member

Avoid in the first skeleton:

  • speculative opcode enums presented as final
  • collapsing the owner-resource helper into plain runtime fields
  • flattening the source/control stream pair into one host-only pointer abstraction if Track A remains active

Bottom Line

The VM lane now supports a real class model, but it should start with ownership and layout rather than with overconfident script-semantic names.

The most defensible current model is runtime owns helper and slot state; contexts are short-lived objects built from slot selection plus owner-loaded source rows.