Files
gen1recomp/tests/game3_link_trade_test.lua
2026-09-24 05:17:32 -04:00

769 lines
31 KiB
Lua

#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local Cache = require("tests.game3_cache")
if not Cache.mount("meta.json") then
print("[skip] link trade: " .. tostring(Cache.reason))
os.exit(0)
end
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
local function eq(a, b, msg)
check(a == b, string.format("%s (%s == %s)", msg, tostring(a), tostring(b)))
end
local TRADE_CENTER = "FR_TRADE_CENTER"
local COUNTER_MAP = "FR_VIRIDIAN_CITY_POKEMON_CENTER_2F"
local UNION_MAP = "FR_UNION_ROOM"
local MAPS = {
[COUNTER_MAP] = {
warps = { { x = 9, y = 1, destMap = TRADE_CENTER, destWarp = 1 } },
},
[TRADE_CENTER] = {
warps = { { x = 5, y = 8, mapGroup = 0x7F, mapNum = 0x7F, destWarp = 128 } },
},
[UNION_MAP] = {
warps = { { x = 7, y = 11, mapGroup = 0x7F, mapNum = 0x7F, destWarp = 128 } },
},
}
local function mon(species, level, _, extra)
local holder = { name = "RED", trainerId = 0x1234, party = {} }
require("src.core.game3.party").giveMon(holder, species, level)
local m = holder.party[1]
m.friendship, m.happiness = 120, 120
for k, v in pairs(extra or {}) do m[k] = v end
if m.otName then m.ot = m.otName end
return m
end
local store = { flags = {}, vars = {} }
local session = {
store = store,
map = TRADE_CENTER,
x = 5,
y = 8,
name = "RED",
gender = 0,
trainerId = 0x1234,
party = { mon(1, 10), mon(4, 12) },
bag = { pockets = { items = {} } },
}
local game = { data = { maps = MAPS }, session = session }
package.loaded["src.core.game3.runtime"] = {
getSession = function() return session end,
isActive = function() return true end,
_game = game,
_mod = nil,
}
package.loaded["src.core.game3.player"] = { cellX = 5, cellY = 8, facing = "up" }
local questEvents = {}
package.loaded["src.core.game3.quest_log_recorder"] = {
event = function(s, key, args) questEvents[#questEvents + 1] = { s = s, key = key, args = args } end,
}
package.loaded["src.core.game3.map"] = { load = function() end, current = TRADE_CENTER }
package.loaded["src.core.game3.objects"] = {
addObject = function() return true end,
removeObject = function() return true end,
refreshGraphics = function() return 0 end,
}
local ctx = { specialVars = {}, stringVars = {} }
local adapters = { log = function() end, playSe = function() end }
package.loaded["src.core.game3.scripting.space"] = {
store = store,
mapId = TRADE_CENTER,
vm = { ctx = ctx, adapters = adapters },
}
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 LT = require("src.core.game3.link.trade")
local Union = require("src.core.game3.link.union_room")
local Trade = require("src.core.game3.scripting.natives_trade")
local TradeScene = require("src.core.game3.trade_scene")
local FakeRelay = require("tests.g3link_fake_relay")
local peer
local function openLink()
Link.reset()
LT.reset()
LT.loopbackCommit = false
local host, other = FakeRelay.pair({ game = game })
host:update(0)
other:update(0)
Link.attach(host)
peer = other
return host
end
local function pumpPeer(handler)
if not peer then return end
peer:update(0)
local msg = peer:take(LT.MSG.CMD)
while msg do
if handler then handler(msg) end
msg = peer:take(LT.MSG.CMD)
end
end
local function peerSend(message)
if peer then peer:send(message) end
end
local Protocol = require("src.link.Protocol")
local Wire = require("src.link.Wire")
local function packedParty(list)
local idx = {}
for i = 1, #list do idx[i] = i end
return Protocol.packParty3(list, idx)
end
local function peerParty(list, extra)
local msg = { type = LT.MSG.PARTY, party = packedParty(list), name = "BLUE",
trainerId = 0x2222, gender = 0, version = 4, progressFlags = 0 }
for k, v in pairs(extra or {}) do msg[k] = v end
peerSend(msg)
end
local function peerMon(block, m, extra)
local msg = { type = LT.MSG.MON, name = "BLUE", trainerId = 0x2222, mon = Protocol.packMon3(m) }
for k, v in pairs(extra or {}) do msg[k] = v end
peerSend(msg)
local theirs = Wire.sanitize(msg).mon
peerSend({ type = LT.MSG.CONFIRM, digest = Protocol.tradeDigest(block.mon, theirs) })
end
local function runTrade()
local before, guard, confirms = LT.completed, 0, 0
while guard < 4000 and LT.completed == before do
pumpPeer(function(msg)
-- pokefirered/src/trade_scene.c:2344
if msg.cmd == LT.LINKCMD.CONFIRM_FINISH_TRADE then
confirms = confirms + 1
peerSend({ type = LT.MSG.CMD, cmd = LT.LINKCMD.CONFIRM_FINISH_TRADE })
end
end)
LT.update(0)
guard = guard + 1
end
return LT.completed > before, confirms
end
print("[test] 2. the trade menu exchanges both parties before anything is chosen")
openLink()
session.party = { mon(1, 10), mon(4, 12) }
LT.startMenu()
peer:update(0)
local sentParty = peer:take(LT.MSG.PARTY)
check(sentParty ~= nil, "this machine sent its party to the other one")
eq(sentParty and sentParty.name, "RED", "with the player's name on it")
eq(sentParty and #sentParty.party, 2, "two mons")
eq(sentParty and sentParty.party[1].species, 1, "the lead is BULBASAUR")
-- pokefirered/src/trade.c:1435 Trade_Memcpy
eq(sentParty and sentParty.party[1].maxHp, nil, "packed with packMon3: no stats go on the wire")
eq(sentParty and sentParty.party[1].moves and sentParty.party[1].moves[1]
and sentParty.party[1].moves[1].id, session.party[1].moves[1],
"and its moves for the selected-mon screen and summary")
local blueSquirtle = mon(7, 11, nil, { otName = "BLUE", otId = 0x2222, friendship = 40 })
peerParty({ mon(25, 9), blueSquirtle })
LT.update(0)
eq(LT.peer and LT.peer.name, "BLUE", "the other player introduced itself")
eq(#LT.peerParty, 2, "and its party is on this screen")
check((LT.peerParty[1].maxHp or 0) > 0, "with stats recomputed by the strict unpack")
eq(LT.state, "menu", "the trade menu is up")
print("[test] 3. the offer both machines have to agree on")
eq(LT.isLeader(), true, "this machine is the link leader")
check(LT.offer(1), "the player offers the mon in slot 1")
eq(LT.playerSelectStatus, LT.STATUS.READY, "which is STATUS_READY for the leader itself")
eq(LT.cursor, 0, "at cursor 0")
eq(LT.state, "ready_wait", "and the leader waits for the other machine")
LT.update(0)
eq(LT.state, "ready_wait", "one ready side is not a trade")
-- pokefirered/src/trade.c:1811 SetReadyToTrade
peerSend({ type = LT.MSG.CMD, cmd = LT.LINKCMD.READY_TO_TRADE, cursor = 1 })
LT.update(0)
eq(LT.partnerCursor, 1, "the other machine named its own mon")
eq(LT.state, "confirm", "so both selections are locked in")
local sawSetMons = false
pumpPeer(function(msg)
if msg.cmd == LT.LINKCMD.SET_MONS_TO_TRADE then sawSetMons = true end
end)
check(sawSetMons, "the leader told the other machine which two mons are on the table")
print("[test] 4. CheckValidityOfTradeMons before the block exchange")
eq(LT.checkValidityOfTradeMons(1, 2), LT.BOTH_MONS_VALID, "two ordinary mons are valid")
local keepParty = session.party
session.party = { mon(1, 10) }
eq(LT.checkValidityOfTradeMons(1, 2), LT.PLAYER_MON_INVALID,
"trading away the only mon left is PLAYER_MON_INVALID")
session.party = keepParty
local keepPeerParty, keepPeerPacked = LT.peerParty, LT._peerPacked
-- pokefirered/src/trade.c:1955 the partner cannot trade an illegitimate DEOXYS or MEW
peerParty({ mon(410, 30, nil, { fatefulEncounter = false }) }, { version = 0, progressFlags = 1 })
LT.pump()
eq(LT.checkValidityOfTradeMons(1, 1), LT.PARTNER_MON_INVALID,
"an illegitimate DEOXYS on the other machine is PARTNER_MON_INVALID")
LT.peerParty, LT._peerPacked = keepPeerParty, keepPeerPacked
print("[test] 5. both confirmations, then the mons go over the cable")
check(LT.confirm(true), "the player answers YES to IS THIS TRADE OKAY?")
eq(LT.lastResult, LT.BOTH_MONS_VALID, "with both mons valid")
eq(LT.playerConfirmStatus, LT.STATUS.READY, "LINKCMD_INIT_BLOCK for the leader itself")
eq(LT.state, "confirm_wait", "and it waits again")
eq(session.party[1].species, 1, "nothing has moved yet")
peerSend({ type = LT.MSG.CMD, cmd = LT.LINKCMD.INIT_BLOCK })
LT.update(0)
eq(LT.state, "exchange", "both confirmations start the trade")
local sawStart = false
pumpPeer(function(msg)
if msg.cmd == LT.LINKCMD.START_TRADE then sawStart = true end
end)
check(sawStart, "the leader sent LINKCMD_START_TRADE")
peer:update(0)
local block = peer:take(LT.MSG.MON)
check(block ~= nil, "this machine put its mon on the cable")
eq(block and block.mon.species, 1, "the mon it offered")
eq(session.party[1].species, 1, "and the party still holds it until the other one arrives")
eq(#session.party, 2, "with nothing lost")
print("[test] 6. the swap only happens after the commit, once both mons are in hand")
local saves = 0
function game:saveGame() saves = saves + 1 end
peerSend({ type = LT.MSG.MON, name = "BLUE", trainerId = 0x2222,
mon = Protocol.packMon3(blueSquirtle) })
LT.update(0)
eq(LT.state, "commit_wait", "both blocks in hand: the confirm went out and it waits for the commit")
peer:update(0)
local sentConfirm = peer:take(LT.MSG.CONFIRM)
check(sentConfirm and type(sentConfirm.digest) == "string" and #sentConfirm.digest == 16,
"game3_trade_confirm carries a 16 hex digest")
eq(TradeScene.isOpen(), false, "no cinema before the commit")
peerSend({ type = LT.MSG.CONFIRM, digest = sentConfirm and sentConfirm.digest })
LT.update(0)
eq(LT.state, "scene", "the commit starts the trade cinema")
eq(TradeScene.isLink(), true, "on its link arm")
eq(session.party[1].species, 1, "the party is untouched while the cinema plays")
local finished, confirms = runTrade()
check(finished, "and it runs to the end of the link tail")
eq(saves, 1, "the save was written once, after the commit")
eq(LT.state, "menu", "then the TRADE CENTER returns to the trade menu")
eq(confirms, 1, "each machine confirms the finished trade exactly once")
eq(session.party[1].species, 7, "SQUIRTLE is in the party now")
eq(session.party[1].otName, "BLUE", "with the other player as its OT")
eq(#session.party, 2, "and the party is still two mons")
-- pokefirered/src/trade_scene.c:1075 TradeMons
eq(session.party[1].friendship, 70, "friendship reset to 70 by TradeMons")
check(session.dex and session.dex.owned and session.dex.owned[7] == true,
"and the received mon is entered in the POKeDEX")
-- pokefirered/src/trade_scene.c:2606
eq(session.gameStats and session.gameStats[21], 1, "GAME_STAT_POKEMON_TRADES counts the trade")
-- pokefirered/src/trade_scene.c:2605
local ql = questEvents[#questEvents]
eq(ql and ql.key, "TradedMon1ForPersonsMon2", "the quest log records QL_EVENT_LINK_TRADED")
eq(ql and ql.args.S1, "BLUE", "naming the partner")
eq(ql and ql.s, session, "on this player's session")
print("[test] 7. a cancel from the other machine leaves the party alone")
openLink()
session.party = { mon(1, 10), mon(4, 12) }
LT.startMenu()
peer:update(0)
peer:take(LT.MSG.PARTY)
peerParty({ mon(7, 11) })
LT.update(0)
LT.offer(1)
peerSend({ type = LT.MSG.CMD, cmd = LT.LINKCMD.REQUEST_CANCEL })
LT.update(0)
eq(LT.state, "canceled", "the trade was canceled")
eq(LT.lastResult, "partner_canceled", "because the other player pressed CANCEL")
eq(session.party[1].species, 1, "the party never changed")
local sawPartnerCancel = false
pumpPeer(function(msg)
if msg.cmd == LT.LINKCMD.PARTNER_CANCEL_TRADE then sawPartnerCancel = true end
end)
check(sawPartnerCancel, "and the other machine was told so")
print("[test] 8. a cable pulled mid-trade loses nothing")
openLink()
session.party = { mon(1, 10), mon(4, 12) }
LT.startMenu()
peer:update(0)
peer:take(LT.MSG.PARTY)
local pulledSquirtle = mon(7, 11)
peerParty({ pulledSquirtle })
LT.update(0)
LT.offer(1)
peerSend({ type = LT.MSG.CMD, cmd = LT.LINKCMD.READY_TO_TRADE, cursor = 0 })
LT.update(0)
LT.confirm(true)
peerSend({ type = LT.MSG.CMD, cmd = LT.LINKCMD.INIT_BLOCK })
LT.update(0)
eq(LT.state, "exchange", "the trade reached the block exchange")
peer:update(0)
local pulledBlock = peer:take(LT.MSG.MON)
peerSend({ type = LT.MSG.MON, name = "BLUE", trainerId = 0x2222,
mon = Protocol.packMon3(pulledSquirtle) })
LT.update(0)
eq(LT.state, "commit_wait", "both blocks crossed and the confirm went out")
check(pulledBlock ~= nil, "the offered mon went over the cable")
saves = 0
peer:close("cable_pulled")
LT.update(0)
eq(LT.state, "off", "the session closed when the cable was pulled before the commit")
eq(LT.lastResult, "peer_dropped", "as a dropped peer")
eq(session.party[1].species, 1, "the offered mon is still in the party")
eq(#session.party, 2, "and the party is whole")
eq(saves, 0, "and nothing was saved")
eq(TradeScene.isOpen(), false, "with no cinema left on screen")
print("[test] 9. the mons the trade menu refuses")
openLink()
-- pokefirered/src/trade.c:2767 the retail build answers CANT_TRADE_NATIONAL for an EGG
session.party = { mon(1, 10), mon(4, 12, 99, { isEgg = true }) }
LT.startMenu()
peerParty({ mon(7, 11) })
LT.pump()
local ok, code = LT.offer(2)
eq(ok, false, "an EGG is refused")
eq(code, Trade.CANT_TRADE_NATIONAL,
"with the retail CANT_TRADE_NATIONAL answer, not the egg string")
session.party = { mon(1, 10), mon(4, 12, 99, { isEgg = true }) }
local ok2, code2 = LT.offer(1)
eq(ok2, false, "the last mon that is not an EGG is refused too")
eq(code2, Trade.CANT_TRADE_LAST_MON, "as CANT_TRADE_LAST_MON")
check(Trade.refusalText(code2) ~= nil, "and the refusal has a message")
-- pokefirered/data/scripts/cable_club.inc:414 CheckPartyTradeRequirements
session.party = { mon(1, 10), mon(4, 12, 99, { heldItem = 175 }) }
local yieldB, berry = Natives.special(ctx, 0x153, adapters)
eq(yieldB, false, "DoesPartyHaveEnigmaBerry does not yield")
eq(berry, 1, "the counter refuses a party carrying an ENIGMA BERRY")
session.party = { mon(1, 10), mon(4, 12, 99, { isBadEgg = true }) }
check(Trade.hasBadEgg(session.party), "and a BAD EGG party is refused at the counter")
print("[test] 10. the union room trading board trade")
openLink()
Union.reset()
session.party = { mon(1, 10), mon(67, 30, 4242) }
session.gameStats = {}
local record = Union.trade()
eq(record.species, 0, "the trading board starts with no registered mon")
-- pokefirered/src/union_room.c:4600 RegisterTradeMonAndGetIsEgg
local isEgg = LT.registerTradeMonAndGetIsEgg(2)
eq(isEgg, false, "registering MACHOKE is not an egg registration")
eq(Union.trade().playerSpecies, 67, "the board shows the registered species")
eq(Union.trade().playerLevel, 30, "and its level")
-- pokefirered/src/union_room.c:4618 GetPartyPositionOfRegisteredMon
eq(LT.partyPositionOfRegisteredMon(Union.trade(), true), 1,
"the registered mon is found in party slot 2")
check(LT.startUnionRoomTrade(), "the board trade starts")
eq(LT.unionRoom, true, "as a union room trade")
eq(LT.cursor, 1, "offering the registered mon")
eq(session.gameStats[50], 1, "and GAME_STAT_NUM_UNION_ROOM_BATTLES went up, as in pret")
peer:update(0)
local urBlock = peer:take(LT.MSG.MON)
eq(urBlock and urBlock.mon.species, 67, "the registered mon went over the wire")
peerMon(urBlock, mon(25, 9, nil, { otName = "BLUE", otId = 0x2222 }))
LT.update(0)
LT.update(0)
eq(LT.state, "scene", "the union room trade goes through the same commit to the same cinema")
check(runTrade(), "and finishes")
eq(LT.state, "done", "a union room trade does not reopen the trade menu")
eq(session.party[2].species, 25, "PIKACHU came back for MACHOKE")
-- pokefirered/src/trade_scene.c:2601
eq(session.gameStats[21], nil, "a union room trade leaves GAME_STAT_POKEMON_TRADES alone")
eq(questEvents[#questEvents] and questEvents[#questEvents].key, "TradedMon1ForTrainersMon2",
"and logs QL_EVENT_LINK_TRADED_UNION")
-- pokefirered/src/union_room.c:1746 ResetUnionRoomTrade
eq(Union.trade().playerSpecies, 0, "and the trading board registration was cleared")
print("[test] 11. trade evolution on the mon that just arrived, saved after it evolved")
openLink()
session.party = { mon(1, 10), mon(4, 12) }
session.dex = { seen = {}, owned = {} }
LT.startMenu()
peer:update(0)
peer:take(LT.MSG.PARTY)
local blueMachoke = mon(67, 30, nil, { otName = "BLUE", otId = 0x2222 })
peerParty({ blueMachoke }, { progressFlags = 1 })
LT.update(0)
LT.offer(1)
peerSend({ type = LT.MSG.CMD, cmd = LT.LINKCMD.READY_TO_TRADE, cursor = 0 })
LT.update(0)
LT.confirm(true)
peerSend({ type = LT.MSG.CMD, cmd = LT.LINKCMD.INIT_BLOCK })
LT.update(0)
peer:update(0)
local evoBlock = peer:take(LT.MSG.MON)
peerMon(evoBlock, blueMachoke)
local savedSpecies
function game:saveGame() savedSpecies = session.party[1].species end
check(runTrade(), "the trade ran to its end")
-- pokefirered/src/trade_scene.c:2322
eq(session.party[1].species, 68, "the MACHOKE that arrived evolved into MACHAMP")
-- pokefirered/src/trade_scene.c:2595
eq(savedSpecies, 68, "and the link save came after the evolution")
game.saveGame = nil
print("[test] 14. the letter the traded mon is holding rides with it")
local Mail = require("src.core.game3.mail")
openLink()
Trade.clearPartnerMail()
session.mail = nil
session.party = { mon(1, 10), mon(4, 12) }
-- pokefirered/include/mail.h:21 ITEM_ORANGE_MAIL
local ORANGE_MAIL = 121
local RETRO_MAIL = 122
-- pokefirered/src/mail_data.c:41 GiveMailToMon
local sentMailId = Mail.giveMailToMon(session, session.party[1], ORANGE_MAIL)
check(sentMailId ~= Mail.MAIL_NONE, "the offered mon is carrying ORANGE MAIL")
LT.startMenu()
peer:update(0)
peer:take(LT.MSG.PARTY)
local mailSquirtle = mon(7, 11, nil, { otName = "BLUE", otId = 0x2222, item = RETRO_MAIL,
heldItem = RETRO_MAIL })
peerParty({ mailSquirtle })
LT.update(0)
LT.offer(1)
peerSend({ type = LT.MSG.CMD, cmd = LT.LINKCMD.READY_TO_TRADE, cursor = 0 })
LT.update(0)
LT.confirm(true)
peerSend({ type = LT.MSG.CMD, cmd = LT.LINKCMD.INIT_BLOCK })
LT.update(0)
peer:update(0)
-- pokefirered/src/union_room.c:1733 the mail goes out beside the mon
local sentBlock = peer:take(LT.MSG.MON)
check(sentBlock ~= nil, "the offered mon went over the cable")
check(sentBlock and type(sentBlock.mail) == "table", "with its letter beside it")
eq(sentBlock and sentBlock.mail and sentBlock.mail.itemId, ORANGE_MAIL,
"the same ORANGE MAIL record")
eq(sentBlock and sentBlock.mail and sentBlock.mail.playerName, "RED", "signed by this player")
peerMon(sentBlock, mailSquirtle, {
mail = {
itemId = RETRO_MAIL, playerName = "BLUE", trainerId = 0x2222, species = 7, design = 1,
words = { 1, 2, 3, 4, 5, 6, 7, 8, 9 },
},
})
LT.update(0)
LT.update(0)
eq(LT.state, "scene", "the cinema starts once both letters are in hand and the trade committed")
-- pokefirered/src/trade_scene.c:2488 gLinkPartnerMail[0] = mail
check(Trade.PARTNER_MAIL[0] ~= nil, "the partner's letter is parked in gLinkPartnerMail")
check(runTrade(), "the trade ran to its end")
eq(session.party[1].species, 7, "the received mon is in the party")
-- pokefirered/src/trade_scene.c:1078 GiveMailToMon2
check(Mail.monHasMail(session.party[1]), "still holding the letter it came with")
local landed = Mail.get(session, session.party[1].mail)
eq(landed and landed.playerName, "BLUE", "written by the other player")
eq(landed and landed.itemId, RETRO_MAIL, "on the RETRO MAIL it was sent on")
eq(landed and landed.words and landed.words[1], 1, "with the words it was written with")
-- pokefirered/src/trade_scene.c:1066 ClearMailStruct
local orangeLeft = 0
for _, record in ipairs(Mail.pool(session)) do
if (tonumber(record.itemId) or 0) == ORANGE_MAIL then orangeLeft = orangeLeft + 1 end
end
eq(orangeLeft, 0, "the letter the sent mon carried left this save with it")
eq(session.party[1].mail, sentMailId,
"and the arriving letter took the slot it freed, as GiveMailToMon does")
print("[test] 15. the seat's own select screen drives the offer and the confirmation")
Link.reset()
LT.reset()
local Menu = require("src.ui.game3.link_trade_menu")
Menu.reset()
openLink()
session.party = { mon(1, 10), mon(4, 12) }
-- pokefirered/src/cable_club.c:958 StartWiredCableClubTrade ends in the trade menu
Natives.special(ctx, NativesLink.SPECIAL.StartWiredCableClubTrade, adapters)
eq(LT.state, "menu", "the wired seat lands in the trade menu")
check(Menu.show(), "and the select screen is up for the player")
local function tick(n)
for _ = 1, n or 1 do
LT.update(0)
Menu.update(0)
end
end
tick(1)
-- pokefirered/src/trade.c:853
eq(Menu.cb, "loading", "the screen holds on standby until the other party arrives")
eq(Menu.message, "gText_Trade_CommunicationStandby", "with the standby message up")
peerParty({ mon(7, 11), mon(25, 14) }, { version = 0, progressFlags = 1 })
tick(12)
eq(#LT.peerParty, 2, "the other machine's party arrived over the wire")
eq(Menu.cb, "main", "then the grid takes input once the fade is done")
eq(Menu.message, nil, "and the standby message is gone")
eq(Menu.pos, 0, "the cursor starts on this machine's lead")
local press = function(key)
return { wasPressed = function(_, k) return k == key end }
end
-- pokefirered/src/trade.c:349 sCursorMoveDestinations
Menu.handleInput(press("down"))
eq(Menu.pos, 12, "DOWN from slot 0 skips the empty rows and lands on CANCEL")
Menu.handleInput(press("up"))
eq(Menu.pos, 7, "UP from CANCEL lands on the other machine's second mon")
Menu.handleInput(press("left"))
eq(Menu.pos, 6, "LEFT walks the other machine's row")
Menu.handleInput(press("left"))
eq(Menu.pos, 1, "LEFT again crosses to this machine's second mon")
eq(Menu.cursor, 2, "which is own slot 2")
Menu.handleInput(press("left"))
eq(Menu.pos, 0, "and LEFT reaches the lead")
eq(Menu.cursor, 1, "own slot 1")
-- pokefirered/src/trade.c:1847
Menu.handleInput(press("a"))
eq(Menu.cb, "selected", "A on an own mon opens SUMMARY / TRADE instead of offering it")
check(Menu.submenuVisible, "the SUMMARY / TRADE window is drawn")
eq(LT.state, "menu", "nothing was offered yet")
Menu.handleInput(press("b"))
eq(Menu.cb, "main", "B closes the submenu")
check(not Menu.submenuVisible, "and erases it")
Menu.handleInput(press("a"))
Menu.handleInput(press("down"))
eq(Menu.subCursor, 2, "DOWN moves to TRADE")
-- pokefirered/src/trade.c:1811 SetReadyToTrade
Menu.handleInput(press("a"))
eq(LT.state, "ready_wait", "TRADE offers the mon, with no hand-fed LT.offer call")
eq(LT.cursor, 0, "and the offered slot is the one the cursor was on")
eq(Menu.message, "gText_Trade_CommunicationStandby", "the standby message comes up")
check(not Menu.cursorVisible, "and the cursor is hidden")
peerSend({ type = LT.MSG.CMD, cmd = LT.LINKCMD.READY_TO_TRADE, cursor = 1 })
tick(1)
eq(LT.state, "confirm", "both machines have picked, so the confirmation comes up")
eq(Menu.cb, "selected_mons", "the chosen mons slide to the middle first")
eq(Menu.message, nil, "the standby message is cleared")
eq(Menu.selected[0].idx, 0, "this machine's pick is its lead")
eq(Menu.selected[1].idx, 1, "the other machine's pick is its second mon")
tick(30)
eq(Menu.cb, "okay_wait", "both selected-mon screens finished drawing")
eq(Menu.bottom, "okay", "and the bottom row asks IS THIS TRADE OKAY?")
check(not Menu.confirming, "the YES / NO is not up yet")
-- pokefirered/src/trade.c:2086
tick(121)
check(Menu.confirming, "the YES / NO comes up 120 frames later")
Menu.handleInput(press("a"))
eq(LT.state, "confirm_wait", "A on YES answers the prompt through LT.confirm")
eq(LT.playerConfirmStatus, LT.STATUS.READY, "with LINKCMD_INIT_BLOCK for the leader itself")
tick(5)
eq(Menu.message, "gText_Trade_CommunicationStandby", "and standby is queued behind the answer")
Menu.reset()
LT.reset()
print("[test] 16. CANCEL asks first, and a crossed cancel comes back to the grid")
Link.reset()
LT.reset()
Menu.reset()
openLink()
session.party = { mon(1, 10), mon(4, 12) }
LT.startMenu()
Menu.show()
peerParty({ mon(7, 11) }, { version = 0, progressFlags = 1 })
tick(12)
Menu.handleInput(press("b"))
eq(Menu.cb, "main", "B on the grid does nothing")
eq(LT.state, "menu", "and cancels nothing")
Menu.handleInput(press("down"))
eq(Menu.pos, 12, "the cursor is on CANCEL")
-- pokefirered/src/trade.c:1867
Menu.handleInput(press("a"))
eq(Menu.cb, "cancel_prompt", "A on CANCEL asks YES / NO")
eq(Menu.bottom, "cancel", "with the bottom row reading CANCEL TRADE?")
Menu.handleInput(press("b"))
eq(Menu.cb, "main", "B answers NO")
eq(Menu.bottom, "choose", "and the bottom row is back to CHOOSE A POKeMON")
Menu.handleInput(press("a"))
Menu.handleInput(press("a"))
eq(LT.state, "ready_wait", "YES cancels through LT.cancelSelect")
eq(LT.playerSelectStatus, LT.STATUS.CANCEL, "as STATUS_CANCEL for the leader")
eq(Menu.message, "gText_WaitingForFriendToFinish", "while waiting for the other player")
peerSend({ type = LT.MSG.CMD, cmd = LT.LINKCMD.READY_TO_TRADE, cursor = 0 })
tick(1)
eq(LT.state, "canceled", "the other machine had picked a mon")
eq(Menu.cb, "trade_canceled", "so the screen reports it")
eq(Menu.message, "gText_FriendWantsToTrade", "as YOUR FRIEND WANTS TO TRADE")
check(Menu.isOpen(), "and stays open")
Menu.handleInput(press("a"))
eq(LT.state, "menu", "A returns both the link and the screen to the grid")
eq(Menu.cb, "main", "the grid takes input again")
check(Menu.cursorVisible, "with the cursor back")
Menu.reset()
LT.reset()
print("[test] 17. a mon that cannot go is refused after the queued delay")
Link.reset()
LT.reset()
Menu.reset()
openLink()
session.party = { mon(1, 10) }
LT.startMenu()
Menu.show()
peerParty({ mon(7, 11) }, { version = 0, progressFlags = 1 })
tick(12)
Menu.handleInput(press("a"))
Menu.handleInput(press("down"))
Menu.handleInput(press("a"))
eq(LT.state, "menu", "the last mon is not offered")
eq(Menu.cb, "trade_canceled", "the screen waits for A")
eq(Menu.message, nil, "the refusal is queued, not printed yet")
-- pokefirered/src/trade.c:86 QUEUE_DELAY_MSG
tick(4)
eq(Menu.message, "gText_OnlyPkmnForBattle", "then it says it is the only POKeMON for battle")
Menu.handleInput(press("a"))
eq(Menu.cb, "main", "A goes back to the grid")
eq(Menu.message, nil, "and clears the message")
Menu.reset()
LT.reset()
print("[test] 19. held D-pad repeats, held items, the partner summary and the exit fades")
Link.reset()
LT.reset()
Menu.reset()
openLink()
session.party = { mon(1, 10, nil, { item = 13 }), mon(4, 12, nil, { heldItem = 121 }) }
LT.startMenu()
Menu.show()
peerParty({ mon(7, 11, nil, { otName = "BLUE", otId = 0x2222 }) }, { version = 0, progressFlags = 1 })
tick(12)
local held = function(key, fresh)
return {
wasPressed = function(_, k) return fresh and k == key end,
isDown = function(_, k) return k == key end,
}
end
-- pokefirered/src/main.c:309
Menu.handleInput(held("right", true))
eq(Menu.pos, 1, "a fresh RIGHT moves once")
for _ = 1, 39 do Menu.handleInput(held("right")) end
eq(Menu.pos, 1, "holding it does nothing for 40 frames")
Menu.handleInput(held("right"))
eq(Menu.pos, 6, "then JOY_REPT fires and the cursor crosses to the other party")
for _ = 1, 4 do Menu.handleInput(held("right")) end
eq(Menu.pos, 6, "the next repeat waits 5 frames")
Menu.handleInput(held("right"))
eq(Menu.pos, 0, "and the next one wraps RIGHT from 6 back to this machine's lead")
Menu.handleInput({ wasPressed = function() return false end, isDown = function() return false end })
-- pokefirered/src/party_menu.c:2785 DrawHeldItemIconsForTrade
eq(Menu.heldItemFrame(session.party[1]), 0, "a held POTION draws the item icon")
eq(Menu.heldItemFrame(session.party[2]), 1, "held MAIL draws the mail icon")
eq(Menu.heldItemFrame(LT.peerParty[1]), nil, "an empty hand draws nothing")
-- pokefirered/src/trade.c:1946
local SummaryMenu = require("src.ui.game3.summary_menu")
local realOpen = SummaryMenu.openMenu
local seen
SummaryMenu.openMenu = function(party, idx, opts) seen = { party = party, idx = idx, opts = opts } end
Menu.handleInput(press("up"))
Menu.handleInput(press("up"))
eq(Menu.pos, 6, "the cursor is on the partner's lead")
Menu.handleInput(press("a"))
tick(10)
SummaryMenu.openMenu = realOpen
check(seen ~= nil, "A on a partner mon opens its summary")
check(seen and seen.opts.enemyParty == true, "as the enemy party (gText_Somewhere, no move swap)")
eq(seen and seen.opts.owner and seen.opts.owner.playerName, "BLUE", "with the partner as the owner")
eq(seen and seen.opts.owner and seen.opts.owner.trainerId, 0x2222, "and the partner's trainer id")
if seen and seen.opts.onClose then seen.opts.onClose() end
tick(10)
eq(Menu.cb, "main", "the grid is back after the summary")
local savedLove = rawget(_G, "love")
love = { graphics = {} }
LT.state = "exit"
LT.lastResult = "both_canceled"
tick(1)
eq(Menu.cb, "exiting", "a double cancel leaves through CB_InitExitCanceledTrade")
check(Menu.isOpen(), "the screen stays up while it fades")
-- pokefirered/src/trade.c:1721
tick(7)
check(Menu.isOpen() and Menu.fade > 0, "fading to black")
tick(2)
check(not Menu.isOpen(), "and closes once the screen is black")
Menu.reset()
Menu.show()
Menu.cb = "idle"
Menu.fade = 0
LT.state = "exchange"
tick(20)
check(Menu.isOpen() and Menu.fade == 0, "the block exchange keeps the screen up")
LT.state = "commit_wait"
tick(20)
check(Menu.isOpen() and Menu.fade == 0, "and so does waiting for trade_commit")
LT.state = "committed"
-- pokefirered/src/trade.c:1293 CB_FadeToStartTrade
tick(15)
eq(Menu.fade, 0, "the leader holds the screen 16 frames before the trade")
check(Menu.isOpen(), "still up")
tick(1)
tick(8)
check(not Menu.isOpen(), "then fades to black and hands off to the trade scene")
love = savedLove
Menu.reset()
LT.reset()
print("[test] 20. a block that is not the mon the other party showed is refused")
openLink()
session.party = { mon(1, 10), mon(4, 12) }
LT.startMenu()
peer:update(0)
peer:take(LT.MSG.PARTY)
peerParty({ mon(7, 11) }, { version = 0, progressFlags = 1 })
LT.update(0)
LT.offer(1)
peerSend({ type = LT.MSG.CMD, cmd = LT.LINKCMD.READY_TO_TRADE, cursor = 0 })
LT.update(0)
eq(LT.checkValidityOfTradeMons(1, 1), LT.BOTH_MONS_VALID, "the previewed SQUIRTLE passes the validity check")
LT.confirm(true)
peerSend({ type = LT.MSG.CMD, cmd = LT.LINKCMD.INIT_BLOCK })
LT.update(0)
eq(LT.state, "exchange", "the block exchange started")
peer:update(0)
peer:take(LT.MSG.MON)
pumpPeer()
-- pokefirered/src/trade.c:1955
peerSend({ type = LT.MSG.MON, name = "BLUE", trainerId = 0x2222,
mon = Protocol.packMon3(mon(151, 30, nil, { fatefulEncounter = false })) })
LT.update(0)
eq(LT.state, "canceled", "an illegitimate MEW in place of the SQUIRTLE is refused")
eq(LT.lastResult, "bad_mon", "as bad_mon")
eq(LT.lastRefusal, "not the POKéMON that was shown", "because it is not the mon that was shown")
peer:update(0)
eq(peer:take(LT.MSG.CONFIRM), nil, "no confirm went to the relay")
local sawRefuse = false
pumpPeer(function(msg)
if msg.cmd == LT.LINKCMD.PLAYER_CANCEL_TRADE then sawRefuse = true end
end)
check(sawRefuse, "the other machine was told to cancel")
eq(session.party[1].species, 1, "and the party is untouched")
LT.reset()
Link.reset()
LT.reset()
Union.reset()
if failed == 0 then
print("[pass] link trade")
os.exit(0)
end
print("[FAIL] link trade: " .. failed .. " failed")
os.exit(1)