Crusader_Decomp/docs/raw-000a-000d.md
MaddoScientisto de42fd1ea1 Add Crusader-specific USECODE data and documentation
- Introduced new file `vm_mask_ladder.tsv` containing detailed mappings for Crusader USECODE VM masks and their associated descriptors.
- Added comprehensive documentation in `scummvm-crusader-reference.md` outlining the structure, findings, and implications for reverse-engineering the Crusader engine within ScummVM.
- Created `usecode-roundtrip-ir.md` to document the plan for converting Crusader USECODE bytes into a human-readable format, detailing the container layout, event names, and intrinsic tables.
- Implemented a PowerShell script `temp_usecode_sample.ps1` for extracting and analyzing USECODE data from the Crusader FLX files, providing insights into class and event structures.
2026-03-22 17:26:39 +01:00

28 KiB
Raw Blame History

Raw 000a & 000d: Tracked Handles, Cache Manager & Proximity Buckets

Content extracted from crusader_decompilation_notes.md. Covers the 000d proximity/visibility bucket cluster, 000a tracked-handle table, generic cache manager, seg082 allocator, seg137/138 palette helpers, and seg004/seg005 startup paths.


Raw 000d Proximity/Visibility Bucket Cluster

Small conservative rename batch from the 000d:cc00-d413 region.

Address Name Evidence
000d:cc00 entity_compute_proximity_or_visibility_bucket Returns bucket 0x40 for null or on-screen entities (entity_projected_bbox_overlaps_viewport), else computes a distance bucket from the current reference entity at 0x7e22 with thresholds 0x17d, 0x281, 0x3c1 mapping to 0x32, 0x20, 0x10, 0x08
000d:d413 entity_refresh_recent_proximity_or_visibility_buckets Walks the last four active records in the 0x69ac array, recomputes the same bucket, stores it back to each entry, and calls 000a:6343 when the bucket changes
000d:cdd0 tracked_entity_bucket_prune_invalid_entries Walks the 0x69ac array, validates backing handles through 000a:637a, and clears entry handles to 0xffff when the backing object is gone
000d:cd62 tracked_entity_bucket_find_free_main_slot Finds the first free entry in the main portion of the 0x69ac array (0 .. count-4)
000d:cd9a tracked_entity_bucket_find_free_aux_slot Finds the first free entry in the auxiliary tail portion of the 0x69ac array (count-4 .. count-1)

Supporting caller notes

  • 000d:ce1e populates one 0x69ac entry by reserving a free slot, computing the initial bucket through entity_compute_proximity_or_visibility_bucket, storing both current and previous bucket fields, then allocating/linking the backing handle through 000a:5f36.
  • 000d:d409 is a thin wrapper that only calls entity_refresh_recent_proximity_or_visibility_buckets.
  • 000d:cfad is an update-or-allocate helper for (param_1,param_2) pairs: it tries to update an existing tracked entry through 000a:606a, clears dead entries, and falls back to 000d:ce1e allocation when no live match remains.
  • 000d:cec5 is the auxiliary-slot allocator: it prunes invalid entries, uses tracked_entity_bucket_find_free_aux_slot, tags the new entry with byte +0x0a = 1, and seeds its handle via 000a:5f36(..., flag=1).
  • 000a:606a = tracked_entity_bucket_handle_update_or_alloc — updates the backing handle for an existing tracked bucket entry when possible, or falls back to allocation via 000a:5f36 if the handle has gone stale.
  • 000d:d350 = tracked_entity_bucket_set_value — finds a tracked (entity_id, entity_ref) entry and pushes a new bucket value into its backing handle through 000a:6343.
  • 000d:d10b = tracked_entity_bucket_clear_ref_field — clears only the +0x02 reference field for all matching entries.
  • 000d:d151 = tracked_entity_bucket_remove_by_ref — marks matching entries' backing handles for removal and clears the local entry handle/reference fields.
  • 000d:d1b1 = tracked_entity_bucket_remove_tagged_by_ref — same removal path, but only for entries whose byte +0x0a tag is set.

