gBoardHudTilemapA and gBoardHudTilemapB are not tilemaps despite their
names: both blobs are 4bpp tile graphics for the board HUD, loaded to
charblock 1 tiles 320-351 and 352-415 on every board transition and
referenced from the BG0 tilemap buffer with palette bank 12. Extracted
as board_hud_tiles_a/b.png following the stage/main conventions.
gPokedexInfoWindowTiles is a genuine 32x32 u16 tilemap. Its content
rows 0-5 are a 26x6 window referencing tile indices 224-409, which are
sheet rows 7-12, cols 0-25 of the 32 wide gPokedexBgText_Gfx tileset.
Extracted as info_window_tilemap.bin following the pokedex tilemap
conventions (bg1/bg2/bg3_tilemap.bin).
All assets verified byte exact against baserom via gbagfx round trip;
make compare passes.
Extract the sapphire board's ball power up and 'hole' light tile graphics from baserom into tracked PNG sources, following the folder conventions established by the ruby board. Resolves#231.
gSphealIntroSprites_Gfx, gCatchTile_RevealTilesGfx, gRayquazaSkyBackgroundGfx
and gBoardActionTilesGfx were the four biggest baserom incbins left in rom_1.s,
40096 bytes between them. Each is now a segmented PNG sheet.
Segment boundaries come from the OAM entries in rom_2.s that draw each sheet,
filtered by the palette bank the sheet is loaded with -- the tile-704 overlay
slot is shared by around 160 labels, so the bank is what says which of them
belong to a given sheet. Piece geometry comes from the same entries.
The Rayquaza clouds are irregular composites that -oam cannot describe, so they
get oam-shape jsons with spacer runs for the holes, as the painter and
ball-open sheets already do.
objpalette.py gains the bindings this needed: SLOT_PALETTES for the catch reveal
and evolution banner, whose palettes are DMA'd beside their sheets; SLOTS for
the Rayquaza sky; and overrides for the sky's unreferenced tail and for Spheal's
pause block, which is the one board leaving OBJ bank 9 blank.
Trailing blank tiles stay as .space rather than padding out the PNGs. Every
segment round-trips png -> 4bpp byte-exact, each rebuilt sheet matches the ROM
blob before and after recolouring, and make compare still passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removes 291 baserom incbins (223,976 bytes). Everything below was checked by
re-encoding the emitted data and comparing against the ROM bytes before the
source was touched, and the build matches at every step.
Sprite sets and tables in rom_2.s
- Convert 212 packed sprite sets to packed_sprite_oam. Note the "l" suffix in
packed_sprite_oaml means an 8-byte entry, not 6, so the entry size is checked
against each set's leading count rather than inferred from its size.
- Convert 79 named tables using their extern declarations to fix each shape,
including the structs defined in .c files rather than headers (KecleonMoveNode,
IntroAnimVelocity). Eight tables whose element count does not divide by the
declared row width are listed flat, with a comment, rather than forced into a
shape the data contradicts.
- gEReaderTextGlyphTable is 14 pages of 3x24 u16. Each entry packs a glyph offset
in bits 4-15 with an advance width in bits 0-3 that the draw code masks off:
i=2, apostrophe/comma/full stop/l=4, word-final n=5, everything else 6. The 42
lines of text are transcribed as comments; the glyph map and the transcription
were checked against each other over all 1008 entries.
- gUnknown_086BBC44 was two things: six sprite sets, then 680 bytes that nothing
in the ROM points at, holding stale pointers past the rom end and a second copy
of the POKEPINAGB signature. That part stays an incbin as gUnusedRomTail.
Large blobs in rom_1.s
- gUnknown_81A6BE4 is an 8x8 ASCII font, 0x20-0x5F in order, with a yen sign in
the backslash slot. The other 30,720 bytes are zero. Now gDebugAsciiFont.
- The idle board configs are attract-mode demos: 4800 ReplayInputFrame each
paired with one PinballGame snapshot. Extracted to data/idle_board. The labels
run 0, 2, 3, 1 in ROM order.
- Kyogre body, Rayquaza body variants, the travel painter mons, the hole
indicator and the Sapphire bumpers are extracted as tile sheets. Frame counts
come from the declared strides; palettes come from the OBJ palette sets, not
the board BG palette, whose banks are black or grey for these sprites. Where a
sprite's pieces tile into a rectangle the sheet is laid out with -oamshape so
the frames are readable; where they do not, or where the tiles are scattered
over several board positions, it is one frame per row and the comment says so.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reverts the screen-based approach. Every BG sheet goes back to a plain
tile-grid PNG, which the existing %.4bpp: %.png rule builds, since
tilemap editing is covered by other tooling.
- Drop the 68 assembled screen and unmapped-strip PNGs, the 21 detilemap
rules, tools/scripts/detilemap.py and the VRAM layout notes.
- Revert Makefile: .SECONDARY goes back to its original unscoped form.
- Write each sheet out coloured rather than restoring the greyscale
originals. A 4bpp PNG carries one 16-colour palette, so each sheet uses
the bank most of its cells reference; multi-bank sheets are therefore
approximate in appearance. Pixel indices are unaffected, so all 25
sheets still round-trip byte-exactly.
- scene4plussleminun keeps its three-segment form: the sheet is 513 tiles
because of the align-1 padding tile and does not fit one grid.
Ruby and Sapphire keep their BG tiles and tilemaps as extracted files
rather than reverting to baserom incbins, so the 73,728 bytes those
removed stay removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 203 segment PNGs behind the seven boards' intro_sprite sheets were
checked in as 4-bit greyscale: a 4bpp sheet carries no palette of its own,
so nothing in the tree recorded which of the 16 OBJ banks a sprite is drawn
through, and every sheet was an unreadable grey blob. All seven are now
indexed colour, byte-for-byte identical through the build.
The inversion
- gbagfx converts a palette-less PNG with invertColors = !image.hasPalette
(main.c:71), so a greyscale sheet stores 15 - index, and gaining a PLTE
turns that off. The recolour inverts on the way in; passing the samples
through unchanged would have quietly rewritten every tile.
Deriving the bank (tools/scripts/objpalette.py)
- data/rom_2.s spells out every OAM entry with its tileNum and paletteNum,
and the g<Board>BoardSpriteSets tables say which a board uses. Segments lie
end to end in tile order, so each owns a tile range and the entries landing
wholly inside it name its bank. An entry crossing a boundary is reading a
different sheet -- OBJ VRAM at 0x06010000 is rewritten constantly -- and is
dropped. Second pass over the g<Board>*OamData arrays for the boss sheets,
which the code indexes frame by frame instead of via a sprite set.
- Tile counts come from the built per-segment .4bpp: -oam and -oamshape
segments are not width*height/64, and PNG dimensions drift ruby by 13 tiles.
- Banks holding under four distinct colours cannot win the vote. Those are
flash states and banks a board leaves at zero, and would render solid.
- The result is recorded in each gfx config as "palette"/"palbank", so it is
reviewable and `objpalette.py check` can verify the PNGs against it.
Scratch regions
Eight tile ranges are streamed over at run time from a table of interchangeable
art, and coloured by a palette that arrives with the replacement tiles rather
than sitting in the board's own set. Which entry the sheet ships is recoverable
by searching the ROM for the segment's tiles -- exact for all but the Geodude
portrait, matched by likeness. Listed with their writers in VRAM_LAYOUT.md.
OBJ bank 11 on the main fields is never its resting ROM value either:
UpdateSpoinkAnimation rewrites it every frame from gFieldPaletteVariants,
three brightness sets crossed with which half of the board is on screen.
Ruby's palset_0 is 0x120 bytes, not the 0x200 the DMA copies, so its OBJ banks
9-15 come from the next label, gBonusStageObjPal. Sapphire leaves banks 12-15
at zero and loads them when the sprites that use them appear.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removes 74 baserom incbins (104,512 bytes) and makes every background
tile sheet in the game readable and correctly coloured, byte-for-byte
identical to the ROM throughout.
Palettes
- Extract 69 standalone palettes to JASC .pal, built via the existing
%.gbapal: %.pal rule. gbagfx's gbapal->pal direction pads anything that
is not 16 or 256 colours up to 256 (gfx.c:822), so the JASC is written
directly from the ROM bytes and gbagfx only does pal->gbapal.
- Convert the 9 legacy raw-binary .pal files to JASC, so a single .pal
format exists. dusclops_anim held 496 colours, over the JASC limit, so
it becomes 31 per-frame files.
BG sheets (tools/scripts/detilemap.py)
- BG tile sheets are deduplicated, so the raw sheet is unreadable as an
image at any width. The checked-in source becomes the assembled screen
(8bpp, pixel = bank*16 + index, which keeps every per-cell palette bank
distinct) and detilemap folds the screens back through their tilemaps.
- Covers all 9 intro scenes and all 8 boards: 21 sheets.
- Ruby and Sapphire stream their board through a 22-slot scroll ring, so
a tilemap row R draws chunk R of a virtual bgtop+bgbottom strip rather
than indexing the sheet. --row-chunk handles that; the model is written
up in data/board_data/VRAM_LAYOUT.md.
- 8 sheets have non-blank tiles no tilemap reaches (BG0 tilemaps are built
at runtime); --extra carries them rather than silently zeroing.
Build
- .SECONDARY was unscoped, which marked $(OBJS) secondary too. Make does
not rebuild a missing secondary file when its dependant looks current,
so deleting an object and running `make compare` relinked a stale ELF
and printed OK. Now scoped to generated art and audio. .PRECIOUS with
patterns would be terser but defeats .DELETE_ON_ERROR, which would let
a half-written .4bpp produce a wrong ROM silently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both of my earlier readings were wrong. These are not frames of a fixed
size at all. UpdateKickbackLogic in main_board_pichu_entity.c does not
just move the sprites -- it writes raw OAM entries straight out of
gCatchOverlayOamData, three u16 per sprite for four sprites, once per
animation frame. So each frame chooses its own sprite sizes and tile
numbers, and no single metatile can describe the data.
Reading that table out gives the real layout: 14 frames referencing 27
distinct sprites packed back to back with no alignment, in sizes from
8x8 to 32x32 (4x4, 4x2, 4x1, 2x1, 1x4 and 1x1 in tiles). 275 of the 288
tiles are referenced; the last 13 never are.
pika_saver_coverage_shape.json is generated from that table and passed
to gbagfx with -oamshape, so every sprite now appears in the sheet at its
true dimensions instead of being sliced on a grid that does not exist.
Two tool limits shaped the packing: pieces must be legal OBJ shapes
(spacers are exempt) and the bounding box is capped at 32x32, so the
sprites are laid out in four columns rather than one, 16x21 tiles.
Graphics only; both .4bpp round-trip byte-identically and make compare
passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
I had assumed these two shared the 4x3 frame shape of the pika saver
banks at 0x084C07EC. They do not: they are drawn as a single
SPRITE_SIZE_32x16 by gPikaKickbackLaunchFxSpriteSet, so the frame is
4x2, and 0x2400 divides into 36 frames rather than 24. Graphics only;
both .4bpp still rebuild byte-identically and make compare passes.
The old 4x3 framing cut through the artwork -- the first frame held a
whole Pikachu face plus the top of the next one. Measuring seam
continuity across tile-row boundaries makes it unambiguous: at a frame
height of 2 rows the boundaries are more discontinuous than the
interiors (+2.17 on full, +0.86 on partial), while at 3, 4 and 6 rows
the contrast is negative, meaning those boundaries fall in the middle of
continuous art. The same measurement confirms 4x3 for the other three
assets, which are unchanged.
The pixel order was never wrong, since a 4x3 OAM metatile in a 4-wide
image decomposes to 32x16 over 32x8 and comes out in plain linear order
either way. What was wrong was the declared frame height, and with it
the sheet layout. The two sheets are now 6 frames per row instead of a
36-frame ribbon, so the frames read as frames.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
57 pieces out of baserom.gba, in colour. ROM still matches
(make compare).
Three groups per board, one per coin value, each with lit variants of
three pieces. The two boards differ in table shape: ruby has four
variant rows with the destinations fifth, sapphire has three plus a
destination row and a row of spacers. Ruby repeats the first variant in
groups 0 and 2, so 57 distinct pieces cover the 72 slots.
Piece 2 of groups 0 and 1 is never copied on either board -- their
destination overlaps the next group's piece 0 -- so there is no DMA size
to take. Those 13 are extracted at their full declared span instead.
Everything else uses the size from DrawRubyCoinRewardMeter and
DrawSapphireCoinRewardMeter.
Palettes are bank 9 of gRubyBoardPalette and bank 11 of
gSapphireBoardPalette, both tilemap-attested. The two boards do not
match here: ruby's coin arrows are green with white numerals, sapphire's
are light blue with white numerals, which is what the field scans show.
Sapphire bank 0 is also blue and scores within 3 RGB units of bank 11
against the scan, but it is navy where the board art is clearly light
blue, so the render decided it rather than the distance.
Left alone: graphics/stage/ruby/ruby_coin_arrows.png, an earlier
extraction of the whole 0xC00 block as one 256x24 sheet, still
referenced by a commented-out incbin above the first symbol. It matches
baserom exactly but cannot be wired up without collapsing the 30
individual symbols, which is presumably why it was left commented. It is
now redundant and could be removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
60 pieces out of baserom.gba, in colour. ROM still matches
(make compare).
Same shape as the evo arrows: three letter groups spelling GET, each
with a set of variants for the lit progression, each variant three
pieces going to three VRAM destinations one tilemap row apart. Group
sizes differ per piece (0x40 or 0x60) and were taken from the
DmaCopy16 calls in DrawRubyCatchArrowProgress and
DrawSapphireCatchArrowProgress. Groups 0 and 2 repeat their first
variant, so 60 distinct pieces cover the 72 table slots.
30 symbols span more ROM than is copied; the remainder is .space where
blank (22) and a baserom incbin otherwise (8).
Palettes are bank 8 of gRubyBoardPalette and bank 11 of
gSapphireBoardPalette. Both are referenced by their board tilemap at
these tiles and both render the arrows red-orange with a white letter,
matching the Ruby and Sapphire Field scans. Index layouts differ per
board again: ruby draws the body at index 5, sapphire at index 7.
Note these are the "Catch 'em Arrows" in the Bulbapedia legends; the
symbol and function names disagree with each other already
(gRubyGetArrowTilePtrs drawn by DrawRubyCatchArrowProgress) and were
left alone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
78 arrow rows out of baserom.gba, in colour. ROM still matches
(make compare).
Each arrow is four rows that go to four separate VRAM destinations one
tilemap row apart, with an off and an on state (mart has four states:
mart off/on and evo off/on, since the ramp is shared). Row widths are
not uniform -- they run 2 to 5 tiles -- so the copied size per row was
taken from the DmaCopy16 calls in ruby_board_indicators.c and
sapphire_board_indicators.c rather than assumed from the symbol spans.
22 symbols span more ROM than the game copies. Only the copied part is
extracted; the remainder stays as a baserom incbin, or .space where it
is blank (6 of them).
gSapphireBumperArrow_082E0860 and _082E08E0 are skipped: they are
already .space, and both the table comment and the fact that
AnimateSapphireBumperArrowPalette only copies rows 1-3 confirm row 0 is
unused.
Palette is bank 11 of gRubyBoardPalette / gSapphireBoardPalette. That
bank is the only one on either board that is both yellow at the arrow
body index and actually referenced by the board tilemap at these tiles.
The two boards do not share index layouts -- ruby draws the body at
index 10 with outline 15, sapphire at index 4 with outline 11 -- so each
was decoded separately. Checked against the Ruby and Sapphire Field
scans on Bulbapedia, where all four arrow types on both boards are
yellow with a dark outline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Finds palettes still on baserom.gba whose symbol name pairs with an
already-extracted graphic (<base>_Gfx / <base>_Pal), extracts them, and
re-renders the matching PNGs in colour instead of greyscale. ROM still
matches (make compare).
gEReaderBackground_Pals 0x81D20 0x200
gMainBoardEvoBanner_Pal 0x5221AC 0x200
gMainCatchModeBanner_Pal 0x5267CC 0x200
gMainBoardJirachiBanner_Pal 0x5269CC 0x200
gMainBoardTravel_Pal 0x526BCC 0x200
gRubyChinchouCatchBurstBanner_Pal 0x51956C 0x20
gRubyLotadCatchBurstBanner_Pal 0x51958C 0x20
gSapphireShroomishCatchBurstBanner_Pal 0x5195AC 0x1C0
The name pairing alone would not say which bank of a 0x200 palette
belongs to the graphic, so that was checked rather than assumed: in
seven of the eight only bank 0 is non-blank and the rest is zero
padding, which makes the choice unambiguous. gEReaderBackground_Pals
has banks 0 and 1 populated and bank 0 is the one embedded.
The PNGs are re-rendered through gbagfx rather than by rewriting the
PNG header. gbagfx uses the colour type as the signal for whether to
invert pixel values (see the comment in convert_png.c), so flipping a
greyscale PNG to indexed in place silently changes every index and the
built .4bpp with it. Going through the tool keeps both directions
consistent; each file was checked to rebuild its .4bpp byte-identically
before this was committed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All five are OBJ tile data (destinations 0x06010480, 0x06010600 and
0x06015800 are all in OBJ VRAM), and all five are banks of 0x180 frames
drawn as SPRITE_SIZE_32x16 over SPRITE_SIZE_32x8 per
gPikachuKickbackSpriteSet / gPichuKickbackSpriteSet, so each frame is a
4x3 tile rectangle and they convert with -mwidth 4 -mheight 3 -oam.
ROM still matches (make compare).
gPikaSaverTilesGfx 3 frames
gDxModePikachuObjTiles 6 frames
gPikachuSaverTilesGfx 6 frames
gPikaSaverFullCoverageGfx 24 frames
gPikaSaverPartialCoverageGfx 24 frames
The first three are one contiguous 15 frame bank: the index expression
gPikaSaverTilesGfx + pikaSaverTileIndex * 0x180 reaches index 9, which
is 0xD80 and lands exactly on gPikachuSaverTilesGfx, so reads run past
the first symbol into the two that follow. The symbol boundaries are
noted in a comment rather than changed.
The coverage pair is declared 0x2420 but only 0x2400 is copied; the
trailing 0x20 is a blank tile and becomes .space 0x20.
The 4x3 shape was not assumed for the coverage pair. At a plain tile
width its sprites are visibly fragmented; as 24 4x3 frames each one
resolves into a single coherent sprite, and the round trip is exact.
PNGs are greyscale, matching pika_spinner.png, the neighbouring pika
asset that uses the same OBJ palette bank (3).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The evolution mode counterpart of the shop mode extraction in the
previous commit, using the same conventions. ROM still matches
(make compare).
gEvoNameDisplay_Pals is a single 16-colour palette with no padding,
dumped as JASC .pal and incbin'd as the built .gbapal.
gEvoModeBG_Gfx and gUnknown_081B5784/6784/7784 are BG0 tilemaps, not
tile graphics, exactly as the shop mode frames are: gShopEvoBGAnimFrames
lists them as the second group of four, and they are copied to the same
VRAM + 0x2000 that BGCNT_SCREENBASE(4) puts BG0's map at. Verified
before dumping: each is 2048 entries (32x64), tile indices 416-511,
palette banks 0 and 12, H/V flip bits in use, with rows 49-63 uniform
0x01FF filler identical across all four frames. Bank 12 is where
gEvoNameDisplay_Pals is loaded (PLTT + 0x180).
As before the gUnknown_ symbols keep their placeholder names; only the
extracted file names record what they are.
gMainBoardEvoBanner_Pal is left on baserom.gba deliberately. It is one
of four consecutive banner palettes (evo, catch mode, jirachi, travel)
that are all still unextracted, so that run is better done as a group.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the baserom.gba incbins for the shop-related symbols in
data/rom_1.s with checked-in assets. ROM still matches (make compare).
Palettes are dumped as JASC .pal and incbin'd as the built .gbapal,
matching how graphics/intro/scene1torchic/sprites.gbapal is handled.
gShopNameDisplay_Pals is one palette plus 0x1C0 of zero padding, so it
is split .incbin/.space.
The four shop mode BG frames are BG0 tilemaps, not tile graphics despite
the _Gfx suffix: BG0 is BGCNT_TXT256x512 with BGCNT_SCREENBASE(4), which
is the BG_VRAM + 0x2000 the DmaCopy16 targets. Each frame is a full 32x64
map; only the first 49 rows (0xC40) are copied. They are dumped as .bin.
The two sprite sheets are laid out from their real OAM geometry rather
than an arbitrary tile width, since sprite tiles are stored per hardware
object rather than row-major:
gShopPortraitOverlayGfx 32x32 + 16x32 -> 6x4 metatile, -oam
gSapphireShopSignTileGfx 64x32 + 32x8 -> shop_sign_shape.json
The sign's two objects do not form a rectangle (36 tiles in a 40-tile
bounding box), so it needs an -oamshape file with a spacer, like
sharpedo_shape.json.
No symbols are renamed by this commit. gUnknown_081B9984/A984/B984 keep
their placeholder names, but their extracted files are named
shop_mode_bg0_frame1/2/3_tilemap.bin, because gShopEvoBGAnimFrames in
data/rom_2.s lists them directly after gShopModeBG_Gfx as the remaining
three frames of the same animation. Renaming the symbols to match is
left to a separate change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>