Complete rewrite of message handler
This commit is contained in:
362
Server/SMH.lua
362
Server/SMH.lua
@ -1,70 +1,36 @@
|
||||
local smallfolk = smallfolk or require("smallfolk")
|
||||
|
||||
local debug = false
|
||||
|
||||
local SMH = {}
|
||||
local links = {}
|
||||
local datacache = {}
|
||||
|
||||
function SMH.OnReceive(event, sender, _type, prefix, _, target)
|
||||
if not sender or not target or not sender.GetName or not target.GetName or type(sender) ~= "userdata" or type(target) ~= "userdata" then
|
||||
return
|
||||
end
|
||||
if sender:GetName() == target:GetName() and _type == 7 then
|
||||
local source, functionId, link, linkCount, MSG = prefix:match("(%D)(%d%d%d)(%d%d)(%d%d)(.+)");
|
||||
if not source or not functionId or not link or not linkCount or not MSG then
|
||||
return
|
||||
end
|
||||
if(source == "C") then
|
||||
functionId, link, linkCount = tonumber(functionId), tonumber(link), tonumber(linkCount);
|
||||
|
||||
links[sender:GetGUIDLow()] = links[sender:GetGUIDLow()] or {}
|
||||
links[sender:GetGUIDLow()][functionId] = links[sender:GetGUIDLow()][functionId] or {count = 0};
|
||||
links[sender:GetGUIDLow()][functionId][link] = MSG;
|
||||
links[sender:GetGUIDLow()][functionId].count = links[sender:GetGUIDLow()][functionId].count + 1;
|
||||
if (links[sender:GetGUIDLow()][functionId].count ~= linkCount) then
|
||||
return
|
||||
end
|
||||
|
||||
local fullMessage = table.concat(links[sender:GetGUIDLow()][functionId]);
|
||||
links[sender:GetGUIDLow()][functionId] = {count = 0};
|
||||
|
||||
local VarTable = ParseMessage(fullMessage)
|
||||
if not VarTable then
|
||||
return
|
||||
end
|
||||
|
||||
if not(SMH[VarTable[1]]) then
|
||||
return
|
||||
end
|
||||
local CSMHMsgPrefix = "♠"
|
||||
local delim = {"♥", "♚", "♛", "♜"}
|
||||
local pck = {REQ = 1, ACK = 2, DAT = 3, NAK = 4}
|
||||
|
||||
local func = SMH[VarTable[1]][functionId]
|
||||
if func then
|
||||
_G[func](sender, VarTable)
|
||||
end
|
||||
return
|
||||
end
|
||||
-- HELPERS START
|
||||
local function debugOut(msg)
|
||||
if(debug == true) then
|
||||
print("SMH Debug: "..msg)
|
||||
end
|
||||
end
|
||||
|
||||
RegisterServerEvent(30, SMH.OnReceive)
|
||||
local function GenerateReqId()
|
||||
local length = 6
|
||||
local reqId = ""
|
||||
|
||||
function RegisterClientRequests(config)
|
||||
-- If a config table with the Prefix already exists, abort loading it into the register.
|
||||
if(SMH[config.Prefix]) then
|
||||
return;
|
||||
end
|
||||
|
||||
-- Create subtable for PrefixName
|
||||
SMH[config.Prefix] = {}
|
||||
|
||||
-- Insert function ID and function name into the register table.
|
||||
for functionId, functionName in pairs(config.Functions) do
|
||||
SMH[config.Prefix][functionId] = functionName
|
||||
for i = 1, length do
|
||||
reqId = reqId .. string.char(math.random(97, 122))
|
||||
end
|
||||
|
||||
return reqId
|
||||
end
|
||||
|
||||
function ParseMessage(str)
|
||||
local function ParseMessage(prefix, str)
|
||||
local output = {}
|
||||
local valTemp = {}
|
||||
local typeTemp = {}
|
||||
local delim = {"♠", "♥", "♚", "♛", "♜"}
|
||||
|
||||
local valMatch = "[^"..table.concat(delim).."]+"
|
||||
local typeMatch = "["..table.concat(delim).."]+"
|
||||
@ -86,11 +52,11 @@ function ParseMessage(str)
|
||||
-- Convert value to correct type
|
||||
for k, v in pairs(valTemp) do
|
||||
local varType = typeTemp[k]
|
||||
if(varType == 3) then -- Ints
|
||||
if(varType == 2) then -- Ints
|
||||
v = tonumber(v)
|
||||
elseif(varType == 4) then -- Tables
|
||||
elseif(varType == 3) then -- Tables
|
||||
v = smallfolk.loads(v)
|
||||
elseif(varType == 5) then -- Booleans
|
||||
elseif(varType == 4) then -- Booleans
|
||||
if(v == "true") then v = true else v = false end
|
||||
end
|
||||
table.insert(output, v)
|
||||
@ -102,44 +68,274 @@ function ParseMessage(str)
|
||||
return output
|
||||
end
|
||||
|
||||
function Player:SendServerResponse(prefix, functionId, ...)
|
||||
-- ♠ = Prefix prefix
|
||||
-- ♥ = ArgumentPrefix for Strings
|
||||
-- ♚ = ArgumentPrefix for Ints
|
||||
-- ♛ = ArgumentPrefix for Tables
|
||||
-- ♜ = ArgumentPrefix for Boolean
|
||||
|
||||
local function ProcessVariables(sender, reqId, ...)
|
||||
local arg = {...}
|
||||
local splitLength = 230
|
||||
local msg = "♠" .. prefix
|
||||
local splitLength = 200
|
||||
local msg = ""
|
||||
|
||||
for _, v in pairs(arg) do
|
||||
if(type(v) == "string") then
|
||||
msg = msg .. "♥"
|
||||
msg = msg .. delim[1]
|
||||
elseif(type(v) == "number") then
|
||||
msg = msg .. "♚"
|
||||
msg = msg .. delim[2]
|
||||
elseif(type(v) == "table") then
|
||||
-- use Smallfolk to convert table structure to string
|
||||
v = smallfolk.dumps(v)
|
||||
msg = msg .. "♛"
|
||||
v = Smallfolk.dumps(v)
|
||||
msg = msg .. delim[3]
|
||||
elseif(type(v) == "boolean") then
|
||||
v = tostring(v)
|
||||
msg = msg .. "♜"
|
||||
msg = msg .. delim[4]
|
||||
end
|
||||
msg = msg .. v
|
||||
end
|
||||
|
||||
local splits = math.ceil(msg:len() / splitLength)
|
||||
local send
|
||||
local counter = 1
|
||||
for i=1, msg:len(), splitLength do
|
||||
send = string.format("%01s%02d%03d%03d", "S", functionId, counter, splits)
|
||||
if ((i + splitLength) > msg:len()) then
|
||||
send = send .. msg:sub(i, msg:len())
|
||||
else
|
||||
send = send .. msg:sub(i, i + splitLength - 1)
|
||||
end
|
||||
counter = counter + 1
|
||||
self:SendAddonMessage(send, "", 7, self)
|
||||
datacache[sender:GetGUIDLow()] = datacache[sender:GetGUIDLow()] or {}
|
||||
|
||||
if not datacache[sender:GetGUIDLow()][reqId] then
|
||||
datacache[sender:GetGUIDLow()][reqId] = { count = 1, data = {}}
|
||||
end
|
||||
end
|
||||
|
||||
for i=1, msg:len(), splitLength do
|
||||
datacache[sender:GetGUIDLow()][reqId]["data"][#datacache[sender:GetGUIDLow()][reqId]["data"]+1] = msg:sub(i,i+splitLength - 1)
|
||||
datacache[sender:GetGUIDLow()][reqId].count = datacache[sender:GetGUIDLow()][reqId].count + 1
|
||||
end
|
||||
|
||||
return datacache[sender:GetGUIDLow()][reqId]
|
||||
end
|
||||
|
||||
-- HELPERS END
|
||||
|
||||
-- Rx START
|
||||
|
||||
function SMH.OnReceive(event, sender, _type, prefix, _, target)
|
||||
-- Make sure the sender and receiver of the addon message is set and is the correct type.
|
||||
-- Prevents error spam in the console
|
||||
if not sender or not target or not sender.GetName or not target.GetName or type(sender) ~= "userdata" or type(target) ~= "userdata" then
|
||||
return
|
||||
end
|
||||
|
||||
-- Ensure the sender and receiver is the same, and the message type is WHISPER
|
||||
if sender:GetName() == target:GetName() and _type == 7 then
|
||||
-- unpack and validate addon message structure
|
||||
local pfx, source, pckId, data = prefix:match("(...)(%u)(%d%d)(.+)")
|
||||
if not pfx or not source or not pckId then
|
||||
return
|
||||
end
|
||||
|
||||
-- Make sure we're only processing addon messages using our framework prefix character as well as client messages
|
||||
if(pfx == CSMHMsgPrefix and source == "C") then
|
||||
debugOut("Received CSMH packet, processing data.")
|
||||
|
||||
-- convert ID to number so we can compare with our packet list
|
||||
pckId = tonumber(pckId)
|
||||
|
||||
if(pckId == pck.REQ) then
|
||||
debugOut("REQ received, data: "..data)
|
||||
SMH.OnREQ(sender, data)
|
||||
elseif(pckId == pck.ACK) then
|
||||
debugOut("ACK received, data: "..data)
|
||||
SMH.OnACK(sender, data)
|
||||
elseif(pckId == pck.DAT) then
|
||||
debugOut("DAT received, data: "..data)
|
||||
SMH.OnDAT(sender, data)
|
||||
elseif(pckId == pck.NAK) then
|
||||
debugOut("NAK received, data: "..data)
|
||||
SMH.OnNAK(sender, data)
|
||||
else
|
||||
debugOut("Invalid packet ID, aborting")
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RegisterServerEvent(30, SMH.OnReceive)
|
||||
|
||||
function SMH.OnREQ(sender, data)
|
||||
debugOut("Processing REQ data")
|
||||
-- split header string into proper variables and ensure the string is the expected format
|
||||
local functionId, linkCount, reqId, addon = data:match("(%d%d)(%d%d)(%w%w%w%w%w%w)(.+)");
|
||||
if not functionId or not linkCount or not reqId or not addon then
|
||||
debugOut("Malformed REQ data, aborting.")
|
||||
return
|
||||
end
|
||||
|
||||
-- make sure the functionId and linkCount is converted to a number
|
||||
functionId, linkCount = tonumber(functionId), tonumber(linkCount);
|
||||
|
||||
-- if the addon does not exist, abort
|
||||
if not SMH[addon] then
|
||||
SMH.SendNAK(sender, reqId)
|
||||
debugOut("Invalid addon, aborting")
|
||||
return
|
||||
end
|
||||
|
||||
-- if the functionId does not exist for said addon, abort
|
||||
if not SMH[addon][functionId] then
|
||||
SMH.SendNAK(sender, reqId)
|
||||
debugOut("Invalid addon function, aborting")
|
||||
return
|
||||
end
|
||||
|
||||
-- header is OK, create cache
|
||||
datacache[sender:GetGUIDLow()] = datacache[sender:GetGUIDLow()] or {}
|
||||
|
||||
-- the request cache already exists, this should not happen.
|
||||
-- abort and send error to the client, as well as purge id from cache.
|
||||
if(datacache[sender:GetGUIDLow()][reqId]) then
|
||||
SMH.SendNAK(sender, reqId)
|
||||
datacache[sender:GetGUIDLow()][reqId] = nil
|
||||
debugOut("Request cache already exists, aborting.")
|
||||
return
|
||||
end
|
||||
|
||||
-- Insert header info for request id and prepare temporary data storage
|
||||
datacache[sender:GetGUIDLow()][reqId] = {addon = addon, funcId = functionId, count = linkCount, data = {}}
|
||||
|
||||
-- send ACK to client notifying client that data is ready to be received
|
||||
debugOut("REQ OK, sending ACK..")
|
||||
SMH.SendACK(sender, reqId)
|
||||
end
|
||||
|
||||
function SMH.OnACK(sender, reqId)
|
||||
-- We received ACK but no data is available in cache. This should never happen
|
||||
if not datacache[sender:GetGUIDLow()][reqId] then
|
||||
debugOut("ACK received but no data available to transmit. Aborting.")
|
||||
return
|
||||
end
|
||||
|
||||
-- If data exists, we send it
|
||||
debugOut("ACK validated, data exists. Sending..")
|
||||
SMH.SendDAT(sender, reqId)
|
||||
end
|
||||
|
||||
function SMH.OnDAT(sender, data)
|
||||
-- Separate REQ ID from payload and verify
|
||||
local reqId, payload = data:match("(%w%w%w%w%w%w)(.*)");
|
||||
if not reqId then
|
||||
return
|
||||
end
|
||||
|
||||
-- If no REQ header info has been cached, abort
|
||||
if not datacache[sender:GetGUIDLow()][reqId] then
|
||||
debugOut("Data received, but not expected. Aborting.")
|
||||
return
|
||||
end
|
||||
|
||||
local reqTable = datacache[sender:GetGUIDLow()][reqId]
|
||||
local sizeOfDataCache = #reqTable.data
|
||||
|
||||
-- Some functions are trigger functions and expect no payload
|
||||
-- Skip the rest of the functionality and call the expected function
|
||||
if reqTable.count == 0 then
|
||||
-- Retrieve the function from global namespace and pass variables if it exists
|
||||
local func = SMH[reqTable.addon][reqTable.funcId]
|
||||
if func then
|
||||
debugOut(func)
|
||||
_G[func](sender, {})
|
||||
datacache[sender:GetGUIDLow()][reqId] = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- If the size of the cache is larger than expected, abort
|
||||
if sizeOfDataCache+1 > reqTable.count then
|
||||
debugOut("Received more data than expected. Aborting.")
|
||||
return
|
||||
end
|
||||
|
||||
-- Add payload to cache and update size variable
|
||||
reqTable["data"][sizeOfDataCache+1] = payload
|
||||
sizeOfDataCache = #reqTable.data
|
||||
|
||||
-- If the last expected message has been received, process it
|
||||
if(sizeOfDataCache == reqTable.count) then
|
||||
-- Concatenate the cache and parse the full payload for function variables to return
|
||||
local fullPayload = table.concat(reqTable.data);
|
||||
local VarTable = ParseMessage(reqTable.addon, fullPayload)
|
||||
|
||||
-- Retrieve the function from global namespace and pass variables if it exists
|
||||
local func = SMH[reqTable.addon][reqTable.funcId]
|
||||
if func then
|
||||
debugOut(func)
|
||||
_G[func](sender, VarTable)
|
||||
end
|
||||
|
||||
-- Delete the request session cache
|
||||
datacache[sender:GetGUIDLow()][reqId] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function SMH.OnNAK(sender, reqId)
|
||||
-- when we receive an error from the server, purge the local cache data
|
||||
debugOut("Purging cache data with REQ ID: "..reqId)
|
||||
datacache[sender:GetGUIDLow()][reqId] = nil
|
||||
end
|
||||
|
||||
-- Rx END
|
||||
|
||||
-- Tx START
|
||||
|
||||
function SMH.SendREQ(sender, functionId, linkCount, reqId, addon)
|
||||
debugOut("Sending REQ with ID: "..reqId)
|
||||
local send = string.format("%01s%01s%02d%02d%02d%06s%0"..tostring(#addon).."s", CSMHMsgPrefix, "S", pck.REQ, functionId, linkCount, reqId, addon)
|
||||
sender:SendAddonMessage(send, "", 7, sender)
|
||||
end
|
||||
|
||||
function SMH.SendACK(sender, reqId)
|
||||
local send = string.format("%01s%01s%02d%06s", CSMHMsgPrefix, "S", pck.ACK, reqId)
|
||||
sender:SendAddonMessage(send, "", 7, sender)
|
||||
end
|
||||
|
||||
function SMH.SendDAT(sender, reqId)
|
||||
-- Build data message header
|
||||
local send = string.format("%01s%01s%02d%06s", CSMHMsgPrefix, "S", pck.DAT, reqId)
|
||||
|
||||
-- iterate all items in the message data cache and send
|
||||
-- functions can also be trigger functions without any data, only send header and no payload
|
||||
if(#datacache[sender:GetGUIDLow()][reqId]["data"] == 0) then
|
||||
sender:SendAddonMessage(send, "", 7, sender)
|
||||
else
|
||||
for _, v in pairs (datacache[sender:GetGUIDLow()][reqId]["data"]) do
|
||||
local payload = send..v
|
||||
sender:SendAddonMessage(payload, "", 7, sender)
|
||||
end
|
||||
end
|
||||
|
||||
debugOut("All data sent, cleaning up cache.")
|
||||
-- all items have been sent, cache can be purged
|
||||
datacache[sender:GetGUIDLow()][reqId] = nil
|
||||
end
|
||||
|
||||
function SMH.SendNAK(sender, reqId)
|
||||
local send = string.format("%01s%01s%02d%06s", CSMHMsgPrefix, "S", pck.NAK, reqId)
|
||||
sender:SendAddonMessage(send, "", 7, sender)
|
||||
end
|
||||
|
||||
-- Tx END
|
||||
|
||||
-- API START
|
||||
|
||||
function RegisterClientRequests(config)
|
||||
-- If a config table with the Prefix already exists, abort loading it into the register.
|
||||
if(SMH[config.Prefix]) then
|
||||
return;
|
||||
end
|
||||
|
||||
-- Create subtable for PrefixName
|
||||
SMH[config.Prefix] = {}
|
||||
|
||||
-- Insert function ID and function name into the register table.
|
||||
for functionId, functionName in pairs(config.Functions) do
|
||||
SMH[config.Prefix][functionId] = functionName
|
||||
end
|
||||
end
|
||||
|
||||
function Player:SendServerResponse(prefix, functionId, ...)
|
||||
local reqId = GenerateReqId()
|
||||
local varTable = ProcessVariables(self, reqId, ...)
|
||||
|
||||
SMH.SendREQ(self, functionId, #varTable["data"], reqId, prefix)
|
||||
end
|
||||
|
||||
-- API END
|
||||
Reference in New Issue
Block a user