Raw 000a Tracked-Handle Table

The 0x4673 table is the backing handle registry for the 0x69ac tracked-entry bucket subsystem. That client layer sits on top of a separate generic cache manager rooted at 0x4688..0x46b7.

Address Name Evidence
000a:5f02 tracked_entity_handle_find_slot Linear scan over 12 entries in the 0x4673 table for a matching 32-bit handle id
000a:602b tracked_entity_handle_is_live Returns true only when a handle exists in 0x4673 and its flag word at +0x0a does not have bit 0x0002 set
000a:60eb tracked_entity_handle_mark_remove Sets bit 0x0002 in the handle-table flag word and dispatches through the unresolved cleanup path
000a:612e tracked_entity_handle_mark_remove_all Iterates all 12 handle-table entries and marks each live handle for removal
000a:6167 tracked_entity_handle_alloc_slot Allocates a slot in one of two ranges (0..7 or 8..11) depending on the aux flag; when full, wraps in a ring and evicts via tracked_entity_handle_mark_remove before reusing the slot
000a:6228 tracked_entity_handle_prune_removed Reaps entries previously marked with bit 0x0002, clears dead slots, and refreshes high-index entries through 000a:6b2d
000a:63bc tracked_entity_handle_find_by_entity Finds the first live handle-table entry whose key/entity word at +0x04 matches the requested entity id

Handle entry layout (stride 0x0c)

Offset Field
+0x00 32-bit handle id
+0x04 key/entity id
+0x06 class/group/source-style selector
+0x08 current bucket/value
+0x0a flags (bit0 = aux-slot allocation, bit1 = pending removal)

Thin public wrappers

Address Name
000a:5276 entity_bucket_track_default_main — gated by 0x45aa; creates or refreshes a main-slot tracked handle with bucket 0x40 and selector 0xff
000a:5294 entity_bucket_track_main — same path, but takes the bucket value as an argument for the main-slot range
000a:52d0 entity_bucket_track_default_aux — aux-slot variant with default bucket 0x40
000a:52ee entity_bucket_track_aux — aux-slot variant with explicit bucket argument

Raw 000a Generic Cache Manager

Follow-up analysis of 000a:6b2d and the 0x4688..0x46b7 globals shows that this region is a generic cache manager used by the tracked-handle layer, not part of the tracked-entity subsystem itself.

Address Name Evidence
000a:6b2d cache_lookup_or_load_entry_by_id Fast-paths the last id via 0x46af/0x46b1, otherwise searches 0x469d, evicts older cache slots until there is room under byte budget 0x46a5, allocates a block from the free-list, clears/initializes the payload, records the id, and dispatches through the loader interface at 0x468c
000a:6a95 cache_release_entry_by_slot Releases a cached slot by index, clears any client references through 000a:62d8, frees its backing block through cache_free_block_by_slot, and marks the slot id in 0x469d as unused (0xffff)
000a:6d07 cache_alloc_block_for_slot Allocates or splits a block from the free-list anchored at 0x4688, tags it with the owning cache slot index, and updates the in-use byte count at 0x46a9
000a:6f4d cache_free_block_by_slot Finds the free-list node for a cache slot, marks it free, subtracts its size from 0x46a9, and coalesces adjacent free blocks
000a:67d9 cache_shutdown Tears down the generic cache manager: flushes/reset state, frees slot arrays at 0x4699/0x469d/0x46b3, frees the free-list container at 0x4688, and closes backing state at 0x4691
000a:6898 cache_set_loader_interface Installs the backend loader/callback interface pointer at 0x468c

Cache globals

