11 KiB
SpriteNode Class Layout
Purpose
This note captures the current class-level read of the SpriteNode family so later Ghidra class work can move quickly and conservatively.
Compared with EntityDispatchEntry, this family has a cleaner explicit virtual/event-dispatch surface and a more bounded ownership model. That makes it an excellent second pilot for class lifting.
Current Best Class-Level Read
SpriteNode is a tree-based render/UI node family with:
- child-chain ownership
- accumulated position and bounds propagation
- dirty-state tracking
- central event dispatch through a small virtual surface
- destructor ownership over child nodes and global focus state
This already looks much closer to an ordinary C++ object family than many gameplay-side structures.
Strongest Evidence Anchors
Destructor
000b:326e sprite_node_destroy
Current best read:
- destructor-style path
- sets vtable ptr to
0x501a - clears global focus pointer
[0x4fd0:0x4fd2]ifself - releases child nodes
- frees object memory through
mem_free
This is the strongest current single proof that SpriteNode should be lifted as an owned object family.
Event dispatch
000b:3ab2 sprite_node_dispatch_event
Current best read:
- large event dispatch switch
- checks event types
2/4/8/0x100 - updates global focus pointer at
[0x4fd0:0x4fd2] - dispatches through virtual slots
+0x14,+0x18,+0x20,+0x24
This is the strongest current proof of a stable virtual method surface.
Dirty/update family
000b:3380 sprite_node_is_dirty000b:33a6 sprite_node_mark_dirty000b:40ee sprite_node_update_and_dispatch
These show that node state changes and redraw/update dispatch are methods on the same family, not just free helper functions wandering across unrelated data.
Recursive tree helpers
000a:b988 sprite_node_get_or_traverse000b:358d sprite_tree_accumulate_pos000b:3a00 sprite_tree_sum_x_offset000b:3a35 sprite_tree_sum_y_offset
These are strong evidence for a child-linked tree object with inherited coordinate accumulation.
Candidate Layout
This layout is intentionally conservative.
| Offset | Current name | Confidence | Current meaning |
|---|---|---|---|
+0x19/+0x1b |
child_or_next_ptr |
High | Child-chain pointer pair used by recursive traversal and offset accumulation. |
+0x21 |
local_x_offset |
High | Summed by sprite_tree_sum_x_offset / accumulate helpers. |
+0x23 |
local_y_offset |
High | Summed by sprite_tree_sum_y_offset / accumulate helpers. |
+0x29 |
dirty_flags |
High | Checked by sprite_node_is_dirty; manipulated by mark/update paths. |
+0x17e |
redraw_flag |
Medium | Cleared by sprite_clear_redraw_flag; likely a subtype or larger-object field tied to one SpriteNode family variant. |
Layout Caveat
The current notes likely mix a compact core SpriteNode with one or more larger derived UI/display objects. The evidence for +0x17e strongly suggests there are bigger family members or wrapper objects in the same virtual ecosystem.
So the safe future modeling strategy is:
- define a small
SpriteNodeBase - keep larger UI/display fields in derived or sibling structs until more offsets are closed
Candidate Method Map
Strong instance methods
| Address | Current function | Candidate method role |
|---|---|---|
000b:326e |
sprite_node_destroy |
Destroy() |
000b:3380 |
sprite_node_is_dirty |
IsDirty() |
000b:33a6 |
sprite_node_mark_dirty |
MarkDirty() |
000b:3ab2 |
sprite_node_dispatch_event |
DispatchEvent() |
000b:40ee |
sprite_node_update_and_dispatch |
UpdateAndDispatch() |
000a:b988 |
sprite_node_get_or_traverse |
GetOrTraverse() |
Strong family-local helpers that may remain free/static
| Address | Current function | Why it may stay non-method |
|---|---|---|
000b:3a00 |
sprite_tree_sum_x_offset |
Pure recursive accumulation helper; method status depends on later decompile readability. |
000b:3a35 |
sprite_tree_sum_y_offset |
Same as above. |
000b:330c |
sprite_tree_dispatch_wrapper |
Looks like a pure thunk wrapper rather than a meaningful source-level method. |
000b:3362 |
sprite_tree_unwind_check |
Stack-segment guard helper; probably not worth presenting as a class method. |
Candidate Virtual Slot Map
DispatchEvent now gives a materially tighter slot map than the earlier placeholder A/B/C/D read.
| Slot offset | Current best role | Evidence |
|---|---|---|
+0x04 |
event 1 handler |
DispatchEvent routes event code 1 here directly |
+0x08 |
event 2 handler |
same dispatcher |
+0x0c |
event 4 handler |
same dispatcher |
+0x10 |
event 8 handler |
same dispatcher |
+0x14 |
event 0x10 handler |
same dispatcher |
+0x18 |
event 0x20 handler |
same dispatcher |
+0x1c |
event 0x40 self handler |
called after the dispatcher walks child nodes for the same event |
+0x24 |
event 0x100 handler |
same dispatcher |
+0x34 |
child-broadcast event 0x40 slot |
used on child nodes during the 0x40 walk, not on the root dispatch object itself |
The seg091 default-slot helpers are also useful evidence:
000a:7b44,000a:7b49,000a:7b53,000a:7b4e,000a:7b78,000a:7b7d,000a:7b30,000a:7b3f,000a:7b35,000a:7b3a000a:7b58returns zero and behaves like a default no-op boolean slot000a:7b5fis a forwarding trampoline slot
These likely belong to one or more shared/default node vtables and should be preserved as vtable evidence even if they never become pretty source-level methods.
Ownership And Global State
Focus/global state
Global focus pointer [0x4fd0:0x4fd2] is updated in the dispatch family and cleared in the destructor.
That gives the family a real interaction with global UI focus/state, but the key point for class work is simpler:
- focus ownership is tied to the node family itself
- this is not just an arbitrary free helper changing global UI state
Child ownership
The destructor and recursive sum/traverse helpers strongly suggest real child ownership or at least managed child linkage.
That means later class modeling should preserve a node/tree mental model rather than flattening everything into stand-alone display items.
Candidate Ghidra Modeling Plan
When class authoring begins, the safest sequence for this family is:
- create class namespace
SpriteNode - move
Destroy,IsDirty,MarkDirty,DispatchEvent,UpdateAndDispatch, andGetOrTraversefirst - create minimal
SpriteNodeBasestruct with the stable offsets around+0x19,+0x21,+0x23, and+0x29 - create provisional vtable with slots
+0x14,+0x18,+0x20,+0x24 - keep recursive tree helpers outside the class until decompiler output shows they benefit from becoming methods
Live Ghidra Authoring Status
Verified first live SpriteNode batch landed on 2026-04-08.
- Created class owner
Remorse::SpriteNodein the activeCRUSADER.EXEdatabase. - Re-anchored the strongest old
000b:method batch into the live1360:segment by preserved offset delta from000b:326e -> 1360:046e. - Created minimal live datatypes
/Remorse/SpriteNodeBaseand/Remorse/SpriteNodeVtablefrom the current safest note anchors. /Remorse/SpriteNodeBasecurrently names only the bounded field block that is directly supported by the live method batch:+0x19 = child_or_next_farptr+0x21 = local_x_offset+0x23 = local_y_offset+0x29 = dirty_flags
/Remorse/SpriteNodeVtableis still the earlier minimal shell in-session, but the liveDispatchEventread now supports a deeper slot map than the first authoring batch encoded:- direct self dispatch uses
+0x04,+0x08,+0x0c,+0x10,+0x14,+0x18,+0x1c, and+0x24 - child broadcast during event
0x40uses child slot+0x34
- direct self dispatch uses
- Moved the first bounded method set under the class owner with short provenance comments:
1360:036a->Create(best current constructor-style anchor; still keep the higher-wrapper caveat visible)1360:046e->Destroy(older note anchor000b:326e)1360:0580->IsDirty(older note anchor000b:3380)1360:05a6->MarkDirty(older note anchor000b:33a6)1360:0955->GetOrTraverse(best current live anchor for older note anchor000a:b988)1360:0cb2->DispatchEvent(older note anchor000b:3ab2)1360:12ee->UpdateAndDispatch(older note anchor000b:40ee)
- The live decompiler now makes the core family surface much easier to navigate directly in-session:
Createnow exists live as the current safest constructor-style anchor: it allocates0x34bytes whenthisis null, stamps the0x501aSpriteNode vtable, initializes the child-link/core offset fields, and links the incoming parent/child lane.- Direct callers now narrow the subtype story materially: the current call set is dominated by
GumpCreate_*and adjacent UI wrapper constructors, which supports treatingCreateas the compact shared base-node constructor used by higher-level gump/display objects rather than as a one-off derived leaf. Destroyrestores the0x501abase vtable, clears the global focus pointer whenthisowns it, releases child linkage, and optionally frees self.IsDirtyandMarkDirtyread and mutate the+0x29dirty-state lane exactly as the note predicted.GetOrTraversenow exists live as the best current000a:b988re-anchor: it recursively walks the child-linked subtree, adjusts the incoming query coordinates by the local offsets, and returns either the matched child node or the default sentinel through the out pointer.DispatchEventupdates the global focus pointer at0x4fd0:0x4fd2and now ties concrete event codes to concrete slot offsets:1/2/4/8 -> +0x04/+0x08/+0x0c/+0x10,0x10/0x20 -> +0x14/+0x18,0x40 -> child +0x34 then optional self +0x1c, and0x100 -> +0x24.UpdateAndDispatchnow clearly shows theIsDirty/MarkDirty/ recompute / child-walk sequence instead of staying as an anonymous seg1360 helper.
- The
SpriteNodefamily is therefore no longer note-only. What remains open is narrower now: chiefly whetherCreateshould remain the family's public constructor-style entry or later be split into a higher derived/UI wrapper and a smaller base-node constructor once more callers and subtype evidence land, plus the deeper vtable-slot and subtype-layout questions.
Open Questions
- whether the current
Createsignature itself can be cleaned up further now that its caller set points to a shared compact base-node constructor used by higher-level gump wrappers - exact root vtable address or addresses for the main SpriteNode family
- whether the
+0x17eredraw flag belongs to a derived display node rather than the compact base node - whether the recovered event-code map can now be promoted from raw event-code labels to prettier semantic slot names without overfitting UI behavior too early
- whether
sprite_tree_accumulate_posshould become a class method, a static helper, or a separate geometry utility
Immediate Follow-Up Value
The most useful next companion work after this note is not more sprite detail by itself. It is the rebuild-ABI note, because once the first few class families are documented this well, the next real risk is drifting away from the original memory and calling-convention model before any code is emitted.