Crusader_Decomp/docs/entity-vm-runtime-owner-resource-layout.md
2026-04-07 00:15:44 +02:00

34 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.
  • Verified seventh live batch landed on 2026-04-06 after the refreshed MCP build came up.
  • Re-exercised set_function_prototype_storage(...) in-session on the two known 16-bit repair cases. The route now reaches the real storage-aware implementation and can preserve the explicit AX:DX return storage in-session, but two live issues remain: stack offsets at 10 and above currently need 0x prefixes to avoid landing at 0x10/0x12/0x14/0x16, and calling_convention='__cdecl16far' still normalizes the repaired functions to plain __cdecl.
  • Updated /Remorse/EntityVmSlotEntry one step deeper from the InitSlotOwnerBuffers and EnsureSlotChunkLoaded evidence:
    • +0x00 match_key_farptr
    • +0x0a owner_chunk_count
    • +0x12 owner_data_base
    • retained the earlier +0x1e..+0x24 owner-buffer and chunk-state pointer pairs
  • Updated local variable typing so AcquireSlotForEntity now carries EntityVmSlotEntry * locals for the current slot cursor/free-slot candidate lane, and InitSlotOwnerBuffers now carries an EntityVmSlotEntry * local for the owner-metadata scratch object.
  • The decompiler payoff is immediate: InitSlotOwnerBuffers now shows owner_chunk_count, owner_buffer_*, and chunk_state_* directly, and EnsureSlotChunkLoaded now shows owner_data_base where the slot metadata seeds the later owner-data window.
  • Tried the stronger storage-aware Create(this: /Remorse/EntityVmRuntime * @ stack:0x4:4, ...) model through the new endpoint, but it still fails with Storage size does not match data type size: 2. That makes the remaining blocker more precise again: the live MCP route is now good enough to express the desired 4-byte storage, but the current EntityVmRuntime * datatype in this 16-bit NE session still resolves to a 2-byte pointer type.
  • Verified eighth live batch landed on 2026-04-06.
  • Reloaded the live plugin and re-verified set_function_prototype_storage(...) on the two known 16-bit proof cases. The route now works in-session and preserves explicit AX:DX return storage cleanly, but calling_convention='__cdecl16far' still normalizes both 1000:42e2 and 1420:1499 to plain __cdecl.
  • Renamed 1420:1d72 to entity_vm_runtime_get_slot_chunk_ptr_at_offset after confirming from CreateFromSlotIndex, Load, and FUN_1418_035f that it is just a wrapper over EnsureSlotChunkLoaded plus a caller-supplied offset.
  • Renamed 1420:1d8d to entity_vm_runtime_release_slot_chunk_ref after confirming from the Interpreter_NextUsecodeOp caller that it decrements one live chunk-state refcount and asserts if the chunk was not retained.
  • Renamed 1420:1e17 to entity_vm_runtime_try_unload_slot_chunk after confirming from entity_vm_runtime_apply_to_matching_owner_rows that it only unloads a chunk when the chunk-state count has reached zero, restoring the owner-buffer entry and freeing runtime budget during cleanup/eviction.
  • Added short decompiler comments to those three helpers so the slot-entry ownership story stays visible in the live database.
  • Verified ninth live batch landed on 2026-04-06.
  • Created provisional datatype /Remorse/EntityVmLoadedChunkRecord with the current stable cleanup/iterator anchors:
    • +0x06 next_offset
    • +0x08 next_segment
    • +0x0e saved_chunk_offset
    • +0x10 saved_chunk_segment
    • +0x12 slot_index
    • +0x14 chunk_index
  • Updated 1420:1e17 entity_vm_runtime_try_unload_slot_chunk so the second parameter is now EntityVmLoadedChunkRecord * loaded_chunk_record, and then tightened the return to byte __cdecl16far with explicit AL storage after caller disassembly at 1420:1f50 and 1420:1fc1 showed both call sites consume only AL.
  • Updated the iterator local uStack_6 in 1420:1f24 entity_vm_runtime_apply_to_matching_owner_rows to EntityVmLoadedChunkRecord *, so the owner-row cleanup walk now renders next_*, slot_index, and chunk_index directly instead of anonymous stack-pair traffic.
  • Confirmed the interpreter-side release helper caller at 1418:3330 pushes the live chunk record's slot_index / chunk_index pair from ES:[BX+0x32] / ES:[BX+0x34] together with the runtime far pointer before calling entity_vm_runtime_release_slot_chunk_ref, which makes the loaded-chunk record a real shared runtime record rather than a one-off cleanup scratch blob.
  • Verified tenth live batch landed on 2026-04-06.
  • Renamed local helper 1418:003c to interpreter_pop_saved_farptr after confirming from its only caller in Interpreter_NextUsecodeOp that it decrements a saved-farptr stack count at +0x80 and returns the far pointer stored at the new top entry.
  • Added short decompiler comments at 1418:003c and 1418:3330 so the interpreter-side release/restore lane stays visible without overcommitting the restored far pointer to a stronger semantic than the current evidence supports.
  • Verified eleventh live batch landed on 2026-04-06.
  • Created class owner Remorse::EntityVmSlotEntry in the live database and moved 1420:2040 under it as CreateOrClear.
  • Tightened Remorse::EntityVmSlotEntry::CreateOrClear so the single parameter is now named this and the explicit far return storage is restored to AX for the EntityVmSlotEntry * result.
  • Moved the previously global runtime cleanup helpers under Remorse::EntityVmRuntime as real methods:
    • 1420:1d72 -> GetSlotChunkPtrAtOffset
    • 1420:1d8d -> ReleaseSlotChunkRef
    • 1420:1cca -> DebugDumpSlotMemory
    • 1420:1e17 -> TryUnloadSlotChunk
    • 1420:1f24 -> ApplyToMatchingOwnerRows
  • Tightened the ReleaseSlotChunkRef parameter names to runtime_farptr, slot_index, and chunk_index, and renamed the DebugDumpSlotMemory far-pointer argument to runtime_farptr so the runtime-owned chunk/refcount lane reads more like method code than detached helper code.
  • Verified twelfth live batch landed on 2026-04-06.
  • Tightened Remorse::EntityVmRuntime::GetSlotChunkPtrAtOffset to dword __stdcall16far GetSlotChunkPtrAtOffset(dword runtime_farptr, int slot_index, int chunk_index, dword intra_chunk_offset) after re-checking the CreateFromSlotIndex and Load callers. The current best read is: load/ensure one slot chunk through the runtime, then add a caller-supplied intra-chunk offset pair to the returned far pointer.
  • Tightened Remorse::EntityVmRuntime::ApplyToMatchingOwnerRows to byte __cdecl16far ApplyToMatchingOwnerRows(dword runtime_farptr, int slot_index_filter, int chunk_index_filter) after re-checking the AcquireSlotForEntity and EnsureSlotChunkLoaded callers. The current best read is: iterate the runtime-owned loaded-chunk list either broadly (-1/-1) or for one current slot/chunk pair.
  • Restored explicit return storage after the storage-aware retype pass so GetSlotChunkPtrAtOffset still returns its far pointer in DX:AX and ApplyToMatchingOwnerRows still returns its boolean result in AL.
  • Verified thirteenth live batch landed on 2026-04-06.
  • Lifted the grouped runtime methods from split-word runtime_farptr parameters to explicit 4-byte EntityVmRuntime * this storage using /Remorse/EntityVmRuntime *32 in-session. The live signatures now read as real methods for:
    • Create
    • InitSlots
    • ReleaseSlots
    • DebugDumpSlotMemory
    • ReleaseSlotChunkRef
    • GetSlotChunkPtrAtOffset
    • TryUnloadSlotChunk
    • ApplyToMatchingOwnerRows
    • EnsureSlotChunkLoaded
  • Remorse::EntityVmRuntime::Create is the biggest change in that batch: it no longer needs the old split-word placeholder form and now holds dword __cdecl16far Create(EntityVmRuntime * this, word owner_type, word owner_id) with the original AX:DX return preserved.
  • EnsureSlotChunkLoaded now also carries the clearer EntityVmRuntime * this, short slot_index, short chunk_index signature with the original far-pointer return preserved in DX:AX.
  • AcquireSlotForEntity and InitSlotOwnerBuffers are now fully over that hurdle too: AcquireSlotForEntity returns EntityVmSlotEntry *32 in DX:AX, and InitSlotOwnerBuffers now carries EntityVmSlotEntry *32 slot_entry as its third parameter.
  • Verified fourteenth live batch landed on 2026-04-06.
  • Finished the remaining straightforward VM pointer cleanup outside the hottest runtime helper cluster:
    • 1430:0000 Remorse::EntityVmOwnerResource::Create -> byte __cdecl16far Create(EntityVmOwnerResource * this, dword owner_resource_spec)
    • 1430:00fd Remorse::EntityVmOwnerResource::Destroy -> Destroy(EntityVmOwnerResource * this, uint destroy_flags)
    • 1420:1601 Remorse::EntityVmRuntime::Destroy -> byte __cdecl16far Destroy(EntityVmRuntime * this, word destroy_flags)
    • 1420:10b6/10da/1162/118f/1278 Remorse::EntityVmContext::{FreeBuffer, SyncGlobalValueAndDispatch, Destroy, Save, Load} now all carry explicit EntityVmContext *32 this
  • That leaves CreateFromSlotIndex as the one clearly still-complex VM signature in this family cluster: the body still shows a far this, but the remaining argument pack needs a dedicated caller-side recovery pass rather than another pointer-only rewrite.
  • Verified fifteenth live batch landed on 2026-04-06.
  • Recovered the mixed caller pack on 1420:0eec Remorse::EntityVmContext::CreateFromSlotIndex far enough to replace the old anonymous split arguments with caller-backed names:
    • dword owner_source_farptr
    • dword pitemno_farptr
    • word mode_flags
    • word slot_index
    • word value_add_offset
    • word intra_chunk_offset
    • dword ucparam_farptr
    • uint ucparamsize
  • Restored explicit far return storage on CreateFromSlotIndex to AX:DX after the storage-aware apply briefly dropped it.
  • The same live pass also made the remaining endpoint weakness more concrete again: once the caller-backed custom-storage pack is applied, the endpoint still textualizes the function as plain dword __cdecl instead of preserving the earlier higher-level UsecodeProcess * / __stdcall16far surface, even though the decompiler now keeps the correct argument boundaries and the return really is back in AX:DX.
  • Current best caller-backed read for CreateFromSlotIndex is now narrower and more useful than the old placeholder form:
    • owner_source_farptr is a real far-pointer input that is persisted to context +0x11b/+0x11d
    • ucparam_farptr is a real far-pointer input copied into the backward-growing buffer at +0x102
    • slot_index, value_add_offset, and intra_chunk_offset are distinct scalar inputs rather than one collapsed anonymous pack
    • the conservative semantic story is still factory/setup bridge that returns a far process/context pointer, not final inheritance-clean constructor signature

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 both as a bounded helper datatype and as a live Remorse class owner. Its current authored surface is intentionally small: one constructor/clear method plus the stable match_key_farptr, owner_chunk_count, owner_data_base, and owner-buffer / chunk-state pointer anchors.
  • /Remorse/EntityVmLoadedChunkRecord now exists as the shared cleanup/iteration record for the chunk-release and conditional-unload lane, with the currently stable next-link, saved-owner-buffer, slot-index, and chunk-index fields named.
  • /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 a conservative semantic return in the notes for the moment. The active live endpoint now textualizes it as dword __cdecl after the caller-packed custom-storage cleanup, but the allocate-and-return behavior is clear, the real return storage is back in AX:DX, and the known callers still consume the result through base-process fields rather than through an inheritance-aware EntityVmContext : UsecodeProcess datatype model.
  • 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 runtime lane is grouped more accurately too: the chunk-access, chunk-ref release, debug-dump, conditional-unload, and owner-row iterator helpers now sit under Remorse::EntityVmRuntime instead of remaining global free functions.
  • The runtime lane is also typed more accurately now: the chunk accessor is no longer a five-word anonymous wrapper, and the owner-row iterator no longer pretends its runtime pointer is two independent split-word parameters.
  • The authored VM lane is now much closer to a real class surface than a namespace grouping: EntityVmRuntime, EntityVmOwnerResource, EntityVmContext, EntityVmSlotEntry, and the helper EntityVmLoadedChunkRecord all now participate in a mostly far-pointer-correct live type model, with CreateFromSlotIndex as the main remaining signature outlier.
  • The slot-entry model is tighter again: beyond the earlier owner_buffer_* and chunk_state_* tails, the datatype now also exposes owner_chunk_count and owner_data_base, which makes the allocator/count path in InitSlotOwnerBuffers and the owner-data window math in EnsureSlotChunkLoaded read as object state rather than anonymous offset pairs.
  • The adjacent helper map is tighter too: the slot-entry consumer side now has one pointer-plus-offset accessor, one chunk-ref release helper, one conditional-unload helper, and one named loaded-chunk iterator record instead of a mix of anonymous 1420: placeholders and anonymous stack-pair scratch state.

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
  • decide whether match_key_farptr at +0x00 should stay as a neutral far-pointer field or can now be promoted to a stronger entity/owner key name from caller-side evidence
  • recover a storage-aware this-typing path for Create specifically; the live route now works well enough to test explicit 4-byte storage, but the remaining blocker is the 2-byte EntityVmRuntime * datatype itself rather than endpoint reachability
  • inspect the broader Interpreter_NextUsecodeOp lane around 1418:3330 now that the release call and interpreter_pop_saved_farptr are anchored, and decide whether the loaded-chunk record can absorb any more of the surrounding save/restore stack traffic without overfitting transient interpreter locals
  • 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
  • decide whether ApplyToMatchingOwnerRows should keep its current generic split-word parameters under Remorse::EntityVmRuntime or whether the first argument pair is now well enough understood to collapse into a typed runtime this
  • decide whether the newly clarified runtime_farptr argument on GetSlotChunkPtrAtOffset and ApplyToMatchingOwnerRows is enough to justify a safe typed-this experiment on those methods, or whether the current EntityVmRuntime * pointer-size issue still makes the explicit dword runtime_farptr form the least misleading representation
  • use the now-recovered CreateFromSlotIndex caller pack as the baseline for any next cleanup, and only chase a prettier return type once the base-process inheritance story is explicit enough to make that promotion a real readability win
  • 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.