Address Name Notes
0x4688 free-list/block-list head Used by cache_alloc_block_for_slot and cache_free_block_by_slot
0x468c cache_loader_interface Backend callback table; +0x34 = size query, +0x0c = load/bind callback
0x4695 arena base pointer Base for the raw cache payload arena
0x4699 per-slot payload-pointer table
0x469d per-slot cached id table 0xffff = unused
0x46a5 byte budget / arena capacity
0x46a9 bytes currently in use
0x46af/0x46b1 fast-path cache Last requested id and slot index
0x46b3 per-slot block metadata mirror Used when releasing or refreshing slots

Follow-up: Cache Init and Runtime State

Address Name Notes
000a:6600 cache_init Stores slot count in 0x46ad; allocates per-slot payload-pointer table; seeds each slot; queries/derives arena size; allocates arena backing object at 0x4691; allocates per-slot metadata mirrors; initializes free-list head at 0x4688; calls cache_reset_runtime_state
000a:68aa cache_reset_runtime_state Shared cache reset/bootstrap helper called from cache_init, cache_shutdown, and external reset paths. Allocates per-slot arena-header nodes, rebinds slot pointers to arena base, clears the cached-id table, seeds the free-list head, and resets 0x46a9 (bytes in use) plus 0x46af (last-id fast path)
000a:703e cache_compact_arena_blocks Compacts live cache arena blocks into earlier free holes when allocation would fail, updates per-slot payload pointers, and merges adjacent free-list headers afterward

Follow-up: Tracked-Handle Table Init/Shutdown

Address Name Notes
000a:5e00 tracked_entity_handle_table_init If 0x4672 is clear, allocates 0x90 bytes at 0x4673/0x4675, aborts through runtime_init_or_abort on failure, calls 000a:577d and local helper 000a:5e95, then sets 0x4672 = 1
000a:5e59 tracked_entity_handle_table_shutdown Matching teardown for tracked_entity_handle_table_init
000a:5e95 tracked_entity_handle_table_clear_and_dispatch When tracked_entity_handle_table_active is set, zeroes the full 0x90-byte handle table at 0x4673, resets adjacent local state at 0x4677/0x4679/0x467b, then dispatches through the remaining thunked follow-up path
000a:5339 tracked_entity_handle_mark_remove_all_if_enabled Thin gate wrapper that only forwards to tracked_entity_handle_mark_remove_all when tracked_entity_bucket_system_enabled is set

Table globals: 0x4672 = tracked_entity_handle_table_active, 0x4673/0x4675 = tracked_entity_handle_table (12 entries × 0x0c = 0x90 bytes).


Follow-up: Tracked Bucket System Init/Shutdown

Address Name Notes
000a:5186 tracked_entity_bucket_system_init Allocates a rotating buffer via 0009:3600, lazily creates tracked_entity_bucket_backend_object through 0009:5600 when absent, installs that object into cache_loader_interface, allocates the tracked handle table via 000a:5e00, allocates the 32-entry 0x69ac bucket array via 000d:cca3(0x20), then sets tracked_entity_bucket_system_enabled
000a:538e tracked_entity_bucket_system_init_if_configured Only calls the init routine when config/feature gate 0x89f4 is non-zero
000a:5223 tracked_entity_bucket_system_shutdown Tears down the tracked handle table, frees the 0x69ac bucket array, calls backend-object vtable slot +0x38 with (3, backend_object), clears tracked_entity_bucket_backend_object; called from the wider engine teardown routine at 0004:621b

System globals: 0x45aa = tracked_entity_bucket_system_enabled, 0x45ab/0x45ad = tracked_entity_bucket_backend_object.

Public thin gate wrappers that feed the 0x69ac tracked-entry layer:

  • 0005:3b34 = tracked_entity_bucket_alloc_main_if_enabled
  • 0005:3b53 = tracked_entity_bucket_alloc_aux_if_enabled
  • 0005:3b72 = tracked_entity_bucket_remove_by_entity_and_ref_if_enabled → forwards into 000d:d086 = tracked_entity_bucket_remove_by_entity_and_ref when 0x45aa is set.

Follow-up: Backend Object Constructor

