diff --git a/src/import/OnlinePanel.lua b/src/import/OnlinePanel.lua index e90ae172..eb139605 100644 --- a/src/import/OnlinePanel.lua +++ b/src/import/OnlinePanel.lua @@ -1403,7 +1403,8 @@ function OnlinePanel.remoteConfirm(imp, yes) end function OnlinePanel.pumpRemoteTrade(imp) - local tr = OnlinePanel.state(imp).trade + local st = OnlinePanel.state(imp) + local tr = st.trade if not tr or not tr.remote then return end local ok, stage = pcall(function() return tr.remote:update() end) if not ok then @@ -1424,9 +1425,12 @@ function OnlinePanel.pumpRemoteTrade(imp) pcall(require("src.online.Trade").pruneBackups, tr.remote.handle.path, 3) tr.remoteResult = Strings("Trade complete.") - else - tr.remoteResult = tostring(result) + OnlinePanel.endRemoteTrade(imp) + OnlinePanel.home(imp) + st.status, st.statusOk = tr.remoteResult, true + return end + tr.remoteResult = tostring(result) OnlinePanel.endRemoteTrade(imp) elseif stage == "cancelled" then tr.remoteDone = true @@ -1679,6 +1683,8 @@ ensureHooks = function() OnlinePanel._pendingStart = payload end) client.on("match_end", function(msg) + local room = client.room() + if room and room.intent == "trade" then return end OnlinePanel.lastResult = OnlinePanel.resultText(msg) end) client.on("tour_match", function(payload) @@ -1924,10 +1930,14 @@ local function entered(imp, id) imp._pcPicker = nil st.status, st.statusOk = nil, false st.confirmLeave = nil - if id == "play" or id == "watch" then + if id == "play" or id == "watch" or id == "tournament" then c.dirty.lobby = true elseif id == "trade" then c.dirty.trade = true + local tr = st.trade + if tr and not tr.remote then + tr.status, tr.remoteResult, tr.remoteError = nil, nil, nil + end elseif id == "wizard" then c.dirty.slots, c.dirty.party = true, true end @@ -2064,7 +2074,16 @@ function OnlinePanel.wizardReady(imp) local id = OnlinePanel.wizardStep(imp) if id == "game" then return OnlinePanel.selectedVersion(imp) ~= nil end if id == "save" then return st.slotId ~= nil end - if id == "team" then return #(st.team or {}) > 0 end + if id == "team" then + local n = #(st.team or {}) + if n == 0 then return false end + local w, target = st.wizard, st.joinTarget + if w and w.kind == "join" and type(target) == "table" + and type(target.rule) == "table" and tonumber(target.rule.partySize) then + return n == OnlinePanel.teamCap(imp) + end + return true + end if id == "role" then if st.tradeRole == "join" then return #OnlinePanel.sanitizeCode(OnlinePanel.tradeState(imp).code or "") @@ -2236,11 +2255,30 @@ function OnlinePanel.startJoin(imp, code, rule, as, tournament) st.status, st.statusOk = Strings("Room codes are 6 characters."), false return false end + if type(rule) ~= "table" then + for _, entry in ipairs(Client().lobby() or {}) do + if entry.code == code then + rule = entry.profile and entry.profile.rule or nil + if entry.intent == "tournament" then tournament = true end + end + end + end st.joinTarget = { code = code, rule = rule, as = as or "player", tournament = tournament == true } return OnlinePanel.startWizard(imp, "join") end +function OnlinePanel.teamCap(imp) + local st = OnlinePanel.state(imp) + local w, target = st.wizard, st.joinTarget + if w and w.kind == "join" and type(target) == "table" + and type(target.rule) == "table" then + local n = tonumber(target.rule.partySize) + if n then return math.max(1, math.min(n, OnlinePanel.TEAM_MAX)) end + end + return OnlinePanel.TEAM_MAX +end + function OnlinePanel.startJoinTournament(imp, code, rule) return OnlinePanel.startJoin(imp, code, rule, "player", true) end @@ -2459,7 +2497,7 @@ local function newCache() dirty = { slots = true, carts = true, party = true, lobby = true, trade = true, summary = true }, slots = {}, carts = {}, party = {}, pc = {}, team = {}, - rooms = {}, watch = {}, mine = nil, + rooms = {}, watch = {}, tours = {}, mine = nil, tradeSlots = {}, tradeRows = { a = {}, b = {} }, tradePc = EMPTY, tradePcKey = nil, counts = { players = 0, lobbies = 0 }, @@ -2679,11 +2717,14 @@ local function refreshLobby(imp, c) c.lobbyKey = key local filter = OnlinePanel.filter(imp) local me = OnlinePanel.mySeatId() - c.rooms, c.watch = {}, {} + c.rooms, c.watch, c.tours = {}, {}, {} for _, entry in ipairs(open) do - if entry.id ~= me and entry.intent ~= "tournament" - and OnlinePanel.entryPasses(filter, entry) then - c.rooms[#c.rooms + 1] = roomRow(imp, entry, profile) + if entry.id ~= me and OnlinePanel.entryPasses(filter, entry) then + if entry.intent == "tournament" then + c.tours[#c.tours + 1] = roomRow(imp, entry, profile) + else + c.rooms[#c.rooms + 1] = roomRow(imp, entry, profile) + end end end for _, entry in ipairs(watch) do @@ -2839,10 +2880,14 @@ function OnlinePanel.update(imp, dt) OnlinePanel._joinError = nil end if st.pending and st.pending.done then - if st.pending.error then - st.status, st.statusOk = tostring(st.pending.error), false - end + local pending = st.pending st.pending = nil + if pending.error and pending.tourFallback + and pending.reason == "not_found" then + OnlinePanel.joinTournamentByCode(imp, pending.code, "spectator") + elseif pending.error then + st.status, st.statusOk = tostring(pending.error), false + end end local room = Client().room() local tour = Client().tournament() @@ -2903,6 +2948,20 @@ function OnlinePanel.autoReadyTrade(imp, room) return OnlinePanel.sendReady(imp) end +function OnlinePanel.spectateByCode(imp, code) + local st = OnlinePanel.state(imp) + code = OnlinePanel.sanitizeCode(code) + for _, entry in ipairs(Client().lobby() or {}) do + if entry.code == code and entry.intent == "tournament" then + return OnlinePanel.joinTournamentByCode(imp, code, "spectator") + end + end + if not OnlinePanel.joinByCode(imp, code, "spectator") then return false end + if st.pending then st.pending.tourFallback = true end + OnlinePanel.go(imp, "room") + return true +end + function OnlinePanel.joinByCode(imp, code, as) local st = OnlinePanel.state(imp) code = OnlinePanel.sanitizeCode(code) diff --git a/src/import/online/Tournaments.lua b/src/import/online/Tournaments.lua index f2a2571f..51e10d13 100644 --- a/src/import/online/Tournaments.lua +++ b/src/import/online/Tournaments.lua @@ -87,6 +87,8 @@ local function lobbyView(imp, x, y, w, m) local version = OnlinePanel.engineVersion(imp) local canPlay = online and profile ~= nil and OnlinePanel.canBattleWith(version) + local c = OnlinePanel.cache(imp) + local rowH = math.max(m.rowH, Kit.tapMin()) local cy = y LV().btn(imp, x, cy, w, btnH, "online-tour-host", @@ -117,8 +119,44 @@ local function lobbyView(imp, x, y, w, m) action = function() OnlinePanel.joinTournamentByCode(imp, st.joinCode, "spectator") end }) - cy = cy + btnH + tiny - return cy - y + cy = cy + btnH + gap + + local rows = c.tours or {} + cy = cy + Ui.label(Strings("Open tournaments"), x, cy) + tiny + if #rows == 0 then + Kit.emptyBox(x, cy, w, rowH, online + and Strings("No public tournaments right now.") + or Strings("Connect to see open tournaments.")) + return (cy + rowH + gap) - y + end + local actW = math.floor(64 * m.s) + for _, row in ipairs(rows) do + local ink = LV().rowHit(imp, x, cy, w, rowH, false, + "online-tour-row-" .. row.id, nil) + local tx = x + math.floor(10 * m.s) + local textW = w - 2 * actW - math.floor(30 * m.s) + Kit.text("small", Kit.ellipsize("small", row.name, textW), tx, + cy + math.floor(5 * m.s), ink or PAL.heading) + Kit.text("micro", Kit.ellipsize("micro", row.sub, textW), tx, + cy + rowH - Kit.textHeight("micro") - math.floor(5 * m.s), PAL.muted) + local code, rule = row.code, row.ruleTable + LV().btn(imp, x + w - 2 * actW - gap - math.floor(6 * m.s), + cy + (rowH - btnH) / 2, actW, btnH, "online-tour-join-" .. row.id, + Strings("Join"), + { kind = "accent", font = "small", + enabled = canPlay and row.stage == "waiting" and row.reason == nil, + action = function() + OnlinePanel.startJoinTournament(imp, code, rule) + end }) + LV().btn(imp, x + w - actW - math.floor(6 * m.s), cy + (rowH - btnH) / 2, + actW, btnH, "online-tour-watch-" .. row.id, Strings("Watch"), + { kind = "ghost", font = "small", enabled = online, + action = function() + OnlinePanel.joinTournamentByCode(imp, code, "spectator") + end }) + cy = cy + rowH + tiny + end + return (cy + gap) - y end function Tournaments.draw(imp, x, y, w, availH, m) diff --git a/src/import/online/TradeScreen.lua b/src/import/online/TradeScreen.lua index 7f6734b2..4d980f1a 100644 --- a/src/import/online/TradeScreen.lua +++ b/src/import/online/TradeScreen.lua @@ -163,7 +163,7 @@ local function remoteTrade(imp, x, y, w, m) end local refusal = OnlinePanel.remoteTradeRefusal(imp) - if refusal then + if refusal and st.slotId then cy = cy + Kit.textWrapped("small", refusal, x, cy, w, PAL.yellow, 3) + tiny end LV().btn(imp, x, cy, w, rowH, "online-trade-remote-start", @@ -172,7 +172,7 @@ local function remoteTrade(imp, x, y, w, m) action = function() OnlinePanel.startWizard(imp, "tradeRemote") end }) cy = cy + rowH + tiny - local note = tr.remoteError or tr.remoteResult or tr.status or st.status + local note = tr.remoteError or tr.remoteResult or tr.status if note then cy = cy + Kit.textWrapped("small", tostring(note), x, cy, w, tr.remoteError and PAL.red or PAL.muted, 3) + tiny diff --git a/src/import/online/Watch.lua b/src/import/online/Watch.lua index fcb97707..03eee9d4 100644 --- a/src/import/online/Watch.lua +++ b/src/import/online/Watch.lua @@ -32,11 +32,7 @@ function Watch.draw(imp, x, y, w, availH, m) "online-spectate-code", Strings("Spectate"), { kind = "primary", font = "small", enabled = online and #st.joinCode == OnlinePanel.CODE_LEN, - action = function() - if OnlinePanel.joinByCode(imp, st.joinCode, "spectator") then - OnlinePanel.go(imp, "room") - end - end }) + action = function() OnlinePanel.spectateByCode(imp, st.joinCode) end }) cy = cy + btnH + gap local rows = c.watch @@ -74,13 +70,7 @@ function Watch.draw(imp, x, y, w, availH, m) cy + (rowH - btnH) / 2, watchW, btnH, "online-watch-go-" .. row.id, tournament and Strings("Watch") or Strings("Spectate"), { kind = "accent", font = "small", enabled = online, - action = function() - if tournament then - OnlinePanel.joinTournamentByCode(imp, code, "spectator") - elseif OnlinePanel.joinByCode(imp, code, "spectator") then - OnlinePanel.go(imp, "room") - end - end }) + action = function() OnlinePanel.spectateByCode(imp, code) end }) cy = cy + rowH + tiny end if #rows > perPage then diff --git a/src/import/online/Wizard.lua b/src/import/online/Wizard.lua index 4513e4e2..40c42a4d 100644 --- a/src/import/online/Wizard.lua +++ b/src/import/online/Wizard.lua @@ -182,6 +182,14 @@ local function stepTeam(imp, x, y, w, m) x, cy, w, PAL.yellow, 2) + tiny end + local target = st.wizard and st.wizard.kind == "join" and st.joinTarget + if target and type(target.rule) == "table" then + local want, have = OnlinePanel.teamCap(imp), #(st.team or {}) + cy = cy + Kit.textWrapped("small", + Strings("Pick %d POKeMON (%d of %d picked) - %s", want, have, want, + OnlinePanel.ruleText(target.rule)), + x, cy, w, have == want and PAL.green or PAL.yellow, 2) + tiny + end cy = cy + Ui.label(Strings("Your party"), x, cy) + tiny if #c.party == 0 then Kit.emptyBox(x, cy, w, rowH, @@ -196,7 +204,7 @@ local function stepTeam(imp, x, y, w, m) cy = cy + Wizard.monRow(imp, row, x, cy, listW, m, function() st.focusMon = row.key if row.refused then return end - OnlinePanel.toggleTeam(st.team, ref, OnlinePanel.TEAM_MAX) + OnlinePanel.toggleTeam(st.team, ref, OnlinePanel.teamCap(imp)) st.ready = false OnlinePanel.invalidate(imp, "party", "summary") end) + tiny diff --git a/src/online/Trade.lua b/src/online/Trade.lua index 4047b058..008514b6 100644 --- a/src/online/Trade.lua +++ b/src/online/Trade.lua @@ -610,6 +610,13 @@ end function Remote:update() if self.link.update then self.link:update() end + local session = self.session + if (self.link.closed or self.link.paired == false) + and session.stage ~= "done" and session.stage ~= "cancelled" then + session.stage = "cancelled" + session.error = "the other trainer left" + return session.stage + end local messages = self.link:poll() or {} for _, msg in ipairs(messages) do if type(msg) == "table" and type(msg.type) == "string" then diff --git a/tests/online_panel.lua b/tests/online_panel.lua index 49570289..c8466d05 100644 --- a/tests/online_panel.lua +++ b/tests/online_panel.lua @@ -1699,4 +1699,134 @@ do Client.room, Client.leaveRoom, Client.you = savedRoom, savedLeave, savedYou2 end +do + local simp = { ready = { red = true }, activeSlot = {}, slots = {}, pulse = 0, + _pages = {}, _uiActions = {}, _actAt = {} } + local sst = OnlinePanel.state(simp) + sst.version, sst.ready = "red", true + local savedJoin, savedTour, savedLobby = + Client.joinRoom, Client.joinTournament, Client.lobby + local roomJoins, tourJoins = {}, {} + local pendingRoom + Client.joinRoom = function(code, as) + roomJoins[#roomJoins + 1] = { code = code, as = as } + pendingRoom = { code = code, done = false } + return pendingRoom + end + Client.joinTournament = function(code, as) + tourJoins[#tourJoins + 1] = { code = code, as = as } + return { code = code, done = false } + end + Client.lobby = function() + return { { id = "t1", code = "TQURA2", intent = "tournament" } } + end + T.check(OnlinePanel.spectateByCode(simp, "tqura2"), + "spectating a listed tournament code") + T.eq(#tourJoins, 1, "goes straight to tour_join") + T.eq(tourJoins[1] and tourJoins[1].as, "spectator", "as a spectator") + T.eq(#roomJoins, 0, "with no room_join") + + T.check(OnlinePanel.spectateByCode(simp, "RMBC23"), + "spectating an unlisted code") + T.eq(#roomJoins, 1, "tries the room first") + T.eq(OnlinePanel.screen(simp), "room", "on the Room screen") + pendingRoom.error, pendingRoom.reason, pendingRoom.done = + "That room code wasn't found.", "not_found", true + OnlinePanel.update(simp, 1 / 60) + T.eq(#tourJoins, 2, "and a not_found answer retries it as a tournament") + T.eq(tourJoins[2] and tourJoins[2].code, "RMBC23", "with the same code") + T.eq(sst.status, nil, "without surfacing the room miss as an error") + + T.check(OnlinePanel.spectateByCode(simp, "RMCD34"), "another unlisted code") + pendingRoom.error, pendingRoom.reason, pendingRoom.done = + "That room is full.", "full", true + OnlinePanel.update(simp, 1 / 60) + T.eq(#tourJoins, 2, "other join errors do not retry") + T.eq(sst.status, "That room is full.", "and are shown") + Client.joinRoom, Client.joinTournament, Client.lobby = + savedJoin, savedTour, savedLobby + OnlinePanel.home(simp) +end + +do + local jimp = { ready = { red = true }, activeSlot = {}, slots = {}, pulse = 0, + _pages = {}, _uiActions = {}, _actAt = {} } + local jst = OnlinePanel.state(jimp) + jst.version, jst.ready = "red", true + local savedLobby = Client.lobby + Client.lobby = function() + return { { id = "t1", code = "TQURA2", intent = "tournament", + profile = { rule = { partySize = 3 } } } } + end + T.check(OnlinePanel.startJoin(jimp, "tqura2"), "joining a listed code") + T.eq(jst.joinTarget.tournament, true, "learns it is a tournament") + T.eq(jst.joinTarget.rule.partySize, 3, "and picks up its rule") + T.eq(OnlinePanel.teamCap(jimp), 3, "so the team step caps at the rule") + local team = {} + for i = 1, 5 do + OnlinePanel.toggleTeam(team, { where = "party", index = i }, + OnlinePanel.teamCap(jimp)) + end + T.eq(#team, 3, "and refuses a fourth pick") + OnlinePanel.wizardTo(jimp, "team") + jst.team = { { where = "party", index = 1 } } + T.check(not OnlinePanel.wizardReady(jimp), + "the team step will not advance short of the count") + jst.team = team + T.check(OnlinePanel.wizardReady(jimp), "and advances at exactly the count") + OnlinePanel.home(jimp) + T.eq(OnlinePanel.teamCap(jimp), OnlinePanel.TEAM_MAX, + "outside the join wizard the cap is the full party") + Client.lobby = savedLobby +end + +do + local dimp = { ready = { red = true }, activeSlot = {}, slots = {}, pulse = 0, + _pages = {}, _uiActions = {}, _actAt = {} } + local dst = OnlinePanel.state(dimp) + dst.version, dst.ready = "red", true + local tr = OnlinePanel.tradeState(dimp) + local closed, left = 0, 0 + local savedLeave, savedRoom = Client.leaveRoom, Client.room + Client.leaveRoom = function() left = left + 1 return true end + Client.room = function() return nil end + tr.mode, tr.chosen = "remote", true + OnlinePanel.go(dimp, "trade") + tr.remote = { handle = { path = "x", version = "red", party = {} }, + session = {}, + update = function() return "done" end, + commit = function() return true end, + close = function() closed = closed + 1 end } + OnlinePanel.pumpRemoteTrade(dimp) + T.eq(tr.remote, nil, "a finished trade closes the remote session") + T.eq(closed, 1, "closing the link") + T.eq(left, 1, "and leaving the room") + T.eq(OnlinePanel.screen(dimp), "home", "then lands on Home") + T.eq(dst.status, "Trade complete.", "saying the trade completed") + T.eq(dst.statusOk, true, "as good news") + OnlinePanel.go(dimp, "trade") + T.eq(tr.remoteResult, nil, "reopening Trade forgets the old result") + T.eq(tr.status, nil, "and any stale trade status") + Client.leaveRoom, Client.room = savedLeave, savedRoom +end + +do + local Trade = require("src.online.Trade") + local link = { closed = false, paired = true, sent = {}, + send = function(self, m) self.sent[#self.sent + 1] = m end, + poll = function() return {} end, close = function() end } + local Protocol = require("src.link.Protocol") + local remote = setmetatable({ + handle = { generation = 1, party = {} }, link = link, + session = { stage = "waitPick", handle = function() return nil end }, + }, getmetatable(Trade.remote({ data = {}, party = {} }, link) or {})) + T.eq(remote:update(), "waitPick", "a live link keeps the trade going") + link.paired = false + T.eq(remote:update(), "cancelled", "the other trainer leaving calls it off") + T.eq(remote.session.error, "the other trainer left", "with a reason") + link.paired, link.closed = true, true + remote.session.stage = "done" + T.eq(remote:update(), "done", "but a finished trade stays finished") +end + T.finish("online panel")