Files
2026-09-24 05:17:32 -04:00

449 lines
18 KiB
Lua

local U = require("tests.drivers.util")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_link_trade"
local CENTER_2F = "FR_VIRIDIAN_CITY_POKEMON_CENTER_2F"
local TRADE_CENTER = "FR_TRADE_CENTER"
-- pokefirered/data/specials.inc:5 SetCableClubWarp
local SET_CABLE_CLUB_WARP = 0x01
-- pokefirered/include/constants/vars.h:163
local VAR_CABLE_CLUB_STATE = 0x406F
local VAR_0x8004 = 0x8004
local VAR_RESULT = 0x800D
-- pokefirered/include/constants/flags.h:1375
local FLAG_SYS_POKEDEX_GET = 0x829
local failures = 0
local function result(ok, label)
print((ok and "PASS " or "FAIL ") .. label)
if not ok then failures = failures + 1 end
return ok
end
local function finish()
if failures == 0 then
print("PASS link_trade")
love.event.quit(0)
else
print("FAIL link_trade failures=" .. failures)
love.event.quit(1)
end
end
return function(game)
print("PASS driver_started")
for _ = 1, 900 do
if game.phase == "boot" and game.boot then break end
U.wait(1)
end
game:_handleBootAction({ action = "new_game", name = "RED" })
U.wait(240)
local Runtime = require("src.core.game3.runtime")
local Map = require("src.core.game3.map")
local Space = require("src.core.game3.scripting.space")
local Flags = require("src.core.game3.scripting.flags")
local Player = require("src.core.game3.player")
local Party = require("src.core.game3.party")
local Natives = require("src.core.game3.scripting.natives")
local NativesLink = require("src.core.game3.scripting.natives_link")
local Link = require("src.core.game3.link")
local LB = require("src.core.game3.link.battle")
local LT = require("src.core.game3.link.trade")
local TradeScene = require("src.core.game3.trade_scene")
local PartyMenu = require("src.ui.game3.party_menu")
local Game3Link = require("src.link.Game3Link")
local Protocol = require("src.link.Protocol")
local Wire = require("src.link.Wire")
local session = Runtime.getSession()
if not result(session ~= nil, "new game reached the game3 field") then return finish() end
local function ctx() return Space.vm and Space.vm.ctx end
local function adapters() return Space.vm and Space.vm.adapters end
local function getVar(id) return tonumber(Flags.getVar(Space.store, ctx(), id)) or 0 end
local function setVar(id, v) Flags.setVar(Space.store, ctx(), id, v) end
local function place(x, y, facing)
Player.cellX, Player.cellY = x, y
Player.px, Player.py = x * 16, y * 16
Player.targetX, Player.targetY = x, y
Player.facing = facing
if game.session then
game.session.x, game.session.y, game.session.facing = x, y, facing
end
end
-- pokefirered/src/pokemon.c:1796 the mon on the other GBA
local peerHolder = { name = "BLUE", trainerId = 0x2222, party = {} }
Party.giveMon(peerHolder, 25, 14)
local peerMonReal = peerHolder.party[1]
peerMonReal.item, peerMonReal.heldItem = 13, 13
local peer, peerSeat, peerParty, peerGot, peerConfirmed = nil, false, false, nil, false
local function pumpPeer()
if not peer then return end
peer:update(0)
local lu = peer:take(LB.MSG.LINKUP)
if lu then
peer:send({ type = LB.MSG.LINKUP, linkType = lu.linkType, players = lu.players })
end
if peer:take(LB.MSG.SEAT) then
peerSeat = true
peer:send({ type = LB.MSG.SEAT, seat = 1 })
end
if peer:take(LT.MSG.PARTY) then
peerParty = true
-- pokefirered/src/trade.c:778 InitTradeMenu
peer:send({
type = LT.MSG.PARTY, name = "BLUE", trainerId = 0x2222, gender = 0,
version = 4, progressFlags = 0,
party = Protocol.packParty3({ peerMonReal }, { 1 }),
})
end
local block = peer:take(LT.MSG.MON)
if block then
peerGot = block
local theirMsg = { type = LT.MSG.MON, name = "BLUE", trainerId = 0x2222,
mon = Protocol.packMon3(peerMonReal) }
peer:send(theirMsg)
peer:send({ type = LT.MSG.CONFIRM,
digest = Protocol.tradeDigest(block.mon, Wire.sanitize(theirMsg).mon) })
end
local cmd = peer:take(LT.MSG.CMD)
while cmd do
-- pokefirered/src/trade.c:1637 Follower_ReadLinkBuffer
if cmd.cmd == LT.LINKCMD.SET_MONS_TO_TRADE then
peer:send({ type = LT.MSG.CMD, cmd = LT.LINKCMD.INIT_BLOCK })
elseif cmd.cmd == LT.LINKCMD.CONFIRM_FINISH_TRADE then
peerConfirmed = true
peer:send({ type = LT.MSG.CMD, cmd = LT.LINKCMD.CONFIRM_FINISH_TRADE })
end
cmd = peer:take(LT.MSG.CMD)
end
end
local function waitP(frames)
for _ = 1, frames do
U.wait(1)
pumpPeer()
end
end
Flags.setFlag(Space.store, ctx(), FLAG_SYS_POKEDEX_GET, true)
session.party = {}
Party.giveMon(session, 1, 16)
Party.giveMon(session, 4, 15)
result(#session.party == 2, "the player walks in with two POKeMON")
-- pokefirered/include/constants/items.h:17, :125
session.party[1].item, session.party[1].heldItem = 13, 13
session.party[2].item, session.party[2].heldItem = 121, 121
local offeredSpecies = session.party[1] and session.party[1].species
-- pokefirered/data/scripts/cable_club.inc:363 CableClub_EventScript_TradeCenter
Map.load(nil, game, CENTER_2F, { x = 9, y = 2, facing = "up" })
place(9, 2, "up")
U.wait(60)
place(9, 1, "up")
U.wait(10)
Natives.special(ctx(), SET_CABLE_CLUB_WARP, adapters())
result(session.dynamicWarp ~= nil and session.dynamicWarp.map == CENTER_2F,
"SetCableClubWarp recorded the walk back to the counter")
local host
host, peer = Game3Link.loopback({ game = game })
host:update(0)
peer:update(0)
Link.attach(host)
LT.loopbackCommit = true
result(host:isReady(), "the other GBA is on the cable")
-- pokefirered/data/scripts/cable_club.inc:373 special TryTradeLinkup
local yielded = Natives.special(ctx(), NativesLink.SPECIAL.TryTradeLinkup, adapters())
result(yielded, "TryTradeLinkup parked the script while the machines agreed")
local linkup, guard = nil, 0
while guard < 300 and linkup == nil do
pumpPeer()
local poll = ctx().nativePoll
if poll and poll() then
linkup = getVar(VAR_RESULT)
ctx().nativePoll = nil
ctx().mode = "bytecode"
elseif LB.linkup ~= nil and LB.linkup ~= Link.LINKUP.ONGOING then
linkup = LB.linkup
end
U.wait(1)
guard = guard + 1
end
result(linkup == Link.LINKUP.SUCCESS, "both machines chose TRADE, so the linkup succeeded")
-- pokefirered/data/scripts/cable_club.inc:386 CableClub_EventScript_EnterTradeCenter
setVar(VAR_0x8004, Link.USING.TRADE_CENTER)
setVar(VAR_CABLE_CLUB_STATE, Link.USING.TRADE_CENTER)
Map.load(nil, game, TRADE_CENTER, { x = 5, y = 8, facing = "up" })
place(5, 8, "up")
waitP(90)
result(Space.mapId == TRADE_CENTER, "the player is inside the TRADE CENTER")
U.shot(game, DIR .. "/link_trade_01_trade_center.png")
place(4, 7, "up")
waitP(20)
guard = 0
while guard < 600 and LT.state ~= "menu" do
if not Player.moving and Player.cellY > 5 then U.hold(game, "up", 12) end
waitP(15)
guard = guard + 15
end
print("[driver] seat: cell=" .. tostring(Player.cellX) .. "," .. tostring(Player.cellY)
.. " lt=" .. tostring(LT.state) .. " seat=" .. tostring(peerSeat))
result(peerSeat, "sitting down told the other machine this player took a trade seat")
if not result(LT.state == "menu", "and the trade menu opened over the cable") then
U.shot(game, DIR .. "/link_trade_99_no_menu.png")
LT.loopbackCommit = false
Link.reset()
return finish()
end
result(peerParty, "both machines swapped their party lists")
local Menu = require("src.ui.game3.link_trade_menu")
local SummaryMenu = require("src.ui.game3.summary_menu")
local function waitFor(pred, limit)
local n = 0
while n < (limit or 300) and not pred() do
waitP(1)
n = n + 1
end
return pred()
end
local function tap(btn)
U.tap(game, btn)
waitP(2)
end
-- pokefirered/src/trade.c:1065
result(waitFor(function() return Menu.cb == "main" and Menu.fade == 0 end),
"the trade screen faded in over the standby message")
local okArt, art = pcall(Menu.loadArt)
result(okArt and art ~= nil and art.menu_bg1 ~= nil, "the trade menu sheets came out of the cache")
waitP(20)
U.shot(game, DIR .. "/link_trade_02_seated.png")
-- pokefirered/src/party_menu.c:2785 DrawHeldItemIconsForTrade
local okHold, holdSheet = pcall(PartyMenu.heldItemSheet)
result(okHold and holdSheet ~= nil, "the held-item sheet came out of the party chrome cache")
result(Menu.heldItemFrame(session.party[1]) == 0 and Menu.heldItemFrame(session.party[2]) == 1
and Menu.heldItemFrame(LT.peerParty[1]) == 0,
"the trade grid puts the item icon on POTION holders and the mail icon on MAIL")
U.shot(game, DIR .. "/link_trade_02a_held_item_icons.png")
-- pokefirered/src/main.c:309 JOY_REPT
table.insert(game.input.pressQueue, "right")
for _ = 1, 43 do
game.input.state.right = true
waitP(1)
end
game.input.state.right = false
waitP(2)
result(Menu.pos == 6, "holding RIGHT repeats after 40 frames onto the other player's lead")
tap("left")
tap("left")
result(Menu.pos == 0, "LEFT twice walks back to the lead")
-- pokefirered/src/trade.c:349 sCursorMoveDestinations
tap("right")
tap("right")
result(Menu.pos == 6, "RIGHT twice crosses to the other player's party")
U.shot(game, DIR .. "/link_trade_02b_cursor_partner.png")
tap("down")
result(Menu.pos == 12, "DOWN from a lone partner mon lands on CANCEL")
U.shot(game, DIR .. "/link_trade_02c_cursor_cancel.png")
-- pokefirered/src/trade.c:1867
tap("a")
result(Menu.cb == "cancel_prompt", "A on CANCEL asks YES / NO")
U.shot(game, DIR .. "/link_trade_02d_cancel_prompt.png")
tap("b")
result(Menu.cb == "main" and LT.state == "menu", "B answers NO and nothing was canceled")
-- pokefirered/src/trade.c:1939
tap("up")
result(Menu.pos == 6, "UP from CANCEL returns to the partner mon")
tap("a")
result(waitFor(function() return SummaryMenu.isOpen() end), "A on a partner mon opens its summary")
waitP(20)
result(SummaryMenu._enemyParty == true and SummaryMenu._owner and SummaryMenu._owner.playerName == "BLUE",
"the partner's summary reads as the enemy party with BLUE as the owner")
do
local SummaryData = require("src.core.game3.summary_data")
local mon = SummaryMenu._party and SummaryMenu._party[SummaryMenu._cursor]
local want = require("src.core.game3.pokemon").gender(mon and mon.species, mon and mon.personality)
-- pokefirered/src/pokemon_summary_screen.c:2114
result(SummaryData.gender(mon) == want and (want == "M" or want == "F"),
"the partner summary prints the gender symbol, " .. tostring(SummaryData.gender(mon)))
end
U.shot(game, DIR .. "/link_trade_02e_partner_summary.png")
tap("b")
result(waitFor(function() return Menu.cb == "main" and Menu.fade == 0 end),
"B on the summary comes back to the trade screen")
result(Menu.pos == 6, "with the cursor on the mon that was viewed")
tap("left")
tap("left")
result(Menu.pos == 0, "LEFT twice walks back to the lead")
-- pokefirered/src/trade.c:1847
tap("a")
result(Menu.cb == "selected" and Menu.submenuVisible, "A on an own mon opens SUMMARY / TRADE")
U.shot(game, DIR .. "/link_trade_02f_summary_trade_menu.png")
tap("down")
-- pokefirered/src/trade.c:1811 SetReadyToTrade
tap("a")
result(LT.state == "ready_wait", "TRADE offers the mon in the first slot")
result(not Menu.cursorVisible, "and hides the cursor behind the standby message")
U.shot(game, DIR .. "/link_trade_02g_standby.png")
peer:send({ type = LT.MSG.CMD, cmd = LT.LINKCMD.READY_TO_TRADE, cursor = 0 })
result(waitFor(function() return Menu.cb == "okay_wait" end),
"both machines named a mon, so both slide to the middle")
waitP(10)
U.still(game, DIR .. "/link_trade_02h_selected_mons.png")
result(Menu.cb == "okay_wait" and not Menu.confirming,
"the selected-mon screen holds before the YES / NO")
-- pokefirered/src/trade.c:2086
result(waitFor(function() return Menu.confirming end), "the YES / NO comes up after the pause")
U.shot(game, DIR .. "/link_trade_02i_is_this_okay.png")
local tradesBefore = tonumber(type(session.gameStats) == "table" and session.gameStats[21]) or 0
local sentName = require("src.core.game3.pokemon").name(offeredSpecies)
local completedBefore = LT.completed
-- pokefirered/src/trade.c:2008 CB_ProcessConfirmTradeInput
tap("a")
result(LT.state ~= "confirm", "the player answers YES")
guard = 0
local sawExit, shotFade = false, false
while guard < 600 and LT.state ~= "scene" do
waitP(1)
guard = guard + 1
if Menu.cb == "exiting" then sawExit = true end
if not shotFade and Menu.isOpen() and Menu.fade >= 6 and Menu.fade <= 10 then
shotFade = true
U.still(game, DIR .. "/link_trade_02j_fade_to_trade.png")
end
end
-- pokefirered/src/trade.c:1293 CB_FadeToStartTrade
print("[driver] fade before scene: menu reached exiting=" .. tostring(sawExit)
.. " fade shot=" .. tostring(shotFade))
result(sawExit and shotFade, "the trade menu fades to black before the trade scene")
print("[driver] exchange: lt=" .. tostring(LT.state) .. " peerGot="
.. tostring(peerGot ~= nil and peerGot.mon and peerGot.mon.species))
result(peerGot ~= nil, "this player's mon went over the cable")
result(LT.state == "scene", "and the trade cinema started")
result(#session.party == 2, "with both mons still accounted for")
guard = 0
while guard < 1200 and TradeScene.phase() ~= "pokeball_depart" do
waitP(1)
guard = guard + 1
end
local sendSt = TradeScene.state() or {}
print("[driver] send scene: phase=" .. tostring(TradeScene.phase()) .. " text=" .. tostring(sendSt.text)
.. " shadow=" .. tostring(sendSt.monShadowBg) .. " hofs=" .. tostring(sendSt.bg2hofs))
-- pokefirered/src/trade_scene.c:1381 gText_ByeByeVar1
result(TradeScene.phase() == "pokeball_depart" and tostring(sendSt.text):find("Bye") ~= nil,
"the send scene is on Bye-bye")
-- pokefirered/src/trade_scene.c:1121
result(sendSt.monShadowBg == true and sendSt.bg2hofs == 0, "the BG2 mon shadow sits under the sent mon")
U.still(game, DIR .. "/link_trade_03_cinema_send.png")
guard = 0
local shotReceive = false
while guard < 6000 and LT.completed == completedBefore do
waitP(1)
guard = guard + 1
if not shotReceive and TradeScene.phase() == "after_new_mon_delay" then
shotReceive = true
local st = TradeScene.state() or {}
print("[driver] arrive text: " .. tostring(st.text))
-- pokefirered/src/trade_scene.c:1756 gText_TakeGoodCareOfX
result(tostring(st.text):find("Take good care of PIKACHU") ~= nil,
"the arrival shot is on Take good care of PIKACHU!")
result(st.monShadowBg == true, "the BG2 mon shadow sits under the received mon")
U.still(game, DIR .. "/link_trade_04_cinema_arrive.png")
end
end
print("[driver] trade over: lt=" .. tostring(LT.state)
.. " confirmed=" .. tostring(peerConfirmed)
.. " party1=" .. tostring(session.party[1] and session.party[1].species)
.. " party2=" .. tostring(session.party[2] and session.party[2].species))
result(LT.completed == completedBefore + 1, "the link trade ran to its end")
-- pokefirered/src/trade.c:1322
result(LT.state == "menu", "and the trade menu came back up")
result(peerConfirmed, "and both machines confirmed the finished trade")
result(TradeScene.isOpen() == false, "the cinema closed")
-- pokefirered/src/trade_scene.c:1054 TradeMons
result(session.party[1] and session.party[1].species == 25,
"the received PIKACHU is in the party")
result(session.party[1] and session.party[1].otName == "BLUE",
"with the other player as its original trainer")
result(peerGot and peerGot.mon and peerGot.mon.species == offeredSpecies,
"and the other machine is holding the mon this player sent")
result(#session.party == 2, "the party is still two mons")
-- pokefirered/src/trade_scene.c:2606
result((tonumber(session.gameStats and session.gameStats[21]) or 0) == tradesBefore + 1,
"GAME_STAT_POKEMON_TRADES went up by one")
-- pokefirered/src/trade_scene.c:2605
local qlScenes = session.questLog and session.questLog.scenes or {}
local qlEvents = qlScenes[#qlScenes] and qlScenes[#qlScenes].events or {}
local ql = qlEvents[#qlEvents]
print("[driver] quest log: " .. tostring(ql and ql.key) .. " S1=" .. tostring(ql and ql.args.S1)
.. " S2=" .. tostring(ql and ql.args.S2) .. " S3=" .. tostring(ql and ql.args.S3))
result(ql and ql.key == "TradedMon1ForPersonsMon2" and ql.args.S1 == "BLUE"
and ql.args.S2 == "PIKACHU" and ql.args.S3 == sentName,
"the quest log records the link trade")
waitP(60)
U.shot(game, DIR .. "/link_trade_05_back_in_the_room.png")
PartyMenu.show(session.party, nil, { session = session })
waitP(60)
local oam = PartyMenu._oam or {}
result(oam[1] and oam[1].item ~= nil and oam[2] and oam[2].item ~= nil,
"the party menu shows the received POTION and the kept MAIL")
U.shot(game, DIR .. "/link_trade_06_party_after.png")
PartyMenu.close()
waitP(30)
local StartMenu = require("src.ui.game3.start_menu")
local TrainerCard = require("src.ui.game3.trainer_card")
StartMenu.show({ session = session })
waitP(10)
local cardEntry
for _ = 1, 10 do
local e = StartMenu.ENTRIES and StartMenu.ENTRIES[StartMenu.cursor or 1]
if e and (e.id == "trainer" or e.id == "trainer_link") then cardEntry = e.id break end
tap("down")
waitP(4)
end
tap("a")
waitP(30)
result(TrainerCard.isOpen(), "the START menu opened the trainer card (" .. tostring(cardEntry) .. ")")
tap("a")
result(waitFor(function() return TrainerCard.side == "back" and not TrainerCard._flip end, 60),
"A flipped the card to the back")
local trades
for _, t in ipairs(TrainerCard.backTexts(TrainerCard._card)) do
if t.id == "trades" then trades = t.text end
end
print("[driver] card trades=" .. tostring(trades) .. " stat=" .. tostring(session.gameStats[21]))
-- pokefirered/src/trainer_card.c:824
result(trades ~= nil and tonumber(trades) == session.gameStats[21] and session.gameStats[21] > 0,
"the card back shows the link trade count")
U.still(game, DIR .. "/2436_card_back_trades.png")
for _ = 1, 20 do
if not TrainerCard.isOpen() then break end
tap("b")
waitP(8)
end
LT.loopbackCommit = false
Link.reset()
finish()
end