Address Name Notes
0009:5600 cache_backend_object_init Allocates a 0x20-byte object when caller passes null; initializes embedded DOS file-handle state via dos_file_handle_init; seeds internal method-table / state fields at object offsets +0x08, +0x0c, +0x10, +0x14, +0x16, +0x18, and +0x1c; dispatches through the object method table during construction; returns the object pointer cached at 0x45ab/0x45ad

Verified callback roles inside cache_lookup_or_load_entry_by_id:

  • backend vtable +0x34 = size query callback for a cache entry id (used before allocation/eviction)
  • backend vtable +0x0c = load/bind callback that populates the newly allocated slot buffer for the requested id

Follow-up: External Reset Paths

  • The path around 0004:25a9 classifies as an external reset sequence: it calls cache_reset_runtime_state, then tracked_entity_handle_table_clear_and_dispatch, then continues through additional tracked-entry/cache-side refresh helpers (000d:cd22, 000d:44b3, 0006:ae66, 0006:ae00, etc.).
  • The path around 0004:eb80 is a conditional tracked-bucket reset/update sequence: when tracked_entity_bucket_system_enabled is set, it calls tracked_entity_handle_mark_remove_all_if_enabled, then tracked_entity_handle_table_clear_and_dispatch, then cache_compact_arena_blocks, before resuming its outer flow.

Follow-up: Repaired seg004 Reset-Path Function Objects

Address Name Notes
0004:2592 runtime_cache_reset_sequence Calls 0008:7bfe; calls game_mode_init(*(0x27c4)); calls import-resolved site 0004:25a4, now verified from the separately imported ASYLUM.DLL as ordinal 24 = _ASS_StopAllSFX; then resets cache runtime state, clears tracked handles, refreshes tracked-entry/cache helpers. Known caller: 0004:262d inside the tiny wrapper at 0004:2620, which sets byte +0x40 on the object at 0x6828 before invoking the reset sequence.
0004:eb1f entity_dispatch_entry_ctor_0f3a_with_cache_reset Allocates/initializes an entity dispatch entry; stamps entry type 0x0f3a; stores its two word payload fields; runs local setup through embedded helper at 0004:ebf4 (which dispatches entity_dispatch_reset_all(*0x7e22, 0x00f0) and — when the local flag plus global 0x0ee1 allow it — allocates a type 0x0f5e dispatch entry and passes it to entity_pair_sync_b); when tracked_entity_bucket_system_enabled is set, performs the tracked-handle removal / clear / cache-compaction sequence before finalizing through 0009:b1c3 in phase 0.
0004:ea00 entity_dispatch_entry_alloc_type_0f5e Reuses the incoming FAR pointer when non-null; otherwise allocates 0x33 bytes through mem_alloc_far; initializes the entry through entity_dispatch_entry_init; stamps the entry type word at +0x00 to 0x0f5e.

Follow-up: seg082 Allocator Cluster

