diff --git a/src/core/game3/pokemon.lua b/src/core/game3/pokemon.lua index 9dd36b35..a0b30cb6 100644 --- a/src/core/game3/pokemon.lua +++ b/src/core/game3/pokemon.lua @@ -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" diff --git a/src/import/CacheContract.lua b/src/import/CacheContract.lua index 3e1c7d5a..3b2f5453 100644 --- a/src/import/CacheContract.lua +++ b/src/import/CacheContract.lua @@ -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", diff --git a/src/import/gba/egg_extract.lua b/src/import/gba/egg_extract.lua index 4aee2c4a..917f111c 100644 --- a/src/import/gba/egg_extract.lua +++ b/src/import/gba/egg_extract.lua @@ -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 diff --git a/src/import/gba/pokemon_extract.lua b/src/import/gba/pokemon_extract.lua index 46b02ffb..f4beff83 100644 --- a/src/import/gba/pokemon_extract.lua +++ b/src/import/gba/pokemon_extract.lua @@ -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) diff --git a/src/import/gba/versions.lua b/src/import/gba/versions.lua index 0294e685..ee5dff12 100644 --- a/src/import/gba/versions.lua +++ b/src/import/gba/versions.lua @@ -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 diff --git a/src/ui/game3/box_storage_ui.lua b/src/ui/game3/box_storage_ui.lua index 93796ed9..14cacbf7 100644 --- a/src/ui/game3/box_storage_ui.lua +++ b/src/ui/game3/box_storage_ui.lua @@ -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] diff --git a/src/ui/game3/party_menu.lua b/src/ui/game3/party_menu.lua index 2af1b24d..e01474fb 100644 --- a/src/ui/game3/party_menu.lua +++ b/src/ui/game3/party_menu.lua @@ -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({ diff --git a/src/ui/game3/pc_chrome.lua b/src/ui/game3/pc_chrome.lua index 553c9340..84997fb2 100644 --- a/src/ui/game3/pc_chrome.lua +++ b/src/ui/game3/pc_chrome.lua @@ -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) diff --git a/src/ui/game3/release_seq.lua b/src/ui/game3/release_seq.lua index 9a8411df..3bf4ffde 100644 --- a/src/ui/game3/release_seq.lua +++ b/src/ui/game3/release_seq.lua @@ -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) diff --git a/tests/drivers/game3_import2_fresh_cache.lua b/tests/drivers/game3_import2_fresh_cache.lua index ce6dff15..76dc14f3 100644 --- a/tests/drivers/game3_import2_fresh_cache.lua +++ b/tests/drivers/game3_import2_fresh_cache.lua @@ -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", } diff --git a/tests/engine/game3_egg_icon_species_test.lua b/tests/engine/game3_egg_icon_species_test.lua new file mode 100644 index 00000000..e80a7efb --- /dev/null +++ b/tests/engine/game3_egg_icon_species_test.lua @@ -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") diff --git a/tests/game3_import2_cache_version_test.lua b/tests/game3_import2_cache_version_test.lua index 9830444f..adcdb34d 100644 --- a/tests/game3_import2_cache_version_test.lua +++ b/tests/game3_import2_cache_version_test.lua @@ -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", diff --git a/tests/game3_import2_misc_assets_test.lua b/tests/game3_import2_misc_assets_test.lua index 54aa47c3..b26719a5 100644 --- a/tests/game3_import2_misc_assets_test.lua +++ b/tests/game3_import2_misc_assets_test.lua @@ -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,