Crusader_Decomp/docs/sprite-node-class-layout.md
2026-04-09 00:32:12 +02:00

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] if self
  • 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_dirty
  • 000b:33a6 sprite_node_mark_dirty
  • 000b: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_traverse
  • 000b:358d sprite_tree_accumulate_pos
  • 000b:3a00 sprite_tree_sum_x_offset
  • 000b: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:7b3a
  • 000a:7b58 returns zero and behaves like a default no-op boolean slot
  • 000a:7b5f is 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:

  1. create class namespace SpriteNode
  2. move Destroy, IsDirty, MarkDirty, DispatchEvent, UpdateAndDispatch, and GetOrTraverse first
  3. create minimal SpriteNodeBase struct with the stable offsets around +0x19, +0x21, +0x23, and +0x29
  4. create provisional vtable with slots +0x14, +0x18, +0x20, +0x24
  5. 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::SpriteNode in the active CRUSADER.EXE database.
  • Re-anchored the strongest old 000b: method batch into the live 1360: segment by preserved offset delta from 000b:326e -> 1360:046e.
  • Created minimal live datatypes /Remorse/SpriteNodeBase and /Remorse/SpriteNodeVtable from the current safest note anchors.
  • /Remorse/SpriteNodeBase currently 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/SpriteNodeVtable is still the earlier minimal shell in-session, but the live DispatchEvent read 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 0x40 uses child slot +0x34
  • 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 anchor 000b:326e)
    • 1360:0580 -> IsDirty (older note anchor 000b:3380)
    • 1360:05a6 -> MarkDirty (older note anchor 000b:33a6)
    • 1360:0955 -> GetOrTraverse (best current live anchor for older note anchor 000a:b988)
    • 1360:0cb2 -> DispatchEvent (older note anchor 000b:3ab2)
    • 1360:12ee -> UpdateAndDispatch (older note anchor 000b:40ee)
  • The live decompiler now makes the core family surface much easier to navigate directly in-session:
    • Create now exists live as the current safest constructor-style anchor: it allocates 0x34 bytes when this is null, stamps the 0x501a SpriteNode 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 treating Create as the compact shared base-node constructor used by higher-level gump/display objects rather than as a one-off derived leaf.
    • Destroy restores the 0x501a base vtable, clears the global focus pointer when this owns it, releases child linkage, and optionally frees self.
    • IsDirty and MarkDirty read and mutate the +0x29 dirty-state lane exactly as the note predicted.
    • GetOrTraverse now exists live as the best current 000a:b988 re-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.
    • DispatchEvent updates the global focus pointer at 0x4fd0:0x4fd2 and 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, and 0x100 -> +0x24.
    • UpdateAndDispatch now clearly shows the IsDirty / MarkDirty / recompute / child-walk sequence instead of staying as an anonymous seg1360 helper.
  • The SpriteNode family is therefore no longer note-only. What remains open is narrower now: chiefly whether Create should 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 Create signature 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 +0x17e redraw 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_pos should 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.