Address Name Notes
0009:a229 (size-only wrapper) Public size-only wrapper around the seg082 allocator. Lazily initializes the allocator on first use through 0009:bcb9, then calls allocator_try_alloc_from_head_table(size, default_tag, 0xff).
0009:bcb9 (lazy initializer) One-time lazy initializer. Parses an optional -x tuning value from the PSP command line, clamps derived percentage into 0x14..0x50, seeds local seg082 helpers, then sets init flag 0x4096 = 1.
0009:b06b allocator_try_alloc_from_head_table Validates requested size, reserves a temporary work token through 0009:e15f, scans the 0x8724 allocator head table in 0x0c-byte entries via allocator_head_try_alloc_block. On success, commits the result through 0009:e2b4, clears failure flag 0x4098. When a pass does not find a fit, interleaves up to two finalize phases through allocator_phase_finalize_pass(phase) before the final retry.
0009:a336 allocator_head_try_alloc_block Per-head first-fit allocator. Normalizes requested size (rounds odd small requests up, page-aligns large non-page-aligned requests), adds 0x0a node header overhead, enforces minimum of 0x10 bytes. Walks the node chain for one allocator head until it finds a free span large enough. On success, unlinks the chosen free node, either consumes it whole or splits off a remainder when >= 0x10 bytes remain. On failure, returns 0.
0009:a5d1 allocator_head_free_block Per-head free paired with allocator_head_try_alloc_block. Rebuilds the node header from a payload pointer (payload - 0x0a), validates the owner/tag word, reinserts the block into one allocator head, and coalesces with adjacent free neighbors when possible.
0009:b224 allocator_free_block_by_ptr Converts the payload pointer back through local header helpers, scans the 0x8724 head table for the owning range, dispatches to allocator_head_free_block, and aborts if no owning head is found. Known wrappers 0009:a24f and 0009:a27a are small checked entry points into this path.
0009:b1c3 allocator_phase_finalize_pass Accepts phase bytes 0 or 1. Forwards that byte twice to the object rooted at 0x4588 through vtable slot +0x08. Then sweeps the allocator head table at 0x8724 up to the active head count at 0x879c, calling allocator_head_finalize_sweep on each entry.
0009:af87 (free-space probe) Walks the node chain rooted at 0x8724. For each node, accumulates node_size - 9 into a running total and tracks the largest single free block. Used by cache_init and seg013 path at 0004:833b.
0009:a961 (per-head finalize sweep) Walks one 0x8724 head's node chain, skips odd-tagged spans, coalesces or rewrites eligible spans, and updates head/back-pointer links when deferred space needs to be merged back into the chain.

Allocator head table structure:

  • 0x8724 = array of 0x0c-byte allocator heads
  • 0x879c = active head count / table limit
  • Per-node size/value encoding manipulated through 0009:c628 and 0009:c6ae, which read/write a packed 32-bit quantity split across word + byte + byte fields

Follow-up: seg137 Palette and Dispatch-Entry Helper Family

A coherent palette-write and palette-backed dispatch-entry emission family tied to the same runtime-state constructor lane.

Address Name Evidence
000d:85da vga_palette_set_all_black Allocates a 0x100-entry palette buffer filled with zero RGB triplets, writes it to VGA, frees the scratch buffer. (Previously mis-named map_object_set_dirty_flag.)
000d:8653 vga_palette_set_all_white Same shape as black — all three RGB components initialized to 0x3f, then written through vga_palette_write.
000d:86cc vga_palette_set_all_rgb Takes caller-supplied RGB bytes, replicates them across a 0x100-entry palette buffer, writes the result to VGA, frees the scratch palette.
000d:82ea dispatch_entry_create_black_palette_state_active Builds a runtime-state dispatch entry of type 0x051e from a black 0x100-entry palette buffer; first sets g_active_dispatch_entry_farptr[+0x40] = 1.
000d:8a47 dispatch_entry_create_black_palette_state Same as above without marking the active dispatch entry.
000d:83be dispatch_entry_create_grayscale_palette_state_active Reads the current VGA palette, normalizes each triplet by copying the first channel across all three RGB bytes, then builds a runtime-state dispatch entry from that grayscale palette while marking the active dispatch entry.
000d:875d dispatch_entry_create_solid_palette_state_active Validates 0..0x3f RGB inputs, fills a scratch 0x100-entry palette buffer with that solid color, builds the same 0x051e runtime-state dispatch entry, marks the active entry.
000d:88b2 dispatch_entry_create_solid_palette_state Same as above without marking the active dispatch entry.

Additional caller-side comments (not renamed) added on:

  • 000d:84f4 — current-palette dispatch entry paired with a second object of type 0x68bf through entity_pair_sync_b
  • 000d:89c6 — parameterized current-palette runtime-state wrapper with active-state flags

Follow-up: seg138 Caller-Side Dispatch-Entry Emission Helper

FUN_000d_938c (000d:938c-000d:9583) — a real caller-side helper with an evidence-preserving decompiler comment added in Ghidra instead of forcing a speculative rename.

