Files
gen1recomp/tests/game3_shop_bag_chrome_test.lua
1jamie 76ede1171e feat: Gen 3 save editor, GBA extraction resilience, and UI/asset polish
- Save Editor (Gen 3):
  - Added full support for EV, IV, Nature, Ability, and Ribbons editing for Gen 3 Pokémon.
  - Implemented stat calculations, personality-based nature derivation, and Gen 3 save data structures.
  - Added comprehensive unit test suite in tests/save_editor_gen3_ev_iv_tests.lua.

- GBA Extraction & Graphics:
  - Added zero-copy ARM-safe ROM accessor and fast LZ77 string decompression.
  - Fixed bag chrome decompression string-buffer indexing preventing solid-color renders.
  - Fixed TM Case HM badge tile offset and palette mapping.
  - Added builtin moves fallback in src/core/game3/battle/builtin_moves.lua.
  - Improved parallel extraction resilience, desktop fallback search paths, and prefix handling.

- Android & Launchers:
  - Added dynamic high-res app launcher shortcuts and color palettes for all supported games.
  - Updated Android icon generator and intent dispatch handlers.
2026-09-25 16:20:40 -05:00

241 lines
8.8 KiB
Lua

#!/usr/bin/env luajit
-- Shop & Bag chrome extraction, contract, state machine, and interaction unit tests.
package.path = "./?.lua;./?/init.lua;" .. package.path
require("tests.game3_cache").requireData("game3_shop_bag_chrome_test")
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
print("[test] 1. Versions Offsets for Bag & Shop")
local Versions = require("src.import.gba.versions")
check(Versions.BAG_BG_GFX == 0xE830CC, "BAG_BG_GFX = 0xE830CC")
check(Versions.BAG_BG_TILEMAP == 0xE832C0, "BAG_BG_TILEMAP = 0xE832C0")
check(Versions.BAG_BG_PAL == 0xE835B4, "BAG_BG_PAL = 0xE835B4")
check(Versions.BAG_MALE_GFX == 0xE8362C, "BAG_MALE_GFX = 0xE8362C")
check(Versions.BAG_FEMALE_GFX == 0xE83DBC, "BAG_FEMALE_GFX = 0xE83DBC")
check(Versions.BAG_SPRITE_PAL == 0xE84560, "BAG_SPRITE_PAL = 0xE84560")
check(Versions.ITEM_ICON_TABLE == 0x3D4294, "ITEM_ICON_TABLE = 0x3D4294")
check(Versions.ITEMS_COUNT == 375, "ITEMS_COUNT = 375")
check(Versions.SHOP_BG_GFX == 0xE85DC8, "SHOP_BG_GFX = 0xE85DC8")
check(Versions.SHOP_BG_TILEMAP == 0xE85EFC, "SHOP_BG_TILEMAP = 0xE85EFC")
check(Versions.SHOP_BG_TM_TILEMAP == 0xE86038, "SHOP_BG_TM_TILEMAP = 0xE86038")
check(Versions.SHOP_BG_PAL == 0xE86170, "SHOP_BG_PAL = 0xE86170")
print("[test] 2. Cache Contract for Bag & Shop")
local CacheContract = require("src.import.CacheContract")
local req = CacheContract.requiredFilesFor("firered")
local reqSet = {}
for _, f in ipairs(req) do reqSet[f] = true end
check(reqSet["data/generated/gba/items/bag/manifest.lua"] == true, "contract has bag manifest.lua")
check(reqSet["data/generated/gba/items/bag/bg.rgba"] == true, "contract has bag bg.rgba")
check(reqSet["data/generated/gba/items/shop/manifest.lua"] == true, "contract has shop manifest.lua")
check(reqSet["data/generated/gba/items/shop/bg.rgba"] == true, "contract has shop bg.rgba")
print("[test] 2b. Bag Chrome Pixel Buffer Decoding")
local BagChromeExtract = require("src.import.gba.bag_chrome_extract")
local Rom = require("src.import.gba.rom")
local f = io.open("1636 - Pokemon Fire Red (U)(Squirrels).gba", "rb")
if f then
local content = f:read("*a")
f:close()
local mockImports = {
info = function() return { size = #content, md5 = "dummy" } end,
read = function(self, id, pos, len) return content:sub(pos + 1, pos + len) end,
}
local testRom = Rom.open(mockImports, "firered")
local testStore = {}
local testCache = {
write = function(self, p, d) testStore[p] = d end,
read = function(self, p) return testStore[p] end,
exists = function(self, p) return testStore[p] ~= nil end,
}
BagChromeExtract.run(testRom, testCache, { cacheRoot = "data/generated/gba" })
local bg = testCache:read("data/generated/gba/items/bag/bg.rgba")
local bagMale = testCache:read("data/generated/gba/items/bag/bag_male.rgba")
check(bg and #bg == 240 * 160 * 4, "bg.rgba extracted with correct size")
check(bagMale and #bagMale == 64 * 256 * 4, "bag_male.rgba extracted with correct size")
local bgColors = {}
for i = 1, #bg, 4 do
local k = string.format("%d,%d,%d,%d", bg:byte(i, i + 3))
bgColors[k] = (bgColors[k] or 0) + 1
end
local distinctBg = 0
for _ in pairs(bgColors) do distinctBg = distinctBg + 1 end
check(distinctBg > 10, "bg.rgba has diverse colors (not solid mono-color): " .. tostring(distinctBg))
local bagColors = {}
for i = 1, #bagMale, 4 do
local k = string.format("%d,%d,%d,%d", bagMale:byte(i, i + 3))
bagColors[k] = (bagColors[k] or 0) + 1
end
local distinctBag = 0
for _ in pairs(bagColors) do distinctBag = distinctBag + 1 end
check(distinctBag > 8, "bag_male.rgba has non-zero sprites: " .. tostring(distinctBag))
end
print("[test] 3. BagMenu Lifecycle & Pockets")
local Bag = require("src.core.game3.bag")
local BagMenu = require("src.ui.game3.bag_menu")
local testBag = Bag.new()
Bag.add(testBag, 1, 5) -- Master Ball (POKE_BALLS)
Bag.add(testBag, 4, 10) -- Poke Ball (POKE_BALLS)
Bag.add(testBag, 13, 3) -- Potion (ITEMS)
Bag.add(testBag, 360, 1) -- Bicycle (KEY_ITEMS)
Bag.add(testBag, 361, 1) -- Town Map (KEY_ITEMS)
local playedSe = {}
local Audio = require("src.core.game3.audio")
local origPlaySe = Audio.playSe
Audio.playSe = function(id)
table.insert(playedSe, id)
if origPlaySe then pcall(origPlaySe, id) end
end
local closed = false
local session = {
bag = testBag,
money = 5000,
party = { { name = "PIKACHU", hp = 20, maxHp = 25 } },
}
BagMenu.show(testBag, {
session = session,
onClose = function() closed = true end,
})
BagMenu.settle()
check(BagMenu.isOpen() == true, "bag menu is open")
check(BagMenu.currentPocket() == "ITEMS", "starts in ITEMS pocket")
local itemsList = BagMenu.list("ITEMS")
check(#itemsList >= 1, "ITEMS list has items")
local ballsList = BagMenu.list("POKE_BALLS")
check(#ballsList >= 2, "POKE_BALLS list has Poké Balls")
-- Input step simulation
local mockInput = {
_pressed = {},
wasPressed = function(self, k) return self._pressed[k] == true end,
set = function(self, k) self._pressed = { [k] = true } end,
clear = function(self) self._pressed = {} end,
}
playedSe = {}
mockInput:set("right")
BagMenu.handleInput(mockInput)
check(BagMenu.currentPocket() == "KEY_ITEMS", "right advances to KEY_ITEMS pocket")
check(#playedSe > 0 and playedSe[#playedSe] == 246, "SE_BAG_POCKET (246) played on pocket switch right")
playedSe = {}
mockInput:set("right")
BagMenu.handleInput(mockInput)
check(BagMenu.currentPocket() == "POKE_BALLS", "right advances to POKE_BALLS pocket")
check(#playedSe > 0 and playedSe[#playedSe] == 246, "SE_BAG_POCKET (246) played on second pocket switch right")
playedSe = {}
mockInput:set("left")
BagMenu.handleInput(mockInput)
check(BagMenu.currentPocket() == "KEY_ITEMS", "left advances to KEY_ITEMS pocket")
check(#playedSe > 0 and playedSe[#playedSe] == 246, "SE_BAG_POCKET (246) played on pocket switch left")
-- Vertical cursor movement
BagMenu.settle()
playedSe = {}
mockInput:set("down")
BagMenu.handleInput(mockInput)
check(#playedSe > 0 and playedSe[#playedSe] == 245, "SE_BAG_CURSOR (245) played on cursor move down")
BagMenu.settle()
mockInput:set("b")
BagMenu.handleInput(mockInput)
BagMenu.settle()
check(BagMenu.isOpen() == false, "B closes bag menu")
check(closed == true, "onClose called")
Audio.playSe = origPlaySe
print("[test] 4. ShopMenu Purchasing and Selling")
local ShopMenu = require("src.ui.game3.shop_menu")
local shopClosed = false
local shopSession = {
bag = Bag.new(),
money = 3000,
}
ShopMenu.show({
items = { 4, 13, 17 }, -- Poke Ball (200), Potion (300), Antidote (100)
session = shopSession,
onClose = function() shopClosed = true end,
})
check(ShopMenu.isOpen() == true, "shop menu is open")
check(ShopMenu.mode == "root", "shop menu starts in root mode")
-- Enter BUY mode
mockInput:set("a")
ShopMenu.handleInput(mockInput)
check(ShopMenu.mode == "buy", "A enters buy mode")
-- Select Poké Ball (index 1)
mockInput:set("a")
ShopMenu.handleInput(mockInput)
check(ShopMenu.mode == "buy_qty", "A enters buy_qty mode")
-- Increase qty to 10
ShopMenu.qty = 10
check(ShopMenu.qty == 10, "qty set to 10")
-- Press A to enter buy confirmation ("...and that'll be ¥2000. OK?")
mockInput:set("a")
ShopMenu.handleInput(mockInput)
check(ShopMenu.mode == "buy_confirm", "A enters buy_confirm mode")
-- Confirm buy of 10 Poké Balls (10 * 200 = 2000)
mockInput:set("a")
ShopMenu.handleInput(mockInput)
check(shopSession.money == 1000, "money reduced by 2000 to 1000")
check(Bag.has(shopSession.bag, 4, 10) == true, "bag received 10 Poké Balls")
check(Bag.has(shopSession.bag, 12, 1) == false, "FRLG gives no PREMIER BALL bonus (shop.c:978)")
-- pokefirered/src/item_menu.c:1787 Task_ItemContext_Sell
ShopMenu.mode = "root"
local SellFlow = require("src.ui.game3.sell_flow")
local soldCb = nil
local flow = SellFlow.start({
itemId = 4, owned = Bag.get(shopSession.bag, 4), session = shopSession, bag = shopSession.bag,
onDone = function(sold) soldCb = sold end,
})
check(flow.state == "qty", "a stack of 10 asks how many to sell")
for _ = 1, 4 do
mockInput:set("up")
flow:handleInput(mockInput)
end
check(flow.qty == 5, "up raises the quantity to 5")
mockInput:set("a")
flow:handleInput(mockInput)
check(flow.state == "confirm", "A asks gText_ICanPayThisMuch_WouldThatBeOkay")
mockInput:set("a")
flow:handleInput(mockInput)
check(shopSession.money == 1500, "selling 5 POKé BALLs added 500 (" .. tostring(shopSession.money) .. ")")
check(Bag.get(shopSession.bag, 4) == 5, "five POKé BALLs left")
mockInput:set("a")
flow:handleInput(mockInput)
check(soldCb == true and not flow:active(), "A after the sale returns to the list")
mockInput:set("b")
ShopMenu.handleInput(mockInput)
check(ShopMenu.isOpen() == false, "B closes shop menu")
check(shopClosed == true, "onClose called")
if failed > 0 then
print(string.format("[FAIL] %d test(s) failed", failed))
os.exit(1)
else
print("[test] all passed")
end