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.
This commit is contained in:
thibautbus
2026-09-22 11:51:01 +02:00
parent ee63ffff2d
commit aaacf02dc0
13 changed files with 92 additions and 28 deletions
+7
View File
@@ -1256,6 +1256,13 @@ function Pokemon.isEgg(mon)
return (mon.isEgg == true) or (mon.egg == true) or (mon.species == 412)
end
-- pokefirered/src/pokemon.c:3245 MON_DATA_SPECIES_OR_EGG: an egg's menu icon is
-- SPECIES_EGG's, not the species it will hatch into (party_menu.c:2655).
function Pokemon.speciesOrEgg(mon)
if Pokemon.isEgg(mon) then return Pokemon.SPECIES_EGG end
return Pokemon.speciesOf(mon)
end
local function read_rgba(species)
local cache = resolve_cache(Pokemon._cache)
local root = (Extract.CACHE_ROOT or "data/generated/gba") .. "/pokemon"
+2
View File
@@ -377,6 +377,8 @@ CacheContract.VERSION_REQUIRED_FILES_OVERRIDE = {
"data/generated/gba/pokemon/egg/hatch.rgba",
"data/generated/gba/pokemon/egg/shard.rgba",
"data/generated/gba/pokemon/front/412.rgba",
-- src/party_menu.c:2655
"data/generated/gba/pokemon/icons/412.rgba",
-- src/battle_records.c:563
"data/generated/gba/trainer_tower/manifest.lua",
"data/generated/gba/trainer_tower/records_bg.rgba",
+11 -1
View File
@@ -3,6 +3,7 @@
local Versions = require("src.import.gba.versions")
local Lz77 = require("src.import.gba.lz77")
local BgBake = require("src.import.gba.bg_bake")
local PokemonExtract = require("src.import.gba.pokemon_extract")
local EggExtract = {}
@@ -77,6 +78,10 @@ function EggExtract.run(rom, cache, opts)
cache:write(cacheRoot .. "/pokemon/front/" .. EggExtract.SPECIES_EGG .. ".rgba",
BgBake.bakeSpriteRgba(tiles, picBank, 0, PIC_W, PIC_H, false, false))
-- src/party_menu.c:2655 draws an egg's icon from MON_DATA_SPECIES_OR_EGG
cache:write(cacheRoot .. "/pokemon/icons/" .. EggExtract.SPECIES_EGG .. ".rgba",
PokemonExtract.iconRgba(rom, EggExtract.SPECIES_EGG))
cache:write(root .. "/manifest.lua", string.format([[
return {
format_version = %d,
@@ -102,7 +107,12 @@ function EggExtract.ready(cache, cacheRoot)
for _, rel in ipairs({ "hatch.rgba", "shard.rgba", "manifest.lua" }) do
if not cache:exists(root .. "/" .. rel) then return false end
end
return cache:exists(cacheRoot .. "/pokemon/front/" .. EggExtract.SPECIES_EGG .. ".rgba")
for _, sub in ipairs({ "front", "icons" }) do
if not cache:exists(cacheRoot .. "/pokemon/" .. sub .. "/" .. EggExtract.SPECIES_EGG .. ".rgba") then
return false
end
end
return true
end
return EggExtract
+18 -20
View File
@@ -648,9 +648,26 @@ local function extract_egg_moves(rom, num)
return eggMoves
end
-- gMonIconTable / gMonIconPaletteIndices entry for one species, both frames
-- (32x64 RGBA); they run past NUM_SPECIES, so SPECIES_EGG has its own icon.
local function icon_rgba(rom, sp, pals)
pals = pals or load_icon_pals(rom)
local w = Versions.MON_ICON_W or 32
local iconH = (Versions.MON_ICON_H or 32) * 2 -- 64 (2 frames)
local iconBytes = Versions.MON_ICON_BYTES or math.floor(w * iconH / 2) -- 1024 for 32x64
local off = gba_off(rom:u32(Versions.MON_ICON_TABLE + sp * 4))
if not off then return string.rep(string.char(0, 0, 0, 0), w * iconH * 4) end
local palIdx = rom:get(Versions.MON_ICON_PAL_INDICES + sp) or 0
if palIdx >= Versions.MON_ICON_PAL_COUNT then palIdx = 0 end
local pixels = decode_4bpp(rom:readBytes(off, iconBytes), w, iconH)
return bake_icon_rgba(pixels, pals[palIdx] or pals[0], w, iconH)
end
-- Exposed for tests (tests/engine/game3_egg_moves.lua).
PokemonExtract.eggMovesFromRom = extract_egg_moves
PokemonExtract.writeEggMovesLua = write_egg_moves_lua
-- src/import/gba/egg_extract.lua bakes the SPECIES_EGG icon with it.
PokemonExtract.iconRgba = icon_rgba
--- Extract full pack into cache under {cacheRoot}/pokemon/.
function PokemonExtract.run(rom, cache, opts)
@@ -669,13 +686,7 @@ function PokemonExtract.run(rom, cache, opts)
local nameBase = Versions.SPECIES_NAMES
local infoBase = Versions.SPECIES_INFO
local natBase = Versions.SPECIES_TO_NATIONAL
local w = Versions.MON_ICON_W or 32
local h = Versions.MON_ICON_H or 32
local iconH = h * 2 -- 64 (2 frames)
local iconBytes = Versions.MON_ICON_BYTES or math.floor(w * iconH / 2) -- 1024 for 32x64
local palIdxBase = Versions.MON_ICON_PAL_INDICES
local pals = load_icon_pals(rom)
local iconTable = Versions.MON_ICON_TABLE
local frontPicTable = (Versions.OAK_SPEECH and Versions.OAK_SPEECH.mon_front_pic_table) or 0x2350AC
local backPicTable = Versions.MON_BACK_PIC_TABLE or 0x23654C
local palTable = (Versions.OAK_SPEECH and Versions.OAK_SPEECH.mon_palette_table) or 0x23730C
@@ -727,20 +738,7 @@ function PokemonExtract.run(rom, cache, opts)
-- Table omits SPECIES_NONE; SpeciesToNationalPokedexNum uses [species - 1].
toNat[sp] = (sp >= 1) and rom:u16(natBase + (sp - 1) * 2) or 0
local ptr = rom:u32(iconTable + sp * 4)
local off = gba_off(ptr)
local palIdx = rom:get(palIdxBase + sp) or 0
if palIdx >= Versions.MON_ICON_PAL_COUNT then palIdx = 0 end
local pal = pals[palIdx] or pals[0]
local rgba
if off then
local bytes = rom:readBytes(off, iconBytes)
local pixels = decode_4bpp(bytes, w, iconH)
rgba = bake_icon_rgba(pixels, pal, w, iconH)
else
rgba = string.rep(string.char(0, 0, 0, 0), w * iconH * 4)
end
put(cache, root .. "/icons/" .. sp .. ".rgba", rgba)
put(cache, root .. "/icons/" .. sp .. ".rgba", icon_rgba(rom, sp, pals))
picsWritten.icons = picsWritten.icons + 1
-- Front Pic (64x64 RGBA)
+3 -1
View File
@@ -37,7 +37,9 @@ Versions.ROM_SIZE = 16777216
-- setptrbyte each carry a leading byte plus a word (were shorter). The
-- old sizes mis-decoded every instruction after one, so every cached
-- script is stale.
Versions.CACHE_VERSION = 113
-- v114: pokemon/icons/412.rgba, the SPECIES_EGG menu icon — eggs were drawn
-- with the icon of the species they hatch into.
Versions.CACHE_VERSION = 114
Versions.NATIVE_VERSION = 6
Versions.OW_VERSION = 1
Versions.ANIM_VERSION = 1
+2 -2
View File
@@ -738,7 +738,7 @@ function BoxStorageUI.draw()
local isHovered = (BoxStorageUI.cursorSlot == s and BoxStorageUI.mode ~= "party_drawer" and not BoxStorageUI.holdingMon)
local bounceY = (isHovered and BoxStorageUI.hoverFrame == 1) and -2 or 0
local f = (isHovered and BoxStorageUI.hoverFrame == 1) and 1 or 0
local sp = Pokemon.speciesOf(mon)
local sp = Pokemon.speciesOrEgg(mon)
local icon = Pokemon.icon(sp)
if icon and icon.image then
@@ -804,7 +804,7 @@ function BoxStorageUI.draw()
-- If holding a mon, draw floating mini-icon under hand cursor
if BoxStorageUI.holdingMon then
local hSp = Pokemon.speciesOf(BoxStorageUI.holdingMon)
local hSp = Pokemon.speciesOrEgg(BoxStorageUI.holdingMon)
local hIcon = Pokemon.icon(hSp)
if hIcon and hIcon.image then
local q = hIcon.quads and hIcon.quads[0]
+1 -1
View File
@@ -446,7 +446,7 @@ local function ensure_slot_sprites(i, mon, selected)
local maxHp = tonumber(mon.maxHp) or tonumber(mon.maxhp) or 1
local hpLevel = get_hp_bar_level(hp, maxHp, mon.isEgg)
local icon = Pokemon.icon(Pokemon.speciesOf(mon))
local icon = Pokemon.icon(Pokemon.speciesOrEgg(mon))
local q0 = icon and icon.quads and icon.quads[0]
if not slot.mon then
local id = select(1, Oam.createSprite({
+2 -2
View File
@@ -381,7 +381,7 @@ function PcChrome.drawPartyDrawer(party, partyCursor, hoverFrame, holdingSource)
local isLeadPickedUp = (holdingSource and holdingSource.loc == "party" and holdingSource.slot == 1)
local leadMon = (not isLeadPickedUp) and party[1]
if leadMon then
local sp = Pokemon.speciesOf(leadMon)
local sp = Pokemon.speciesOrEgg(leadMon)
local icon = Pokemon.icon(sp)
if icon and icon.image then
local isHovered = (partyCursor == 1)
@@ -401,7 +401,7 @@ function PcChrome.drawPartyDrawer(party, partyCursor, hoverFrame, holdingSource)
local isPickedUp = (holdingSource and holdingSource.loc == "party" and holdingSource.slot == p)
local pMon = (not isPickedUp) and party[p]
if pMon then
local sp = Pokemon.speciesOf(pMon)
local sp = Pokemon.speciesOrEgg(pMon)
local icon = Pokemon.icon(sp)
if icon and icon.image then
local isHovered = (partyCursor == p)
+1 -1
View File
@@ -125,7 +125,7 @@ function ReleaseSeq.draw()
local curX = ReleaseSeq.startX
local curY = ReleaseSeq.startY - (progress * 40) -- float upward 40px
local icon = ReleaseSeq.mon and Pokemon.icon(Pokemon.speciesOf(ReleaseSeq.mon))
local icon = ReleaseSeq.mon and Pokemon.icon(Pokemon.speciesOrEgg(ReleaseSeq.mon))
if icon and icon.image then
local q = icon.quads and icon.quads[0]
love.graphics.setColor(1, 1, 1, alpha)
@@ -42,6 +42,7 @@ local ROUND2_SAMPLE = {
"data/generated/gba/move_relearner/bg.rgba",
"data/generated/gba/pokemon/egg/hatch.rgba",
"data/generated/gba/pokemon/front/412.rgba",
"data/generated/gba/pokemon/icons/412.rgba",
"data/generated/gba/native/layouts/alt_366.mid",
}
@@ -0,0 +1,37 @@
#!/usr/bin/env luajit
-- An egg's menu icon is SPECIES_EGG's, as pret draws it from
-- GetMonData(MON_DATA_SPECIES_OR_EGG) (pokefirered/src/pokemon.c:3245,
-- party_menu.c:2655), not the icon of the species it will hatch into.
package.path = "./?.lua;./?/init.lua;" .. package.path
love = require("tests.love_stub")
local T = require("tests.harness")
local check = T.check
local Pokemon = require("src.core.game3.pokemon")
Pokemon._names = { [25] = "PIKACHU", [172] = "PICHU" } -- a minimal pack, so no ROM is needed
check(Pokemon.speciesOrEgg({ species = 172, isEgg = true }) == 412, "a Pichu egg's icon is the EGG's")
check(Pokemon.speciesOrEgg({ species = 172, egg = true }) == 412, "whichever egg flag it carries")
check(Pokemon.speciesOrEgg({ species = 412 }) == 412, "an egg known only by the egg species stays one")
check(Pokemon.speciesOrEgg({ species = 172, isEgg = false }) == 172, "a hatched mon keeps its species")
check(Pokemon.speciesOrEgg({ species = 25 }) == 25, "and so does any other mon")
check(Pokemon.speciesOrEgg(nil) == nil, "no mon, no species")
-- Every screen that draws a party or box mon's icon goes through it.
local SITES = {
{ "src/ui/game3/party_menu.lua", 1 },
{ "src/ui/game3/box_storage_ui.lua", 2 },
{ "src/ui/game3/pc_chrome.lua", 2 },
{ "src/ui/game3/release_seq.lua", 1 },
}
for _, site in ipairs(SITES) do
local f = assert(io.open(site[1], "rb"))
local src = f:read("*a")
f:close()
local n = select(2, src:gsub("Pokemon%.speciesOrEgg%(", ""))
check(n == site[2], ("%s picks %d icon(s) by speciesOrEgg (found %d)"):format(site[1], site[2], n))
end
T.finish("game3_egg_icon_species_test")
@@ -164,6 +164,7 @@ local ROUND2_KEYS = {
"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",
-- src/trainer_card.c:265, :1454, :1560
"data/generated/gba/trainer_card/front_0.rgba",
"data/generated/gba/trainer_card/front_4_female.rgba",
+6
View File
@@ -95,6 +95,7 @@ local WANT = {
"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",
@@ -165,6 +166,8 @@ 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 = {}
@@ -234,6 +237,7 @@ local SHEETS = {
{ "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 },
@@ -389,6 +393,8 @@ 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,