Current verified behavior:

  • When the mode/global gate is not already in the 0x13:0x0008 state and entity byte +0x33 is clear, it allocates a scratch palette buffer, constructs a dispatch entry, sets type 0x051e, and initializes runtime state through entity_dispatch_entry_init_runtime_state with entry kind 0x3c.
  • Later in the same helper it constructs a second dispatch entry from the current palette globals at 0x4e4:0x4e6, again sets type 0x051e, and initializes runtime state with entry kind 0x14 and active-state parameters (1,0,1).
  • Both created entries are polled until their runtime flag word clears bit 0x0002, after which the helper redraws the global sprite path, syncs display-state byte 0x58e from the entity when the global display object exists, calls FUN_0006_16e1, clears g_active_dispatch_entry_farptr[+0x40], and finally dispatches through the input object's vtable slot +0x08.

Follow-up: seg005 Startup/Display Orchestration

Address Name Notes
0004:60c0 FUN_0004_60c0 Startup/display orchestration path: broad setup calls, reads the live VGA palette, validates a caller-provided object through vtable slot +0x0c, drives the sprite/object lane through 0x4f38, runs the seg137 palette and dispatch-entry helper family, creates the default active dispatch entry through active_dispatch_entry_create_default, programs mouse interrupt state via seg056 INT 33h wrappers, then hands off into the still-unrecovered 0004:1e00 routine.
000d:7600 active_dispatch_entry_mark_enabled Marks the active dispatch entry enabled
000d:760e active_dispatch_entry_mark_disabled Marks the active dispatch entry disabled
000d:761c active_dispatch_entry_create_default Creates the default active dispatch entry

Follow-up: 0x4588 Object-Role Evidence

The 0x4588 FAR object is a runtime-installed callback/dispatch object that participates in conditional render or presentation-side flow. It has an explicit install, clear, callback, and teardown lifecycle.

Verified lifecycle

  • Install: 000a:4932 and 000a:4936 store the incoming dword into 0x4590 and 0x458c, then 000a:493e stores the incoming FAR object pointer into 0x4588.
  • Clear: 0004:5b8c and 0004:5bbf both clear 0x4588 immediately before the fatal/reporting-style seg091 call through 000a:454d; 0004:5ea7 and 0004:6430 both clear 0x4588 and then immediately run the one-shot teardown path 000a:4a56(1).
  • Teardown: 000a:4a56 checks a once-flag at 0x4595, clears 0x4588 when non-null, optionally performs a vtable +0x0c callback when 0x4590 != 0x458c, then calls vtable slot +0x04 followed by FUN_0009_0d30().
  • Callbacks: 000a:b9e5, 000a:ba66, 000d:9d5e, and 000d:a3b7 all push a two-word value pair followed by the 0x4588 FAR pointer and call vtable slot +0x0c. entity_conditional_render_dispatch calls the same vtable slot with a single literal 0x0101 argument.

Payload pairs from payload sync callsites

  • 000d:9d5e → vtable +0x0c payload from object fields +0x12d/+0x12f
  • 000d:a3b7 → vtable +0x0c payload from object fields +0x74f/+0x751
  • 000a:b9e5, 000a:ba66 → emitting only when the candidate two-word pair differs from the current pair, then mirroring that pair through 000b:1e39 using global sprite/object pointer 0x4f38/0x4f3a

Globals

Address Name
0x4588 runtime FAR object pointer (nullable)
0x458c callback sync field (compared against 0x4590 in teardown)
0x4590 paired sync field
0x4594/0x4595 state flags
0x45a6 clock/cookie global used by assert_buffer_valid
0x39ca dispatch callback-table pointer
0x6828 g_active_dispatch_entry_farptr

Follow-up: VM Owner/Resource Loader and Owner-Loaded Class Validation

The next ScummVM-guided validation step now confirms that the sampled owner-loaded EUSECODE classes are compatible with the ScummVM indexing model even though one header detail remains open.

