18 KiB
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:
EntityDispatchEntryBaseEntityDispatchEntryTimedorEntityDispatchEntryPeriodicfor the0x3aa6timing/period variantEntityDispatchEntryRuntimeStatefor the later000d:7e00/8078runtime-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, and0x3afe - 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
0x100at+0x16 - zeroes extension words
+0x32/+0x34
0008:d214 entity_dispatch_entry_ctor_vtbl_3aa6
- allocates
0x40bytes if null - reuses
0008:cefb - sets vtable
0x3aa6 - sets flag
0x200at+0x16 - zeroes fields
+0x38..+0x3e
Related alloc/init helpers
0004:ea00 entity_dispatch_entry_alloc_type_0f5e0004: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
1is 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:
- create a minimal
EntityDispatchEntryBase - create derived or overlay structs for subtype-specific tails
- 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 byentity_call_update_vfunc14+0x28= callback slot used by the periodic and proximity-style dispatch helpers- embedded subobject/member surfaces at
+0x1eand+0x28are also dispatched through helper wrappers infar-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:
- create class namespace
EntityDispatchEntry - move only the strong base methods first
- create minimal
EntityDispatchEntryBasestruct with the stable fields through+0x18 - create subtype overlay structs for word-list, timed, and runtime-state tails
- 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::EntityDispatchEntryin the activeCRUSADER.EXEdatabase. - Created provisional base datatype
/Remorse/EntityDispatchEntryBasewith the current stable field block through+0x18:type_or_kindslot_index_or_countsource_typeevent_type_or_list_ptr_logroup_id_bytelink_or_state_word_0a..10target_farptrflags1flags2
- Created provisional vtable datatype
/Remorse/EntityDispatchEntryVtablewith 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 liveCRUSADER.EXEsession 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 requiredmethodsproperty, so this first shell pass used the now-working liverun_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:ba00pilot cluster is now re-anchored in the liveCRUSADER.EXEsession as the11e0: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 anchor0008:ba00)11e0:01b6->SetSourceType(older note anchor0008:bbb6)11e0:0227->SetEventTypeChecked(older note anchor0008:bc27)11e0:02a8->SetGroupId(older note anchor0008:bca8)11e0:0353->Unlink(older note anchor0008:bd53)11e0:0405->IncrementGroupId(older note anchor0008:be05)
- Those six methods now carry provisional
EntityDispatchEntryBase * thissignatures in-session plus decompiler comments recording the old0008: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_Processsubstrate 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 live1440:fade/palette cluster by explicit decompiler segment metadata and matching offset delta:1440:0000 FadeProcess_Create->Remorse::EntityDispatchEntry::InitRuntimeState(older note anchor000d:7e00)1440:0278 FUN_1440_0278->Remorse::EntityDispatchEntry::ReleaseRuntimeState(older note anchor000d:8078)
- Created
/Remorse/EntityDispatchEntryRuntimeStateas a provisional overlay datatype. It preserves the stable base block through+0x18, keeps+0x1a..+0x3fexplicit 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 * thissignatures and in-session comments tying them back to the older000d: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 older0008:note cluster by preserved offset delta from0008:ba00 -> 11e0:0000and now live underRemorse::EntityDispatchEntrywith short provenance comments:11e0:14fb Process_Create_0x36byte->ConstructVtable3AD2(older note anchor0008:cefb)11e0:1814 Process_Init0x40ByteProc->ConstructVtable3AA6(older note anchor0008:d214)11e0:187e Process_Set_MaybeTimesPerSecond->SetUpdatePeriodAndReschedule(older note anchor0008:d27e)11e0:1913 FUN_11e0_1913->TickPeriodic(older note anchor0008:d313)11e0:19e6 Process_11e0_19e6->EnableActiveCounters(older note anchor0008:d3e6)11e0:1a33 Process_11e0_1a33->DisableActiveCounters(older note anchor0008:d433)
- The earlier word-list blocker is now closed too, but by re-anchoring rather than by
11e0:boundary repair. The expected live11e0:2000..25a1window is not code in the current database; the actual word-list-owned subtype lives in the11e8:MList_*cluster, with the root at11e8:0000carrying explicit old0008:da00segment metadata in the decompiler. That full batch now also lives underRemorse::EntityDispatchEntrywith short provenance comments:11e8:0000 MList_11e8_0000->SetWordList0408Terminated(older note anchor0008:da00)11e8:01a3 MList_11e8_01a3->FreeWordList(older note anchor0008:dba3)11e8:01ec MList_11e8_01ec->Destroy(older note anchor0008:dbec)11e8:0238 FUN_11e8_0238->EnsureWordListContains(older note anchor0008:dc38)11e8:02ab MList_11e8_02ab->AppendUniqueWord(older note anchor0008:dcab)11e8:03af MList_11e8_03af->RemoveWordValue(older note anchor0008:ddaf)11e8:04ea MList_GetInt16->GetWordAt(older note anchor0008:deea)11e8:051b MList_11e8_051b->SetWordAt(older note anchor0008:df1b)11e8:05a1 FUN_11e8_05a1->FindUnflaggedWordById10(older note anchor0008: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 underEntityDispatchEntryalone or be split further into an explicit derived/overlay class once a safe instance-size boundary is chosen - whether
+0x00should be modeled as a literalkindfield in all variants or only in some factory-built subtypes - exact ownership split between the base object and the embedded surfaces at
+0x1eand+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.