mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-08 09:42:55 -04:00
547 lines
18 KiB
Lua
547 lines
18 KiB
Lua
local Handshake = require("src.link.Handshake")
|
|
local Session = require("src.link.Session")
|
|
local VersionsGame = require("src.import.gba.versions_game")
|
|
local Fingerprint = require("src.link.Fingerprint")
|
|
|
|
local Game3Link = {}
|
|
Game3Link.__index = Game3Link
|
|
|
|
-- pokefirered/include/link.h:88
|
|
Game3Link.LINKTYPE = {
|
|
TRADE = 0x1111,
|
|
TRADE_CONNECTING = 0x1122,
|
|
TRADE_SETUP = 0x1133,
|
|
TRADE_DISCONNECTED = 0x1144,
|
|
BATTLE = 0x2211,
|
|
SINGLE_BATTLE = 0x2233,
|
|
DOUBLE_BATTLE = 0x2244,
|
|
MULTI_BATTLE = 0x2255,
|
|
BERRY_BLENDER_SETUP = 0x4411,
|
|
BERRY_BLENDER = 0x4422,
|
|
BATTLE_TOWER = 0x2288,
|
|
RECORD_MIX_BEFORE = 0x3311,
|
|
RECORD_MIX_AFTER = 0x3322,
|
|
MYSTERY_EVENT = 0x5501,
|
|
}
|
|
|
|
Game3Link.GENERATION = 3
|
|
Game3Link.HANDSHAKE_SECONDS = 10
|
|
Game3Link.BYE = "game3_bye"
|
|
Game3Link.EXIT = "game3_exit_link_room"
|
|
Game3Link.HELLO = "game3_hello"
|
|
Game3Link.SEAT_ROLES = { [0] = "host", [1] = "guest", [2] = "seat2", [3] = "seat3", [4] = "seat4" }
|
|
|
|
-- pokefirered/src/link.c:343 InitLocalLinkPlayer
|
|
local function localPlayer(game)
|
|
local rt = package.loaded["src.core.game3.runtime"]
|
|
local s = rt and rt.getSession and rt.getSession()
|
|
local name = s and (s.name or s.playerName)
|
|
if type(name) ~= "string" or name == "" then
|
|
name = game and game.save and game.save.player and game.save.player.name or nil
|
|
end
|
|
return name, require("src.core.game3.link.family").trainerId(s),
|
|
(s and (s.gender == "female" or s.gender == 1)) and 1 or 0
|
|
end
|
|
|
|
local function handshakeView(game)
|
|
if type(game) ~= "table" then return nil end
|
|
return { data = game.data, save = game.save, mods = game.mods }
|
|
end
|
|
|
|
local function family()
|
|
return require("src.core.game3.link.family")
|
|
end
|
|
|
|
local function gameVersionOf(game, player)
|
|
local Family = family()
|
|
local v = type(player) == "table" and player.version or nil
|
|
if type(v) ~= "string" and type(game) == "table" then
|
|
v = game.version or (type(game.session) == "table" and game.session.version)
|
|
or (type(game.save) == "table" and game.save.version)
|
|
end
|
|
if Family.isGame3(v) then return v end
|
|
return Family.activeVersion()
|
|
end
|
|
|
|
local function localSession(game)
|
|
local rt = package.loaded["src.core.game3.runtime"]
|
|
local s = rt and rt.getSession and rt.getSession()
|
|
if type(s) == "table" then return s end
|
|
if type(game) == "table" then return game.session or game.save end
|
|
return nil
|
|
end
|
|
|
|
Game3Link.BATTLE_TYPES = {
|
|
[Game3Link.LINKTYPE.BATTLE] = true,
|
|
[Game3Link.LINKTYPE.SINGLE_BATTLE] = true,
|
|
[Game3Link.LINKTYPE.DOUBLE_BATTLE] = true,
|
|
[Game3Link.LINKTYPE.MULTI_BATTLE] = true,
|
|
}
|
|
|
|
Game3Link.TRADE_TYPES = {
|
|
[Game3Link.LINKTYPE.TRADE] = true,
|
|
[Game3Link.LINKTYPE.TRADE_CONNECTING] = true,
|
|
[Game3Link.LINKTYPE.TRADE_SETUP] = true,
|
|
[Game3Link.LINKTYPE.TRADE_DISCONNECTED] = true,
|
|
}
|
|
|
|
function Game3Link.rules(version)
|
|
local ok, value = pcall(Fingerprint.rulesGen3, version)
|
|
return ok and value or nil
|
|
end
|
|
|
|
local function helloFor(game, linkType, player)
|
|
local hello = Handshake.hello(handshakeView(game), nil)
|
|
local okA, ArenaData = pcall(require, "src.online.ArenaData")
|
|
if okA and type(ArenaData) == "table" and ArenaData.stripCosmeticMods then
|
|
ArenaData.stripCosmeticMods(hello, game)
|
|
end
|
|
hello.generation = Game3Link.GENERATION
|
|
hello.type = Game3Link.HELLO
|
|
hello.ruleset = Handshake.DEFAULT_RULESET
|
|
local name, trainerId, gender = localPlayer(game)
|
|
if type(player) == "table" then
|
|
if player.name ~= nil then name = player.name end
|
|
if player.trainerId ~= nil then trainerId = tonumber(player.trainerId) or 0 end
|
|
if player.gender ~= nil then gender = (player.gender == 1 or player.gender == "female") and 1 or 0 end
|
|
end
|
|
hello.name = name
|
|
local version = gameVersionOf(game, player)
|
|
local mod = VersionsGame.game(version)
|
|
local Family = family()
|
|
local lp = Family.localLinkPlayer(type(player) == "table" and player.session or localSession(game), version)
|
|
if type(player) == "table" and player.progressFlags ~= nil then
|
|
lp.progressFlags = tonumber(player.progressFlags) or 0
|
|
end
|
|
hello.game3 = {
|
|
cacheVersion = mod.CACHE_VERSION,
|
|
nativeVersion = mod.NATIVE_VERSION,
|
|
linkType = tonumber(linkType),
|
|
trainerId = trainerId,
|
|
gender = gender,
|
|
version = version,
|
|
family = Family.of(version),
|
|
gameVersion = lp.version,
|
|
progressFlags = lp.progressFlags,
|
|
language = lp.language,
|
|
rules = Game3Link.rules(version),
|
|
}
|
|
local data = type(game) == "table" and game.data or nil
|
|
if type(data) == "table" and Fingerprint.generationOf(data) == 3 then
|
|
local okC, core = pcall(Fingerprint.coreGen3, data, hello.mods)
|
|
local okM, moves = pcall(Fingerprint.movesGen3, data)
|
|
hello.game3.core = okC and core or nil
|
|
hello.game3.moves = okM and moves or nil
|
|
end
|
|
return hello
|
|
end
|
|
|
|
Game3Link.hello = helloFor
|
|
|
|
local function transportSeat(transport)
|
|
if type(transport) == "table" and type(transport.seat) == "function" then
|
|
local ok, s = pcall(transport.seat, transport)
|
|
if ok then return tonumber(s) end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function transportSeats(transport)
|
|
if type(transport) == "table" and type(transport.seats) == "function" then
|
|
local ok, n = pcall(transport.seats, transport)
|
|
if ok then return tonumber(n) end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function Game3Link.attach(transport, opts)
|
|
opts = opts or {}
|
|
local seat = tonumber(opts.seat) or transportSeat(transport)
|
|
if seat == nil then seat = opts.role == "guest" and 1 or 0 end
|
|
local seats = tonumber(opts.seats) or transportSeats(transport) or 2
|
|
local role = seat == 0 and "host" or "guest"
|
|
local self = setmetatable({
|
|
role = role,
|
|
seat = seat,
|
|
nseats = seats,
|
|
linkType = tonumber(opts.linkType) or Game3Link.LINKTYPE.BATTLE,
|
|
game = opts.game,
|
|
state = "handshake",
|
|
elapsed = 0,
|
|
closed = false,
|
|
onReady = opts.onReady,
|
|
onClosed = opts.onClosed,
|
|
timeout = tonumber(opts.timeout) or Game3Link.HANDSHAKE_SECONDS,
|
|
peerHellos = {},
|
|
peerHello = nil,
|
|
departed = {},
|
|
_transport = transport,
|
|
_session = Session.new(transport, { role = role, kind = "game3" }),
|
|
}, Game3Link)
|
|
local hello = opts.hello
|
|
if type(hello) == "table" then
|
|
local copy = {}
|
|
for k, v in pairs(hello) do copy[k] = v end
|
|
copy.game3 = {}
|
|
for k, v in pairs(type(hello.game3) == "table" and hello.game3 or {}) do copy.game3[k] = v end
|
|
hello = copy
|
|
else
|
|
hello = helloFor(opts.game, self.linkType)
|
|
end
|
|
hello.type = Game3Link.HELLO
|
|
hello.generation = Game3Link.GENERATION
|
|
hello.game3 = hello.game3 or {}
|
|
hello.game3.seat = seat
|
|
if hello.game3.linkType == nil then hello.game3.linkType = self.linkType end
|
|
self.myHello = hello
|
|
self._session:send(self.myHello)
|
|
return self
|
|
end
|
|
|
|
function Game3Link.loopback(opts)
|
|
opts = opts or {}
|
|
local Net = require("src.link.Net")
|
|
local a, b = Net.loopbackPair()
|
|
local host = Game3Link.attach(a, {
|
|
seat = 0, seats = 2, game = opts.game, linkType = opts.linkType,
|
|
timeout = opts.timeout, onReady = opts.onHostReady, onClosed = opts.onHostClosed,
|
|
})
|
|
local guest = Game3Link.attach(b, {
|
|
seat = 1, seats = 2, game = opts.game, linkType = opts.linkType,
|
|
timeout = opts.timeout, onReady = opts.onGuestReady, onClosed = opts.onGuestClosed,
|
|
})
|
|
return host, guest
|
|
end
|
|
|
|
function Game3Link:isOpen()
|
|
return not self.closed
|
|
end
|
|
|
|
function Game3Link:isReady()
|
|
return self.state == "ready" and not self.closed
|
|
end
|
|
|
|
function Game3Link:getStatus()
|
|
return self.state
|
|
end
|
|
|
|
function Game3Link:getSeat()
|
|
return self.seat
|
|
end
|
|
|
|
function Game3Link:seatCount()
|
|
return self.nseats
|
|
end
|
|
|
|
function Game3Link:peerName()
|
|
return self.peerHello and self.peerHello.name or nil
|
|
end
|
|
|
|
function Game3Link:peerLinkType()
|
|
local g3 = self.peerHello and self.peerHello.game3
|
|
return g3 and tonumber(g3.linkType) or nil
|
|
end
|
|
|
|
local function row(hello, seat, isLocal)
|
|
local g3 = type(hello) == "table" and type(hello.game3) == "table" and hello.game3 or {}
|
|
return {
|
|
name = hello and hello.name,
|
|
trainerId = tonumber(g3.trainerId) or 0,
|
|
language = tonumber(g3.language) or 2,
|
|
gender = tonumber(g3.gender) or 0,
|
|
version = g3.version,
|
|
role = Game3Link.SEAT_ROLES[seat] or "guest",
|
|
seat = seat,
|
|
isLocal = isLocal,
|
|
}
|
|
end
|
|
|
|
-- pokefirered/src/link.c:1069 GetLinkPlayerCount_2
|
|
function Game3Link:players()
|
|
local list = { row(self.myHello, self.seat, true) }
|
|
for seat, hello in pairs(self.peerHellos) do
|
|
list[#list + 1] = row(hello, seat, false)
|
|
end
|
|
table.sort(list, function(a, b) return a.seat < b.seat end)
|
|
return list
|
|
end
|
|
|
|
function Game3Link:send(message)
|
|
if self.closed or type(message) ~= "table" then return false end
|
|
self._session:send(message)
|
|
return true
|
|
end
|
|
|
|
function Game3Link:take(messageType, predicate)
|
|
return self._session:take(messageType, predicate)
|
|
end
|
|
|
|
function Game3Link:poll()
|
|
return self._session:poll()
|
|
end
|
|
|
|
function Game3Link:peerGone()
|
|
local t = self._transport
|
|
if not t then return true end
|
|
if t.closed == true or t.error then return true end
|
|
if t.peerEnd and t.peerEnd.closed == true then return true end
|
|
local status = self._session:getStatus()
|
|
return status == "closed" or status == "failed"
|
|
end
|
|
|
|
function Game3Link:_finish(state, reason)
|
|
if self.closed then return false end
|
|
self.closed = true
|
|
self.state = state
|
|
self.reason = reason
|
|
self._session:close()
|
|
local cb = self.onClosed
|
|
if cb then cb(reason, state) end
|
|
return true
|
|
end
|
|
|
|
-- pokefirered/src/link.c:419 CloseLink
|
|
function Game3Link:close(reason)
|
|
if self.closed then return false end
|
|
pcall(function()
|
|
self._session:send({ type = Game3Link.BYE, seat = self.seat, reason = tostring(reason or "bye") })
|
|
end)
|
|
return self:_finish("closed", reason or "close_link")
|
|
end
|
|
|
|
-- pokeemerald/src/overworld.c:2411
|
|
function Game3Link:depart(seat)
|
|
seat = tonumber(seat)
|
|
if seat == nil or seat < 0 or seat == self.seat or self.departed[seat] then return false end
|
|
self.departed[seat] = true
|
|
self.peerHellos[seat] = nil
|
|
self.peerHello = self:_primaryHello()
|
|
local t = self._transport
|
|
if type(t) == "table" and type(t.forgetSeat) == "function" then pcall(t.forgetSeat, t, seat) end
|
|
return true
|
|
end
|
|
|
|
function Game3Link:leave(keepRoom)
|
|
local t = self._transport
|
|
if type(t) == "table" and type(t.leave) == "function" then
|
|
pcall(t.leave, t, keepRoom)
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function stampsMatch(g3, id)
|
|
local ok, mod = pcall(VersionsGame.game, id)
|
|
return ok and type(mod) == "table" and tonumber(g3.cacheVersion) == mod.CACHE_VERSION
|
|
and tonumber(g3.nativeVersion) == mod.NATIVE_VERSION
|
|
end
|
|
|
|
local function helloVersion(g3, mine)
|
|
if type(g3) ~= "table" then return "firered" end
|
|
if type(g3.version) == "string" then return g3.version end
|
|
local own = type(mine) == "table" and mine.version
|
|
if type(own) == "string" and stampsMatch(g3, own) then return own end
|
|
local GameVersion = require("src.core.GameVersion")
|
|
for _, id in ipairs(GameVersion.ORDER) do
|
|
if VersionsGame.GAMES[id] and family().isGame3(id) and stampsMatch(g3, id) then return id end
|
|
end
|
|
return "firered"
|
|
end
|
|
|
|
local function helloFamily(g3, mine)
|
|
if type(g3) == "table" and type(g3.family) == "string" then return g3.family end
|
|
return family().of(helloVersion(g3, mine))
|
|
end
|
|
|
|
local function helloRules(g3, mine)
|
|
if type(g3) == "table" and g3.rules ~= nil then return g3.rules end
|
|
return Game3Link.rules(helloVersion(g3, mine))
|
|
end
|
|
|
|
function Game3Link.peerVersionOf(myHello, peer)
|
|
return helloVersion(peer and peer.game3, myHello and myHello.game3)
|
|
end
|
|
|
|
function Game3Link.crossFamily(myHello, peer)
|
|
local mine = myHello and myHello.game3
|
|
return helloFamily(mine) ~= helloFamily(peer and peer.game3, mine)
|
|
end
|
|
|
|
local function decideOne(myHello, peer, linkType)
|
|
local g3 = peer and peer.game3
|
|
if type(g3) ~= "table" then
|
|
return "refused", "peer_is_not_firered"
|
|
end
|
|
local Family = family()
|
|
if tonumber(linkType) == Game3Link.LINKTYPE.MYSTERY_EVENT then
|
|
if not Family.isRubySapphire(helloVersion(myHello and myHello.game3))
|
|
or not Family.isRubySapphire(helloVersion(g3, myHello and myHello.game3))
|
|
or tonumber((myHello.game3 or {}).language) ~= tonumber(g3.language) then
|
|
return "refused", "activity_unavailable"
|
|
end
|
|
end
|
|
if tonumber(linkType) == Game3Link.LINKTYPE.BATTLE_TOWER
|
|
and (Family.isRubySapphire(helloVersion(myHello and myHello.game3))
|
|
or Family.isRubySapphire(helloVersion(g3, myHello and myHello.game3))) then
|
|
return "refused", "activity_unavailable"
|
|
end
|
|
local verdict, reason = Handshake.checkCompat(myHello, peer)
|
|
local cross, hostRules = false, false
|
|
local modified = (myHello and myHello.linkModified) or peer.linkModified
|
|
local battle = Game3Link.BATTLE_TYPES[tonumber(linkType) or -1]
|
|
local mine = myHello and myHello.game3
|
|
if verdict == "subset" and not modified and type(mine) == "table"
|
|
and helloVersion(mine) ~= helloVersion(g3, mine)
|
|
and Game3Link.TRADE_TYPES[tonumber(linkType) or -1] then
|
|
cross = true
|
|
elseif verdict == "subset" and not modified and battle and type(mine) == "table"
|
|
and mine.core ~= nil and mine.core == g3.core
|
|
and tostring(myHello.ruleset or Handshake.DEFAULT_RULESET) == tostring(peer.ruleset or Handshake.DEFAULT_RULESET) then
|
|
hostRules = true
|
|
cross = Game3Link.crossFamily(myHello, peer)
|
|
elseif verdict ~= "full" then
|
|
return "refused", reason or verdict
|
|
end
|
|
local peerVersion = helloVersion(g3, mine)
|
|
local ok, mod = pcall(VersionsGame.game, peerVersion)
|
|
if not (ok and type(mod) == "table" and family().isGame3(peerVersion)) then
|
|
return "refused", "unknown_game_version"
|
|
end
|
|
if tonumber(g3.cacheVersion) ~= mod.CACHE_VERSION then
|
|
return "refused", "cache_version_mismatch"
|
|
end
|
|
if tonumber(g3.nativeVersion) ~= mod.NATIVE_VERSION then
|
|
return "refused", "native_version_mismatch"
|
|
end
|
|
if battle and helloRules(mine) ~= helloRules(g3, mine) then
|
|
if modified then return "refused", "rules_mismatch" end
|
|
hostRules = true
|
|
cross = Game3Link.crossFamily(myHello, peer)
|
|
end
|
|
return "full", nil, cross, hostRules
|
|
end
|
|
|
|
Game3Link.decideOne = decideOne
|
|
|
|
function Game3Link:decide()
|
|
local linkType = self.linkType
|
|
if next(self.peerHellos) == nil then
|
|
local verdict, reason, cross, hostRules = decideOne(self.myHello, self.peerHello, linkType)
|
|
self.crossVersion = cross == true
|
|
self.hostRules = hostRules == true
|
|
return verdict, reason
|
|
end
|
|
local seats = {}
|
|
for seat in pairs(self.peerHellos) do seats[#seats + 1] = seat end
|
|
table.sort(seats)
|
|
local anyCross, anyHost = false, false
|
|
for _, seat in ipairs(seats) do
|
|
local verdict, reason, cross, hostRules = decideOne(self.myHello, self.peerHellos[seat], linkType)
|
|
if verdict ~= "full" then return verdict, reason end
|
|
anyCross = anyCross or cross == true
|
|
anyHost = anyHost or hostRules == true
|
|
end
|
|
self.crossVersion = anyCross
|
|
self.hostRules = anyHost
|
|
return "full", nil
|
|
end
|
|
|
|
-- pokeemerald/src/battle_controllers.c:397
|
|
function Game3Link:hostGame3()
|
|
local hello = self.seat == 0 and self.myHello or self.peerHellos[0] or self.peerHello
|
|
return hello and hello.game3 or nil
|
|
end
|
|
|
|
function Game3Link:peerGame()
|
|
local g3 = self.peerHello and self.peerHello.game3
|
|
if type(g3) ~= "table" then return nil end
|
|
local mine = self.myHello and self.myHello.game3
|
|
return {
|
|
version = helloVersion(g3, mine),
|
|
family = helloFamily(g3, mine),
|
|
gameVersion = tonumber(g3.gameVersion),
|
|
progressFlags = tonumber(g3.progressFlags) or 0,
|
|
}
|
|
end
|
|
|
|
function Game3Link:_helloSeat(hello)
|
|
local s = tonumber(hello.seat)
|
|
if s == nil or s < 0 then
|
|
local g3 = type(hello.game3) == "table" and hello.game3 or {}
|
|
s = tonumber(g3.seat)
|
|
end
|
|
if s == nil or s < 0 or s == self.seat then
|
|
if self.nseats <= 2 then return 1 - (self.seat == 1 and 1 or 0) end
|
|
return nil
|
|
end
|
|
return s
|
|
end
|
|
|
|
function Game3Link:_allHellos()
|
|
local n = 0
|
|
for _ in pairs(self.peerHellos) do n = n + 1 end
|
|
return n >= self.nseats - 1
|
|
end
|
|
|
|
function Game3Link:_primaryHello()
|
|
if self.seat ~= 0 and self.peerHellos[0] then return self.peerHellos[0] end
|
|
local best
|
|
for seat, hello in pairs(self.peerHellos) do
|
|
if best == nil or seat < best then best = seat end
|
|
end
|
|
return best ~= nil and self.peerHellos[best] or nil
|
|
end
|
|
|
|
function Game3Link:update(dt)
|
|
if self.closed then return self.state end
|
|
self.elapsed = self.elapsed + (tonumber(dt) or 0)
|
|
self._session:update()
|
|
|
|
local exit = self._session:take(Game3Link.EXIT)
|
|
while exit do
|
|
self:depart(exit.seat)
|
|
exit = self._session:take(Game3Link.EXIT)
|
|
end
|
|
local departed = self.departed
|
|
while self._session:take(Game3Link.BYE, function(m) return departed[tonumber(m.seat)] == true end) do end
|
|
|
|
if self._session:take(Game3Link.BYE) then
|
|
self:_finish("closed", "peer_left")
|
|
return self.state
|
|
end
|
|
|
|
if self.state == "handshake" then
|
|
local hello = self._session:take(Game3Link.HELLO) or self._session:take("hello")
|
|
while hello do
|
|
local seat = self:_helloSeat(hello)
|
|
if seat ~= nil and not self.departed[seat] then self.peerHellos[seat] = hello end
|
|
hello = self._session:take(Game3Link.HELLO)
|
|
end
|
|
self.peerHello = self:_primaryHello()
|
|
if self.peerHello and self:_allHellos() then
|
|
local verdict, reason = self:decide()
|
|
self.verdict = verdict
|
|
if verdict == "full" then
|
|
self.state = "ready"
|
|
local cb = self.onReady
|
|
if cb then cb(self) end
|
|
else
|
|
self:close(reason or "refused")
|
|
return self.state
|
|
end
|
|
end
|
|
end
|
|
|
|
if self:peerGone() then
|
|
self:_finish("failed", self.peerHello and "peer_dropped" or "no_peer")
|
|
return self.state
|
|
end
|
|
|
|
if self.state == "handshake" and self.timeout > 0 and self.elapsed >= self.timeout then
|
|
self:_finish("failed", "handshake_timeout")
|
|
end
|
|
return self.state
|
|
end
|
|
|
|
return Game3Link
|