204 lines
No EOL
11 KiB
Markdown
204 lines
No EOL
11 KiB
Markdown
# 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. |