Files
gen1recomp/tests/game3_import2_misc_assets_test.lua
thibautbus aaacf02dc0 Draw a FireRed egg's menu icon as an egg
The party, the PC boxes and the release animation drew an egg with the icon
of the species it will hatch into, so a Pichu egg showed as a Pichu. pret's
party menu picks the icon from GetMonData(MON_DATA_SPECIES_OR_EGG), which is
SPECIES_EGG for any egg (pokefirered/src/pokemon.c:3245, party_menu.c:2655).

Pokemon.speciesOrEgg does the same, and the icon call sites use it. The
species pass only extracted icons up to NUM_SPECIES - 1, so the egg extractor
now also bakes gMonIconTable[SPECIES_EGG] as pokemon/icons/412.rgba. Cache
version 114 re-imports existing caches so the icon appears.
2026-09-22 12:10:44 +02:00

404 lines
18 KiB
Lua

#!/usr/bin/env luajit
-- src/script_menu.c:1161, src/region_map.c:790, src/learn_move.c:403,
-- src/battle_records.c:563, src/trainer_card.c:265, :1454, :1560
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local function eq(a, b, msg)
check(a == b, string.format("%s (%s == %s)", msg, tostring(a), tostring(b)))
end
local function finish()
if failed > 0 then
print(string.format("[result] %d CHECK(S) FAILED", failed))
os.exit(1)
end
print("[result] all checks passed")
os.exit(0)
end
local CacheContract = require("src.import.CacheContract")
local Versions = require("src.import.gba.versions")
local MuseumExtract = require("src.import.gba.museum_extract")
local MoveRelearnerExtract = require("src.import.gba.move_relearner_extract")
local TrainerTowerExtract = require("src.import.gba.trainer_tower_extract")
local TrainerCardExtract = require("src.import.gba.trainer_card_extract")
local RegionMapExtract = require("src.import.gba.region_map_extract")
local EggExtract = require("src.import.gba.egg_extract")
print("[test] 1. the ROM offsets follow pret's declaration order")
-- src/script_menu.c:647 aerodactyl tiles, palette, then kabutops tiles, palette
eq(Versions.MUSEUM_AERODACTYL_GFX, 0x3E0780, "MUSEUM_AERODACTYL_GFX is sMuseumAerodactylSprTiles")
eq(Versions.MUSEUM_AERODACTYL_GFX + 64 * 64 / 2, Versions.MUSEUM_AERODACTYL_PAL,
"the aerodactyl palette follows its 2048-byte 64x64 sheet")
eq(Versions.MUSEUM_AERODACTYL_PAL + 32, Versions.MUSEUM_KABUTOPS_GFX,
"the kabutops sheet follows the aerodactyl palette bank")
eq(Versions.MUSEUM_KABUTOPS_GFX + 64 * 64 / 2, Versions.MUSEUM_KABUTOPS_PAL,
"the kabutops palette follows its sheet")
eq(Versions.MUSEUM_FOSSIL_SIZE, 64, "the fossil sprite is SPRITE_SIZE(64x64)")
-- src/learn_move.c:403, pokefirered.map:34302
eq(Versions.MOVE_RELEARNER_PAL, 0xE97DDC, "MOVE_RELEARNER_PAL is gMoveRelearner_Pal")
eq(Versions.MOVE_RELEARNER_PAL + 32, Versions.MOVE_RELEARNER_GFX,
"gMoveRelearner_Gfx follows the one loaded palette bank")
eq(Versions.MOVE_RELEARNER_TILEMAP, 0xE97EC4, "MOVE_RELEARNER_TILEMAP is gMoveRelearner_Tilemap")
-- src/battle_records.c:37 sTiles, :38 sPalette, :39 sTilemap
eq(Versions.BATTLE_RECORDS_GFX + 0xC0, Versions.BATTLE_RECORDS_PAL,
"sPalette follows sTiles' 0xC0 bytes")
eq(Versions.BATTLE_RECORDS_PAL + 32, Versions.BATTLE_RECORDS_TILEMAP,
"sTilemap follows the one palette bank")
-- src/trainer_card.c:174 sKantoTrainerCardBadges_Pal, not the Hoenn bank one slot earlier
eq(Versions.TRAINER_CARD_BADGES_PAL, 0x3CD2E0, "TRAINER_CARD_BADGES_PAL is the Kanto bank")
eq(Versions.TRAINER_CARD_BADGES_PAL + 32, Versions.TRAINER_CARD_STAR_PAL,
"sTrainerCardStar_Pal follows sKantoTrainerCardBadges_Pal")
eq(Versions.TRAINER_CARD_STAR_PAL + 32, Versions.TRAINER_CARD_STICKER_PAL1,
"sTrainerCardStickerPal1 follows sTrainerCardStar_Pal")
for i, off in ipairs({ Versions.TRAINER_CARD_STICKER_PAL2, Versions.TRAINER_CARD_STICKER_PAL3,
Versions.TRAINER_CARD_STICKER_PAL4 }) do
eq(Versions.TRAINER_CARD_STICKER_PAL1 + 32 * i, off,
"sTrainerCardStickerPal" .. (i + 1) .. " is one bank on")
end
for i, off in ipairs({ Versions.TRAINER_CARD_BRONZE_PAL, Versions.TRAINER_CARD_SILVER_PAL,
Versions.TRAINER_CARD_GOLD_PAL }) do
eq(Versions.TRAINER_CARD_GREEN_PAL + 0xC0 * i, off,
"the " .. (i + 1) .. "-star palette is 0xC0 past the green one")
end
eq(Versions.TRAINER_CARD_STAR_TILE, 143, "the star is card tile 143")
-- src/daycare.c:137 sEggPalette, then the two raw 4bpp sheets
eq(Versions.EGG_PALETTE, 0x25F842, "EGG_PALETTE is sEggPalette")
eq(Versions.EGG_PALETTE + 32, Versions.EGG_HATCH_GFX, "sEggHatchTiles follows the one egg bank")
eq(Versions.EGG_HATCH_GFX + 2048, Versions.EGG_SHARD_GFX,
"sEggShardTiles follows the four 32x32 hatch frames")
eq(EggExtract.SPECIES_EGG, 412, "the EGG is species 412, one past the last species")
print("[test] 2. the cache contract requires every new key")
local required = CacheContract.requiredFiles("firered")
local have = {}
for _, path in ipairs(required) do have[path] = true end
local WANT = {
"data/generated/gba/museum/manifest.lua",
"data/generated/gba/museum/kabutops.rgba",
"data/generated/gba/museum/aerodactyl.rgba",
"data/generated/gba/region_map/dungeon_icon_visited.rgba",
"data/generated/gba/region_map/dungeon_icon_visited.png",
"data/generated/gba/move_relearner/manifest.lua",
"data/generated/gba/move_relearner/bg.rgba",
"data/generated/gba/pokemon/egg/hatch.rgba",
"data/generated/gba/pokemon/egg/shard.rgba",
"data/generated/gba/pokemon/front/412.rgba",
"data/generated/gba/pokemon/icons/412.rgba",
"data/generated/gba/trainer_tower/manifest.lua",
"data/generated/gba/trainer_tower/records_bg.rgba",
"data/generated/gba/trainer_card/front_0.rgba",
"data/generated/gba/trainer_card/back_0.rgba",
"data/generated/gba/trainer_card/back_4_female.rgba",
"data/generated/gba/trainer_card/screen_0.rgba",
"data/generated/gba/trainer_card/star.rgba",
"data/generated/gba/trainer_card/stickers.rgba",
"data/generated/gba/teachy_tv/static.rgba",
"data/generated/gba/teachy_tv/bg3.rgba",
}
for _, path in ipairs(WANT) do
check(have[path] == true, "the firered contract requires " .. path)
end
for _, path in ipairs(WANT) do
local fs = {
prefix = "",
exists = function(candidate) return candidate ~= path end,
}
local complete, missing = CacheContract.allRequiredFilesExist("firered", fs)
check(complete == false and missing == path,
"a cache without " .. path .. " is incomplete (" .. tostring(missing) .. ")")
end
print("[test] 3. ready() refuses a half-written group, so an old cache repairs")
local written = {}
local stub = {
write = function(_, rel, bytes) written[rel] = bytes end,
read = function(_, rel) return written[rel] end,
exists = function(_, rel) return written[rel] ~= nil end,
}
check(MuseumExtract.ready(stub, "x") == false, "museum ready() is false with nothing written")
written["x/museum/kabutops.rgba"] = "stub"
written["x/museum/manifest.lua"] = "stub"
check(MuseumExtract.ready(stub, "x") == false, "museum ready() is false without aerodactyl")
written["x/museum/aerodactyl.rgba"] = "stub"
check(MuseumExtract.ready(stub, "x") == true, "museum ready() accepts both fossils")
check(MoveRelearnerExtract.ready(stub, "x") == false, "relearner ready() is false with no bg")
written["x/move_relearner/bg.rgba"] = "stub"
check(MoveRelearnerExtract.ready(stub, "x") == false, "relearner ready() wants its manifest too")
written["x/move_relearner/manifest.lua"] = "stub"
check(MoveRelearnerExtract.ready(stub, "x") == true, "relearner ready() accepts the pair")
written["x/trainer_tower.lua"] = "stub"
check(TrainerTowerExtract.ready(stub, "x") == false,
"tower ready() is false with the floor table but no records background")
written["x/trainer_tower/records_bg.rgba"] = "stub"
written["x/trainer_tower/manifest.lua"] = "stub"
check(TrainerTowerExtract.ready(stub, "x") == true, "tower ready() accepts table plus records art")
local cardCache = {}
local cardStub = {
read = function(_, rel) return cardCache[rel] end,
}
cardCache["x/trainer_card/bg.rgba"] = string.rep("z", 240 * 160 * 4)
check(TrainerCardExtract.ready(cardStub, "x") == false,
"card ready() is false with only the legacy bg composite")
cardCache["x/trainer_card/back_0.rgba"] = string.rep("z", 240 * 160 * 4)
cardCache["x/trainer_card/star.rgba"] = string.rep("z", 8 * 8 * 4)
check(TrainerCardExtract.ready(cardStub, "x") == false, "card ready() still wants the stickers")
cardCache["x/trainer_card/stickers.rgba"] = string.rep("z", 64 * 64 * 4)
check(TrainerCardExtract.ready(cardStub, "x") == true, "card ready() accepts the full set")
check(EggExtract.ready(stub, "x") == false, "egg ready() is false with no hatch sheet")
written["x/pokemon/egg/hatch.rgba"] = "stub"
written["x/pokemon/egg/shard.rgba"] = "stub"
written["x/pokemon/egg/manifest.lua"] = "stub"
check(EggExtract.ready(stub, "x") == false, "egg ready() also wants the SPECIES_EGG front pic")
written["x/pokemon/front/412.rgba"] = "stub"
check(EggExtract.ready(stub, "x") == false, "egg ready() also wants the SPECIES_EGG icon")
written["x/pokemon/icons/412.rgba"] = "stub"
check(EggExtract.ready(stub, "x") == true, "egg ready() accepts the whole group")
local regionFiles = {}
for _, name in ipairs(RegionMapExtract.FILES) do regionFiles[name] = true end
check(regionFiles["dungeon_icon_visited.png"] == true,
"region_map ready() lists the visited dungeon marker")
print("[test] 4. a built cache carries the baked art")
local Cache = require("tests.game3_cache")
local root = Cache.root("museum/manifest.lua")
if not root then
print("[skip] museum, marker, relearner, records and trainer card art: "
.. tostring(Cache.reason))
finish()
end
print("[info] FireRed cache at " .. root)
local function readFile(rel)
local f = io.open(root .. "/" .. rel, "rb")
if not f then return nil end
local d = f:read("*a")
f:close()
return d
end
local function loadTable(rel)
local src = readFile(rel)
if not src then return nil end
local chunk = (loadstring or load)(src)
if not chunk then return nil end
local ok, t = pcall(chunk)
if ok and type(t) == "table" then return t end
return nil
end
local function pixel(blob, w, x, y)
local o = (y * w + x) * 4
return blob:byte(o + 1), blob:byte(o + 2), blob:byte(o + 3), blob:byte(o + 4)
end
local function distinctColors(blob)
local seen, n = {}, 0
for i = 1, #blob, 4 do
local key = blob:sub(i, i + 3)
if not seen[key] then
seen[key] = true
n = n + 1
end
end
return n
end
local function opaquePixels(blob)
local n = 0
for i = 4, #blob, 4 do
if blob:byte(i) == 255 then n = n + 1 end
end
return n
end
local SHEETS = {
{ "museum/kabutops.rgba", 64, 64 },
{ "museum/aerodactyl.rgba", 64, 64 },
{ "region_map/dungeon_icon.rgba", 8, 8 },
{ "region_map/dungeon_icon_visited.rgba", 8, 8 },
{ "move_relearner/bg.rgba", 240, 160 },
{ "pokemon/egg/hatch.rgba", 32, 128 },
{ "pokemon/egg/shard.rgba", 32, 8 },
{ "pokemon/front/412.rgba", 64, 64 },
{ "pokemon/icons/412.rgba", 32, 64 },
{ "trainer_tower/records_bg.rgba", 240, 160 },
{ "trainer_card/front_0.rgba", 240, 160 },
{ "trainer_card/front_4_female.rgba", 240, 160 },
{ "trainer_card/back_0.rgba", 240, 160 },
{ "trainer_card/back_4.rgba", 240, 160 },
{ "trainer_card/screen_0.rgba", 240, 160 },
{ "trainer_card/star.rgba", 8, 8 },
{ "trainer_card/stickers.rgba", 64, 64 },
}
local blobs = {}
for _, sheet in ipairs(SHEETS) do
local rel, w, h = sheet[1], sheet[2], sheet[3]
local blob = readFile(rel)
blobs[rel] = blob
eq(blob and #blob or nil, w * h * 4, rel .. " is " .. w .. "x" .. h)
end
print("[test] 5. the museum fossil pictures are the cart's sprites")
-- src/script_menu.c:1173 CreateSprite(&sMuseumFossilSprTemplate), OAM colour 0 transparent
check(distinctColors(blobs["museum/kabutops.rgba"]) > 4, "the KABUTOPS fossil is painted art")
check(distinctColors(blobs["museum/aerodactyl.rgba"]) > 4, "the AERODACTYL fossil is painted art")
check(blobs["museum/kabutops.rgba"] ~= blobs["museum/aerodactyl.rgba"],
"the two fossils are different pictures")
local _, _, _, kCorner = pixel(blobs["museum/kabutops.rgba"], 64, 0, 0)
eq(kCorner, 0, "the fossil sheet corner is transparent, as OAM index 0 is")
local kOpaque = opaquePixels(blobs["museum/kabutops.rgba"])
check(kOpaque > 500 and kOpaque < 64 * 64,
"the KABUTOPS skeleton covers part of the 64x64 frame (" .. kOpaque .. " px)")
local museumMan = loadTable("museum/manifest.lua")
check(type(museumMan) == "table", "museum/manifest.lua loads")
if museumMan then
eq(museumMan.width, 64, "the manifest carries the 64x64 size")
eq(museumMan.species.kabutops, 141, "the manifest names SPECIES_KABUTOPS")
eq(museumMan.species.aerodactyl, 142, "the manifest names SPECIES_AERODACTYL")
end
print("[test] 6. the second dungeon marker frame")
-- src/region_map.c:790 sAnim_DungeonIconVisited is ANIMCMD_FRAME(1), :795 not-visited is frame 0
check(blobs["region_map/dungeon_icon.rgba"] ~= blobs["region_map/dungeon_icon_visited.rgba"],
"frame 1 is different art from frame 0")
local blobOpaque = opaquePixels(blobs["region_map/dungeon_icon.rgba"])
local ringOpaque = opaquePixels(blobs["region_map/dungeon_icon_visited.rgba"])
check(blobOpaque > 0 and ringOpaque > 0, "both marker frames paint pixels")
check(ringOpaque > blobOpaque,
string.format("the visited ring covers more of the 8x8 cell than the unvisited blob (%d > %d)",
ringOpaque, blobOpaque))
local visitedPng = readFile("region_map/dungeon_icon_visited.png")
check(type(visitedPng) == "string" and visitedPng:sub(1, 8) == "\137PNG\r\n\026\n",
"dungeon_icon_visited.png is a PNG, which is what the Town Map loads")
print("[test] 7. the move relearner and battle records backgrounds")
-- src/learn_move.c:403, BG1 covers 19x15 tiles and leaves the rest on the backdrop
check(distinctColors(blobs["move_relearner/bg.rgba"]) > 4, "the relearner background is painted art")
local pr, pg, pb = pixel(blobs["move_relearner/bg.rgba"], 240, 8, 8)
check(pr > 100 and pg > 150 and pb > 150,
string.format("the relearner panel is the cart's pale green (%d,%d,%d)", pr, pg, pb))
local br, bg2, bb = pixel(blobs["move_relearner/bg.rgba"], 240, 239, 159)
check(br == 0 and bg2 == 0 and bb == 0,
"outside the 19x15 tile panel the relearner background is the backdrop colour")
local relearnerMan = loadTable("move_relearner/manifest.lua")
check(relearnerMan ~= nil and relearnerMan.width == 240 and relearnerMan.height == 160,
"move_relearner/manifest.lua is a 240x160 sheet")
check(distinctColors(blobs["trainer_tower/records_bg.rgba"]) > 4,
"the battle records board is painted art")
local towerMan = loadTable("trainer_tower/manifest.lua")
check(towerMan ~= nil and towerMan.recordsBg ~= nil and towerMan.recordsBg.width == 240,
"trainer_tower/manifest.lua carries recordsBg 240x160")
print("[test] 8. the five trainer card star palettes, the star and the stickers")
-- src/trainer_card.c:1484 LoadPalette(sKantoTrainerCardPals[stars], BG_PLTT_ID(0), 3 banks)
local legacy = readFile("trainer_card/bg.rgba")
check(legacy == blobs["trainer_card/front_0.rgba"],
"front_0 is the blue 0-star card the old bg.rgba already was")
check(readFile("trainer_card/bg_female.rgba") == readFile("trainer_card/front_0_female.rgba"),
"front_0_female matches the old bg_female.rgba")
local seen = {}
for stars = 0, 4 do
local blob = readFile(string.format("trainer_card/front_%d.rgba", stars))
eq(blob and #blob or nil, 240 * 160 * 4, "front_" .. stars .. " is 240x160")
check(seen[blob] == nil, "the " .. stars .. "-star front is its own palette")
seen[blob] = stars
end
check(blobs["trainer_card/back_0.rgba"] ~= blobs["trainer_card/front_0.rgba"],
"the back of the card is a different tilemap from the front")
check(blobs["trainer_card/screen_0.rgba"] ~= blobs["trainer_card/front_0.rgba"],
"the flip screen is the bg layer alone")
-- src/trainer_card.c:1494 the female override only repaints BG bank 1
check(blobs["trainer_card/front_4_female.rgba"] ~= readFile("trainer_card/front_4.rgba"),
"the female card differs from the male one")
-- src/trainer_card.c:1560 FillBgTilemapBufferRect(3, 143, ..., palette 4)
local starOpaque = opaquePixels(blobs["trainer_card/star.rgba"])
check(starOpaque > 8 and starOpaque < 64,
"the star tile paints a star on transparency (" .. starOpaque .. " px)")
-- src/trainer_card.c:1450 sticker slot i is the 2x2 group at tile 4i, palettes 11..14
local stickers = blobs["trainer_card/stickers.rgba"]
local rows = {}
for p = 0, 3 do
rows[p] = stickers:sub(p * 16 * 64 * 4 + 1, (p + 1) * 16 * 64 * 4)
end
for p = 1, 3 do
check(rows[p] ~= rows[0], "sticker row " .. p .. " is its own palette bank")
end
local slot0 = 0
for y = 0, 15 do
for x = 0, 15 do
local _, _, _, a = pixel(stickers, 64, x, y)
if a == 255 then slot0 = slot0 + 1 end
end
end
check(slot0 > 32, "sticker slot 0 paints its 16x16 cell (" .. slot0 .. " px)")
local cardMan = loadTable("trainer_card/manifest.lua")
check(cardMan ~= nil and cardMan.starCount == 5 and cardMan.stickerSlots == 4
and cardMan.stickerPalettes == 4,
"trainer_card/manifest.lua carries the star and sticker metrics")
print("[test] 9. the EGG front pic, the hatch frames and the shell shards")
-- src/daycare.c:158 the four hatch anims are frames 0, 16, 32 and 48 of one sheet
local hatch = blobs["pokemon/egg/hatch.rgba"]
local hatchFrames = {}
for f = 0, 3 do hatchFrames[f] = hatch:sub(f * 32 * 32 * 4 + 1, (f + 1) * 32 * 32 * 4) end
for f = 1, 3 do
check(hatchFrames[f] ~= hatchFrames[f - 1],
"hatch frame " .. f .. " cracks further than frame " .. (f - 1))
end
local cracked = 0
for i = 1, #hatchFrames[0], 4 do
if hatchFrames[0]:sub(i, i + 3) ~= hatchFrames[3]:sub(i, i + 3) then cracked = cracked + 1 end
end
check(cracked > 20,
string.format("the last hatch frame repaints the shell with cracks (%d px differ)", cracked))
-- src/daycare.c:243 the four shard anims are single 8x8 tiles, one after the other
local shard = blobs["pokemon/egg/shard.rgba"]
local shardCells = {}
for f = 0, 3 do
local cell, rows = 0, {}
for y = 0, 7 do
for x = 0, 7 do
local r, g, b, a = pixel(shard, 32, f * 8 + x, y)
rows[#rows + 1] = string.char(r or 0, g or 0, b or 0, a or 0)
if a == 255 then cell = cell + 1 end
end
end
check(cell > 0 and cell < 64, "shell shard " .. f .. " paints part of its tile (" .. cell .. " px)")
shardCells[f] = table.concat(rows)
end
for f = 1, 3 do
check(shardCells[f] ~= shardCells[f - 1], "shell shard " .. f .. " is its own piece")
end
local Pokemon = require("src.core.game3.pokemon")
eq(Pokemon.SPECIES_EGG, EggExtract.SPECIES_EGG,
"the engine and the extractor agree on the EGG species id")
local eggPic = blobs["pokemon/front/412.rgba"]
check(distinctColors(eggPic) > 4, "the EGG front pic is painted art")
local _, _, _, eggCorner = pixel(eggPic, 64, 0, 0)
eq(eggCorner, 0, "the EGG pic corner is transparent, as pic index 0 is")
local eggIcon = blobs["pokemon/icons/412.rgba"]
check(distinctColors(eggIcon) > 2, "the EGG menu icon is painted art")
local eggMan = loadTable("pokemon/egg/manifest.lua")
check(eggMan ~= nil and eggMan.hatch ~= nil and eggMan.hatch.frames == 4
and eggMan.hatch.sheetHeight == 128 and eggMan.shard.sheetWidth == 32,
"pokemon/egg/manifest.lua carries both sheet layouts")
finish()