Crusader_Decomp/docs/entity-dispatch-entry-class-layout.md

311 lines
18 KiB
Markdown
Raw Permalink Normal View History

2026-04-05 18:27:09 +02:00
# 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.
2026-04-08 00:03:10 +02:00
## 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.
2026-04-09 00:32:12 +02:00
- 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`.
2026-04-08 00:03:10 +02:00
2026-04-05 18:27:09 +02:00
## Questions To Close Later
2026-04-09 00:32:12 +02:00
- 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
2026-04-05 18:27:09 +02:00
- 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.