Files
2026-09-24 05:17:32 -04:00

445 lines
18 KiB
Lua

return function(game)
local U = dofile("tests/drivers/util.lua")
local PORT = tonumber(os.getenv("POKEPORT_C2_RELAY_PORT") or "") or 18431
local SERVER = os.getenv("POKESERVER_DIR") or "../pokeserver"
local failures = 0
local function check(cond, msg)
if cond then
U.log("ok ", msg)
else
failures = failures + 1
U.log("FAIL", msg)
end
return cond
end
local pidFile = "/tmp/pokeserver_c2_" .. PORT .. ".pid"
local function stopServer()
local handle = io.open(pidFile, "r")
if handle then
local pid = handle:read("*l")
handle:close()
if pid and pid:match("^%d+$") then
os.execute("kill " .. pid .. " >/dev/null 2>&1")
end
end
os.remove(pidFile)
end
local function finish()
stopServer()
U.log(failures == 0 and "online c2 relay passed"
or (failures .. " online c2 relay check(s) failed"))
love.event.quit(failures == 0 and 0 or 1)
while true do coroutine.yield() end
end
local probe = io.open(SERVER .. "/server.js", "r")
if not probe then
U.log("FAIL", SERVER .. "/server.js is not checked out")
love.event.quit(1)
return
end
probe:close()
os.execute(("(cd %q && PORT=%d HTTP_PORT=%d SYNC_ENABLED=0 CARTS_ENABLED=0 "
.. "STATS_ENABLED=0 GIFTS_ENABLED=0 CART_MIRROR_ENABLED=0 node server.js "
.. ">/tmp/pokeserver_c2_%d.log 2>&1 & echo $! > %q)")
:format(SERVER, PORT, PORT + 1, PORT, pidFile))
local Net = require("src.link.Net")
local function reachable()
local net = Net.new()
if not net:connectTCP("127.0.0.1:" .. PORT) then return false end
for _ = 1, 30 do
net:update()
if net.closed then
pcall(function() net:close() end)
return false
end
if not net.connecting then
pcall(function() net:close() end)
return true
end
end
pcall(function() net:close() end)
return false
end
local up = false
local startedAt = love.timer.getTime()
while love.timer.getTime() - startedAt < 20 do
up = reachable()
if up then break end
U.wait(2)
end
check(up, "the spawned pokeserver answered on 127.0.0.1:" .. PORT)
if not up then return finish() end
local function freshClient()
local saved = package.loaded["src.online.Client"]
package.loaded["src.online.Client"] = nil
local C = require("src.online.Client")
package.loaded["src.online.Client"] = saved
C.reset()
C.configure({ relayAddress = "127.0.0.1:" .. PORT })
return C
end
local L = require("src.online.Client")
L.reset()
L.configure({ relayAddress = "127.0.0.1:" .. PORT })
local OnlinePanel = require("src.import.OnlinePanel")
OnlinePanel._hooked = nil
OnlinePanel._events = {}
OnlinePanel._pendingStart = nil
local Version = require("src.core.Version")
local Fingerprint = require("src.link.Fingerprint")
local Dataset = require("src.core.game3.dataset")
local cache = Dataset.cache()
local fp = Fingerprint.compute({ generation = 3,
gen3Inputs = Fingerprint.gen3Inputs(function(rel) return cache:read(rel) end) }, {}, 3)
local function profile(version, ruleset, size)
return { engine = 3, version = version, engineVersion = Version.engine,
apiVersion = Version.modApi, fingerprint = fp, rulesetId = ruleset,
kind = "vanilla", rule = { partySize = size or 3 } }
end
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 RelayTransport = require("src.core.game3.link.relay_transport")
local files = {}
local savedFs = SaveData.portableFs
SaveData.portableFs = function()
return {
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,
}
end
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[1], row[2])) end
return SaveData.decode(SaveData.encode(Schema.toSaveTable(session)))
end
local DATA = { generation = 3, Pokemon = Pokemon, pokemon = Pokemon }
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 mySave = makeSave("firered", "RED", 11111,
{ { 6, 50 }, { 9, 50 }, { 3, 50 }, { 25, 40 } })
local hL = handle("firered", "slot1", mySave)
local savedOpen = Trade.openSlot
Trade.openSlot = function() return hL end
local imp = { ready = { firered = true, leafgreen = true }, activeSlot = {}, slots = {},
pulse = 0, _pages = {}, _uiActions = {}, _actAt = {} }
local played
imp.playArena = function(_, version, cartId, spec)
played = { version = version, cartId = cartId, spec = spec }
return true
end
local st = OnlinePanel.state(imp)
st.version, st.slotId, st.setupDone, st.kind = "firered", "slot1", true, "vanilla"
st.profiles["firered|vanilla|-"] = { profile = profile("firered", "g3_single", 3) }
st.slotRead = { key = "firered|slot1|-",
data = { generation = 3, party = mySave.party, save = mySave } }
local peers = {}
local peer
local function pump(n)
for _ = 1, n or 1 do
L.update(1 / 60)
for _, C in ipairs(peers) do C.update(1 / 60) end
OnlinePanel.update(imp, 1 / 60)
if peer then peer:update(1 / 60) end
coroutine.yield()
end
end
local function waitFor(fn, frames, what)
local limit = math.max(5, (frames or 600) / 60)
local from = love.timer.getTime()
while love.timer.getTime() - from < limit do
if fn() then return true end
pump(1)
end
if fn() then return true end
check(false, "timed out waiting for " .. tostring(what))
return false
end
local function team(n)
local out = {}
for i = 1, n do out[i] = { where = "party", index = i } end
return out
end
L.connect({ name = "RED#001", profiles = { profile("firered", "g3_single", 3) },
presence = { where = "launcher", status = "idle", version = "firered" } })
for i, name in ipairs({ "LEAF#002", "GOLD#003", "KRIS#004", "MAY#005" }) do
peers[i] = freshClient()
peers[i].connect({ name = name, profiles = { profile("leafgreen", "g3_single", 3) } })
end
if not waitFor(function()
if L.state() ~= "online" then return false end
for _, C in ipairs(peers) do if C.state() ~= "online" then return false end end
return true
end, 900, "every client online") then
U.log("launcher", L.state(), tostring(L.error()))
return finish()
end
local lastRoom
local function leaveAll()
lastRoom = L.room() and L.room().room or lastRoom
OnlinePanel.home(imp)
L.leaveRoom()
for _, C in ipairs(peers) do C.leaveRoom() end
pump(30)
local stale
local quietFrom = love.timer.getTime()
while love.timer.getTime() - quietFrom < 1 do
stale = stale or (L.room() and L.room().room)
pump(1)
end
check(stale == nil, "no stale room_state comes back after leaving: " .. tostring(stale))
played = nil
OnlinePanel._pendingStart = nil
end
local function newRoom()
local r = L.room()
return r ~= nil and r.room ~= lastRoom and r or nil
end
OnlinePanel.setRuleset(imp, "g3_double")
st.team = team(3)
check(OnlinePanel.hostBattle(imp), "the launcher hosts a FireRed double battle")
waitFor(function() return L.room() ~= nil end, 600, "the double room")
local room = L.room()
check(room and room.engine == 3, "the relay made an engine 3 room")
check(room and room.profile and room.profile.rulesetId == "g3_double", "a double battle")
check(room and room.seats == 2, "with two seats")
local row
waitFor(function()
OnlinePanel.invalidate(imp, "lobby")
local found
for _, e in ipairs(peers[1].lobby() or {}) do
if room and e.room == room.room then found = e end
end
row = found
return found ~= nil
end, 600, "the room in another trainer's lobby")
check(row and row.engine == 3, "the lobby row carries engine 3")
check(row and row.profile and row.profile.version == "firered", "and FireRed")
peers[1].joinRoom(room.room, "player", profile("leafgreen", "g3_double", 3))
waitFor(function() return played ~= nil end, 600, "the double battle to start")
check(peers[1].role() == "guest", "the LeafGreen trainer took seat 1")
check(played and played.version == "firered", "the launcher boots FireRed")
check(played and played.spec.mode == "double", "into a double battle arena")
check(played and played.spec.seat == 0 and played.spec.seats == 2, "on seat 0 of 2")
check(played and #played.spec.myParty == 3, "sending its three picks")
check(played and type(played.spec.seed) == "number", "on the relay's seed")
if played then
local returned
local savedReturn = game.returnToLauncher
game.returnToLauncher = function(o) returned = o or {} end
local Game3Link = require("src.link.Game3Link")
local LB = require("src.core.game3.link.battle")
local Protocol = require("src.link.Protocol")
local linkType = LB.arenaLinkType(LB.MODE_OF.double)
local rsG = peers[1].roomSession()
local peerT = RelayTransport.new(rsG, { client = peers[1] })
local peerLink = Game3Link.attach(peerT, { seat = 1, seats = 2, linkType = linkType,
game = game, hello = Game3Link.hello(game, linkType,
{ name = "LEAF", trainerId = 22222, gender = 1 }) })
local peerSave = makeSave("leafgreen", "LEAF", 22222,
{ { 94, 50 }, { 131, 50 }, { 59, 50 } })
OnlinePanel.lastResult = nil
game:enterArena(played.spec)
check(game.phase == "arena", "the launcher's spec boots the Game3 arena")
local setup
waitFor(function()
peerLink:update(1 / 60)
setup = setup or peerLink:take(LB.MSG.SETUP)
return setup ~= nil
end, 900, "the arena's party at the other seat")
check(peerLink:isReady(), "the arena's Game3Link hello matches over the real relay")
check(setup ~= nil and #(setup.party or {}) == 3, "the arena sends the three picked POKeMON")
peerLink:send({ type = LB.MSG.LINKUP, linkType = linkType, players = 2 })
peerLink:send({ type = LB.MSG.SEAT, seat = 1 })
peerLink:send({ type = LB.MSG.SETUP, mode = LB.MODE_OF.double, unionRoom = false,
name = "LEAF", trainerId = 22222, gender = 1, seat = 1,
party = Protocol.packParty3(peerSave.party) })
waitFor(function()
peerLink:update(1 / 60)
return game.arena and game.arena.stage == "battle"
end, 900, "the arena battle to begin")
check(game.arena and game.arena.stage == "battle",
"the arena reaches the battle against the other seat's party")
peers[1].leaveRoom()
waitFor(function() return returned ~= nil end, 1200,
"the arena to hand back to the launcher")
check(returned and returned.tab == "online", "the arena returns to the ONLINE tab")
check(OnlinePanel.lastResult ~= nil, "the launcher hears the arena's result: "
.. tostring(OnlinePanel.lastResult))
game.returnToLauncher = savedReturn
end
leaveAll()
OnlinePanel.setRuleset(imp, "g3_multi")
st.team = team(3)
check(OnlinePanel.hostBattle(imp), "the launcher hosts a multi battle")
waitFor(function() return newRoom() ~= nil end, 600, "the multi room")
room = newRoom()
check(room and room.seats == 4, "the relay gave it four seats")
for i = 1, 3 do
peers[i].joinRoom(room.room, "player", profile("leafgreen", "g3_multi", 3))
pump(20)
end
waitFor(function() return played ~= nil end, 600, "the multi battle to start")
check(peers[3].role() == "seat3", "the fourth trainer sits on seat 3")
check(played and played.spec.mode == "multi", "the launcher boots a multi battle")
check(played and played.spec.seats == 4, "for four seats")
check(played and #(played.spec.players or {}) == 4, "naming all four trainers")
leaveAll()
local hostC = peers[1]
hostC.createRoom({ intent = "battle", profile = profile("leafgreen", "g3_multi", 3),
playing = true, maxSpectators = 8, private = false, seats = 4, auto = false })
waitFor(function() return hostC.room() ~= nil end, 600, "a LeafGreen multi room")
room = hostC.room()
OnlinePanel.setRuleset(imp, "g3_single")
st.joinTarget = { room = room.room, engine = 3, rulesetId = "g3_multi", as = "spectator" }
check(OnlinePanel.joinRoom(imp, room.room, "spectator"), "the launcher watches it")
waitFor(function() return L.room() ~= nil end, 600, "the spectator seat")
for i = 2, 4 do
peers[i].joinRoom(room.room, "player", profile("leafgreen", "g3_multi", 3))
pump(20)
end
waitFor(function() return played ~= nil end, 600, "the spectator arena")
check(played and played.spec.role == "spectator", "the launcher watches as a spectator")
check(played and played.spec.seats == 4 and played.spec.seat == nil,
"of all four seats, with no seat of its own")
leaveAll()
local disk = files[hL.path]
local savedLive = Trade.hostIsLive
Trade.hostIsLive = nil
check(OnlinePanel.hostTrade(imp), "the launcher hosts a FireRed trade")
waitFor(function() return newRoom() ~= nil end, 600, "the trade room")
room = newRoom()
check(room and room.intent == "trade", "the relay made a trade room")
check(room and room.profile.rulesetId == "g3_link", "on the link ruleset")
local partner = peers[2]
partner.joinRoom(room.room, "player", profile("leafgreen", "g3_link", 3))
waitFor(function() return partner.room() ~= nil end, 600, "the partner seat")
local hP = handle("leafgreen", "slot2", makeSave("leafgreen", "GOLD", 22222,
{ { 95, 25 }, { 1, 8 } }))
local rsP = partner.roomSession()
peer = Trade.remote(hP, rsP, { transport = RelayTransport.new(rsP, { client = partner }) })
check(peer ~= nil, "the partner opens its side of the Trade Center")
if peer then peer:start() end
local tr = OnlinePanel.tradeState(imp)
waitFor(function()
return tr.remote and tr.remote:stage() == "picking" and peer and peer:stage() == "picking"
end, 900, "both trade menus")
check(OnlinePanel.screen(imp) == "trade", "the launcher is on the Trade screen")
local _, theirs = OnlinePanel.remoteRows(tr.remote)
check(#theirs == 2, "showing the partner's two POKeMON")
check(OnlinePanel.remotePick(imp, 4), "the launcher offers its Pikachu")
check(peer and peer:pick(1), "the partner offers its first")
waitFor(function()
return tr.remote and tr.remote:stage() == "confirming" and peer:stage() == "confirming"
end, 600, "both confirm prompts")
OnlinePanel.remoteConfirm(imp, true)
peer:confirm(true)
waitFor(function() return st.status == "Trade complete." or tr.remoteResult ~= nil end,
900, "the relay's trade commit")
check(st.status == "Trade complete.", "the relay commit finishes the launcher's trade: "
.. tostring(st.status) .. " / " .. tostring(tr.remoteResult))
check(files[hL.path] ~= disk, "and only then writes the launcher's save")
check(peer and peer:stage() == "committed", "the partner committed too")
peer = nil
Trade.hostIsLive = savedLive
leaveAll()
local invitee = peers[3]
local closedMsg, sentMsg
L.on("invite_closed", function(m) closedMsg = m end)
L.on("invite_sent", function(m) sentMsg = m end)
local inviteIn
invitee.on("invite_in", function(m) inviteIn = m end)
local inviteeId = invitee.you() and invitee.you().id
st.team = team(3)
OnlinePanel.startInvite(imp, { id = inviteeId, name = "KRIS#004", engine = 3,
version = "leafgreen" }, "battle_double")
local answered = false
local askedAt = love.timer.getTime()
while love.timer.getTime() - askedAt < 4 do
if sentMsg or closedMsg or inviteIn then answered = true break end
pump(1)
end
if not answered then
U.log("SKIP", "the relay never answered an invite (SRV-A A3 invites not in relay.js yet)")
else
waitFor(function() return inviteIn ~= nil end, 600, "the invite at the other trainer")
check(inviteIn and inviteIn.activity == "battle_double", "a double battle invite")
if inviteIn then
local open = invitee.invites() or {}
U.log("note", "invitee holds " .. #open .. " invites; id=" .. tostring(inviteIn.id)
.. " expiresAt=" .. tostring(inviteIn.expiresAt) .. " serverTime="
.. tostring(invitee.serverTime()))
check(invitee.replyInvite(inviteIn.id, true) ~= false, "the other trainer accepts")
end
if not waitFor(function() return played ~= nil end, 600, "the invite battle") then
local r = L.room()
U.log("note", "status=" .. tostring(st.status), "room=" .. tostring(r and r.room),
"stage=" .. tostring(r and r.stage), "closed=" .. tostring(closedMsg and closedMsg.why),
"invitee=" .. tostring(invitee.room() and invitee.room().room))
end
check(played and played.spec.mode == "double", "the accepted invite boots a double")
leaveAll()
local sender = peers[4]
local senderClosed
sender.on("invite_closed", function(m) senderClosed = m end)
local meId = L.you() and L.you().id
sender.invite(meId, "chat", {}, profile("leafgreen", "g3_link", 3))
waitFor(function() return senderClosed ~= nil end, 600, "the chat invite answer")
check(senderClosed and senderClosed.why == "declined",
"a chat invite to the launcher is declined at once: " .. tostring(senderClosed and senderClosed.why))
check(OnlinePanel.toast(imp) == nil, "without a toast")
sender.invite(meId, "battle_single", { ruleset = "g3_single" },
profile("leafgreen", "g3_single", 3))
waitFor(function() return OnlinePanel.toast(imp) ~= nil end, 600, "the launcher's invite toast")
local toast = OnlinePanel.toast(imp)
check(toast and OnlinePanel.inviteLine(toast):find("MAY#005", 1, true) ~= nil,
"the toast names the trainer")
OnlinePanel.toastAction(imp, "accept")
waitFor(function() return played ~= nil end, 600, "the accepted invite battle")
check(played and played.spec.seat == 1, "accepting puts the launcher on seat 1")
check(played and played.spec.mode == "single", "for a single battle")
leaveAll()
end
SaveData.portableFs = savedFs
Trade.openSlot = savedOpen
L.disconnect()
for _, C in ipairs(peers) do C.disconnect() end
return finish()
end