#!/usr/bin/env luajit -- pokefirered/src/player_pc.c:151 BedroomPC, pokefirered/src/player_pc.c:100 gNewGamePCItems package.path = "./?.lua;./?/init.lua;" .. package.path require("tests.game3_cache").stubSpeciesNames() require("tests.fixture_data.game3_items").install() package.loaded["src.core.game3.rom_text"] = { plain = function(key) return key end, box = function(key) return key end, ascii = function(key) return key end, has = function() return true end, key = function(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end, at = function(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end, count = function() return 0 end, list = function() return {} end, lazy = function(map) return setmetatable({}, { __index = function(_, k) return map[k] end }) end, } local failed = 0 local function check(cond, msg) if cond then print("[ok] " .. msg) else failed = failed + 1 print("[FAIL] " .. msg) end end package.loaded["src.ui.game3.stack"] = { push = function() end, pop = function() end, busy = function() return false end, drawOrder = function() return {} end, } package.loaded["src.core.game3.audio"] = { playSe = function() end, playCry = function() end, } print("[test] 1. BedroomPC special id + handler") local Std = require("src.core.game3.scripting.stdscripts") check(Std.SPECIAL.BedroomPC == 249, "BedroomPC = 249 (got " .. tostring(Std.SPECIAL.BedroomPC) .. ")") local Natives = require("src.core.game3.scripting.natives") check(Natives.ALLOW["special:249"] ~= nil, "special:249 handler registered") print("[test] 2. special 249 yields into openPc with bedroom opts") local gotOpts, gotDone local ctx = { mode = "bytecode", status = "running", specialVars = {} } local PcAnim = require("src.core.game3.pc_anim") local mtSets = {} PcAnim.setter(function(x, y, mid, imp) mtSets[#mtSets + 1] = { x = x, y = y, mid = mid, imp = imp } end) local realPlayer = package.loaded["src.core.game3.player"] package.loaded["src.core.game3.player"] = { cellX = 1, cellY = 2, facing = "up" } local adapters = { openPc = function(done, opts) gotDone = done gotOpts = opts end, log = function() end, } local yielded = Natives.special(ctx, 249, adapters) check(yielded == true, "special 249 yields") check(type(gotOpts) == "table" and gotOpts.bedroom == true, "openPc got bedroom=true") check(ctx.nativePoll and ctx.nativePoll() == false, "native wait pending while PC open") if gotDone then gotDone() end check(ctx.nativePoll and ctx.nativePoll() == true, "native wait finishes on close") local last = mtSets[#mtSets] check(ctx.specialVars[0x8004] == 1, "ShutDownPC sets VAR_0x8004 = 1") check(last and last.x == 1 and last.y == 1 and last.mid == 0x28F and last.imp == true, "bedroom close swaps PC at (1,1) to PlayersPCOff 0x28F") print("[test] 3. host openPc routes bedroom into player_pc with closeOnExit") do local shown local realPc = package.loaded["src.ui.game3.pc_menu"] local realRt = package.loaded["src.core.game3.runtime"] package.loaded["src.ui.game3.pc_menu"] = { show = function(o) shown = o end } package.loaded["src.core.game3.runtime"] = { isActive = function() return true end, getSession = function() return {} end, } local okA, Adapters = pcall(require, "src.core.game3.scripting.adapters") local okH, host = false, nil if okA then okH, host = pcall(Adapters.host, nil, {}, nil) end check(okH and host and host.openPc, "Adapters.host builds openPc") if okH and host and host.openPc then host.openPc(function() end, { bedroom = true }) check(shown and shown.startMode == "player_pc" and shown.closeOnExit == true, "bedroom -> startMode player_pc, closeOnExit") shown = nil host.openPc(function() end) check(shown and shown.startMode == nil and not shown.closeOnExit, "no opts -> root hub") end package.loaded["src.ui.game3.pc_menu"] = realPc package.loaded["src.core.game3.runtime"] = realRt end print("[test] 4. PcMenu bedroom mode: starts in player_pc, B and TURN OFF close") local PcMenu = require("src.ui.game3.pc_menu") local function input(btn) return { wasPressed = function(_, b) return b == btn end } end local closed = 0 PcMenu.show({ session = {}, onClose = function() closed = closed + 1 end, startMode = "player_pc", closeOnExit = true }) check(PcMenu.mode == "player_pc", "starts in player_pc (got " .. tostring(PcMenu.mode) .. ")") check(PcMenu._status == "gText_WhatWouldYouLikeToDo", "status What would you like to do?") PcMenu.handleInput(input("b")) check(not PcMenu.isOpen() and closed == 1, "B closes bedroom PC and fires onClose") PcMenu.show({ session = {}, onClose = function() closed = closed + 1 end, startMode = "player_pc", closeOnExit = true }) PcMenu.handleInput(input("up")) check(PcMenu.cursor == 1, "top menu does not wrap up") PcMenu.handleInput(input("down")) PcMenu.handleInput(input("down")) PcMenu.handleInput(input("down")) check(PcMenu.cursor == 3, "cursor on TURN OFF, no wrap down") PcMenu.handleInput(input("a")) check(not PcMenu.isOpen() and closed == 2, "TURN OFF closes bedroom PC") PcMenu.show({ session = {}, onClose = function() closed = closed + 1 end }) check(PcMenu.mode == "root", "default show opens root hub") PcMenu.cursor = 2 PcMenu.handleInput(input("a")) check(PcMenu.mode == "player_pc", "hub -> player_pc") PcMenu.handleInput(input("b")) check(PcMenu.isOpen() and PcMenu.mode == "root" and closed == 2, "hub player_pc B returns to root") PcMenu.close() print("[test] 5. New game seeds PC POTION; withdraw moves it to bag; save round-trip") local Schema = require("src.core.game3.save_schema_firered") local Bag = require("src.core.game3.bag") local Storage = require("src.core.game3.storage") local session = Schema.newGame({ rngSeed = 1 }) local it = session.storage and session.storage.items and session.storage.items[1] check(it and it.id == 13 and it.qty == 1, "newGame storage.items[1] = POTION x1") check(session.storage and #session.storage.items == 1, "exactly one PC item") local saved = Schema.toSaveTable(session) check(type(saved.storage) == "table" and saved.storage.items[1] and saved.storage.items[1].id == 13, "toSaveTable writes storage") local restored = Schema.fromSaveTable(saved) check(restored.storage and restored.storage.items[1] and restored.storage.items[1].id == 13, "fromSaveTable restores storage") local legacy = Schema.fromSaveTable({ map = "FR_PALLET_TOWN", x = 1, y = 1 }) check(legacy.storage and legacy.storage.items[1] and legacy.storage.items[1].id == 13, "legacy save without storage gets seeded POTION") check(Storage.withdrawItem(session, 1, 1) == true, "withdrawItem POTION ok") check(Bag.has(session.bag, 13, 1), "bag has POTION") check(#session.storage.items == 0, "PC storage empty after withdraw") print("[test] 6. cart labels, descriptions, MAILBOX, no TOSS") local function joined(list) local t = {} for i, a in ipairs(list) do t[i] = a.label end return table.concat(t, "/") end check(joined(PcMenu.TOP_ACTIONS) == "sMenuActions_TopMenu[0]/sMenuActions_TopMenu[1]/sMenuActions_TopMenu[2]", "top menu labels (got " .. joined(PcMenu.TOP_ACTIONS) .. ")") check(joined(PcMenu.ITEM_STORAGE_ACTIONS) == "sMenuActions_ItemPc[0]/sMenuActions_ItemPc[1]/sMenuActions_ItemPc[2]", "item storage labels (got " .. joined(PcMenu.ITEM_STORAGE_ACTIONS) .. ")") local seLog = {} package.loaded["src.core.game3.audio"].playSe = function(id) seLog[#seLog + 1] = id end local s6 = { bag = Bag.new(), storage = Storage.new() } s6.storage.items = {} -- clear to test empty WITHDRAW message local closed6 = 0 PcMenu.show({ session = s6, startMode = "player_pc", closeOnExit = true, onClose = function() closed6 = closed6 + 1 end }) check(#seLog == 0, "bedroom PC open plays no SE_PC_LOGIN") PcMenu.handleInput(input("a")) check(PcMenu.mode == "item_storage" and PcMenu.cursor == 1 and PcMenu._status == "sItemStorageActionDescriptionPtrs[0]", "ITEM STORAGE -> submenu, WITHDRAW desc") PcMenu.handleInput(input("up")) check(PcMenu.cursor == 1, "submenu does not wrap up") PcMenu.handleInput(input("down")) check(PcMenu._status == "sItemStorageActionDescriptionPtrs[1]", "DEPOSIT desc") PcMenu.handleInput(input("down")) check(PcMenu._status == "sItemStorageActionDescriptionPtrs[2]", "CANCEL desc") PcMenu.handleInput(input("down")) check(PcMenu.cursor == 3, "submenu does not wrap down") PcMenu.handleInput(input("up")) PcMenu.handleInput(input("up")) PcMenu.handleInput(input("a")) check(PcMenu.mode == "msg" and PcMenu._status == "gText_ThereAreNoItems", "empty WITHDRAW -> There are no items.") PcMenu.handleInput(input("a")) check(PcMenu.mode == "item_storage" and PcMenu.cursor == 1 and PcMenu._status == "sItemStorageActionDescriptionPtrs[0]", "no-items message returns to submenu") PcMenu.handleInput(input("b")) check(PcMenu.mode == "player_pc" and PcMenu.cursor == 1 and PcMenu._status == "gText_WhatWouldYouLikeToDo", "B in submenu returns to top menu") PcMenu.handleInput(input("down")) PcMenu.handleInput(input("a")) check(PcMenu.mode == "msg" and PcMenu._status == "gText_TheresNoMailHere", "MAILBOX -> There's no MAIL here.") PcMenu.handleInput(input("a")) check(PcMenu.mode == "player_pc" and PcMenu.cursor == 1 and PcMenu._status == "gText_WhatWouldYouLikeToDo", "mail message returns to top menu") s6.storage.items[1] = { id = 13, qty = 1 } PcMenu.handleInput(input("a")) PcMenu.handleInput(input("a")) local ItemPc = require("src.ui.game3.item_pc") -- pokefirered/src/player_pc.c:378 check(PcMenu.mode == "item_pc" and ItemPc.isOpen(), "WITHDRAW ITEM opens the item PC") for _ = 1, 30 do ItemPc.handleInput(input()) end check(ItemPc._fx == nil and ItemPc.mode == "list", "the PC screen turn-on effect finishes") ItemPc.handleInput(input("a")) check(ItemPc.mode == "submenu", "A on POTION opens WITHDRAW / GIVE / CANCEL") ItemPc.handleInput(input("a")) check(ItemPc.mode == "result" and ItemPc.resultText == "gText_WithdrewQuantItem", "a single POTION skips the quantity") ItemPc.handleInput(input("a")) check(Bag.has(s6.bag, 13, 1) and #s6.storage.items == 0, "POTION withdrawn") ItemPc.handleInput(input("b")) for _ = 1, 30 do if ItemPc.isOpen() then ItemPc.handleInput(input()) end end check(not ItemPc.isOpen() and PcMenu.mode == "item_storage" and PcMenu.cursor == 1, "B turns the item PC off and returns to ITEM STORAGE") PcMenu.handleInput(input("b")) PcMenu.handleInput(input("b")) check(not PcMenu.isOpen() and closed6 == 1, "B at top menu turns the PC off") package.loaded["src.core.game3.audio"].playSe = function() end print("[test] 7. AnimatePcTurnOn flicker / AnimatePcTurnOff") mtSets = {} local actx = { specialVars = { [0x8004] = 1 } } check(Natives.special(actx, Std.SPECIAL.AnimatePcTurnOn, { log = function() end }) == false, "AnimatePcTurnOn does not block the script") local frames = {} for f = 1, 40 do local before = #mtSets PcAnim.update() if #mtSets > before then frames[#frames + 1] = f end end local mids = {} for i, s in ipairs(mtSets) do mids[i] = string.format("%X@%d,%d", s.mid, s.x, s.y) end check(table.concat(mids, " ") == "28A@1,1 28F@1,1 28A@1,1 28F@1,1 28A@1,1", "flicker on/off/on/off/on (got " .. table.concat(mids, " ") .. ")") check(table.concat(frames, ",") == "7,13,19,25,31", "flicker every 6 frames (got " .. table.concat(frames, ",") .. ")") check(PcAnim.task == nil, "turn-on task ends after 5 swaps") mtSets = {} Natives.special({ specialVars = { [0x8004] = 0 } }, Std.SPECIAL.AnimatePcTurnOff, { log = function() end }) check(mtSets[1] and mtSets[1].mid == 0x062, "center PC turn off uses Building_PCOff 0x062") package.loaded["src.core.game3.player"] = { cellX = 5, cellY = 5, facing = "left" } mtSets = {} PcAnim.turnOff({ specialVars = { [0x8004] = 0 } }) check(mtSets[1] and mtSets[1].x == 4 and mtSets[1].y == 4, "facing west targets (x-1, y-1)") package.loaded["src.core.game3.player"] = realPlayer PcAnim.setter(nil) do local realMap = package.loaded["src.core.game3.map"] local realTs = package.loaded["src.core.game3.tileset_native"] package.loaded["src.core.game3.map"] = { currentDef = function() return { pair = "players_house" } end } package.loaded["src.core.game3.tileset_native"] = { ready = function() return true end, get = function() return { midToSlot = { [0x28F] = 7 } } end, } check(PcAnim.drawable(0x28F) == true, "drawable: mid in the pair atlas") check(PcAnim.drawable(0x28A) == false, "drawable: mid missing from atlas would draw the black void slot") package.loaded["src.core.game3.map"] = realMap package.loaded["src.core.game3.tileset_native"] = realTs end do local NativePack = require("src.import.gba.native_pack") local grids = { FR_PLAYERS_HOUSE_2F = { pair = "player_house", cells = { { mid = 0x280 }, { mid = 0x28F } } }, SEVII_ONE_ISLAND_POKECENTER = { pair = "network", cells = { { mid = 0x062 } } }, FR_PALLET_TOWN = { pair = "pallet_outdoor", cells = { { mid = 0x001 } } }, } local function has(list, mid) for _, m in ipairs(list) do if m == mid then return true end end return false end local ph = NativePack.collectMidsForPair(grids, {}, "player_house") check(has(ph, 0x28F) and has(ph, 0x28A), "atlas mids: bedroom PC off 0x28F brings PlayersPCOn 0x28A") local net = NativePack.collectMidsForPair(grids, {}, "network") check(has(net, 0x062) and has(net, 0x063), "atlas mids: center PC off 0x062 brings Building_PCOn 0x063") local pal = NativePack.collectMidsForPair(grids, {}, "pallet_outdoor") check(not has(pal, 0x063) and not has(pal, 0x28A), "atlas mids: no PC on mids without a PC off mid") end print("[test] 8. one PC table: session.storage, legacy pc migration, sidecar") local ng = Schema.newGame({ rngSeed = 2 }) check(ng.pc == nil, "newGame has no session.pc") check(Schema.toSaveTable(ng).pc == nil, "save table writes no pc") local mig = Schema.fromSaveTable({ map = "FR_PALLET_TOWN", x = 1, y = 1, pc = { items = { { id = 13, qty = 2 } }, mons = { { species = 16, level = 4 } } } }) check(mig.pc == nil and mig.storage and mig.storage.items[1] and mig.storage.items[1].id == 13 and mig.storage.items[1].qty == 2, "legacy pc.items migrated into storage") check(mig.storage and Storage.countTotalMons(mig.storage) == 1 and mig.storage.boxes[1].mons[1].species == 16, "legacy pc.mons migrated into box 1") local both = Schema.fromSaveTable({ storage = { items = { { id = 17, qty = 1 } }, boxes = { [1] = { mons = { [1] = { species = 25 } } } } }, pc = { items = { { id = 13, qty = 2 } }, mons = { { species = 16 } } }, }) check(#both.storage.items == 1 and both.storage.items[1].id == 17, "storage items win over stale pc items") check(Storage.countTotalMons(both.storage) == 1 and both.storage.boxes[1].mons[1].species == 25, "no duplicate box mons from stale pc.mons") local okB, Bridge = pcall(require, "src.core.game3.bridge") check(okB, "bridge loads") if okB then local sc = { pc = { items = { { id = 13, qty = 9 } } } } Bridge.storageToSidecar(sc, { storage = mig.storage }) check(sc.pc == nil and sc.storage and sc.storage.items[1].qty == 2 and sc.storage.boxes[1].mons[1].species == 16, "sidecar keeps storage and drops pc") local back = Storage.restore(sc.storage, sc.pc) check(back and back.items[1].id == 13 and Storage.countTotalMons(back) == 1, "sidecar storage restores") local legacySc = Storage.restore(nil, { items = { { id = 13, qty = 1 } } }) check(legacySc and legacySc.items[1].id == 13, "sidecar with only pc migrates") end local okC, Catching = pcall(require, "src.core.game3.battle.catching") local okD, Dex = pcall(require, "src.core.game3.dex") if okC and okD then local cs = { name = "RED", id = 1, dex = Dex.new(), party = {} } for i = 1, 6 do cs.party[i] = { species = 1, level = 5, hp = 20, maxHp = 20 } end local okS, res = pcall(Catching.storeCaught, cs, { species = 16, mon = { species = 16, level = 4, hp = 5, maxHp = 18 } }, 4) check(okS and res.location == "pc" and cs.pc == nil and Storage.countTotalMons(cs.storage) == 1, "catch with full party writes only session.storage") else check(false, "catching/dex load") end if failed == 0 then print("\nAll game3 bedroom PC tests passed.") os.exit(0) else print("\n" .. failed .. " bedroom PC test(s) failed.") os.exit(1) end