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