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

284 lines
No EOL
21 KiB
Markdown

# 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](docs/raw-0008-000c.md) and [docs/raw-000a-000d.md](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`.