# EntityDispatchEntry Class Layout ## Purpose 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. ### Derived constructor variants #### `0008:cefb` `entity_dispatch_entry_ctor_vtbl_3ad2` - allocates if null - reinitializes through `entity_dispatch_entry_init` - sets vtable `0x3ad2` - sets flag `0x100` at `+0x16` - zeroes extension words `+0x32/+0x34` #### `0008:d214` `entity_dispatch_entry_ctor_vtbl_3aa6` - allocates `0x40` bytes if null - reuses `0008:cefb` - sets vtable `0x3aa6` - sets flag `0x200` at `+0x16` - zeroes fields `+0x38..+0x3e` #### Related alloc/init helpers - `0004:ea00 entity_dispatch_entry_alloc_type_0f5e` - `0004:eb1f entity_dispatch_entry_ctor_0f3a_with_cache_reset` These look more like subtype-specific factory/create helpers than pure base constructors, but they still belong in the family map. ## Destroy / Release Surface ### Base-owned word-list destruction #### `0008:dbec` `entity_word_list_destroy` - resets vtable to `0x2d10` - frees list storage if present - optionally frees object when destroy flag bit `1` is set This is the clearest current destructor-style path on the base object. ### Runtime-state release #### `000d:8078` `entity_dispatch_entry_release_runtime_state` - frees paired owned buffers - 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`. | | `+0x46/+0x48` | `owned_buffer_a` | High | Runtime-state owned work/palette-like buffer. | | `+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 | |---|---|---| | `0008:ba00` | `entity_dispatch_entry_init` | `InitBase()` | | `0008:bbb6` | `entity_set_source_type` | `SetSourceType()` | | `0008:bc27` | `entity_set_event_type_checked` | `SetEventTypeChecked()` | | `0008:bca8` | `entity_set_group_id` | `SetGroupId()` | | `0008:bd53` | `entity_dispatch_entry_unlink` | `Unlink()` | | `0008:be05` | `entity_increment_group_id` | `IncrementGroupId()` | | `0008:c01d` | `entity_refresh_dispatch_state` | `RefreshDispatchState()` | | `0008:bfb2` | `entity_clear_status_bits_from_flags` | `ClearStatusBitsFromFlags()` | | `0008:bf8e` | `entity_call_update_vfunc14` | `CallUpdateSlot14()` | | `0008:beee` | `entity_run_flagged_handlers` | `RunFlaggedHandlers()` | ### Pair/link/target helpers | Address | Current function | Candidate method role | |---|---|---| | `0008:c7f1` | `entity_pair_update_link_slot_a` | `UpdateLinkSlotA()` | | `0008:c890` | `entity_pair_update_link_slot_b` | `UpdateLinkSlotB()` | | `0008:c92f` | `entity_pair_sync_a` | `PairSyncA()` | | `0008:ca18` | `entity_pair_sync_b` | `PairSyncB()` | | `0008:c9ee` | `entity_pair_mark_and_sync_a` | `MarkAndPairSyncA()` | | `0008:cad7` | `entity_pair_mark_and_sync_b` | `MarkAndPairSyncB()` | | `0008:cb2c` | `entity_flag20_clear_and_update_target` | `ClearFlag20AndUpdateTarget()` | | `0008:cb5c` | `entity_flag20_set_and_init_target` | `SetFlag20AndInitTarget()` | ### Periodic/timed subtype methods | Address | Current function | Candidate method role | |---|---|---| | `0008:cefb` | `entity_dispatch_entry_ctor_vtbl_3ad2` | `ConstructVtable3AD2()` | | `0008:d214` | `entity_dispatch_entry_ctor_vtbl_3aa6` | `ConstructVtable3AA6()` | | `0008:d313` | `entity_periodic_accumulate_and_dispatch` | `TickPeriodic()` | | `0008:d3e6` | `entity_set_flag2000_and_update_active_counters` | `EnableActiveCounters()` | | `0008:d433` | `entity_clear_flag2000_and_update_active_counters` | `DisableActiveCounters()` | | `0008:d27e` | `entity_set_update_period_and_reschedule` | `SetUpdatePeriodAndReschedule()` | ### Word-list-owning subtype methods | Address | Current function | Candidate method role | |---|---|---| | `0008:da00` | `entity_word_list_set_0408_terminated` | `SetWordList0408Terminated()` | | `0008:dba3` | `entity_word_list_free_existing` | `FreeWordList()` | | `0008:dbec` | `entity_word_list_destroy` | `Destroy()` | | `0008:dc38` | `entity_word_list_ensure_contains` | `EnsureWordListContains()` | | `0008:dcab` | `entity_word_list_append_unique` | `AppendUniqueWord()` | | `0008:ddaf` | `entity_word_list_remove_value` | `RemoveWordValue()` | | `0008:deea` | `entity_word_list_get_at` | `GetWordAt()` | | `0008:df1b` | `entity_word_list_set_at` | `SetWordAt()` | | `0008:dfa1` | `entity_word_list_find_unflagged_by_id10` | `FindUnflaggedWordById10()` | ### Runtime-state subtype methods | Address | Current function | Candidate method role | |---|---|---| | `000d:7e00` | `entity_dispatch_entry_init_runtime_state` | `InitRuntimeState()` | | `000d:8078` | `entity_dispatch_entry_release_runtime_state` | `ReleaseRuntimeState()` | ## Candidate Virtual Surface The current evidence does not justify a fully named vtable yet, but some slot use is already real: - `+0x14` = update callback slot used by `entity_call_update_vfunc14` - `+0x28` = callback slot used by the periodic and proximity-style dispatch helpers - embedded subobject/member surfaces at `+0x1e` and `+0x28` are also dispatched through helper wrappers in `far-call-targets.md` Recommended future vtable note shape: | Slot offset | Current best role | Evidence quality | |---|---|---| | `+0x14` | update/refresh callback | High | | `+0x28` | periodic/dispatch callback | High | | others | unknown/default stubs | Low | ## Safe Future Ghidra Modeling Steps When manual class work starts, the safest order for this family is: 1. create class namespace `EntityDispatchEntry` 2. move only the strong base methods first 3. create minimal `EntityDispatchEntryBase` struct with the stable fields through `+0x18` 4. create subtype overlay structs for word-list, timed, and runtime-state tails 5. create a small provisional vtable for only the verified slots Do not start by forcing one complete 0x520-byte monolithic class. ## Live Ghidra Authoring Status 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`: - `11e0:0000` -> `InitBase` (older note anchor `0008:ba00`) - `11e0:01b6` -> `SetSourceType` (older note anchor `0008:bbb6`) - `11e0:0227` -> `SetEventTypeChecked` (older note anchor `0008:bc27`) - `11e0:02a8` -> `SetGroupId` (older note anchor `0008:bca8`) - `11e0:0353` -> `Unlink` (older note anchor `0008:bd53`) - `11e0:0405` -> `IncrementGroupId` (older note anchor `0008:be05`) - 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: - `1440:0000 FadeProcess_Create` -> `Remorse::EntityDispatchEntry::InitRuntimeState` (older note anchor `000d:7e00`) - `1440:0278 FUN_1440_0278` -> `Remorse::EntityDispatchEntry::ReleaseRuntimeState` (older note anchor `000d:8078`) - 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: - `11e0:14fb Process_Create_0x36byte` -> `ConstructVtable3AD2` (older note anchor `0008:cefb`) - `11e0:1814 Process_Init0x40ByteProc` -> `ConstructVtable3AA6` (older note anchor `0008:d214`) - `11e0:187e Process_Set_MaybeTimesPerSecond` -> `SetUpdatePeriodAndReschedule` (older note anchor `0008:d27e`) - `11e0:1913 FUN_11e0_1913` -> `TickPeriodic` (older note anchor `0008:d313`) - `11e0:19e6 Process_11e0_19e6` -> `EnableActiveCounters` (older note anchor `0008:d3e6`) - `11e0:1a33 Process_11e0_1a33` -> `DisableActiveCounters` (older note anchor `0008:d433`) - 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: - `11e8:0000 MList_11e8_0000` -> `SetWordList0408Terminated` (older note anchor `0008:da00`) - `11e8:01a3 MList_11e8_01a3` -> `FreeWordList` (older note anchor `0008:dba3`) - `11e8:01ec MList_11e8_01ec` -> `Destroy` (older note anchor `0008:dbec`) - `11e8:0238 FUN_11e8_0238` -> `EnsureWordListContains` (older note anchor `0008:dc38`) - `11e8:02ab MList_11e8_02ab` -> `AppendUniqueWord` (older note anchor `0008:dcab`) - `11e8:03af MList_11e8_03af` -> `RemoveWordValue` (older note anchor `0008:ddaf`) - `11e8:04ea MList_GetInt16` -> `GetWordAt` (older note anchor `0008:deea`) - `11e8:051b MList_11e8_051b` -> `SetWordAt` (older note anchor `0008:df1b`) - `11e8:05a1 FUN_11e8_05a1` -> `FindUnflaggedWordById10` (older note anchor `0008:dfa1`) - 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`. ## Questions To Close Later - 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.