mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 05:32:07 -04:00
76ede1171e
- 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.
241 lines
8.8 KiB
Lua
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
|