Files
CSMH/Client/CMH.lua
2021-07-21 17:01:33 +02:00

292 lines
8.2 KiB
Lua

local debug = false
local CMH = {}
local datacache = {}
local delim = {"", "", "", "", ""}
local pck = {REQ = 1, DAT = 2}
-- HELPERS START
local function debugOut(prefix, x, msg)
if(debug == true) then
print("["..date("%X", time()).."][CSMH]["..x.."]["..prefix.."]: "..msg)
end
end
local function GenerateReqId()
local length = 6
local reqId = ""
for i = 1, length do
reqId = reqId .. string.char(math.random(97, 122))
end
return reqId
end
local function ParseMessage(str)
str = str or ""
local output = {}
local valTemp = {}
local typeTemp = {}
local valMatch = "[^"..table.concat(delim).."]+"
local typeMatch = "["..table.concat(delim).."]+"
-- Get values
for value in str:gmatch(valMatch) do
table.insert(valTemp, value)
end
-- Get type from delimiter
for varType in str:gmatch(typeMatch) do
for k, v in pairs(delim) do
if(v == varType) then
table.insert(typeTemp, k)
end
end
end
-- Convert value to correct type
for k, v in pairs(valTemp) do
local varType = typeTemp[k]
if(varType == 3) then -- Ints
v = tonumber(v)
elseif(varType == 4) then -- Tables
v = Smallfolk.loads(v, #v)
elseif(varType == 5) then -- Booleans
if(v == "true") then v = true else v = false end
end
table.insert(output, v)
end
valTemp = nil
typeTemp = nil
return output
end
local function ProcessVariables(reqId, ...)
local splitLength = 200
local arg = {...}
local msg = ""
for _, v in pairs(arg) do
if(type(v) == "string") then
msg = msg .. delim[2]
elseif(type(v) == "number") then
msg = msg .. delim[3]
elseif(type(v) == "table") then
-- use Smallfolk to convert table structure to string
v = Smallfolk.dumps(v)
msg = msg .. delim[4]
elseif(type(v) == "boolean") then
v = tostring(v)
msg = msg .. delim[5]
end
msg = msg .. v
end
if not datacache[reqId] then
datacache[reqId] = { count = 0, data = {}}
end
for i=1, msg:len(), splitLength do
datacache[reqId].count = datacache[reqId].count + 1
datacache[reqId]["data"][datacache[reqId].count] = msg:sub(i,i+splitLength - 1)
end
return datacache[reqId]
end
-- HELPERS END
-- Rx START
function CMH.OnReceive(self, event, header, data, Type, sender)
-- Ensure the sender and receiver is the same, the message is an addon message, and the message type is WHISPER
if event == "CHAT_MSG_ADDON" and sender == UnitName("player") and Type == "WHISPER" then
-- unpack and validate addon message structure
local pfx, source, pckId = header: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 == delim[1] and source == "S") then
-- convert ID to number so we can compare with our packet list
pckId = tonumber(pckId)
if(pckId == pck.REQ) then
debugOut("REQ", "Rx", "REQ received, data: "..data)
CMH.OnREQ(sender, data)
elseif(pckId == pck.DAT) then
debugOut("DAT", "Rx", "DAT received, data: "..data)
CMH.OnDAT(sender, data)
else
debugOut("ERR", "Rx", "Invalid packet type, aborting")
return
end
end
end
end
local CMHFrame = CreateFrame("Frame")
CMHFrame:RegisterEvent("CHAT_MSG_ADDON")
CMHFrame:SetScript("OnEvent", CMH.OnReceive)
function CMH.OnREQ(sender, data)
debugOut("REQ", "Rx", "Processing 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%d)(%w%w%w%w%w%w)(.+)");
if not functionId or not linkCount or not reqId or not addon then
debugOut("REQ", "Rx", "Malformed 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 CMH[addon] then
debugOut("REQ", "Rx", "Invalid addon, aborting.")
return
end
-- if the functionId does not exist for said addon, abort
if not CMH[addon][functionId] then
debugOut("REQ", "Rx", "Invalid addon function, aborting.")
return
end
-- 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[reqId]) then
datacache[reqId] = nil
debugOut("REQ", "Rx", "Cache already exists, aborting.")
return
end
-- Insert header info for request id and prepare temporary data storage
datacache[reqId] = {addon = addon, funcId = functionId, count = linkCount, data = {}}
debugOut("REQ", "Rx", "Header validated, cache created. Awaitng data..")
end
function CMH.OnDAT(sender, data)
debugOut("DAT", "Rx", "Validating data..")
-- Separate REQ ID from payload and verify
local reqId = data:sub(1, 6)
local payload = data:sub(#reqId+1)
if not reqId then
debugOut("DAT", "Rx", "Request ID missing, aborting.")
return
end
-- If no REQ header info has been cached, abort
if not datacache[reqId] then
debugOut("DAT", "Rx", "Cache does not exist, aborting.")
return
end
local reqTable = datacache[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
debugOut("DAT", "Rx", "Function expects no data, triggering function..")
-- Retrieve the function from global namespace and pass variables if it exists
local func = CMH[reqTable.addon][reqTable.funcId]
if func then
_G[func](sender, {})
datacache[reqId] = nil
debugOut("DAT", "Rx", "Function "..func.." @ "..reqTable.addon.." executed, cache cleared.")
end
return
end
-- If the size of the cache is larger than expected, abort
if sizeOfDataCache+1 > reqTable.count then
debugOut("DAT", "Rx", "Received more data than expected. Aborting.")
return
end
-- Add payload to cache and update size variable
reqTable["data"][sizeOfDataCache+1] = payload
sizeOfDataCache = #reqTable.data
debugOut("DAT", "Rx", "Data part "..sizeOfDataCache.." of "..reqTable.count.." added to cache.")
-- If the last expected message has been received, process it
if(sizeOfDataCache == reqTable.count) then
debugOut("DAT", "Rx", "All expected data received, processing..")
-- Concatenate the cache and parse the full payload for function variables to return
local fullPayload = table.concat(reqTable.data);
local VarTable = ParseMessage(fullPayload)
-- Retrieve the function from global namespace and pass variables if it exists
local func = CMH[reqTable.addon][reqTable.funcId]
if func then
_G[func](sender, VarTable)
datacache[reqId] = nil
debugOut("DAT", "Rx", "Function "..func.." @ "..reqTable.addon.." executed, cache cleared.")
end
end
end
-- Rx END
-- Tx START
function CMH.SendREQ(functionId, linkCount, reqId, addon)
local header = string.format("%01s%01s%02d", delim[1], "C", pck.REQ)
local data = string.format("%02d%03d%06s%0"..tostring(#addon).."s", functionId, linkCount, reqId, addon)
SendAddonMessage(header, data, "WHISPER", UnitName("player"))
debugOut("REQ", "Tx", "Sent REQ with ID "..reqId..", sending DAT..")
end
function CMH.SendDAT(reqId)
-- Build data message header
local header = string.format("%01s%01s%02d", delim[1], "C", pck.DAT)
-- 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[reqId]["data"] == 0) then
SendAddonMessage(header, reqId, "WHISPER", UnitName("player"))
else
for _, v in pairs (datacache[reqId]["data"]) do
local payload = reqId..v
SendAddonMessage(header, payload, "WHISPER", UnitName("player"))
end
end
-- all items have been sent, cache can be purged
datacache[reqId] = nil
debugOut("DAT", "Tx", "Sent all DAT for ID "..reqId..", cache cleared, closing.")
end
-- Tx END
-- API START
function RegisterServerResponses(config)
if(CMH[config.Prefix]) then
return;
end
CMH[config.Prefix] = {}
for functionId, functionName in pairs(config.Functions) do
CMH[config.Prefix][functionId] = functionName
end
end
function SendClientRequest(prefix, functionId, ...)
local reqId = GenerateReqId()
local varTable = ProcessVariables(reqId, ...)
CMH.SendREQ(functionId, varTable.count, reqId, prefix)
CMH.SendDAT(reqId)
end
--A API END