mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-20 12:40:21 +02:00
Route link play through multiplayer sessions
This commit is contained in:
+88
-82
@@ -10,6 +10,7 @@ local Net = require("src.link.Net")
|
|||||||
local Protocol = require("src.link.Protocol")
|
local Protocol = require("src.link.Protocol")
|
||||||
local Runtime = require("src.mods.Runtime")
|
local Runtime = require("src.mods.Runtime")
|
||||||
local Screens = require("src.ui.Screens")
|
local Screens = require("src.ui.Screens")
|
||||||
|
local Session = require("src.link.Session")
|
||||||
local TextBox = require("src.render.TextBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
local Strings = require("src.core.Strings")
|
local Strings = require("src.core.Strings")
|
||||||
|
|
||||||
@@ -44,10 +45,8 @@ local function forceLevelLabel(v)
|
|||||||
return (v == ANY or v == nil) and "ANY" or ("AUTO " .. tostring(v))
|
return (v == ANY or v == nil) and "ANY" or ("AUTO " .. tostring(v))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- stages before any Net object is meaningfully "this session's link" --
|
-- stages before a successful transport has become this link's Session;
|
||||||
-- .net can still be a leftover failed attempt sitting on self, so error/
|
-- terminal checks skip them rather than keying off self.net's presence
|
||||||
-- closed checks below skip these rather than keying off self.net's
|
|
||||||
-- presence alone
|
|
||||||
local PRE_CONNECT_STAGES = { menu = true, lanMenu = true, onlineMenu = true }
|
local PRE_CONNECT_STAGES = { menu = true, lanMenu = true, onlineMenu = true }
|
||||||
|
|
||||||
-- how long the host waits for a v2 hello before deciding the peer predates
|
-- how long the host waits for a v2 hello before deciding the peer predates
|
||||||
@@ -70,6 +69,16 @@ local function ipDigits(ip)
|
|||||||
return digits
|
return digits
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function openSession(role, connect)
|
||||||
|
local transport = Net.new()
|
||||||
|
if connect(transport) then
|
||||||
|
return Session.new(transport, { role = role, kind = "link" })
|
||||||
|
end
|
||||||
|
local detail = transport.error or "?"
|
||||||
|
transport:close()
|
||||||
|
return nil, detail
|
||||||
|
end
|
||||||
|
|
||||||
function LinkState.new(game)
|
function LinkState.new(game)
|
||||||
local self = setmetatable({}, LinkState)
|
local self = setmetatable({}, LinkState)
|
||||||
self.game = game
|
self.game = game
|
||||||
@@ -89,12 +98,15 @@ end
|
|||||||
-- "connecting with this code", same as if the player had typed it in
|
-- "connecting with this code", same as if the player had typed it in
|
||||||
function LinkState.newJoinOnline(game, code)
|
function LinkState.newJoinOnline(game, code)
|
||||||
local self = LinkState.new(game)
|
local self = LinkState.new(game)
|
||||||
self.net = Net.new()
|
local session, detail = openSession("guest", function(transport)
|
||||||
if self.net:joinOnline(nil, code) then
|
return transport:joinOnline(nil, code)
|
||||||
|
end)
|
||||||
|
if session then
|
||||||
|
self.net = session
|
||||||
self.stage = "onlineJoining"
|
self.stage = "onlineJoining"
|
||||||
else
|
else
|
||||||
self.stage = "menu" -- exitWith below needs a real stage to unwind from
|
self.stage = "menu" -- exitWith below needs a real stage to unwind from
|
||||||
self:exitWith(Strings("Link error:\n%s", self.net.error or "?"))
|
self:exitWith(Strings("Link error:\n%s", detail))
|
||||||
end
|
end
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
@@ -163,21 +175,13 @@ end
|
|||||||
-- take the peer's hello out of the inbox without eating anything that
|
-- take the peer's hello out of the inbox without eating anything that
|
||||||
-- shares the batch with it
|
-- shares the batch with it
|
||||||
function LinkState:pollHello()
|
function LinkState:pollHello()
|
||||||
local msgs = self.net:poll()
|
local message
|
||||||
local keep, got = {}, false
|
if not self.peerHello then message = self.net:take("hello") end
|
||||||
for _, msg in ipairs(msgs) do
|
if message then
|
||||||
if msg.type == "hello" and not self.peerHello then
|
self.peerHello = message
|
||||||
self.peerHello = msg
|
self.peerName = message.name
|
||||||
self.peerName = msg.name
|
|
||||||
got = true
|
|
||||||
else
|
|
||||||
keep[#keep + 1] = msg
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
for i = #keep, 1, -1 do
|
return message ~= nil, self.net:hasPending()
|
||||||
table.insert(self.net.inbox, 1, keep[i])
|
|
||||||
end
|
|
||||||
return got, #keep > 0
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function LinkState:sendHello(mode)
|
function LinkState:sendHello(mode)
|
||||||
@@ -220,13 +224,15 @@ function LinkState:update(dt)
|
|||||||
local input = self.game.input
|
local input = self.game.input
|
||||||
if self.net then
|
if self.net then
|
||||||
self.net:update()
|
self.net:update()
|
||||||
if self.net.error and not PRE_CONNECT_STAGES[self.stage] then
|
local status = self.net:getStatus()
|
||||||
self:exitWith(Strings("Link error:\n%s", self.net.error:sub(1, 60)))
|
if status == "failed" and not PRE_CONNECT_STAGES[self.stage] then
|
||||||
|
self:exitWith(Strings("Link error:\n%s",
|
||||||
|
(self.net.error or "?"):sub(1, 60)))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
-- the peer vanished without a bye (only once the inbox is drained,
|
-- the peer vanished without a bye (only once the session FIFO drains,
|
||||||
-- so a final message travelling with the disconnect still counts)
|
-- so a final message travelling with the disconnect still counts)
|
||||||
if self.net.closed and #self.net.inbox == 0
|
if status == "closed"
|
||||||
and not PRE_CONNECT_STAGES[self.stage] and self.stage ~= "addrEntry"
|
and not PRE_CONNECT_STAGES[self.stage] and self.stage ~= "addrEntry"
|
||||||
and self.stage ~= "codeEntry" and self.stage ~= "notice"
|
and self.stage ~= "codeEntry" and self.stage ~= "notice"
|
||||||
and self.stage ~= "battleRunning" then
|
and self.stage ~= "battleRunning" then
|
||||||
@@ -269,12 +275,15 @@ function LinkState:update(dt)
|
|||||||
self.stage = "menu"
|
self.stage = "menu"
|
||||||
self.index = 1
|
self.index = 1
|
||||||
elseif input:wasPressed("a") then
|
elseif input:wasPressed("a") then
|
||||||
self.net = Net.new()
|
|
||||||
if self.index == 1 then
|
if self.index == 1 then
|
||||||
if self.net:host() then
|
local session, detail = openSession("host", function(transport)
|
||||||
|
return transport:host()
|
||||||
|
end)
|
||||||
|
if session then
|
||||||
|
self.net = session
|
||||||
self.stage = "hosting"
|
self.stage = "hosting"
|
||||||
else
|
else
|
||||||
self:exitWith(Strings("Link error:\n%s", self.net.error or "?"))
|
self:exitWith(Strings("Link error:\n%s", detail))
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
self.stage = "addrEntry"
|
self.stage = "addrEntry"
|
||||||
@@ -289,11 +298,14 @@ function LinkState:update(dt)
|
|||||||
self.index = 2
|
self.index = 2
|
||||||
elseif input:wasPressed("a") then
|
elseif input:wasPressed("a") then
|
||||||
if self.index == 1 then
|
if self.index == 1 then
|
||||||
self.net = Net.new()
|
local session, detail = openSession("host", function(transport)
|
||||||
if self.net:hostOnline() then
|
return transport:hostOnline()
|
||||||
|
end)
|
||||||
|
if session then
|
||||||
|
self.net = session
|
||||||
self.stage = "onlineHosting"
|
self.stage = "onlineHosting"
|
||||||
else
|
else
|
||||||
self:exitWith(Strings("Link error:\n%s", self.net.error or "?"))
|
self:exitWith(Strings("Link error:\n%s", detail))
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
self.stage = "codeEntry"
|
self.stage = "codeEntry"
|
||||||
@@ -327,11 +339,14 @@ function LinkState:update(dt)
|
|||||||
CodeEntry.right(self.codeEntry)
|
CodeEntry.right(self.codeEntry)
|
||||||
elseif input:wasPressed("a") then
|
elseif input:wasPressed("a") then
|
||||||
local code = CodeEntry.text(self.codeEntry)
|
local code = CodeEntry.text(self.codeEntry)
|
||||||
self.net = Net.new()
|
local session, detail = openSession("guest", function(transport)
|
||||||
if self.net:joinOnline(nil, code) then
|
return transport:joinOnline(nil, code)
|
||||||
|
end)
|
||||||
|
if session then
|
||||||
|
self.net = session
|
||||||
self.stage = "onlineJoining"
|
self.stage = "onlineJoining"
|
||||||
else
|
else
|
||||||
self:exitWith(Strings("Link error:\n%s", self.net.error or "?"))
|
self:exitWith(Strings("Link error:\n%s", detail))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -367,10 +382,15 @@ function LinkState:update(dt)
|
|||||||
+ self.addr[base + 2] * 10
|
+ self.addr[base + 2] * 10
|
||||||
+ self.addr[base + 3])
|
+ self.addr[base + 3])
|
||||||
end
|
end
|
||||||
if self.net:join(table.concat(octets, ".")) then
|
local address = table.concat(octets, ".")
|
||||||
|
local session, detail = openSession("guest", function(transport)
|
||||||
|
return transport:join(address)
|
||||||
|
end)
|
||||||
|
if session then
|
||||||
|
self.net = session
|
||||||
self.stage = "joining"
|
self.stage = "joining"
|
||||||
else
|
else
|
||||||
self:exitWith(Strings("Link error:\n%s", self.net.error or "?"))
|
self:exitWith(Strings("Link error:\n%s", detail))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -428,19 +448,11 @@ function LinkState:update(dt)
|
|||||||
|
|
||||||
elseif self.stage == "waitMode" then -- guest waits for host's pick
|
elseif self.stage == "waitMode" then -- guest waits for host's pick
|
||||||
if input:wasPressed("b") then self:exitWith(nil) return end
|
if input:wasPressed("b") then self:exitWith(nil) return end
|
||||||
local msgs = self.net:poll()
|
local message = self.net:take("hello")
|
||||||
for i, msg in ipairs(msgs) do
|
if message then
|
||||||
if msg.type == "hello" then
|
self.peerHello = message
|
||||||
self.peerHello = msg
|
self.peerName = message.name
|
||||||
self.peerName = msg.name
|
self:decideCompat(message.mode, false)
|
||||||
-- the host's next messages (party, ...) can share this batch;
|
|
||||||
-- put them back so the new stage's poll sees them
|
|
||||||
for j = #msgs, i + 1, -1 do
|
|
||||||
table.insert(self.net.inbox, 1, msgs[j])
|
|
||||||
end
|
|
||||||
self:decideCompat(msg.mode, false)
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
elseif self.stage == "notice" then
|
elseif self.stage == "notice" then
|
||||||
@@ -456,40 +468,34 @@ function LinkState:update(dt)
|
|||||||
|
|
||||||
elseif self.stage == "battleWait" then
|
elseif self.stage == "battleWait" then
|
||||||
if input:wasPressed("b") then self:exitWith(nil) return end
|
if input:wasPressed("b") then self:exitWith(nil) return end
|
||||||
local msgs = self.net:poll()
|
local message = self.net:take("party")
|
||||||
for i, msg in ipairs(msgs) do
|
if message then
|
||||||
if msg.type == "party" then
|
-- the host owns this rule (same as mode); the guest only learns
|
||||||
-- the host owns this rule (same as mode); the guest only learns
|
-- it here, off the host's own party message
|
||||||
-- it here, off the host's own party message
|
if not self.isHost then self.forceLevel = message.forceLevel end
|
||||||
if not self.isHost then self.forceLevel = msg.forceLevel end
|
local LinkBattle = require("src.link.LinkBattle")
|
||||||
local LinkBattle = require("src.link.LinkBattle")
|
local opts = {
|
||||||
local opts = {
|
myParty = Protocol.packParty(self.game.save.party),
|
||||||
myParty = Protocol.packParty(self.game.save.party),
|
theirParty = message.mons,
|
||||||
theirParty = msg.mons,
|
theirName = self.peerName or "FOE",
|
||||||
theirName = self.peerName or "FOE",
|
seed = self.isHost and self.linkSeed or message.seed,
|
||||||
seed = self.isHost and self.linkSeed or msg.seed,
|
verdict = self.verdict,
|
||||||
verdict = self.verdict,
|
strict = Handshake.strict(self.verdict),
|
||||||
strict = Handshake.strict(self.verdict),
|
forceLevel = self.forceLevel,
|
||||||
forceLevel = self.forceLevel,
|
}
|
||||||
}
|
local battle, why
|
||||||
local battle, why
|
if self.isHost then
|
||||||
if self.isHost then
|
battle, why = LinkBattle.newHost(self.game, self.net, opts)
|
||||||
battle, why = LinkBattle.newHost(self.game, self.net, opts)
|
else
|
||||||
else
|
battle, why = LinkBattle.newGuest(self.game, self.net, opts)
|
||||||
battle, why = LinkBattle.newGuest(self.game, self.net, opts)
|
|
||||||
end
|
|
||||||
if not battle then
|
|
||||||
self.net:send({ type = "bye" })
|
|
||||||
self:exitWith(why or Strings("Link battle\ncan't start."), "error")
|
|
||||||
return
|
|
||||||
end
|
|
||||||
self.game.stack:push(battle)
|
|
||||||
self.stage = "battleRunning"
|
|
||||||
for j = #msgs, i + 1, -1 do
|
|
||||||
table.insert(self.net.inbox, 1, msgs[j])
|
|
||||||
end
|
|
||||||
break
|
|
||||||
end
|
end
|
||||||
|
if not battle then
|
||||||
|
self.net:send({ type = "bye" })
|
||||||
|
self:exitWith(why or Strings("Link battle\ncan't start."), "error")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self.game.stack:push(battle)
|
||||||
|
self.stage = "battleRunning"
|
||||||
end
|
end
|
||||||
|
|
||||||
elseif self.stage == "battleRunning" then
|
elseif self.stage == "battleRunning" then
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ local function fakeTransport(options)
|
|||||||
return transport
|
return transport
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function readFile(path)
|
||||||
|
local handle = assert(io.open(path, "rb"))
|
||||||
|
local body = handle:read("*a")
|
||||||
|
handle:close()
|
||||||
|
return body
|
||||||
|
end
|
||||||
|
|
||||||
do
|
do
|
||||||
local host, guest = sessionPair()
|
local host, guest = sessionPair()
|
||||||
T.eq(host:getRole(), "host", "host role is assigned locally")
|
T.eq(host:getRole(), "host", "host role is assigned locally")
|
||||||
@@ -299,4 +306,22 @@ do
|
|||||||
T.eq(transport:poll()[1].name, "RED", "valid application packet stays intact")
|
T.eq(transport:poll()[1].name, "RED", "valid application packet stays intact")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local source = readFile("src/link/LinkState.lua")
|
||||||
|
T.check(source:find('require("src.link.Session")', 1, true) ~= nil,
|
||||||
|
"LinkState depends on the session boundary")
|
||||||
|
T.check(source:find('kind = "link"', 1, true) ~= nil,
|
||||||
|
"LinkState assigns the link session kind locally")
|
||||||
|
T.check(source:find("self.net.inbox", 1, true) == nil,
|
||||||
|
"LinkState never mutates a transport inbox")
|
||||||
|
T.check(source:find("self.net = Net.new()", 1, true) == nil,
|
||||||
|
"LinkState stores only successful session wrappers")
|
||||||
|
T.check(source:find(':take("hello")', 1, true) ~= nil,
|
||||||
|
"LinkState retrieves hello without draining unrelated packets")
|
||||||
|
T.check(source:find(':take("party")', 1, true) ~= nil,
|
||||||
|
"LinkState leaves battle handoff packets in session order")
|
||||||
|
T.check(source:find("getStatus()", 1, true) ~= nil,
|
||||||
|
"LinkState uses the session lifecycle instead of raw terminal flags")
|
||||||
|
end
|
||||||
|
|
||||||
T.finish("link_session")
|
T.finish("link_session")
|
||||||
|
|||||||
Reference in New Issue
Block a user