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

342 lines
11 KiB
Lua

-- Drives two src/online/Client instances against a real ../pokeserver over
-- TCP: room create, join, ready, a full Gen 1 LinkBattle over roomSession(),
-- room_report from both sides, and the relay's room_result. Runs under love
-- because plain luajit on macOS has no luasocket.
--
-- POKEPORT_IDENTITY=online_smoke POKEPORT_TOUCH=0 \
-- POKEPORT_DRIVER=tests/drivers/online_relay_smoke.lua love .
return function(game)
local U = dofile("tests/drivers/util.lua")
local PORT = tonumber(os.getenv("POKEPORT_LINK_PORT") or "") or 17778
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
end
local function finish()
U.log(failures == 0 and "online relay smoke passed"
or (failures .. " online relay smoke 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()
local pidFile = "/tmp/pokeserver_smoke_" .. PORT .. ".pid"
os.execute(("(cd %q && PORT=%d HTTP_PORT=%d node server.js " ..
">/tmp/pokeserver_smoke.log 2>&1 & echo $! > %q)")
:format(SERVER, PORT, PORT + 1, pidFile))
local function stopServer()
local handle = io.open(pidFile, "r")
if handle then
local pid = handle:read("*l")
handle:close()
if pid and pid ~= "" then os.execute("kill " .. pid .. " >/dev/null 2>&1") end
end
os.remove(pidFile)
end
local Net = require("src.link.Net")
local function reachable()
local probe = Net.new()
if not probe:connectTCP("127.0.0.1:" .. PORT) then return false end
for _ = 1, 30 do
probe:update()
if probe.closed then
pcall(function() probe:close() end)
return false
end
if not probe.connecting then
pcall(function() probe:close() end)
return true
end
end
pcall(function() probe:close() end)
return false
end
local up = false
for _ = 1, 300 do
up = reachable()
if up then break end
U.wait(2)
end
if not up then
check(false, "the spawned pokeserver answered on 127.0.0.1:" .. PORT)
stopServer()
return finish()
end
U.log("pokeserver up on", PORT)
local function freshClient()
package.loaded["src.online.Client"] = nil
local C = require("src.online.Client")
C.reset()
C.configure({ relayAddress = "127.0.0.1:" .. PORT })
return C
end
local Host = freshClient()
local Guest = freshClient()
local function pump(n)
for _ = 1, n or 1 do
Host.update(1 / 60)
Guest.update(1 / 60)
coroutine.yield()
end
end
local function waitFor(fn, frames, what)
for _ = 1, frames or 600 do
if fn() then return true end
pump(1)
end
check(false, "timed out waiting for " .. tostring(what))
return false
end
local Data = game.data
local Version = require("src.core.Version")
local PROFILE = {
engine = 1, version = "red", engineVersion = Version.engine,
apiVersion = Version.modApi, fingerprint = "relay-smoke",
rulesetId = "gen1_faithful", kind = "vanilla", rule = { partySize = 1 },
}
local YELLOW = {}
for k, v in pairs(PROFILE) do YELLOW[k] = v end
YELLOW.version = "yellow"
Host.connect({ name = "RED", profiles = { PROFILE } })
Guest.connect({ name = "BLUE", profiles = { YELLOW } })
if not waitFor(function()
return Host.state() == "online" and Guest.state() == "online"
end, 600, "both clients to come online") then
U.log("host", Host.state(), tostring(Host.error()),
"guest", Guest.state(), tostring(Guest.error()))
stopServer()
return finish()
end
check(Host.you() ~= nil and Guest.you() ~= nil,
"the real relay welcomes both v2 clients")
local room = Host.createRoom({ intent = "battle", profile = PROFILE,
playing = true, maxSpectators = 4 })
waitFor(function() return room.done end, 600, "room_create to answer")
check(room.id ~= nil, "the real relay creates a room: " .. tostring(room.error))
if not room.id then
stopServer()
return finish()
end
local joined = Guest.joinRoom(room.id, "player", YELLOW)
waitFor(function() return joined.done end, 600, "room_join to answer")
check(joined.error == nil,
"a yellow guest joins a red room: " .. tostring(joined.error))
waitFor(function()
return Host.room() and #Host.room().players == 2
end, 600, "the host to see two players")
package.loaded["src.online.Client"] = nil
local Watcher = require("src.online.Client")
Watcher.reset()
Watcher.configure({ relayAddress = "127.0.0.1:" .. PORT })
Watcher.connect({ name = "GREEN", profiles = { PROFILE } })
local function pump3(n)
for _ = 1, n or 1 do
Host.update(1 / 60)
Guest.update(1 / 60)
Watcher.update(1 / 60)
coroutine.yield()
end
end
for _ = 1, 600 do
if Watcher.state() == "online" then break end
pump3(1)
end
local badProfile = {}
for k, v in pairs(PROFILE) do badProfile[k] = v end
badProfile.fingerprint = "deadbeef"
local refused = Watcher.joinRoom(room.id, "spectator", badProfile)
for _ = 1, 600 do
if refused.done then break end
pump3(1)
end
check(refused.reason == "profile_mismatch",
"a spectator whose profile differs is refused: " ..
tostring(refused.reason))
local watching = Watcher.joinRoom(room.id, "spectator", PROFILE)
for _ = 1, 600 do
if watching.done then break end
pump3(1)
end
check(watching.error == nil, "a matching spectator is admitted: " ..
tostring(watching.error))
local function rejoin(profile)
Watcher.leaveRoom()
pump3(10)
local pending = profile and Watcher.joinRoom(room.id, "spectator", profile)
or Watcher.joinRoom(room.id, "spectator")
for _ = 1, 600 do
if pending.done then break end
pump3(1)
end
return pending
end
Watcher.setProfiles({ badProfile })
local stale = rejoin(nil)
check(stale.reason == "profile_mismatch",
"a join with no profile argument falls back to the stale snapshot " ..
"and is refused: " .. tostring(stale.reason))
local fresh = rejoin(PROFILE)
check(fresh.error == nil,
"the same session joins once the picked profile is passed: " ..
tostring(fresh.error))
Watcher.setProfiles({})
local blind = rejoin(nil)
check(blind.reason == "bad_profile",
"a join with no profile at all is refused as bad_profile: " ..
tostring(blind.reason))
Watcher.disconnect()
pump(5)
local hostStart, guestStart, hostEnd, guestEnd
Host.on("match_start", function(p) hostStart = p end)
Guest.on("match_start", function(p) guestStart = p end)
Host.on("match_end", function(p) hostEnd = p end)
Guest.on("match_end", function(p) guestEnd = p end)
local Input = require("src.core.Input")
local LinkBattle = require("src.link.LinkBattle")
local Pokemon = require("src.pokemon.Pokemon")
local Protocol = require("src.link.Protocol")
local SaveData = require("src.core.SaveData")
local function headless(name, species)
local save = SaveData.newGame()
save.player.name = name
save.party = { Pokemon.new(Data, species, 50) }
local stack = { list = {} }
function stack:push(state, ...)
table.insert(self.list, state)
if state.enter then state:enter(...) end
end
function stack:pop() return table.remove(self.list) end
function stack:top() return self.list[#self.list] end
function stack:update(dt)
local top = self:top()
if top and top.update then top:update(dt) end
end
return { data = Data, input = Input, stack = stack, save = save }
end
local gameH = headless("RED", "CHARIZARD")
local gameG = headless("BLUE", "BLASTOISE")
local packedH = Protocol.packParty(gameH.save.party)
local packedG = Protocol.packParty(gameG.save.party)
Host.ready(packedH, "dh")
Guest.ready(packedG, "dg")
waitFor(function() return hostStart ~= nil and guestStart ~= nil end, 600,
"match_start on both seats")
check(hostStart ~= nil and guestStart ~= nil, "the relay starts the match")
if not (hostStart and guestStart) then
stopServer()
return finish()
end
check(hostStart.role == "host" and guestStart.role == "guest",
"the relay assigns host and guest by join order")
check(type(hostStart.match) == "string" and hostStart.match == guestStart.match,
"both seats share one match token: " .. tostring(hostStart.match))
check(hostStart.seed == guestStart.seed and hostStart.seed ~= nil,
"both seats share one seed: " .. tostring(hostStart.seed))
check(hostStart.theirParty and hostStart.theirParty[1] and
hostStart.theirParty[1].species == "BLASTOISE",
"match_start carries the peer's party")
local battleH = LinkBattle.newHost(gameH, Host.roomSession(), {
myParty = packedH, theirParty = hostStart.theirParty,
theirName = hostStart.peerName, seed = hostStart.seed,
ruleset = hostStart.ruleset, keepNetOpen = true,
})
local battleG = LinkBattle.newGuest(gameG, Guest.roomSession(), {
myParty = packedG, theirParty = guestStart.theirParty,
theirName = guestStart.peerName, seed = guestStart.seed,
ruleset = guestStart.ruleset, keepNetOpen = true,
})
check(battleH ~= nil and battleG ~= nil,
"LinkBattle builds over a real relay room session")
if not (battleH and battleG) then
stopServer()
return finish()
end
local resH, resG
battleH.onFinish = function(r) resH = r end
battleG.onFinish = function(r) resG = r end
gameH.stack:push(battleH)
gameG.stack:push(battleG)
for _ = 1, 20000 do
if resH and resG then break end
Input.pressed = { a = true }
gameH.stack:update(1 / 60)
gameG.stack:update(1 / 60)
pump(1)
end
check(resH ~= nil and resG ~= nil,
("the battle finishes over the real relay (%s / %s)")
:format(tostring(resH), tostring(resG)))
check(battleH.player.mon.hp == battleG.enemy.mon.hp and
battleH.enemy.mon.hp == battleG.player.mon.hp,
"both simulations agree on HP across the relay")
check(Host.report(resH) ~= false, "the host reports its result")
check(Guest.report(resG) ~= false, "the guest reports its result")
waitFor(function() return hostEnd ~= nil and guestEnd ~= nil end, 900,
"room_result on both seats")
check(hostEnd ~= nil, "the relay answers room_report with room_result")
if hostEnd then
check(hostEnd.match == hostStart.match, "room_result names the match")
check(hostEnd.how == "agreed" or hostEnd.how == "reported",
"the relay resolved the reports: " .. tostring(hostEnd.how))
check(hostEnd.winnerId ~= nil, "room_result names a winner id")
check(hostEnd.youWon == (resH == "win"),
"youWon matches what the host's own simulation said")
check(guestEnd and guestEnd.winnerId == hostEnd.winnerId,
"both seats are told the same winner")
end
waitFor(function()
return Host.room() and Host.room().stage == "waiting"
end, 600, "the room to return to waiting")
check(Host.room() and Host.room().stage == "waiting",
"the room is ready for a rematch")
Host.leaveRoom()
Guest.leaveRoom()
pump(10)
Host.disconnect()
Guest.disconnect()
pump(5)
stopServer()
return finish()
end