mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 12:15:31 +02:00
CLOSES #1496
This commit is contained in:
@@ -270,7 +270,10 @@ end
|
||||
|
||||
local function index(mods)
|
||||
local byId = {}
|
||||
for _, mod in ipairs(mods or {}) do byId[tostring(mod.id)] = mod end
|
||||
if type(mods) ~= "table" then return byId end
|
||||
for _, mod in ipairs(mods) do
|
||||
if type(mod) == "table" then byId[tostring(mod.id)] = mod end
|
||||
end
|
||||
return byId
|
||||
end
|
||||
|
||||
@@ -327,7 +330,8 @@ end
|
||||
-- lines for the incompatibility screen: what differs, then what still works
|
||||
function Handshake.describe(localHello, remoteHello, verdict, mode)
|
||||
local lines = {}
|
||||
local peer = (remoteHello and remoteHello.name) or "THEY"
|
||||
local peerName = remoteHello and remoteHello.name
|
||||
local peer = type(peerName) == "string" and peerName or "THEY"
|
||||
if verdict == "refused" then
|
||||
-- checked before the v1 arm for the same reason checkCompat checks it
|
||||
-- first: a Gen 1 peer meeting a Gen 2 one has no `protocol` to read yet
|
||||
|
||||
+13
-5
@@ -4,6 +4,8 @@
|
||||
|
||||
local Json = {}
|
||||
|
||||
Json.MAX_DEPTH = 64
|
||||
|
||||
local function encodeValue(v, out)
|
||||
local t = type(v)
|
||||
if v == nil then
|
||||
@@ -109,7 +111,9 @@ local function decodeString(s, i)
|
||||
error("unterminated string")
|
||||
end
|
||||
|
||||
decodeValue = function(s, i)
|
||||
decodeValue = function(s, i, depth)
|
||||
depth = (depth or 0) + 1
|
||||
assert(depth <= Json.MAX_DEPTH, "json nested too deeply")
|
||||
i = skipWs(s, i)
|
||||
local c = s:sub(i, i)
|
||||
if c == '"' then
|
||||
@@ -124,7 +128,7 @@ decodeValue = function(s, i)
|
||||
i = skipWs(s, i)
|
||||
assert(s:sub(i, i) == ":", "expected :")
|
||||
local val
|
||||
val, i = decodeValue(s, i + 1)
|
||||
val, i = decodeValue(s, i + 1, depth)
|
||||
obj[key] = val
|
||||
i = skipWs(s, i)
|
||||
local d = s:sub(i, i)
|
||||
@@ -138,7 +142,7 @@ decodeValue = function(s, i)
|
||||
if s:sub(i, i) == "]" then return arr, i + 1 end
|
||||
while true do
|
||||
local val
|
||||
val, i = decodeValue(s, i)
|
||||
val, i = decodeValue(s, i, depth)
|
||||
arr[#arr + 1] = val
|
||||
i = skipWs(s, i)
|
||||
local d = s:sub(i, i)
|
||||
@@ -162,9 +166,13 @@ decodeValue = function(s, i)
|
||||
end
|
||||
end
|
||||
|
||||
function Json.decode(s)
|
||||
function Json.decode(s, maxLength)
|
||||
if type(s) ~= "string" then return nil, "json input is not a string" end
|
||||
if maxLength and #s > maxLength then
|
||||
return nil, ("json input is %d bytes (max %d)"):format(#s, maxLength)
|
||||
end
|
||||
local ok, v = pcall(function()
|
||||
local val = select(1, decodeValue(s, 1))
|
||||
local val = select(1, decodeValue(s, 1, 0))
|
||||
return val
|
||||
end)
|
||||
if ok then return v end
|
||||
|
||||
@@ -29,7 +29,9 @@ local LinkBattle = {}
|
||||
-- Deterministic Park-Miller PRNG: both sides must roll identical
|
||||
-- streams, so love.math.random can't be used.
|
||||
local function makeRng(seed)
|
||||
local s = seed % 2147483647
|
||||
local s = tonumber(seed) or 1
|
||||
if s ~= s or s == math.huge or s == -math.huge then s = 1 end
|
||||
s = math.floor(s) % 2147483647
|
||||
if s <= 0 then s = s + 2147483646 end
|
||||
return function(a, b)
|
||||
s = (s * 16807) % 2147483647
|
||||
|
||||
+19
-5
@@ -40,6 +40,10 @@ Net.__index = Net
|
||||
Net.DEFAULT_PORT = 7777
|
||||
Net.DEFAULT_RELAY_ADDRESS = "147.182.215.255:7778"
|
||||
|
||||
Net.MAX_LINE = 256 * 1024
|
||||
Net.MAX_RX_PER_FRAME = 512 * 1024
|
||||
Net.ENET_BANDWIDTH = 256 * 1024
|
||||
|
||||
function Net.available()
|
||||
return enet ~= nil
|
||||
end
|
||||
@@ -115,7 +119,8 @@ function Net:host(port)
|
||||
return false
|
||||
end
|
||||
port = tonumber(port) or Net.defaultPort()
|
||||
local ok, h, err = pcall(enet.host_create, ("*:%d"):format(port), 2, 1)
|
||||
local ok, h, err = pcall(enet.host_create, ("*:%d"):format(port), 2, 1,
|
||||
Net.ENET_BANDWIDTH, Net.ENET_BANDWIDTH)
|
||||
if not ok or not h then
|
||||
self.error = ("can't open UDP port %d (%s)"):format(
|
||||
port, tostring(ok and err or h))
|
||||
@@ -255,7 +260,7 @@ local function handleGenericRelayControl(self, msg)
|
||||
end
|
||||
|
||||
function Net:handleTCPLine(line)
|
||||
local msg = Json.decode(line)
|
||||
local msg = Json.decode(line, Net.MAX_LINE)
|
||||
if msg == nil then
|
||||
Logger.warn("link: bad relay message %q", line:sub(1, 60))
|
||||
return
|
||||
@@ -277,6 +282,11 @@ function Net:drainLines()
|
||||
self.rxBuf = self.rxBuf:sub(nl + 1)
|
||||
if #line > 0 then self:handleTCPLine(line) end
|
||||
end
|
||||
if #self.rxBuf > Net.MAX_LINE then
|
||||
self.rxBuf = ""
|
||||
self.error = Strings("The other side\nsent bad data.")
|
||||
self.closed = true
|
||||
end
|
||||
end
|
||||
|
||||
-- non-blocking pump for the relay TCP backend: flush queued writes, drain
|
||||
@@ -300,10 +310,14 @@ function Net:updateTCP()
|
||||
return
|
||||
end
|
||||
end
|
||||
while true do
|
||||
local budget = Net.MAX_RX_PER_FRAME
|
||||
while budget > 0 do
|
||||
local data, err, partial = sock:receive(8192)
|
||||
local chunk = data or partial or ""
|
||||
if #chunk > 0 then self.rxBuf = self.rxBuf .. chunk end
|
||||
if #chunk > 0 then
|
||||
self.rxBuf = self.rxBuf .. chunk
|
||||
budget = budget - #chunk
|
||||
end
|
||||
if err == "closed" then
|
||||
self.closed = true
|
||||
break
|
||||
@@ -350,7 +364,7 @@ function Net:update()
|
||||
for _, msg in ipairs(queued) do self:send(msg) end
|
||||
end
|
||||
elseif event.type == "receive" then
|
||||
local msg = Json.decode(event.data)
|
||||
local msg = Json.decode(event.data, Net.MAX_LINE)
|
||||
if msg ~= nil then
|
||||
table.insert(self.inbox, msg)
|
||||
else
|
||||
|
||||
+89
-42
@@ -17,6 +17,25 @@ local Runtime = require("src.mods.Runtime")
|
||||
|
||||
local Protocol = {}
|
||||
|
||||
local MAX_WIRE_NAME = 40
|
||||
|
||||
local function num(v, default)
|
||||
local n = tonumber(v)
|
||||
if n == nil or n ~= n or n == math.huge or n == -math.huge then
|
||||
return default
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
local function tbl(v)
|
||||
return type(v) == "table" and v or {}
|
||||
end
|
||||
|
||||
local function text(v)
|
||||
if type(v) ~= "string" then return nil end
|
||||
return v:sub(1, MAX_WIRE_NAME)
|
||||
end
|
||||
|
||||
Protocol.hello = Handshake.hello
|
||||
Protocol.checkCompat = Handshake.checkCompat
|
||||
|
||||
@@ -78,6 +97,10 @@ function Protocol.unpackMon(data, packed, opts)
|
||||
local Stats = require("src.pokemon.Stats")
|
||||
local Growth = require("src.pokemon.Growth")
|
||||
local strict = opts and opts.strict
|
||||
if type(packed) ~= "table" then
|
||||
if strict then return nil, "unknown POKéMON" end
|
||||
return nil
|
||||
end
|
||||
-- forceLevel comes from an "auto-level" ruling. The picker's ANY choice
|
||||
-- ("use each mon's real level", Gen1's only mode) is a string sentinel on
|
||||
-- the LinkState/Tournament side (see levelForWire) that must mean "no
|
||||
@@ -91,7 +114,7 @@ function Protocol.unpackMon(data, packed, opts)
|
||||
if strict then return nil, "unknown POKéMON" end
|
||||
return nil
|
||||
end
|
||||
local level = math.max(2, math.min(100, math.floor(packed.level or 5)))
|
||||
local level = math.max(2, math.min(100, math.floor(num(packed.level, 5))))
|
||||
-- "auto-level" tournaments/matches: every participant's real level is
|
||||
-- ignored and everyone rebuilds at the same fixed level instead, so a
|
||||
-- Lv12 and a Lv100 party can battle on equal footing. Both sides pass
|
||||
@@ -99,25 +122,27 @@ function Protocol.unpackMon(data, packed, opts)
|
||||
if forceLevel then
|
||||
level = math.max(2, math.min(100, math.floor(forceLevel)))
|
||||
end
|
||||
local packedDvs, packedStatExp = tbl(packed.dvs), tbl(packed.statExp)
|
||||
local dvs = {}
|
||||
for _, k in ipairs({ "hp", "attack", "defense", "speed", "special" }) do
|
||||
dvs[k] = math.max(0, math.min(15, math.floor((packed.dvs or {})[k] or 0)))
|
||||
dvs[k] = math.max(0, math.min(15, math.floor(num(packedDvs[k], 0))))
|
||||
end
|
||||
local statExp = {}
|
||||
for _, k in ipairs({ "hp", "attack", "defense", "speed", "special" }) do
|
||||
statExp[k] = math.max(0, math.min(65535, math.floor((packed.statExp or {})[k] or 0)))
|
||||
statExp[k] = math.max(0, math.min(65535, math.floor(num(packedStatExp[k], 0))))
|
||||
end
|
||||
local stats = Stats.calc(def, level, dvs, statExp)
|
||||
local moves = {}
|
||||
for _, mv in ipairs(packed.moves or {}) do
|
||||
for _, entry in ipairs(tbl(packed.moves)) do
|
||||
local mv = tbl(entry)
|
||||
local mdef = data.moves[mv.id]
|
||||
if mdef and #moves < 4 then
|
||||
local ppUps = math.max(0, math.min(3, math.floor(mv.ppUps or 0)))
|
||||
local ppUps = math.max(0, math.min(3, math.floor(num(mv.ppUps, 0))))
|
||||
local maxPP = mdef.pp + ppUps * math.floor(mdef.pp / 5)
|
||||
local entry = { id = mv.id,
|
||||
pp = math.max(0, math.min(maxPP, math.floor(mv.pp or 0))) }
|
||||
if mv.ppUps ~= nil then entry.ppUps = ppUps end
|
||||
table.insert(moves, entry)
|
||||
local move = { id = mv.id,
|
||||
pp = math.max(0, math.min(maxPP, math.floor(num(mv.pp, 0)))) }
|
||||
if mv.ppUps ~= nil then move.ppUps = ppUps end
|
||||
table.insert(moves, move)
|
||||
end
|
||||
end
|
||||
if #moves == 0 then
|
||||
@@ -132,27 +157,29 @@ function Protocol.unpackMon(data, packed, opts)
|
||||
-- same as a standardized tournament format would
|
||||
local forced = forceLevel
|
||||
local hp = forced and stats.hp
|
||||
or math.max(0, math.min(stats.hp, math.floor(packed.hp or stats.hp)))
|
||||
local status = forced and nil or packed.status
|
||||
or math.max(0, math.min(stats.hp, math.floor(num(packed.hp, stats.hp))))
|
||||
local status = forced and nil or text(packed.status)
|
||||
-- preserve the sender's original-trainer identity (party_struct MON_OTID +
|
||||
-- wPartyMonOT on a real cable), clamped/typed like every other field so a
|
||||
-- tampered packet can't inject a bad ID or a huge name. Left nil when the
|
||||
-- packet omits them (a v1/old peer) -- no worse than before for that legacy
|
||||
-- path, and once ot is set the load-time stampOT backfill (mon.ot or ...)
|
||||
-- becomes a no-op so the sender's identity survives save/reload (#215).
|
||||
local otId = packed.otId
|
||||
and math.max(0, math.min(65535, math.floor(packed.otId))) or nil
|
||||
local packedOtId = num(packed.otId)
|
||||
local otId = packedOtId
|
||||
and math.max(0, math.min(65535, math.floor(packedOtId))) or nil
|
||||
local ot = type(packed.ot) == "string" and packed.ot:sub(1, 10) or nil
|
||||
return {
|
||||
species = packed.species,
|
||||
level = level,
|
||||
exp = math.max(0, math.floor(packed.exp or Growth.expForLevel(def.growthRate, level))),
|
||||
exp = math.max(0, math.floor(num(packed.exp,
|
||||
Growth.expForLevel(def.growthRate, level)))),
|
||||
dvs = dvs,
|
||||
statExp = statExp,
|
||||
stats = stats,
|
||||
hp = hp,
|
||||
status = status,
|
||||
nickname = packed.nickname,
|
||||
nickname = text(packed.nickname),
|
||||
ot = ot,
|
||||
otId = otId,
|
||||
moves = moves,
|
||||
@@ -246,6 +273,10 @@ end
|
||||
function Protocol.unpackMon2(data, packed, opts)
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
local strict = opts and opts.strict
|
||||
if type(packed) ~= "table" then
|
||||
if strict then return nil, "unknown POKéMON" end
|
||||
return nil
|
||||
end
|
||||
local forceLevel = opts and tonumber(opts.forceLevel) or nil
|
||||
local def = data and data.pokemon and data.pokemon[packed.species]
|
||||
if not def then
|
||||
@@ -253,31 +284,33 @@ function Protocol.unpackMon2(data, packed, opts)
|
||||
return nil
|
||||
end
|
||||
local level = math.max(1, math.min(Mon.MAX_LEVEL,
|
||||
math.floor(packed.level or 5)))
|
||||
math.floor(num(packed.level, 5))))
|
||||
if forceLevel then
|
||||
level = math.max(1, math.min(Mon.MAX_LEVEL, math.floor(forceLevel)))
|
||||
end
|
||||
local packedDvs, packedStatExp = tbl(packed.dvs), tbl(packed.statExp)
|
||||
local dvs = {}
|
||||
for _, k in ipairs(GEN2_DVS) do
|
||||
dvs[k] = math.max(0, math.min(Mon.MAX_DV,
|
||||
math.floor((packed.dvs or {})[k] or 0)))
|
||||
math.floor(num(packedDvs[k], 0))))
|
||||
end
|
||||
-- derived, never taken from the packet (see GEN2_DVS above)
|
||||
dvs.hp = Mon.hpDV(dvs)
|
||||
local statExp = {}
|
||||
for _, k in ipairs(GEN2_STAT_EXP) do
|
||||
statExp[k] = math.max(0, math.min(65535,
|
||||
math.floor((packed.statExp or {})[k] or 0)))
|
||||
math.floor(num(packedStatExp[k], 0))))
|
||||
end
|
||||
local stats = Mon.stats(def.baseStats, dvs, level, statExp)
|
||||
local moves = {}
|
||||
for _, mv in ipairs(packed.moves or {}) do
|
||||
for _, packedMove in ipairs(tbl(packed.moves)) do
|
||||
local mv = tbl(packedMove)
|
||||
local mdef = data.moves and data.moves[mv.id]
|
||||
if mdef and #moves < 4 then
|
||||
local ppUps = math.max(0, math.min(3, math.floor(mv.ppUps or 0)))
|
||||
local ppUps = math.max(0, math.min(3, math.floor(num(mv.ppUps, 0))))
|
||||
local maxPp = (mdef.pp or 0) + ppUps * math.floor((mdef.pp or 0) / 5)
|
||||
local entry = { id = mv.id, maxPp = maxPp,
|
||||
pp = math.max(0, math.min(maxPp, math.floor(mv.pp or 0))) }
|
||||
pp = math.max(0, math.min(maxPp, math.floor(num(mv.pp, 0)))) }
|
||||
if mv.ppUps ~= nil then entry.ppUps = ppUps end
|
||||
table.insert(moves, entry)
|
||||
end
|
||||
@@ -296,26 +329,27 @@ function Protocol.unpackMon2(data, packed, opts)
|
||||
-- an item the peer cannot represent), and the Gen 2 arm of
|
||||
-- Protocol.eligibleParty is what keeps it from ever reaching here on a
|
||||
-- negotiated trade.
|
||||
local item = packed.item
|
||||
local item = type(packed.item) == "string" and packed.item or nil
|
||||
if item ~= nil and not (data.items and data.items[item]) then
|
||||
if strict then return nil, "unknown item" end
|
||||
item = nil
|
||||
end
|
||||
local forced = forceLevel
|
||||
local hp = forced and stats.hp
|
||||
or math.max(0, math.min(stats.hp, math.floor(packed.hp or stats.hp)))
|
||||
local status = forced and nil or packed.status
|
||||
local otId = packed.otId
|
||||
and math.max(0, math.min(65535, math.floor(packed.otId))) or nil
|
||||
or math.max(0, math.min(stats.hp, math.floor(num(packed.hp, stats.hp))))
|
||||
local status = forced and nil or text(packed.status)
|
||||
local packedOtId = num(packed.otId)
|
||||
local otId = packedOtId
|
||||
and math.max(0, math.min(65535, math.floor(packedOtId))) or nil
|
||||
local ot = type(packed.ot) == "string" and packed.ot:sub(1, 10) or nil
|
||||
local growth = Mon.growthFor(data, def.growthRate)
|
||||
local mon = {
|
||||
species = packed.species,
|
||||
name = def.name or packed.species,
|
||||
nickname = packed.nickname,
|
||||
nickname = text(packed.nickname),
|
||||
level = level,
|
||||
experience = math.max(0, math.floor(packed.experience
|
||||
or Mon.experienceForLevel(growth, level))),
|
||||
experience = math.max(0, math.floor(num(packed.experience,
|
||||
Mon.experienceForLevel(growth, level)))),
|
||||
dvs = dvs,
|
||||
statExp = statExp,
|
||||
stats = stats,
|
||||
@@ -328,10 +362,10 @@ function Protocol.unpackMon2(data, packed, opts)
|
||||
-- GiveEgg starts a hatched mon at 120 and a caught one at 70; a traded mon
|
||||
-- keeps what it arrived with, clamped to the byte the cart stores it in
|
||||
happiness = math.max(0, math.min(255,
|
||||
math.floor(packed.happiness or 70))),
|
||||
pokerus = math.max(0, math.min(255, math.floor(packed.pokerus or 0))),
|
||||
math.floor(num(packed.happiness, 70)))),
|
||||
pokerus = math.max(0, math.min(255, math.floor(num(packed.pokerus, 0)))),
|
||||
caughtLevel = math.max(1, math.min(Mon.MAX_LEVEL,
|
||||
math.floor(packed.caughtLevel or level))),
|
||||
math.floor(num(packed.caughtLevel, level)))),
|
||||
ot = ot,
|
||||
otName = ot,
|
||||
otId = otId,
|
||||
@@ -339,7 +373,7 @@ function Protocol.unpackMon2(data, packed, opts)
|
||||
}
|
||||
if packed.isEgg then
|
||||
mon.isEgg = true
|
||||
mon.eggSteps = math.max(0, math.floor(packed.eggSteps or 0))
|
||||
mon.eggSteps = math.max(0, math.floor(num(packed.eggSteps, 0)))
|
||||
end
|
||||
-- Derived from the DVs on the RECEIVING side, exactly as they were derived on
|
||||
-- the sending one: shininess, gender and an Unown's letter are all functions
|
||||
@@ -401,17 +435,19 @@ end
|
||||
-- which slots are in play and a pick can never land on a different mon.
|
||||
function Protocol.eligibleParty(party, myRecords, theirRecords)
|
||||
local eligible, reasons = {}, {}
|
||||
theirRecords = theirRecords or {}
|
||||
local theirSpecies = theirRecords.pokemon or {}
|
||||
local theirMoves = theirRecords.moves or {}
|
||||
local mySpecies = (myRecords or {}).pokemon or {}
|
||||
local myMoves = (myRecords or {}).moves or {}
|
||||
theirRecords = tbl(theirRecords)
|
||||
myRecords = tbl(myRecords)
|
||||
local theirSpecies = tbl(theirRecords.pokemon)
|
||||
local theirMoves = tbl(theirRecords.moves)
|
||||
local mySpecies = tbl(myRecords.pokemon)
|
||||
local myMoves = tbl(myRecords.moves)
|
||||
-- Gen 2 only, and absent on both sides of a Gen 1 trade, which is what keeps
|
||||
-- the loop below unchanged for Red: a mon with no `item` never reaches the
|
||||
-- held-item arm at all.
|
||||
local theirHeld = theirRecords.heldItems
|
||||
local myHeld = (myRecords or {}).heldItems or {}
|
||||
for i, mon in ipairs(party or {}) do
|
||||
local theirHeld = theirRecords.heldItems ~= nil
|
||||
and tbl(theirRecords.heldItems) or nil
|
||||
local myHeld = tbl(myRecords.heldItems)
|
||||
for i, mon in ipairs(tbl(party)) do
|
||||
local reason
|
||||
if not theirSpecies[mon.species] then
|
||||
reason = "not on the other game"
|
||||
@@ -551,7 +587,7 @@ function TradeSession:handle(msg)
|
||||
end
|
||||
if self.stage == "waitParty" then self.stage = "picking" end
|
||||
elseif msg.type == "pick" then
|
||||
self.theirPick = msg.index
|
||||
self.theirPick = num(msg.index)
|
||||
self:advance()
|
||||
elseif msg.type == "confirm" then
|
||||
self.theirConfirm = msg.ok
|
||||
@@ -583,7 +619,18 @@ function TradeSession:confirm(ok)
|
||||
return { type = "confirm", ok = ok }
|
||||
end
|
||||
|
||||
function TradeSession:pickResolves()
|
||||
local index = self.theirPick
|
||||
if type(index) ~= "number" then return false end
|
||||
return self.theirParty ~= nil and self.theirParty[index] ~= nil
|
||||
end
|
||||
|
||||
function TradeSession:advance()
|
||||
if self.theirPick ~= nil and self.theirParty and not self:pickResolves() then
|
||||
self.stage = "cancelled"
|
||||
self.error = "the other game picked a POKéMON that isn't there"
|
||||
return
|
||||
end
|
||||
if self.stage == "picking" and self.myPick then
|
||||
self.stage = self.theirPick and "confirming" or "waitPick"
|
||||
elseif self.stage == "waitPick" and self.theirPick then
|
||||
|
||||
+12
-9
@@ -1,3 +1,6 @@
|
||||
local Logger = require("src.core.Logger")
|
||||
local Wire = require("src.link.Wire")
|
||||
|
||||
local Session = {}
|
||||
Session.__index = Session
|
||||
|
||||
@@ -23,6 +26,7 @@ function Session.new(transport, options)
|
||||
_status = "connecting",
|
||||
_terminal = nil,
|
||||
_transportCloseCalled = false,
|
||||
dropped = 0,
|
||||
paired = false,
|
||||
closed = false,
|
||||
error = nil,
|
||||
@@ -120,16 +124,15 @@ function Session:update()
|
||||
end
|
||||
else
|
||||
for index = 1, #messages do
|
||||
local message = messages[index]
|
||||
if type(message) ~= "table" or type(message.type) ~= "string" then
|
||||
if not failureReason then
|
||||
failureReason = "protocol_error"
|
||||
failureDetail = ("message %d must be a table with string type")
|
||||
:format(index)
|
||||
end
|
||||
break
|
||||
local raw = messages[index]
|
||||
local ok, message = pcall(Wire.sanitize, raw)
|
||||
if ok and message then
|
||||
self._inbox[#self._inbox + 1] = message
|
||||
else
|
||||
self.dropped = (self.dropped or 0) + 1
|
||||
local label = type(raw) == "table" and tostring(raw.type) or type(raw)
|
||||
Logger.warn("link: dropped malformed message (%s)", label)
|
||||
end
|
||||
self._inbox[#self._inbox + 1] = message
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
local Wire = {}
|
||||
|
||||
local MAX_INT = 2147483647
|
||||
local MAX_STRING = 64
|
||||
local MAX_NAME = 40
|
||||
local MAX_LIST = 64
|
||||
local MAX_PARTY = 32
|
||||
local MAX_MOVES = 8
|
||||
local MAX_MODS = 256
|
||||
local MAX_RECORDS = 4096
|
||||
local MAX_EXTRA_DEPTH = 8
|
||||
local MAX_ROUNDS = 16
|
||||
local MAX_MATCHES = 128
|
||||
|
||||
function Wire.num(v, default, min, max)
|
||||
local n = tonumber(v)
|
||||
if n == nil or n ~= n then return default end
|
||||
min = min or -MAX_INT
|
||||
max = max or MAX_INT
|
||||
if n < min then return min end
|
||||
if n > max then return max end
|
||||
return math.floor(n)
|
||||
end
|
||||
|
||||
function Wire.str(v, default, maxLen)
|
||||
if type(v) ~= "string" then return default end
|
||||
maxLen = maxLen or MAX_STRING
|
||||
if #v > maxLen then return v:sub(1, maxLen) end
|
||||
return v
|
||||
end
|
||||
|
||||
function Wire.bool(v, default)
|
||||
if type(v) == "boolean" then return v end
|
||||
return default
|
||||
end
|
||||
|
||||
function Wire.list(v, maxN, fn)
|
||||
local out = {}
|
||||
if type(v) ~= "table" then return out end
|
||||
local n = math.min(#v, maxN or MAX_LIST)
|
||||
for i = 1, n do
|
||||
local entry = fn(v[i])
|
||||
if entry ~= nil then out[#out + 1] = entry end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function Wire.records(v)
|
||||
local out = {}
|
||||
if type(v) ~= "table" then return out end
|
||||
local n = 0
|
||||
for k, val in pairs(v) do
|
||||
if type(k) == "string" then
|
||||
out[k] = Wire.str(val, nil, MAX_STRING) or tostring(Wire.num(val, 0))
|
||||
n = n + 1
|
||||
if n >= MAX_RECORDS then break end
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function Wire.plain(v, depth)
|
||||
if type(v) ~= "table" then return nil end
|
||||
depth = depth or 0
|
||||
if depth > MAX_EXTRA_DEPTH then return nil end
|
||||
local out = {}
|
||||
for k, val in pairs(v) do
|
||||
local kt, vt = type(k), type(val)
|
||||
if kt == "string" or kt == "number" then
|
||||
if vt == "string" then out[k] = Wire.str(val, nil, MAX_STRING)
|
||||
elseif vt == "number" or vt == "boolean" then out[k] = val
|
||||
elseif vt == "table" then out[k] = Wire.plain(val, depth + 1) end
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local STAT_KEYS = { "hp", "attack", "defense", "speed", "special" }
|
||||
|
||||
local function statMap(v)
|
||||
local out = {}
|
||||
if type(v) ~= "table" then return out end
|
||||
for _, k in ipairs(STAT_KEYS) do
|
||||
out[k] = Wire.num(v[k], nil, 0, 65535)
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local function move(v)
|
||||
if type(v) ~= "table" then return { id = nil } end
|
||||
return {
|
||||
id = Wire.str(v.id, nil, MAX_STRING),
|
||||
pp = Wire.num(v.pp, nil, 0, 255),
|
||||
ppUps = Wire.num(v.ppUps, nil, 0, 255),
|
||||
maxPp = Wire.num(v.maxPp, nil, 0, 255),
|
||||
}
|
||||
end
|
||||
|
||||
local function mon(v)
|
||||
if type(v) ~= "table" then return {} end
|
||||
return {
|
||||
species = Wire.str(v.species, nil, MAX_STRING),
|
||||
level = Wire.num(v.level, nil, 0, 65535),
|
||||
exp = Wire.num(v.exp, nil, 0, MAX_INT),
|
||||
experience = Wire.num(v.experience, nil, 0, MAX_INT),
|
||||
hp = Wire.num(v.hp, nil, 0, 65535),
|
||||
status = Wire.str(v.status, nil, MAX_STRING),
|
||||
nickname = Wire.str(v.nickname, nil, MAX_NAME),
|
||||
dvs = statMap(v.dvs),
|
||||
statExp = statMap(v.statExp),
|
||||
moves = Wire.list(v.moves, MAX_MOVES, move),
|
||||
ot = Wire.str(v.ot, nil, MAX_NAME),
|
||||
otId = Wire.num(v.otId, nil, 0, MAX_INT),
|
||||
item = Wire.str(v.item, nil, MAX_STRING),
|
||||
happiness = Wire.num(v.happiness, nil, 0, 65535),
|
||||
pokerus = Wire.num(v.pokerus, nil, 0, 65535),
|
||||
caughtLevel = Wire.num(v.caughtLevel, nil, 0, 65535),
|
||||
isEgg = Wire.bool(v.isEgg, nil),
|
||||
eggSteps = Wire.num(v.eggSteps, nil, 0, MAX_INT),
|
||||
extra = Wire.plain(v.extra),
|
||||
}
|
||||
end
|
||||
|
||||
local function modEntry(v)
|
||||
if type(v) ~= "table" then return nil end
|
||||
return {
|
||||
id = Wire.str(v.id, nil, MAX_NAME),
|
||||
version = Wire.str(v.version, nil, MAX_NAME)
|
||||
or Wire.num(v.version, nil, 0, MAX_INT),
|
||||
affectsLink = Wire.bool(v.affectsLink, nil),
|
||||
language = Wire.bool(v.language, nil),
|
||||
}
|
||||
end
|
||||
|
||||
local function name(v)
|
||||
return Wire.str(v, nil, MAX_NAME)
|
||||
end
|
||||
|
||||
local sanitize
|
||||
|
||||
local SCHEMAS = {}
|
||||
|
||||
SCHEMAS.hello = function(m)
|
||||
return {
|
||||
protocol = Wire.num(m.protocol, nil, 0, MAX_INT),
|
||||
name = name(m.name),
|
||||
mode = Wire.str(m.mode, nil, MAX_STRING),
|
||||
engineVersion = Wire.str(m.engineVersion, nil, MAX_STRING),
|
||||
apiVersion = Wire.str(m.apiVersion, nil, MAX_STRING),
|
||||
generation = Wire.num(m.generation, nil, 0, 255),
|
||||
fingerprint = Wire.str(m.fingerprint, nil, MAX_STRING),
|
||||
linkModified = Wire.bool(m.linkModified, nil),
|
||||
mods = Wire.list(m.mods, MAX_MODS, modEntry),
|
||||
}
|
||||
end
|
||||
|
||||
SCHEMAS.records = function(m)
|
||||
return {
|
||||
pokemon = Wire.records(m.pokemon),
|
||||
moves = Wire.records(m.moves),
|
||||
heldItems = m.heldItems ~= nil and Wire.records(m.heldItems) or nil,
|
||||
}
|
||||
end
|
||||
|
||||
SCHEMAS.party = function(m)
|
||||
return {
|
||||
mons = Wire.list(m.mons, MAX_PARTY, mon),
|
||||
seed = Wire.num(m.seed, nil, 0, MAX_INT),
|
||||
forceLevel = Wire.num(m.forceLevel, nil, 0, 65535),
|
||||
}
|
||||
end
|
||||
|
||||
SCHEMAS.pick = function(m)
|
||||
return { index = Wire.num(m.index, nil, -MAX_INT, MAX_INT) }
|
||||
end
|
||||
|
||||
SCHEMAS.confirm = function(m)
|
||||
return { ok = Wire.bool(m.ok, false) }
|
||||
end
|
||||
|
||||
SCHEMAS.action = function(m)
|
||||
return {
|
||||
kind = Wire.str(m.kind, "", MAX_STRING),
|
||||
slot = Wire.num(m.slot, nil, 1, MAX_MOVES),
|
||||
index = Wire.num(m.index, nil, 1, MAX_PARTY),
|
||||
}
|
||||
end
|
||||
|
||||
SCHEMAS.hash = function(m)
|
||||
local parts
|
||||
if type(m.parts) == "table" then
|
||||
parts = {
|
||||
actives = Wire.str(m.parts.actives, nil, MAX_STRING),
|
||||
volatile = Wire.str(m.parts.volatile, nil, MAX_STRING),
|
||||
bench = Wire.str(m.parts.bench, nil, MAX_STRING),
|
||||
}
|
||||
end
|
||||
return {
|
||||
turn = Wire.num(m.turn, 0, 0, MAX_INT),
|
||||
value = Wire.str(m.value, nil, MAX_STRING),
|
||||
parts = parts,
|
||||
}
|
||||
end
|
||||
|
||||
SCHEMAS.replace = function(m)
|
||||
return { index = Wire.num(m.index, 1, 1, MAX_PARTY) }
|
||||
end
|
||||
|
||||
SCHEMAS.bye = function() return {} end
|
||||
SCHEMAS.forfeit = function() return {} end
|
||||
|
||||
SCHEMAS.hosted = function(m)
|
||||
return { code = Wire.str(m.code, nil, MAX_NAME) }
|
||||
end
|
||||
SCHEMAS.paired = function() return {} end
|
||||
SCHEMAS.peer_gone = function() return {} end
|
||||
SCHEMAS.join_error = function(m)
|
||||
return { reason = Wire.str(m.reason, "", MAX_STRING) }
|
||||
end
|
||||
|
||||
local function rule(m)
|
||||
return {
|
||||
requiredPartySize = Wire.num(m.requiredPartySize, nil, 0, 255),
|
||||
minLevel = Wire.num(m.minLevel, nil, 0, 65535),
|
||||
maxLevel = Wire.num(m.maxLevel, nil, 0, 65535),
|
||||
turnLimit = Wire.num(m.turnLimit, nil, 0, 65535),
|
||||
forceLevel = Wire.num(m.forceLevel, nil, 0, 65535),
|
||||
}
|
||||
end
|
||||
|
||||
SCHEMAS.tournament_hosted = function(m)
|
||||
local out = rule(m)
|
||||
out.code = Wire.str(m.code, nil, MAX_NAME)
|
||||
out.participating = Wire.bool(m.participating, nil)
|
||||
return out
|
||||
end
|
||||
|
||||
SCHEMAS.tournament_host_error = function(m)
|
||||
local out = rule(m)
|
||||
out.reason = Wire.str(m.reason, "", MAX_STRING)
|
||||
return out
|
||||
end
|
||||
|
||||
SCHEMAS.tournament_join_error = SCHEMAS.tournament_host_error
|
||||
|
||||
SCHEMAS.tournament_roster = function(m)
|
||||
local out = rule(m)
|
||||
out.players = Wire.list(m.players, MAX_MATCHES, name)
|
||||
out.spectators = Wire.list(m.spectators, MAX_MATCHES, name)
|
||||
return out
|
||||
end
|
||||
|
||||
local function match(v)
|
||||
if type(v) ~= "table" then return nil end
|
||||
return {
|
||||
a = name(v.a), b = name(v.b), winner = name(v.winner),
|
||||
bye = Wire.bool(v.bye, false),
|
||||
state = Wire.str(v.state, nil, MAX_STRING),
|
||||
}
|
||||
end
|
||||
|
||||
local function round(v)
|
||||
if type(v) ~= "table" then return nil end
|
||||
return {
|
||||
round = Wire.num(v.round, 0, 0, MAX_ROUNDS),
|
||||
matches = Wire.list(v.matches, MAX_MATCHES, match),
|
||||
}
|
||||
end
|
||||
|
||||
SCHEMAS.bracket_update = function(m)
|
||||
local t = type(m.tournament) == "table" and m.tournament or {}
|
||||
local out = rule(t)
|
||||
out.code = Wire.str(t.code, nil, MAX_NAME)
|
||||
out.status = Wire.str(t.status, nil, MAX_STRING)
|
||||
out.round = Wire.num(t.round, 0, 0, MAX_ROUNDS)
|
||||
out.champion = name(t.champion)
|
||||
out.rounds = Wire.list(t.rounds, MAX_ROUNDS, round)
|
||||
return { tournament = out }
|
||||
end
|
||||
|
||||
SCHEMAS.match_start = function(m)
|
||||
return {
|
||||
opponent = name(m.opponent),
|
||||
round = Wire.num(m.round, 0, 0, MAX_ROUNDS),
|
||||
turnLimit = Wire.num(m.turnLimit, nil, 0, 65535),
|
||||
role = Wire.str(m.role, "", MAX_STRING),
|
||||
}
|
||||
end
|
||||
|
||||
SCHEMAS.match_start_spectate = function(m)
|
||||
return {
|
||||
round = Wire.num(m.round, 0, 0, MAX_ROUNDS),
|
||||
playerHost = name(m.playerHost),
|
||||
playerGuest = name(m.playerGuest),
|
||||
}
|
||||
end
|
||||
|
||||
SCHEMAS.tournament_bye = function(m)
|
||||
return { round = Wire.num(m.round, 0, 0, MAX_ROUNDS) }
|
||||
end
|
||||
|
||||
SCHEMAS.tournament_over = function(m)
|
||||
return { champion = name(m.champion) }
|
||||
end
|
||||
|
||||
local SPECTATABLE = {
|
||||
action = true, replace = true, bye = true, forfeit = true,
|
||||
hello = true, party = true, hash = true,
|
||||
}
|
||||
|
||||
SCHEMAS.spectate = function(m)
|
||||
if type(m.msg) ~= "table" or not SPECTATABLE[m.msg.type] then return nil end
|
||||
local inner = sanitize(m.msg)
|
||||
if not inner then return nil end
|
||||
return { side = Wire.str(m.side, "", MAX_STRING), msg = inner }
|
||||
end
|
||||
|
||||
Wire.SCHEMAS = SCHEMAS
|
||||
|
||||
local function passthrough(m)
|
||||
local out = Wire.plain(m) or {}
|
||||
out.type = nil
|
||||
return out
|
||||
end
|
||||
|
||||
sanitize = function(msg)
|
||||
if type(msg) ~= "table" then return nil end
|
||||
local kind = msg.type
|
||||
if type(kind) ~= "string" or #kind > MAX_STRING then return nil end
|
||||
local schema = SCHEMAS[kind]
|
||||
local out
|
||||
if schema then
|
||||
out = schema(msg)
|
||||
if not out then return nil end
|
||||
else
|
||||
out = passthrough(msg)
|
||||
end
|
||||
out.type = kind
|
||||
return out
|
||||
end
|
||||
|
||||
Wire.sanitize = sanitize
|
||||
|
||||
return Wire
|
||||
Reference in New Issue
Block a user