NCGR tile data and NCLR palette data are separate Nitro resources. The right palette for an NCGR is found by walking these rules in order; the first match wins.
BGR555 color format
NCLR colors are 16-bit little-endian:
bits 0..4 R (5 bits)
bits 5..9 G (5 bits)
bits 10..14 B (5 bits)
bit 15 ignored on hardware
R8 = (c >> 0) & 0x1F << 3 # scale 5β8 by left-shifting 3
G8 = (c >> 5) & 0x1F << 3
B8 = (c >> 10) & 0x1F << 3 Slot 0 is conventionally transparent in OBJ palettes.
Palette banks (4bpp NCGR)
A 4bpp NCGR uses 16-color palettes β but an NCLR can hold many such banks (up to 16 banks Γ 16 colors = 256 entries). The selection of which bank applies to a given tile is encoded:
- For BG (NSCR) cells: bits 12..15 of the screen cell word.
- For OBJ (NCER) cells: bits 12..15 of OAM
attr2.
When an NCLR has 2+ sections, the PMCP section maps each
local bank index to a VRAM bank index. Without PMCP, banks
are assumed sequential 0..N-1.
Four-tier resolution rule
Tier 1: Sibling NCLR in same RESO
Within a container, the NCLR adjacent to the NCGR (in TOC index order,
immediately following the NCGR or preceding the next NCGR sentinel) is
the sibling palette. Implemented by group_siblings() in
src/translator/_sprite_assoc.py. Covers the
NCGR/NCLR/NCER triplet pattern.
Tier 2: Container-wide shared NCLR
When a container batches several NCGRs at the start with no per-NCGR
NCLR, the container's first NCLR serves all of them. Used by
buyitem.lzs and similar shop containers (5 NCGRs + 3 shared
NCLRs + 6 NSCRs).
Tier 3: ARM9-pinned external NCLR
The palette lives in a different container, pinned by ARM9 loader code. Empirically resolved mappings:
| Source container (NCGRs) | NCGR count | bpp | Palette container + entry | Notes |
|---|---|---|---|---|
item/itemicon.ofs | 656 | 8bpp | global.lzs RESO entry 5 | 256-color. Entry 6 = grayed-out variant. |
2d/inputmagic/mgcall.ofs | 389 | 4bpp | 2d/inputmagic/mgc80.ofs entry 0 | 16-color. Entry 2 = alt scheme. |
2d/inputmagic/mgc01..09c.lzs | 823 total | 4bpp | 2d/inputmagic/mgc80.ofs entry 0 | Shared with mgcall. |
Tier 4: Single-NCLR container
When a container has exactly one NCLR (e.g.
trendranking/npcicon.ofs β 141 NCGRs + 1 NCLR), that single
NCLR is the shared palette for every NCGR. The sibling heuristic alone
misses this because the NCLR isn't grouped adjacent.
Fallback
Render with a grayscale palette. Used as a last resort.
Resolution stats
From notes/palette_association.json (4,337 total NCGR groups):
| Render kind | Count |
|---|---|
| NSCR BG screen | 1,099 |
| NCER cell composition | 926 |
| External magic glyph | 823 |
| External item icon | 656 |
| Single sprite | 466 |
| Tilebank-only | 296 |
| Orphan (no palette resolved) | 469 |
Of the remaining 469 orphan NCGRs, the bulk are in:
2d/signboard.lzs (80), 2d/designlogo.lzs (74),
2d/designcloth.lzs (58), 2d/selmaterial.lzs (52),
2d/expect.ofs (51), 2d/buyitem.lzs (31, shop bg
variants).
ARM9 external-palette evidence (item icons)
The 256-color OBJ palette for item/itemicon.ofs lives in
global.lzs, RESO entries 5 (main) and 6 (greyed-out). Each
NCLR is a 570-byte chunk with bpp_code=4 (8bpp = 256
colors), palette_data_size=512. The loader is the generic
asset-loader at 0x02052D20:
02052D4C LDR R0, [PC, #0x30] ; r0 = 0x020AA740 (item-list ptr)
02052D4E LDR R1, [PC, #0x34] ; r1 = 0x020A55D8 ("item/itemicon.ofs")
02052D50 LSLS R3, R3, #0xA ; r3 = 0x7800 (flags)
02052D52 BLX 0x02009894 ; call generic file-loader
; r2 = u16 sub-index from caller's [r5+0x84]
No static (icon_index β palette_index) table exists because
none is needed: every item icon uses the same global OBJ palette uploaded
once at boot from global.lzs.
Discovering external mappings
Recipe used when palette mapping is unknown:
- Determine NCGR bpp distribution in the container.
- If 4bpp β palette is 16-color (32 bytes + header). If 8bpp β 256-color (512 bytes + header).
- Histogram palette indices used across all NCGRs in the container. Clustering in one half of the index range constrains which slots must be populated.
- Filter candidate NCLRs by color count and visual content (slot 0 is often a saturated transparency key).
- Score each candidate by color variety of actually-used indices when rendered.
Source
src/translator/_sprite_assoc.pyβ sibling + container scannersrc/translator/_build_assoc_v2.pyβ producespalette_association.jsonnotes/palette_association.jsonβ full mappingnotes/asset_extraction_methodology.mdΒ§"Palette association hierarchy"