Repository navigation
Sprite rendering
This is a partial runtime trace, not a complete scene interpreter. Addresses
refer to the original Japanese ROM. The editable NCD groups, animations,
frames, cells and palettes live under graphics/battle and graphics/ui.
SprSet (08011F40) forwards (id, property, 0, 0, value, 1, 0) to
08010E44. Properties 0–5 enter the interpolation task path at 08010C58;
08010CCC reads and writes the corresponding signed halfword. Its switch also
handles property 6. The sprite lookup at 080106C8 returns
engineState + 0xB90 + id * 40. The update loop at 08010730 visits 32 records.
| Record offset | Evidence / interpretation |
|---|---|
| 0, 1 | Packed flags; not completely named |
| 2 | NCD container selector |
| 4 | Group index |
| 6 | Animation index within group |
| 8 | Frame index within animation, updated after animation advancement |
| A, C | X and Y pixels; SprSet properties 0 and 1 |
| E, 10 | Properties 2 and 3; copied to runtime instance offsets 1C, 1E |
| 12 | Property 5; copied to runtime transform offset 2A |
| 14, 16 | Properties 6 and 4 respectively; copied to runtime scale offsets 2C, 2E |
| 1A | Property task occupancy bits |
| 24 | Pointer to the higher-level render object |
08010730 copies X/Y to render-object offsets 3E/40. The visibility check at
0800E518 subtracts the signed integer halves of camera coordinates at
03003BD4/03003BD8 and accepts -64 < x <= 303, -64 < y <= 223.
The object traversal at 08008AFC subtracts that camera again to produce the
NCD instance's screen X/Y at offsets 18/1A, unless object flag 3C bit 0
requests camera-independent positioning. Therefore script X/Y are pixel
positions, but not every object must be interpreted as world-relative.
The editor labels these two properties and can project the selected assignment as a yellow axis guide. It deliberately does not pair arbitrary nearby X/Y calls: CODE order does not establish executed branches, time, or active map.
The instance begins eight bytes into the higher-level render object. 08010730 passes its container/group/animation/frame to 0807BC7C, which follows registered NCD tables: group.firstAnimation + animation, then animation.firstFrame + frame, then frame.firstCell. It loads frame duration and cell count, allocates per-cell handles and requests cell tile uploads through 0807D444. Cell byte 13 hex (offset 19 decimal) is passed to allocation and upload; its complete encoding is still under investigation.
0807BFD0 decrements the 16-bit duration counter and advances when its signed value becomes negative. It either loops to frame zero or marks completion, depending on instance flags. This is not simply a frame advance every tick.
0807BDAC was incorrectly named GetObjectPaletteSlot. It is now named
NcdQueueSprite: it appends the instance to a linked list through 0807A878
and increments the queued-object count. Instance offset 26 bits 2–3 select
one of four queue arrays; the second argument selects a 12-byte bucket.
No palette is returned or looked up by this function.
0807BDEC traverses those queues. It dispatches to 0807C188 for ordinary instances, or 0807C2DC / 0807C758 for transformed instances according to flags at 27. The transformed path rejects scales <= 4. These drawing routines still require full C recovery and verification of OAM allocation, affine transforms, clipping, ordering and transfer timing.
src/ncd_sprite.c recovers the complete 80-byte initializer at 0807BC2C.
include/ncd.h describes its 52-byte instance. Link/address fields use explicit
32-bit GBA addresses. Initialization clears the record, selects no container,
sets animation/frame to -1, identity scales to 256, and default flag bits.
Unknown flag names retain their offsets instead of guessed meanings.
Map tiles, raw attributes and literal hit-region calls are editable today. Complete event control flow, spawn/resource association, world versus screen flags, sprite dragging with correct script ownership, collision-attribute semantics, and the full hardware rendering pipeline remain unfinished.
The normal ARM OAM callback is 0807D718, installed into engine offset 628
by initialization at 0807B14x. It sign-extends cell X from nine bits and Y
from eight bits, adds the instance offsets, then subtracts half-width and
half-height using tables at 0807D834 / 0807D844 before writing OAM. Thus
source/cells.json X/Y are centers, not top-left corners. Raw cell records
remain unchanged; ncd.cell additionally exposes left and top for display.
Both frame composition and pixel-to-source ownership use these bounds.
CHR frame 61 demonstrates the error: its 32×32 body is centered at (0,-28), 32×16 legs at (0,-4), and 16×8 hair at (0,-48). Their top-lefts are respectively (-16,-44), (-16,-12), and (-8,-52). The hair meets the body at Y=-44. The corrected frame bounds are (-16,-52), 32×56, rather than (0,-48), 32×60. This fixes ordinary cell assembly; runtime affine and blending effects still require further decoding. Gallery regeneration also refreshes the historical named first-frame previews from current frame PNGs.
The native registry at 081AFF30/081AFF38 identifies SprInit (08011ECC)
and SprChg (08011EF4). Both take sprite ID, NCD container selector,
resource-name string, animation index, and a fifth integer. SprChg forwards
that fifth integer as the frame index to ScriptSpriteSelect (08010C0C),
now recovered as matching C. It only changes active records, uppercases the
name in a 16-byte local buffer, finds the group through FindResourceByName,
and stores signed 16-bit selectors. Failed group lookup remains -1, as in
the original. Position and unrelated flag bits are preserved; draw-order bits
are reset to 3.
SprInit instead schedules 08010AEC / 08010B6C. That task waits for sprite
cleanup, allocates a 72-byte render object, initializes it, resolves the group,
and starts frame zero. The inspected path does not use the native call's fifth
integer as a starting frame. This distinction must be preserved by any scene
preview or script rewrite.
MAP01_A contains SprInit calls whose resource literals are MAP01_O and
PS_WK02. Their bytecode interleaves a string expression and temporary-variable
operations among integer pushes, so the existing integer-only editor correctly
leaves these calls read-only. Recognizing an embedded name is not yet proof of
the complete operand stack or executed control flow; automatic scene placement
must wait for that decoder rather than pairing arbitrary nearby calls.