Sampled class-record findings

  • Using the extracted chunks plus the live raw path 000d:44df -> 000d:4c99 -> 000d:7000, the large chunk at table offset 0x88 behaves as object 1.
  • For representative class bodies, deriving object_index = (table_offset - 0x80) / 8, then class_id = object_index - 2, and then reading object 1 at 4 + 13 * class_id yields the expected names: EVENT, NPCTRIG, SURCAMNS, JELYHACK, REE_BOOT, SURCAMEW, and SFXTRIG.
  • This is the first direct local confirmation that the owner-loaded records match the ScummVM object 1 name-table plus classid + 2 body lookup at the indexing level.

Header and event-table shape

  • The sampled class records do contain a stable 4-byte header field at bytes 8..11.
  • The observed values are small boundaries: 0x00d4, 0x00da, and 0x00e6 in the current sample set.
  • Treating that dword directly as the first post-event-table offset makes the layout line up cleanly: (dword_at_8 - 20) / 6 yields valid tables of 32, 33, or 35 slots before inline payload/name data begins.
  • The region at class + 0x14 is therefore now directly confirmed as repeated 6-byte slots with u16 unknown_word + u32 code_or_payload_field layout.
  • Representative low-slot examples are JELYHACK slot 1 = {word=0x002a, dword=0x00000001}, SURCAMNS slot 1 = {word=0x0051, dword=0x000000d2}, SURCAMEW slot 1 = {word=0x00f7, dword=0x000000d2}, EVENT slot 10 = {word=0x1fd6, dword=0x00000001}, and REE_BOOT slots 10/15/16 = {0x034b,1}, {0x025c,0x034c}, {0x003b,0x05a8}.
  • The leading event word is still not decoded semantically.

What remains open

  • Scanning with the previously noted ScummVM-style (base_offset + 19) / 6 interpretation overruns into inline payload/name bytes on these owner-loaded records, so the local sample set does not support that exact event-count formula as written.
  • The best current arithmetic fit is now tighter: ScummVM's decremented base_offset is also used as the live code-stream base in uc_machine.cpp, so the local owner-loaded records fit best if bytes 8..11 are the first code-byte offset and event-count derivation is (base_offset - 19) / 6, which is exactly equivalent here to (raw_u32_at_8_11 - 20) / 6.
  • Current 000d loader evidence does not point to a header rewrite before VM consumption. entity_vm_runtime_init_from_path_if_configured (000d:44df) only builds the external path and creates the runtime, entity_vm_runtime_create (000d:4c99) only installs the helper returned by 000d:7000, entity_vm_runtime_owner_resource_create (000d:7000) only allocates the child owner table and fills it through helper vtable +0x0c, and entity_vm_context_create_from_slot_index (000d:46ec) directly reads slot-backed source data from that owner table. No local step is yet verified as rewriting the sampled class headers.
  • entity_vm_runtime_owner_resource_create (000d:7000) still does not expose a direct binary-side class-name lookup or explicit classid + 2 arithmetic. What it does expose is an indexed file-set loader contract: helper-owned count at +0x14, far-pointer table at +0x10, paired per-entry word table at +0x18, vtable +0x04 size query, and vtable +0x0c materialization of the 0x0d-stride owner records later consumed by entity_vm_context_create_from_slot_index.
  • Safe event-label correlation remains intentionally narrow after this pass. The sampled low slot ids are now concrete, but none of them yet have a verified binary-side behavior match strong enough to promote a ScummVM label like look, use, or cachein.

Conservative parser rule from this batch

  • For current owner-loaded/raw EUSECODE work, keep bytes 8..11 raw and derive event count only with (raw_u32_at_8_11 - 20) / 6 when divisibility and object-size bounds checks succeed.
  • Keep the decremented code_base_minus_one = raw_u32_at_8_11 - 1 as a separate code-addressing field rather than collapsing it into the event-count rule.
  • Preserve the 6-byte event rows and their leading word verbatim until the event-entry word semantics are verified.