mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
449 lines
18 KiB
Lua
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
|