This note is the first focused class-layout working paper for the Remorse C++ lift.
It takes the broad `EntityDispatchEntry*` inventory entry and narrows it into a base/derived object model that can later be pushed into Ghidra as class namespaces, instance structs, vtable structs, and method ownership.
The goal is not to claim a final C++ API. The goal is to lock down the pieces that are already stable enough to support later implementation work.
## Why This Family Goes First
`EntityDispatchEntry` is the strongest current pilot family because it already has:
- a clear constructor-style base init path
- multiple derived constructor variants
- explicit owned state and word-list teardown
- stable field groups with known offsets
- repeated virtual-slot dispatch through known offsets
- strong caller evidence across scheduler, runtime-state, palette, and startup/display lanes
That makes it the best place to prototype the full later workflow:
- class namespace creation
- method ownership
- instance-struct typing
- vtable typing
- base/derived split
- later C++ skeleton emission
## Candidate High-Level Model
Current best working split:
-`EntityDispatchEntryBase`
-`EntityDispatchEntryTimed` or `EntityDispatchEntryPeriodic` for the `0x3aa6` timing/period variant
-`EntityDispatchEntryRuntimeState` for the later `000d:7e00/8078` runtime-state owned-buffer family
This should stay a working model, not a hard rename, until the class work lands in Ghidra.
## Base Constructor Surface
### `0008:ba00` `entity_dispatch_entry_init`
Current best read:
- optional allocate/init path for a `0x32`-byte base object
- stamps base vtable/list-link state using `0x3b06`, `0x2d10`, and `0x3afe`
- zeroes core state fields
- seeds the group/layer byte through `entity_set_group_id`
This is the strongest current candidate for the base constructor-style init method.
- updates shared hold/owner propagation through `g_active_dispatch_entry_farptr`
- destroys embedded word-list members
This reads as the release/destructor path for the runtime-state derived family rather than for the whole base type.
## Current Base Layout
This table is a working layout, not a finished header.
| Offset | Current name | Confidence | Current meaning |
|---|---|---|---|
| `+0x00` | `type_or_kind` | Medium | Constructor/factory helpers stamp type words such as `0x0f3a`, `0x0f5e`, or `0x051e` here in some subfamilies. Base-vtable interpretation remains separate. |
| `+0x02` | `slot_index_or_count` | Medium | Used as entry slot/index in several wrappers; also used as count in the base word-list family, so exact role may vary by subtype or overlay. |
| `+0x04` | `source_type` | High | Written by `entity_set_source_type`. |
| `+0x06` | `event_type_or_list_ptr_lo` | Medium | Written by `entity_set_event_type_checked`, but also participates in word-list storage in the list-owning variant. This is likely one of the current overlay collisions to resolve later. |
| `+0x08` | `group_id_byte` | High | Low 5-bit group/layer value managed by `entity_set_group_id`. |
| `+0x0a/+0x0c/+0x0e/+0x10` | `link_or_state_words` | High | Cleared by `entity_dispatch_entry_unlink`; belong to link/extent/target/reset state. |
| `+0x12/+0x14` | `target_farptr` | High | Managed by `entity_flag20_*_target` helpers. |
| `+0x16` | `flags1` | High | Holds bits `0x10`, `0x20`, `0x100`, `0x200`, `0x4000`, and other subtype/state gates. |
| `+0x18` | `flags2` | High | Holds bits `0x40`, `0x80`, `0x100`, `0x400`, `0x1000`; used by unlink, periodic, and refresh paths. |
| `+0x1e/+0x28` | `embedded_dispatch_or_word_list_members` | Medium | Many callsites treat these as subobject or vtable-dispatch bases. Exact split still needs a dedicated subobject note. |
| `+0x24/+0x26`, `+0x2e/+0x30` | `optional_member_ptrs` | Medium | Checked before freeing both embedded word-list members. |
| `+0x32/+0x34` | `extension_words_a` | High | Zeroed by the `0x3ad2` constructor variant; also used by later runtime/VM helper flows. |
| `+0x36/+0x38/+0x3a` | `period_or_schedule_words` | Medium | Written by `entity_set_update_period_and_reschedule`; clearly timing-related in the periodic variant. |
| `+0x3c/+0x3e` | `accumulator_words` | High | Used by `entity_periodic_accumulate_and_dispatch`. |
| `+0x40` | `hold_token` | High | Shared/borrowed hold byte in startup/display and runtime-state families. |
| `+0x41/+0x42/+0x44` | `runtime_state_flags` | High | Initialized by `entity_dispatch_entry_init_runtime_state`. |
| `+0x4a/+0x4c` | `owned_buffer_b` | High | Second runtime-state owned buffer. |
| `+0x49` | `file_family_selector` | High for the seg126 subtype | Local selector state in startup/display transition family. Likely subtype-specific, not general base meaning. |
| `+0x5b` | `state_flags` | High for the seg126 subtype | State-machine bits in the `000c` startup/display lane. Likely subtype-specific overlay. |
| `+0x520` | `selected_resource` | Medium | Loaded file/resource object in the transition-file-family subtype. |
## Important Layout Caveat
This family is almost certainly not one flat struct with universally stable semantics at every offset. Current evidence already shows subtype overlays:
- base scheduler/dispatch-entry state
- word-list-owning variants
- periodic/timer variants
- startup/display transition variants
- runtime-state/palette-backed variants
So the safest future Ghidra modeling strategy is:
1. create a minimal `EntityDispatchEntryBase`
2. create derived or overlay structs for subtype-specific tails
3. avoid prematurely forcing every offset into one monolithic universal class layout
## Candidate Method Map
### Strong base methods
| Address | Current function | Candidate method role |
Verified first live `EntityDispatchEntry` shell batch landed on 2026-04-07.
- Created class owner `Remorse::EntityDispatchEntry` in the active `CRUSADER.EXE` database.
- Created provisional base datatype `/Remorse/EntityDispatchEntryBase` with the current stable field block through `+0x18`:
-`type_or_kind`
-`slot_index_or_count`
-`source_type`
-`event_type_or_list_ptr_lo`
-`group_id_byte`
-`link_or_state_word_0a..10`
-`target_farptr`
-`flags1`
-`flags2`
- Created provisional vtable datatype `/Remorse/EntityDispatchEntryVtable` with only the two currently verified callback slots exposed:
-`+0x14 = update_callback_slot14`
-`+0x28 = dispatch_callback_slot28`
- Kept the remaining vtable lanes explicit as unresolved padding instead of inventing slot names too early.
- Did not move methods yet. The current source note still anchors this family through the older `0008:` / `000d:` address notation, and those entrypoints are not yet mapped into the active live `CRUSADER.EXE` session strongly enough to justify owner moves by guesswork.
- The current live MCP `apply_class_layout(...)` path also rejected the minimal shell bind with an undocumented required `methods` property, so this first shell pass used the now-working live `run_write_script(...)` fallback to author the class namespace and datatypes directly.
This means the family is now started in-session rather than remaining note-only, but it is still in the pre-method phase.
That is no longer true after the next live pass on 2026-04-07.
- The older `0008:ba00` pilot cluster is now re-anchored in the live `CRUSADER.EXE` session as the `11e0:` process-substrate segment by direct offset mapping from the decompiler's embedded original-segment metadata.
- The first strong base-method batch is now moved under `Remorse::EntityDispatchEntry`:
- Those six methods now carry provisional `EntityDispatchEntryBase * this` signatures in-session plus decompiler comments recording the old `0008:` provenance so later note cleanup does not have to re-derive the mapping.
- The current live surface is still deliberately conservative. The decompiler still shows the underlying `struct_Process` substrate in several bodies, so this batch should be treated as class ownership plus field-layout alignment, not proof that every inherited process-style helper name is final.
- The next derived-family batch is now landed too. The older runtime-state pair from the note's `000d:` anchors is re-anchored in the live `1440:` fade/palette cluster by explicit decompiler segment metadata and matching offset delta:
- Created `/Remorse/EntityDispatchEntryRuntimeState` as a provisional overlay datatype. It preserves the stable base block through `+0x18`, keeps `+0x1a..+0x3f` explicit as unresolved subtype overlay space, and names the recovered runtime-state tail fields:
-`+0x40 = hold_token`
-`+0x41 = runtime_state_flag_41`
-`+0x42 = runtime_state_counter_42`
-`+0x44 = runtime_state_delta_44`
-`+0x46/+0x48 = owned_buffer_a`
-`+0x4a/+0x4c = owned_buffer_b`
- Those two runtime-state methods now carry provisional `EntityDispatchEntryRuntimeState * this` signatures and in-session comments tying them back to the older `000d:` evidence, which is enough to treat the runtime-state lane as class-authored rather than only documented.
- The next derived-family step is now landed too for the periodic/timed branch in the live `11e0:` substrate segment. Six more methods are re-anchored from the older `0008:` note cluster by preserved offset delta from `0008:ba00 -> 11e0:0000` and now live under `Remorse::EntityDispatchEntry` with short provenance comments:
- The earlier word-list blocker is now closed too, but by re-anchoring rather than by `11e0:` boundary repair. The expected live `11e0:2000..25a1` window is not code in the current database; the actual word-list-owned subtype lives in the `11e8:``MList_*` cluster, with the root at `11e8:0000` carrying explicit old `0008:da00` segment metadata in the decompiler. That full batch now also lives under `Remorse::EntityDispatchEntry` with short provenance comments:
- That correction matters more than the names alone. The pilot family is no longer blocked on a missing word-list method surface; the remaining uncertainty is now about how explicitly the word-list-owned subtype should be split in datatypes and eventual C++ modeling, not about whether those methods exist in live `CRUSADER.EXE`.
- whether the live `11e8:` word-list-owned subtype should stay modeled as a method batch under `EntityDispatchEntry` alone or be split further into an explicit derived/overlay class once a safe instance-size boundary is chosen
- whether `+0x00` should be modeled as a literal `kind` field in all variants or only in some factory-built subtypes
- exact ownership split between the base object and the embedded surfaces at `+0x1e` and `+0x28`
- whether the seg126 startup/display subtype is truly part of the same inheritance family or only shares a lower-level dispatch-entry substrate
- final base-size versus subtype-size boundaries once class namespaces exist in Ghidra
## Immediate Next Documentation Value
The next best companion note after this one is a slot-focused `SpriteNode` virtual table note, because that gives a second family with a cleaner explicit virtual surface and helps calibrate how aggressive the first Ghidra class conversion should be.