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

282 lines
9.6 KiB
Lua

#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local Cache = require("tests.game3_cache")
if not Cache.root("meta.json") then
print("[skip] online_trade_interop3: " .. tostring(Cache.reason))
os.exit(0)
end
if not _G.love then _G.love = require("tests.love_stub") end
love.graphics = nil
local failed, total = 0, 0
local function check(cond, msg)
total = total + 1
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 MAPS = {
[TRADE_CENTER] = {
warps = { { x = 5, y = 8, mapGroup = 0x7F, mapNum = 0x7F, destWarp = 128 } },
},
}
local function isWorldModule(k)
return k:match("^src%.") ~= nil
end
local current
local function stash()
if not current then return end
for k, v in pairs(package.loaded) do
if isWorldModule(k) then current.mods[k] = v end
end
end
local function clearWorld()
for k in pairs(package.loaded) do
if isWorldModule(k) then package.loaded[k] = nil end
end
end
local function within(w, fn, ...)
if current ~= w then
stash()
clearWorld()
for k, v in pairs(w.mods) do package.loaded[k] = v end
current = w
end
return fn(...)
end
local function newWorld()
stash()
clearWorld()
local w = { mods = {} }
current = w
Cache.mount("meta.json")
return w
end
local function gameWorld(name, trainerId, species)
local w = newWorld()
w.saves = 0
local session = {
store = { flags = {}, vars = {} }, map = TRADE_CENTER, x = 5, y = 8,
name = name, gender = 0, trainerId = trainerId, party = {},
bag = { pockets = { items = {} } }, dex = { seen = {}, owned = {}, caught = {} },
gameStats = {},
}
local game = { data = { maps = MAPS, generation = 3 }, session = session }
function game:saveGame() w.saves = w.saves + 1 end
package.loaded["src.core.game3.runtime"] = {
getSession = function() return session end,
isActive = function() return true end,
_game = game,
}
package.loaded["src.core.game3.player"] = { cellX = 5, cellY = 8, facing = "up" }
package.loaded["src.core.game3.quest_log_recorder"] = { event = function() end }
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) return n .. "[" .. i .. "]" end,
at = function(n, i) return 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,
}
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,
}
package.loaded["src.core.game3.scripting.space"] = {
store = session.store, mapId = TRADE_CENTER,
vm = { ctx = { specialVars = {}, stringVars = {} }, adapters = { log = function() end } },
}
require("src.core.game3.pokemon").install(nil)
local Party = require("src.core.game3.party")
for _, sp in ipairs(species) do Party.giveMon(session, sp[1], sp[2]) end
w.session, w.game = session, game
w.Link = require("src.core.game3.link")
w.LT = require("src.core.game3.link.trade")
w.Game3Link = require("src.link.Game3Link")
stash()
return w
end
local function launcherWorld(name, trainerId, species)
local w = newWorld()
w.files = {}
local SaveData = require("src.core.SaveData")
SaveData.portableFs = function()
return {
getInfo = function(n) return w.files[n] and { type = "file" } or nil end,
read = function(n) return w.files[n] end,
write = function(n, body) w.files[n] = body return true end,
remove = function(n) w.files[n] = nil return true end,
createDirectory = function() return true end,
}
end
require("src.core.GameVersion").set("leafgreen")
local Pokemon = require("src.core.game3.pokemon")
Pokemon.install(nil)
local Schema = require("src.core.game3.save_schema_firered")
local Party = require("src.core.game3.party")
local s = Schema.newGame({ version = "leafgreen", name = name, rngSeed = trainerId })
s.trainerId = trainerId
s.party = {}
for _, sp in ipairs(species) do Party.giveMon(s, sp[1], sp[2]) end
local save = SaveData.decode(SaveData.encode(Schema.toSaveTable(s)))
local Trade = require("src.online.Trade")
local path = Trade.slotPath("leafgreen", "slot1")
w.files[path] = SaveData.encode(save)
w.path, w.disk = path, w.files[path]
w.handle = { version = "leafgreen", generation = 3, slotId = "slot1", save = save,
path = path, party = save.party,
data = { generation = 3, Pokemon = Pokemon, pokemon = Pokemon } }
w.Trade, w.SaveData = Trade, SaveData
stash()
return w
end
local function newRelay()
local R = { seats = {}, confirms = {}, n = 1, commits = 0, aborts = 0 }
local function deliver(from, msg)
for s, t in pairs(R.seats) do
if s ~= from and not t.closed then
local m = {}
for k, v in pairs(msg) do m[k] = v end
m.seat = from
t.inbox[#t.inbox + 1] = m
end
end
end
function R.receive(seat, msg)
if msg.type == "game3_trade_confirm" then
R.confirms[seat] = msg.digest
local d0, d1 = R.confirms[0], R.confirms[1]
if d0 and d1 then
R.confirms = {}
if d0 == d1 then
R.commits = R.commits + 1
deliver(-1, { type = "trade_commit", n = R.n, digests = { d0, d1 } })
R.n = R.n + 1
else
R.aborts = R.aborts + 1
deliver(-1, { type = "trade_abort", n = R.n, why = "digest" })
end
end
return
end
deliver(seat, msg)
end
function R.transport(seat)
local t = { relay = true, paired = true, closed = false, inbox = {} }
function t:update() end
function t:send(msg)
if self.closed then return false end
local copy = {}
for k, v in pairs(msg) do copy[k] = v end
copy.seat = nil
R.receive(seat, copy)
return true
end
function t:poll()
local out = self.inbox
self.inbox = {}
return out
end
function t:take() return nil end
function t:close() self.closed = true end
function t:seat() return seat end
function t:seats() return 2 end
function t:peerOnline(s) local o = R.seats[s] return o ~= nil and not o.closed end
R.seats[seat] = t
return t
end
return R
end
local function scenario(label, gameSeat)
print("[test] " .. label)
local launcherSeat = 1 - gameSeat
local relay = newRelay()
local G = gameWorld("RED", 0x1234, { { 64, 30 }, { 25, 14 } })
local L = launcherWorld("LEAF", 0x2222, { { 95, 25 }, { 1, 8 } })
local tG, tL = relay.transport(gameSeat), relay.transport(launcherSeat)
within(G, function()
G.Link.reset()
G.LT.reset()
G.Link.attach(G.Game3Link.attach(tG, { seat = gameSeat, seats = 2, game = G.game,
linkType = G.LT.LINKTYPE.TRADE }))
end)
local remote = within(L, function()
local r = assert(L.Trade.remote(L.handle, tL, { transport = tL }))
r:start()
return r
end)
local function step(n)
for _ = 1, n or 1 do
within(G, function() G.Link.update(0) end)
within(L, function() remote:update() end)
end
end
step(3)
check(within(G, function() return G.Link.link and G.Link.link:isReady() end),
"the in-game link handshakes with the launcher as full")
within(G, function()
G.Link.link:send({ type = "game3_battle_seat", seat = gameSeat })
G.LT.startMenu({ screen = false })
end)
step(3)
eq(remote:stage(), "picking", "the launcher reaches the trade menu")
eq(within(G, function() return #G.LT.peerParty end), 2, "the game sees the launcher's party")
eq(within(G, function() return G.LT.peerParty[1].species end), 95, "packed as packMon3")
eq(#remote.session.theirParty, 2, "the launcher sees the game's party")
within(G, function() G.LT.offer(1) end)
within(L, function() remote:pick(1) end)
step(3)
eq(remote:stage(), "confirming", "the launcher is asked to confirm")
eq(within(G, function() return G.LT.state end), "confirm", "and so is the game")
within(G, function() G.LT.confirm(true) end)
within(L, function() remote:confirm(true) end)
local done = false
for _ = 1, 6000 do
step(1)
if within(G, function() return G.LT.completed end) == 1 and remote:stage() == "committed" then
done = true
break
end
end
check(done, "the in-game scene finished and the launcher committed ("
.. tostring(within(G, function() return G.LT.state .. "/" .. tostring(G.LT.lastResult)
.. "/" .. tostring(G.LT.lastRefusal) end)) .. ", " .. tostring(remote:stage()) .. ")")
eq(relay.commits, 1, "one trade_commit")
eq(G.saves, 1, "the game saved once")
eq(within(G, function() return G.session.party[1].species end), 95,
"the game holds the launcher's Onix")
eq(within(G, function() return G.session.party[1].otName end), "LEAF",
"with the launcher player as its OT")
local disk = within(L, function() return L.SaveData.decode(L.files[L.path]) end)
eq(disk and disk.party[1].species, 65, "the launcher's file holds the Kadabra, now Alakazam")
check(within(L, function() return L.files[L.path] end) ~= L.disk, "the launcher wrote its file")
end
scenario("1. in-game seat 0 trades with a launcher at seat 1", 0)
scenario("2. a launcher at seat 0 leads an in-game seat 1", 1)
print(("%d/%d checks passed"):format(total - failed, total))
os.exit(failed == 0 and 0 or 1)