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

568 lines
18 KiB
Lua

-- Scripted ONLINE tab states for the launcher screenshot harness. Armed once
-- by OnlinePanel from POKEPORT_ONLINE_SHOT.
--
-- POKEPORT_LAUNCHER_TAB=online POKEPORT_ONLINE_SHOT=play \
-- POKEPORT_WIN=1280x800 POKEPORT_LAUNCHER_SHOT=/tmp/play.png love .
--
-- States: home, play, play-full, play-hosting,
-- host-game, host-save, host-team, host-rules, host-visibility,
-- host-summary, pc-picker,
-- tour-game, tour-save, tour-playing, tour-team, tour-rules,
-- tour-shotclock, tour-spectators, tour-summary,
-- join-summary, join-team, trade-role, trade-summary,
-- room, room-result, room-trade, watch, tournament,
-- tournament-lobby, trade, trade-local, trade-pc, trade-remote,
-- trade-cross, trade-pc-picked, trade-pc-commit (writes both saves),
-- trade-modal, trade-modal-done, trade-commit (writes both saves).
local function Client()
return require("src.online.Client")
end
local PROFILE = {
engine = 1, version = "red", engineVersion = "1.2.3", apiVersion = 4,
fingerprint = "shot", rulesetId = "gen1_faithful", kind = "vanilla",
rule = { partySize = 3 },
}
local NAMES = { "RED", "BLUE", "GREEN", "LEAF", "ETHAN", "LYRA", "SILVER",
"KRIS", "GOLD", "MAY", "BRENDAN", "WALLY" }
local function fakeLobby(count, myProfile)
local rows = {}
for i = 1, count do
local profile = {}
for key, value in pairs(myProfile or PROFILE) do profile[key] = value end
if i % 5 == 0 then profile.fingerprint = "other" end
if i % 7 == 0 then
profile.kind = "cart"
profile.cart = { id = "kanto", version = "1.0.0", hash = "abc" }
end
local tour = i % 11 == 0
rows[i] = {
id = "e" .. i,
open = true,
spectators = i % 4,
room = not tour and ("r%016x"):format(i) or nil,
tour = tour and ("t%016x"):format(i) or nil,
locked = not tour and i % 8 == 0 or nil,
name = NAMES[((i - 1) % #NAMES) + 1] .. "#" .. (100 + i),
verified = i % 3 == 0,
intent = tour and "tournament" or "battle",
note = (i % 4 == 0) and "first to three" or nil,
profile = profile,
stage = (i % 6 == 0) and "battling" or "waiting",
}
end
return rows
end
local function goOnline(OnlinePanel, imp, count)
local client = Client()
client.state = function() return "online" end
client.you = function()
return { id = "me", name = "RED#417", verified = true }
end
client.serverTime = function() return 1000 end
local cached, cachedKey
local function lobby()
local mine = OnlinePanel.myProfile(imp)
local key = tostring(mine and mine.fingerprint)
if key ~= cachedKey then
cachedKey = key
cached = fakeLobby(count, mine or PROFILE)
end
return cached
end
client.lobby = lobby
client.openRooms = function()
local out = {}
for _, entry in ipairs(lobby()) do
if entry.open and entry.intent ~= "tournament" then
out[#out + 1] = entry
end
end
return out
end
client.watchable = function()
local out = {}
for _, entry in ipairs(lobby()) do
if entry.stage == "battling" or entry.intent == "tournament" then
out[#out + 1] = entry
end
end
return out
end
client.counts = function()
return { players = count + 3, openRooms = #client.openRooms() }
end
return client
end
local ROOM = {
room = ("r%016x"):format(0xab2cd3), host = "me", stage = "waiting", intent = "battle",
profile = PROFILE,
players = {
{ id = "me", name = "RED#417", verified = true, ready = false },
{ id = "p2", name = "BLUE#221", ready = true },
},
spectators = { { id = "s1", name = "GREEN#009" } },
deadlines = { ready = 61000 },
}
local TOUR = {
tour = ("t%016x"):format(0x234), code = "TRN234", creator = "me", stage = "running", round = 2, shotClock = 6,
players = {
{ id = "me", name = "RED#417", verified = true, online = true },
{ id = "p2", name = "BLUE#221", online = true },
{ id = "p3", name = "GREEN#009", online = false, eliminated = true },
{ id = "p4", name = "LEAF#712", online = true },
},
spectators = { { id = "s1", name = "WATCH" } },
profile = PROFILE, rule = { partySize = 3 },
bracket = {
{ round = 1, matches = {
{ match = "m1", a = "me", b = "p3", winner = "me", how = "agreed",
state = "done" },
{ match = "m2", a = "p2", b = "p4", winner = "p2", how = "reported",
state = "done" } } },
{ round = 2, matches = {
{ match = "m3", a = "me", b = "p2", state = "live" } } },
},
live = "m3",
deadlines = { shot = 7000 },
}
-- ------------------------------------------------------------------ trade
local function peerParty(entry)
if not entry then return {} end
local TeamPick = require("src.online.TeamPick")
local ok, slot = pcall(TeamPick.readSlot, entry.version, entry.slotId,
entry.cartId)
if not ok or type(slot) ~= "table" then return {} end
return slot.party or {}
end
local function scriptedLink(party)
local Protocol = require("src.link.Protocol")
local inbox = {}
return {
seat = function() return 0 end,
send = function(_, msg)
if type(msg) ~= "table" then return end
if msg.type == "party" then
inbox[#inbox + 1] = { type = "party",
mons = Protocol.packParty(party) }
elseif msg.type == "pick" then
inbox[#inbox + 1] = { type = "pick", index = 1 }
elseif msg.type == "confirm" then
inbox[#inbox + 1] = { type = "confirm", ok = msg.ok }
end
end,
poll = function()
local out = inbox
inbox = {}
return out
end,
close = function() end,
}
end
local function remoteShot(OnlinePanel, imp, rows)
local st = OnlinePanel.state(imp)
local tr = OnlinePanel.tradeState(imp)
local entry = rows[1]
if not entry then return end
tr.mode, tr.chosen = "remote", true
st.version, st.slotId, st.cartId = entry.version, entry.slotId, entry.cartId
local Trade = require("src.online.Trade")
local handle = Trade.openSlot(entry.version, entry.slotId, entry.cartId)
if not handle then return end
local remote = Trade.remote(handle, scriptedLink(peerParty(rows[2] or rows[1])),
{ peerName = "BLUE" })
if not remote then return end
tr.remote, tr.peerName = remote, "BLUE"
remote:start()
remote:update()
remote:pick(1)
remote:update()
end
local function tradeablePair(OnlinePanel, imp, pickA, pickB)
local tr = OnlinePanel.tradeState(imp)
local a = OnlinePanel.tradeSideView(imp, "a")
local b = OnlinePanel.tradeSideView(imp, "b")
local partyA = (a and a.handle and a.handle.party) or {}
local partyB = (b and b.handle and b.handle.party) or {}
for i = 1, #partyA do
for j = 1, #partyB do
tr.picks.a = pickA or i
tr.picks.b = pickB or j
tr.plan, tr.lines, tr.convertLines = nil, nil, nil
if OnlinePanel.tradeModalPreview(imp) then return true end
if pickA and pickB then return false end
end
end
return false
end
local function envPick(name)
local raw = os.getenv(name)
if not raw or raw == "" then return nil end
local box, index = raw:match("^box:(%d+):(%d+)$")
if box then
return { where = "box", box = tonumber(box), index = tonumber(index) }
end
return tonumber(raw)
end
local function localShot(OnlinePanel, imp, want, rows)
local tr = OnlinePanel.tradeState(imp)
tr.mode, tr.chosen = "local", true
local second = rows[2]
local wantCross = want == "trade-cross" or want:sub(1, 11) == "trade-modal"
or want == "trade-commit"
if wantCross and rows[1] then
for _, row in ipairs(rows) do
if row.generation ~= rows[1].generation then
second = row
break
end
end
end
OnlinePanel.tradeSetSide(imp, "a", rows[1])
OnlinePanel.tradeSetSide(imp, "b", second)
local pickA = envPick("POKEPORT_ONLINE_PICK_A")
local pickB = envPick("POKEPORT_ONLINE_PICK_B")
OnlinePanel.tradePick(imp, "a", pickA or 1)
OnlinePanel.tradePick(imp, "b", pickB or 1)
if want ~= "trade-modal" and want ~= "trade-modal-done"
and want ~= "trade-commit" then
OnlinePanel.tradePreview(imp)
return
end
if not tradeablePair(OnlinePanel, imp, pickA, pickB) then
print("[shot] no pair this cache can trade: " ..
tostring(OnlinePanel.tradeState(imp).status))
return
end
if want == "trade-commit" then
OnlinePanel.tradeModalConfirm(imp)
return
end
if want ~= "trade-modal-done" then return end
local mo = OnlinePanel.tradeModal(imp)
if not mo then return end
mo.view, mo.ok, mo.message = "result", true, "Traded."
mo.resultLines = OnlinePanel.tradeResultLines(
OnlinePanel.tradeState(imp).plan, mo.labels)
end
-- ----------------------------------------------------------------- states
local function firstSlot(OnlinePanel, imp)
local st = OnlinePanel.state(imp)
local version = OnlinePanel.selectedVersion(imp)
for _, row in ipairs(OnlinePanel.slotsIn(imp, version, nil)) do
if row.exists then
st.slotId = row.id
return row.id
end
end
return nil
end
local STATES = {}
local function ready(OnlinePanel, imp, picks)
local st = OnlinePanel.state(imp)
firstSlot(OnlinePanel, imp)
st.setupDone = true
st.team = {}
local pick = OnlinePanel.readTeamSlot(imp)
if pick then
require("src.online.OnlineSprites").prime(
OnlinePanel.selectedVersion(imp), pick.party)
for index = 1, math.min(picks or 2, #pick.party) do
OnlinePanel.toggleTeam(st.team, index)
end
st.focusMon = OnlinePanel.refKey(st.team[1])
end
OnlinePanel.invalidate(imp)
return st
end
function STATES.home(OnlinePanel, imp)
goOnline(OnlinePanel, imp, 6)
OnlinePanel.home(imp)
end
function STATES.play(OnlinePanel, imp)
goOnline(OnlinePanel, imp, 6)
ready(OnlinePanel, imp, 3)
OnlinePanel.state(imp).note = "first to three"
OnlinePanel.home(imp)
OnlinePanel.go(imp, "play")
end
STATES["play-full"] = function(OnlinePanel, imp)
goOnline(OnlinePanel, imp, 50)
ready(OnlinePanel, imp, 3)
OnlinePanel.home(imp)
OnlinePanel.go(imp, "play")
end
STATES["play-hosting"] = function(OnlinePanel, imp)
local client = goOnline(OnlinePanel, imp, 6)
client.room = function() return ROOM end
ready(OnlinePanel, imp, 3)
OnlinePanel.home(imp)
OnlinePanel.go(imp, "play")
OnlinePanel.refresh(imp)
end
local function wizardAt(OnlinePanel, imp, kind, step, picks)
goOnline(OnlinePanel, imp, 4)
ready(OnlinePanel, imp, picks or 2)
OnlinePanel.home(imp)
OnlinePanel.go(imp, "play")
OnlinePanel.startWizard(imp, kind)
OnlinePanel.wizardTo(imp, step)
OnlinePanel.refresh(imp)
end
local HOST_STEPS = { "game", "save", "team", "rules", "visibility", "summary" }
for _, step in ipairs(HOST_STEPS) do
STATES["host-" .. step] = function(O, imp)
wizardAt(O, imp, "hostBattle", step)
end
end
local TOUR_STEPS = { "game", "save", "playing", "team", "rules", "shotclock",
"spectators", "summary" }
for _, step in ipairs(TOUR_STEPS) do
STATES["tour-" .. step] = function(O, imp)
wizardAt(O, imp, "hostTournament", step)
end
end
STATES["join-summary"] = function(OnlinePanel, imp)
goOnline(OnlinePanel, imp, 4)
ready(OnlinePanel, imp, 3)
OnlinePanel.home(imp)
OnlinePanel.go(imp, "play")
OnlinePanel.startJoin(imp, { room = ROOM.room }, { partySize = 3 })
OnlinePanel.refresh(imp)
end
STATES["join-team"] = function(OnlinePanel, imp)
STATES["join-summary"](OnlinePanel, imp)
OnlinePanel.wizardTo(imp, "team")
OnlinePanel.refresh(imp)
end
STATES["trade-role"] = function(OnlinePanel, imp)
goOnline(OnlinePanel, imp, 4)
ready(OnlinePanel, imp, 1)
OnlinePanel.home(imp)
OnlinePanel.go(imp, "trade")
OnlinePanel.tradeState(imp).chosen = true
OnlinePanel.tradeMode(imp, "remote")
OnlinePanel.startWizard(imp, "tradeRemote")
OnlinePanel.wizardTo(imp, "role")
OnlinePanel.refresh(imp)
end
STATES["trade-summary"] = function(OnlinePanel, imp)
STATES["trade-role"](OnlinePanel, imp)
OnlinePanel.wizardTo(imp, "summary")
OnlinePanel.refresh(imp)
end
STATES["pc-picker"] = function(OnlinePanel, imp)
wizardAt(OnlinePanel, imp, "hostBattle", "team")
OnlinePanel.pcOpen(imp)
OnlinePanel.update(imp, 1 / 60)
OnlinePanel.refresh(imp)
end
function STATES.room(OnlinePanel, imp)
local client = goOnline(OnlinePanel, imp, 4)
client.room = function() return ROOM end
ready(OnlinePanel, imp, 3)
OnlinePanel.home(imp)
OnlinePanel.go(imp, "play")
OnlinePanel.go(imp, "room")
end
STATES["room-result"] = function(OnlinePanel, imp)
STATES.room(OnlinePanel, imp)
OnlinePanel.lastResult = "win"
end
STATES["room-refused"] = function(OnlinePanel, imp)
local client = goOnline(OnlinePanel, imp, 4)
client.room = function() return nil end
ready(OnlinePanel, imp, 3)
OnlinePanel.home(imp)
OnlinePanel.go(imp, "play")
OnlinePanel.go(imp, "room")
local st = OnlinePanel.state(imp)
st.status, st.statusOk = "Your game doesn't match the room. " ..
"(game version differs: the room has crystal, you have red)", false
end
STATES["room-trade"] = function(OnlinePanel, imp)
local client = goOnline(OnlinePanel, imp, 4)
client.room = function()
return { room = ("r%016x"):format(0x7292), host = "me", stage = "waiting",
intent = "trade", profile = PROFILE,
players = { { id = "me", name = "RED#417", verified = true } },
spectators = {} }
end
ready(OnlinePanel, imp, 1)
OnlinePanel.home(imp)
OnlinePanel.go(imp, "trade")
OnlinePanel.go(imp, "room")
end
function STATES.watch(OnlinePanel, imp)
goOnline(OnlinePanel, imp, 14)
OnlinePanel.home(imp)
OnlinePanel.go(imp, "watch")
end
function STATES.tournament(OnlinePanel, imp)
local client = goOnline(OnlinePanel, imp, 4)
client.tournament = function() return TOUR end
OnlinePanel.home(imp)
OnlinePanel.go(imp, "tournament")
end
STATES["tournament-lobby"] = function(OnlinePanel, imp)
goOnline(OnlinePanel, imp, 4)
OnlinePanel.home(imp)
OnlinePanel.go(imp, "tournament")
end
STATES["trade-pc"] = function(OnlinePanel, imp)
OnlinePanel.home(imp)
OnlinePanel.go(imp, "trade")
local tr = OnlinePanel.tradeState(imp)
tr.mode, tr.chosen = "local", true
local rows = OnlinePanel.tradeSlots(imp)
OnlinePanel.tradeSetSide(imp, "a", rows[1])
OnlinePanel.tradeSetSide(imp, "b", rows[2])
local view = OnlinePanel.tradeSideView(imp, "a")
local handle = view and view.handle or nil
if not handle then return end
if #OnlinePanel.tradeBoxRows(imp, "a") == 0 and handle.party[1] then
handle.save.boxes = type(handle.save.boxes) == "table"
and handle.save.boxes or {}
handle.save.boxes[2] = { handle.party[1] }
handle.boxes = handle.save.boxes
end
for _, side in ipairs({ "a", "b" }) do
if OnlinePanel.pcOpen(imp, { side = side }) then
OnlinePanel.refresh(imp)
local row = OnlinePanel.pcRows(imp)[1]
if row then OnlinePanel.pcPick(imp, row) else OnlinePanel.pcClose(imp) end
end
end
OnlinePanel.pcOpen(imp, { side = "a" })
OnlinePanel.update(imp, 1 / 60)
OnlinePanel.refresh(imp)
end
STATES["trade-pc-picked"] = function(OnlinePanel, imp)
STATES["trade-pc"](OnlinePanel, imp)
OnlinePanel.pcClose(imp)
OnlinePanel.refresh(imp)
end
STATES["trade-pc-commit"] = function(OnlinePanel, imp)
OnlinePanel.home(imp)
OnlinePanel.go(imp, "trade")
local tr = OnlinePanel.tradeState(imp)
tr.mode, tr.chosen = "local", true
local rows = OnlinePanel.tradeSlots(imp)
local a, b = rows[1], nil
for _, row in ipairs(rows) do
if a and row.generation == a.generation
and not OnlinePanel.sameSlot(row, a) then
b = row
break
end
end
if not (a and b) then
print("[shot] no same-generation pair on this machine")
return
end
OnlinePanel.tradeSetSide(imp, "a", a)
OnlinePanel.tradeSetSide(imp, "b", b)
if not OnlinePanel.pcOpen(imp, { side = "a" }) then
print("[shot] the PC popup would not open")
return
end
OnlinePanel.refresh(imp)
local want = envPick("POKEPORT_ONLINE_PICK_A")
local picked
for _, row in ipairs(OnlinePanel.pcRows(imp)) do
if not want or (row.ref.box == want.box and row.ref.index == want.index) then
picked = row
break
end
end
if not picked then
print("[shot] nothing in that save's PC")
OnlinePanel.pcClose(imp)
return
end
print("[shot] from the PC: " .. tostring(picked.label) .. " (" ..
tostring(picked.source) .. ")")
OnlinePanel.pcPick(imp, picked)
OnlinePanel.tradePick(imp, "b", envPick("POKEPORT_ONLINE_PICK_B") or 1)
OnlinePanel.refresh(imp)
if not OnlinePanel.tradeModalPreview(imp) then
print("[shot] preview refused: " .. tostring(tr.status))
return
end
OnlinePanel.tradeModalConfirm(imp)
print("[shot] trade-pc-commit: " .. tostring(tr.status))
end
function STATES.trade(OnlinePanel, imp)
OnlinePanel.home(imp)
OnlinePanel.go(imp, "trade")
OnlinePanel.tradeState(imp).chosen = false
end
local function armDemo(module, OnlinePanel, imp)
local ok, err = xpcall(function()
return require(module)(OnlinePanel, imp)
end, debug.traceback)
if not ok then print("[demo] ARM ERROR " .. tostring(err)) end
end
STATES["demo-host"] = function(OnlinePanel, imp)
return armDemo("tests.drivers.online_demo_host", OnlinePanel, imp)
end
STATES["demo-guest"] = function(OnlinePanel, imp)
return armDemo("tests.drivers.online_demo_guest", OnlinePanel, imp)
end
return function(OnlinePanel, imp, want)
if os.getenv("POKEPORT_REDUCE_MOTION") ~= "0" then
require("src.ui.kit.Transition").reduceMotion = true
end
local run = STATES[want]
if run then return run(OnlinePanel, imp) end
OnlinePanel.home(imp)
OnlinePanel.go(imp, "trade")
local rows = OnlinePanel.tradeSlots(imp)
if want == "trade-remote" then return remoteShot(OnlinePanel, imp, rows) end
return localShot(OnlinePanel, imp, want, rows)
end