mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
365 lines
13 KiB
Lua
365 lines
13 KiB
Lua
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
if not _G.love then _G.love = require("tests.love_stub") end
|
|
|
|
local Cache = require("tests.game3_cache")
|
|
if not Cache.mount("meta.json") then
|
|
print("[skip] online_trade_remote3: " .. tostring(Cache.reason))
|
|
os.exit(0)
|
|
end
|
|
|
|
local GameVersion = require("src.core.GameVersion")
|
|
GameVersion.set("firered")
|
|
local SaveData = require("src.core.SaveData")
|
|
local Trade = require("src.online.Trade")
|
|
local Pokemon = require("src.core.game3.pokemon")
|
|
local Party = require("src.core.game3.party")
|
|
local Schema = require("src.core.game3.save_schema_firered")
|
|
local Relay = require("tests.online_trade_fakerelay")
|
|
Pokemon.install(nil)
|
|
T.check(Pokemon._names ~= nil, "FireRed species tables load from the cache")
|
|
|
|
local files = {}
|
|
local fs = {
|
|
getInfo = function(name) return files[name] and { type = "file" } or nil end,
|
|
read = function(name) return files[name] end,
|
|
write = function(name, body) files[name] = body return true end,
|
|
remove = function(name) files[name] = nil return true end,
|
|
createDirectory = function() return true end,
|
|
}
|
|
SaveData.portableFs = function() return fs end
|
|
|
|
local DATA = { generation = 3, Pokemon = Pokemon, pokemon = Pokemon }
|
|
|
|
local function makeSave(version, name, trainerId, mons)
|
|
local session = Schema.newGame({ version = version, name = name, rngSeed = trainerId })
|
|
session.trainerId = trainerId
|
|
session.dex.nationalUnlocked = true
|
|
session.party = {}
|
|
for _, row in ipairs(mons) do
|
|
assert(Party.giveMon(session, row.species, row.level, row.nickname))
|
|
local mon = session.party[#session.party]
|
|
for k, v in pairs(row.extra or {}) do mon[k] = v end
|
|
end
|
|
return SaveData.decode(SaveData.encode(Schema.toSaveTable(session)))
|
|
end
|
|
|
|
local function handle(version, slotId, save)
|
|
local path = Trade.slotPath(version, slotId)
|
|
files[path] = SaveData.encode(save)
|
|
return {
|
|
version = version, generation = 3, slotId = slotId, save = save,
|
|
path = path, party = save.party, data = DATA,
|
|
}
|
|
end
|
|
|
|
local function onDisk(h)
|
|
return SaveData.decode(files[h.path] or "")
|
|
end
|
|
|
|
local function pair(opts)
|
|
opts = opts or {}
|
|
local fr = makeSave("firered", "RED", 11111, {
|
|
{ species = 64, level = 30, nickname = "ABRA CAD" },
|
|
{ species = 25, level = 12 },
|
|
})
|
|
local lg = makeSave("leafgreen", "LEAF", 22222, {
|
|
{ species = 95, level = 25, extra = { item = 199, heldItem = 199 } },
|
|
{ species = 1, level = 8 },
|
|
})
|
|
local relay = Relay.new({ tamper = opts.tamper, rewrite = opts.rewrite })
|
|
local rsA, rsB = relay:join(0, "RED"), relay:join(1, "LEAF")
|
|
local hA = handle("firered", opts.slot or "slot1", fr)
|
|
local hB = handle("leafgreen", opts.slot or "slot1", lg)
|
|
local A = assert(Trade.remote(hA, rsA, { transport = relay:transport(rsA), peerName = "LEAF" }))
|
|
local B = assert(Trade.remote(hB, rsB, { transport = relay:transport(rsB), peerName = "RED" }))
|
|
return { relay = relay, rsA = rsA, rsB = rsB, A = A, B = B, hA = hA, hB = hB,
|
|
diskA = files[hA.path], diskB = files[hB.path] }
|
|
end
|
|
|
|
local function pump(p, n)
|
|
for _ = 1, n or 1 do
|
|
p.A:update()
|
|
p.B:update()
|
|
end
|
|
end
|
|
|
|
local function toConfirming(p)
|
|
p.A:start()
|
|
p.B:start()
|
|
pump(p, 3)
|
|
T.eq(p.A:stage(), "picking", "seat 0 reaches the trade menu")
|
|
T.eq(p.B:stage(), "picking", "seat 1 reaches the trade menu")
|
|
T.eq(#p.A.session.theirParty, 2, "each side unpacked the other's party")
|
|
T.eq(p.A.session.theirParty[1].species, 95, "as real mons")
|
|
T.check(p.A:canPick(1), "a legit pick is allowed")
|
|
T.check(p.A:pick(1), "seat 0 offers its first mon")
|
|
T.check(p.B:pick(1), "seat 1 offers its first mon")
|
|
pump(p, 2)
|
|
T.eq(p.A:stage(), "confirming", "the leader set the mons to trade")
|
|
T.eq(p.B:stage(), "confirming", "the follower heard it")
|
|
T.eq(p.A.session.theirPick, 1, "seat 0 knows what it gets")
|
|
T.eq(p.B.session.theirPick, 1, "seat 1 knows what it gets")
|
|
end
|
|
|
|
do
|
|
local p = pair()
|
|
toConfirming(p)
|
|
p.A:confirm(true)
|
|
p.B:confirm(true)
|
|
pump(p, 1)
|
|
T.eq(p.B:stage(), "commit_wait", "seat 1 sent its digest and waits")
|
|
T.eq(p.A:stage(), "exchange", "seat 0 is still exchanging")
|
|
p.A:update()
|
|
T.eq(p.A:stage(), "commit_wait", "seat 0 sent its digest")
|
|
T.eq(p.relay.commits, 1, "the relay committed once")
|
|
T.eq(files[p.hA.path], p.diskA, "nothing is written before trade_commit reaches seat 0")
|
|
T.eq(files[p.hB.path], p.diskB, "nor seat 1")
|
|
pump(p, 2)
|
|
T.eq(p.A:stage(), "committed", "seat 0 committed")
|
|
T.eq(p.B:stage(), "committed", "seat 1 committed")
|
|
T.eq(p.A:commit(), true, "commit reports the cached result")
|
|
local frNow, lgNow = onDisk(p.hA), onDisk(p.hB)
|
|
T.eq(frNow and frNow.party[1].species, 208, "FireRed's file holds the Onix, now Steelix")
|
|
T.eq(frNow and frNow.party[1].item, 0, "the Metal Coat was used up")
|
|
T.eq(lgNow and lgNow.party[1].species, 65, "LeafGreen's file holds the Kadabra, now Alakazam")
|
|
T.eq(lgNow and lgNow.party[1].nickname, "ABRA CAD", "keeping its nickname")
|
|
T.eq(lgNow and lgNow.party[1].otName, "RED", "and its OT")
|
|
T.eq(frNow and frNow.party[2].species, 25, "untraded mons stay")
|
|
T.check(pcall(Schema.fromSaveTable, frNow), "the FireRed file loads back")
|
|
T.check(pcall(Schema.fromSaveTable, lgNow), "the LeafGreen file loads back")
|
|
pump(p, 1)
|
|
T.check(p.A.peerFinished and p.B.peerFinished, "both sent CONFIRM_FINISH_TRADE after committing")
|
|
p.A:close()
|
|
p.B:close()
|
|
T.check(p.rsA.left and p.rsB.left, "closing leaves the room")
|
|
end
|
|
|
|
do
|
|
local p = pair({ slot = "slot2" })
|
|
toConfirming(p)
|
|
p.A:confirm(true)
|
|
p.B:confirm(true)
|
|
pump(p, 1)
|
|
T.eq(p.B:stage(), "commit_wait", "seat 1 confirmed its digest")
|
|
p.rsA.close()
|
|
pump(p, 2)
|
|
T.eq(p.relay.aborts, 1, "the relay aborts when seat 0 leaves before confirming")
|
|
T.eq(p.B:stage(), "cancelled", "seat 1 calls the trade off")
|
|
T.eq(p.B.session.error, "left", "because the other trainer left")
|
|
T.eq(p.A:stage(), "cancelled", "and seat 0 is out too")
|
|
T.eq(files[p.hA.path], p.diskA, "a drop before commit writes nothing on seat 0")
|
|
T.eq(files[p.hB.path], p.diskB, "or on seat 1")
|
|
end
|
|
|
|
do
|
|
local p = pair({ slot = "slot3" })
|
|
toConfirming(p)
|
|
p.A:confirm(true)
|
|
p.B:confirm(true)
|
|
pump(p, 1)
|
|
p.rsB.online = false
|
|
p.A:update()
|
|
T.eq(p.relay.commits, 1, "both digests reached the relay")
|
|
p.A:update()
|
|
T.eq(p.A:stage(), "committed", "seat 0 commits while seat 1 is dropped")
|
|
for _ = 1, 3 do p.B:update() end
|
|
T.eq(p.B:stage(), "commit_wait", "a reconnecting seat 1 keeps waiting")
|
|
T.eq(files[p.hB.path], p.diskB, "and has written nothing yet")
|
|
p.rsB.online = true
|
|
p.B:update()
|
|
T.eq(p.B:stage(), "committed", "the replayed commit lands after the reconnect")
|
|
T.eq(onDisk(p.hA).party[1].species, 208, "seat 0's file is written")
|
|
T.eq(onDisk(p.hB).party[1].species, 65, "and seat 1's")
|
|
end
|
|
|
|
do
|
|
local p = pair({ slot = "slot4" })
|
|
toConfirming(p)
|
|
p.A:confirm(true)
|
|
p.B:confirm(true)
|
|
pump(p, 1)
|
|
p.A:update()
|
|
p.rsA.close()
|
|
p.B:update()
|
|
T.eq(p.B:stage(), "committed", "a commit already sent survives the other side leaving")
|
|
T.eq(onDisk(p.hB).party[1].species, 65, "so seat 1 is written")
|
|
p.A:update()
|
|
T.eq(p.A:stage(), "committed", "and seat 0, who left after the relay committed, writes too")
|
|
T.eq(onDisk(p.hA).party[1].species, 208, "both files are written")
|
|
end
|
|
|
|
do
|
|
local p = pair({ slot = "slot5", tamper = function(seat, digest)
|
|
if seat == 1 then return ("0"):rep(16) end
|
|
return digest
|
|
end })
|
|
toConfirming(p)
|
|
p.A:confirm(true)
|
|
p.B:confirm(true)
|
|
pump(p, 3)
|
|
T.eq(p.relay.aborts, 1, "mismatched digests abort")
|
|
T.eq(p.A:stage(), "cancelled", "seat 0 cancels")
|
|
T.eq(p.A.session.error, "digest", "naming the digest")
|
|
T.eq(p.B:stage(), "cancelled", "seat 1 cancels")
|
|
T.eq(files[p.hA.path], p.diskA, "nothing written on seat 0")
|
|
T.eq(files[p.hB.path], p.diskB, "nothing written on seat 1")
|
|
end
|
|
|
|
do
|
|
local p = pair({ slot = "slot6" })
|
|
toConfirming(p)
|
|
p.A:confirm(true)
|
|
p.B:confirm(false)
|
|
pump(p, 2)
|
|
T.eq(p.A:stage(), "picking", "a no at the confirm returns both to the menu")
|
|
T.eq(p.B:stage(), "picking", "on both sides")
|
|
T.eq(p.A.session.myPick, nil, "with the picks cleared")
|
|
T.eq(files[p.hA.path], p.diskA, "and nothing written")
|
|
p.relay:_relayed({ type = "trade_abort", n = 9, why = "timeout" })
|
|
pump(p, 1)
|
|
T.eq(p.A:stage(), "picking", "a stale abort from an older barrier is ignored")
|
|
end
|
|
|
|
do
|
|
local p = pair({ slot = "slot7" })
|
|
p.A:start()
|
|
p.B:start()
|
|
pump(p, 3)
|
|
p.relay:deliver(1, { type = "game3_trade_party", party = { { species = 25, level = 5 } },
|
|
name = "EVIL", trainerId = 1, gender = 0, version = 4, progressFlags = 0 })
|
|
p.A:update()
|
|
T.eq(p.A:stage(), "cancelled", "a hostile party is refused whole")
|
|
T.check(p.A.session.error ~= nil, "with a reason: " .. tostring(p.A.session.error))
|
|
T.eq(files[p.hA.path], p.diskA, "and nothing written")
|
|
p.B:update()
|
|
T.eq(p.B:stage(), "cancelled", "the peer is told BOTH_CANCEL_TRADE")
|
|
T.eq(p.B.session.error, "both_canceled", "and exits too")
|
|
end
|
|
|
|
local Protocol = require("src.link.Protocol")
|
|
|
|
local function repick(p, a, b)
|
|
T.check(p.A:pick(a), "seat 0 picks again")
|
|
T.check(p.B:pick(b), "seat 1 picks again")
|
|
pump(p, 2)
|
|
T.eq(p.A:stage(), "confirming", "the second pick reaches the confirm")
|
|
p.A:confirm(true)
|
|
p.B:confirm(true)
|
|
pump(p, 6)
|
|
end
|
|
|
|
do
|
|
local swap = true
|
|
local p
|
|
p = pair({ slot = "slot8", rewrite = function(from, msg)
|
|
if swap and from == 1 and msg.type == "game3_trade_mon" then
|
|
local out = {}
|
|
for k, v in pairs(msg) do out[k] = v end
|
|
out.mon = Protocol.packMon3(p.hB.party[2])
|
|
return out
|
|
end
|
|
end })
|
|
toConfirming(p)
|
|
p.A:confirm(true)
|
|
p.B:confirm(true)
|
|
pump(p, 1)
|
|
T.eq(p.B:stage(), "commit_wait", "seat 1 confirmed round 1 with the mon it really sent")
|
|
pump(p, 2)
|
|
T.eq(p.A.lastRefusal, "not the POKéMON that was shown",
|
|
"a block that isn't the previewed mon is refused (trade.c:1951 runs on what was shown)")
|
|
T.eq(p.A:stage(), "picking", "seat 0 goes back to the menu")
|
|
T.eq(p.B:stage(), "picking", "seat 1 withdraws from commit_wait")
|
|
T.eq(p.relay.commits, 0, "the relay never committed")
|
|
T.eq(files[p.hA.path], p.diskA, "and nothing is written")
|
|
swap = false
|
|
repick(p, 2, 1)
|
|
T.check(p.relay.dropped >= 1, "the withdrawn side's confirm hit the still-open round")
|
|
T.eq(p.relay.aborts, 1, "which settles as one stale abort")
|
|
T.eq(p.A:stage(), "committed", "the stale round's abort doesn't cancel the second trade")
|
|
T.eq(p.B:stage(), "committed", "on either side")
|
|
T.eq(p.relay.commits, 1, "the relay commits the next round")
|
|
T.eq(onDisk(p.hB).party[1].species, 25, "seat 1 holds the Pikachu it was shown")
|
|
T.eq(onDisk(p.hA).party[2].species, 208, "seat 0 holds the Onix, now Steelix")
|
|
end
|
|
|
|
do
|
|
local p = pair({ slot = "slot9" })
|
|
toConfirming(p)
|
|
p.A:confirm(true)
|
|
p.B:confirm(true)
|
|
pump(p, 1)
|
|
p.B.phase, p.B._staleRound = "commit_wait", 1
|
|
p.B.digest = p.B.digest or ("ab"):rep(8)
|
|
p.relay:_relayed({ type = "trade_abort", n = 2, why = "digest" })
|
|
p.B:update()
|
|
T.eq(p.B:stage(), "cancelled", "an abort past the stale round still cancels")
|
|
end
|
|
|
|
do
|
|
local p = pair({ slot = "slot10" })
|
|
toConfirming(p)
|
|
local realPrepare = Trade.prepare
|
|
Trade.prepare = function(plan)
|
|
if plan.sides[1].handle == p.hB then return false, "that save didn't validate" end
|
|
return realPrepare(plan)
|
|
end
|
|
p.A:confirm(true)
|
|
p.B:confirm(true)
|
|
pump(p, 4)
|
|
Trade.prepare = realPrepare
|
|
T.eq(p.B.lastRefusal, "that save didn't validate", "a save that won't build is caught before the confirm")
|
|
T.eq(p.B:stage(), "picking", "seat 1 backs out to the menu")
|
|
T.eq(p.A:stage(), "picking", "and seat 0 with it")
|
|
T.eq(p.relay.commits, 0, "so the relay never commits")
|
|
T.eq(files[p.hA.path], p.diskA, "and seat 0 writes nothing")
|
|
T.eq(files[p.hB.path], p.diskB, "nor seat 1")
|
|
end
|
|
|
|
do
|
|
local LT = require("src.core.game3.link.trade")
|
|
local p = pair({ slot = "slot11" })
|
|
toConfirming(p)
|
|
p.A:confirm(true)
|
|
p.B:confirm(true)
|
|
pump(p, 1)
|
|
T.eq(p.B:stage(), "commit_wait", "seat 1 confirmed")
|
|
local jPath = Trade.journalPath(p.hB.path)
|
|
local j = files[jPath] and SaveData.decode(files[jPath]) or nil
|
|
local e = j and j.entries and j.entries[#j.entries] or {}
|
|
T.eq(e.room, p.relay.room, "its journal names the relay room")
|
|
T.eq(e.digest, p.B.digest, "and the confirmed digest")
|
|
T.eq(e.sent and e.sent.personality, p.hB.party[1].personality,
|
|
"it holds the packed mon sent, as the in-game resolver reads it")
|
|
T.eq(e.mon and e.mon.species, 64, "and the packed mon received")
|
|
T.eq(e.name, "RED", "with the partner's name for the quest log")
|
|
p.rsB.online = false
|
|
p.A:update()
|
|
p.A:update()
|
|
T.eq(p.A:stage(), "committed", "seat 0 commits")
|
|
p.B:close()
|
|
T.eq(onDisk(p.hB).party[1].species, 95, "seat 1 lost the link before trade_commit")
|
|
local realWith = Trade.withDataset
|
|
Trade.withDataset = function(_, fn)
|
|
local ok, result = pcall(fn, DATA)
|
|
if not ok then return nil, tostring(result) end
|
|
return result
|
|
end
|
|
LT.outcomeClient = p.relay:outcomeClient()
|
|
local done = false
|
|
for _ = 1, 60 do
|
|
if Trade.pumpPending(1000) == true then done = true break end
|
|
end
|
|
LT.outcomeClient = nil
|
|
Trade.withDataset = realWith
|
|
T.check(done, "the next launcher start settles the journal")
|
|
local lgNow = onDisk(p.hB)
|
|
T.eq(lgNow and lgNow.party[1].species, 65, "the Kadabra arrives, now Alakazam")
|
|
T.eq(lgNow and lgNow.party[1].otName, "RED", "keeping its OT")
|
|
T.check(pcall(Schema.fromSaveTable, lgNow), "the LeafGreen file loads back")
|
|
T.eq(files[jPath], nil, "and the journal is gone")
|
|
end
|
|
|
|
T.finish()
|