diff --git a/Client/CMH.lua b/Client/CMH.lua index 1eb395b..6fea3ed 100644 --- a/Client/CMH.lua +++ b/Client/CMH.lua @@ -1,65 +1,34 @@ +local debug = false + local CMH = {} +local datacache = {} -local links = {} +local CSMHMsgPrefix = "♠" +local delim = {"♥", "♚", "♛", "♜"} +local pck = {REQ = 1, ACK = 2, DAT = 3, NAK = 4} -function CMH.OnReceive(self, event, prefix, _, Type, sender) - if event == "CHAT_MSG_ADDON" and sender == UnitName("player") and Type == "WHISPER" then - local source, functionId, link, linkCount, MSG = prefix:match("(%D)(%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 == "S") then - functionId, link, linkCount = tonumber(functionId), tonumber(link), tonumber(linkCount); - links[functionId] = links[functionId] or {count = 0}; - links[functionId][link] = MSG; - links[functionId].count = links[functionId].count + 1; - if (links[functionId].count ~= linkCount) then - return - end - - local fullMessage = table.concat(links[functionId]); - links[functionId] = {count = 0}; - - local VarTable = ParseMessage(fullMessage) - if not VarTable then - return - end - - if not(CMH[VarTable[1]]) then - return - end - - local func = CMH[VarTable[1]][functionId] - if func then - _G[func](sender, VarTable) - end - return - end +-- HELPERS START +local function debugOut(msg) + if(debug == true) then + print("CMH Debug: "..msg) end end -local CMHFrame = CreateFrame("Frame") -CMHFrame:RegisterEvent("CHAT_MSG_ADDON") -CMHFrame:SetScript("OnEvent", CMH.OnReceive) +local function GenerateReqId() + local length = 6 + local reqId = "" -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 + 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).."]+" @@ -81,11 +50,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, #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) @@ -97,45 +66,262 @@ function ParseMessage(str) return output end -function SendClientRequest(prefix, functionId, ...) - -- ♠ = Prefix prefix - -- ♥ = ArgumentPrefix for Strings - -- ♚ = ArgumentPrefix for Ints - -- ♛ = ArgumentPrefix for Tables - -- ♜ = ArgumentPrefix for Boolean - +local function ProcessVariables(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 .. "♛" + 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%03d%02d%02d", "C", 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 - - SendAddonMessage(send, "", "WHISPER", UnitName("player")) + if not datacache[reqId] then + datacache[reqId] = { count = 1, data = {}} end -end \ No newline at end of file + + for i=1, msg:len(), splitLength do + datacache[reqId]["data"][#datacache[reqId]["data"]+1] = msg:sub(i,i+splitLength - 1) + datacache[reqId].count = datacache[reqId].count + 1 + end + + return datacache[reqId] +end + +-- HELPERS END + +-- Rx START + +function CMH.OnReceive(self, event, prefix, _, 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, data = prefix:match("(...)(%u)(%d%d)(.+)") + if not pfx or not source or not pckId or not data then + return + end + + -- Make sure we're only processing addon messages using our framework prefix character as welll as server messages + if(pfx == CSMHMsgPrefix and source == "S") 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) + CMH.OnREQ(sender, data) + elseif(pckId == pck.ACK) then + debugOut("ACK received, data: "..data) + CMH.OnACK(sender, data) + elseif(pckId == pck.DAT) then + debugOut("DAT received, data: "..data) + CMH.OnDAT(sender, data) + elseif(pckId == pck.NAK) then + debugOut("NAK received, data: "..data) + CMH.OnNAK(sender, data) + else + debugOut("Invalid packet ID, 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("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 CMH[addon] then + CMH.SendNAK(reqId) + debugOut("Invalid addon, aborting") + return + end + + -- if the functionId does not exist for said addon, abort + if not CMH[addon][functionId] then + CMH.SendNAK(reqId) + debugOut("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 + CMH.SendNAK(reqId) + datacache[reqId] = nil + debugOut("Request 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 = {}} + + -- send ACK to client notifying client that data is ready to be received + debugOut("REQ OK, sending ACK..") + CMH.SendACK(reqId) +end + +function CMH.OnACK(sender, reqId) + -- We received ACK but no data is available in cache. This should never happen + if not datacache[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..") + CMH.SendDAT(reqId) +end + +function CMH.OnDAT(sender, data) + -- Separate REQ ID from payload and verify + local reqId, payload = data:match("(%w%w%w%w%w%w)(.*)"); + if not reqId and not payload then + return + end + + -- If no REQ header info has been cached, abort + if not datacache[reqId] then + debugOut("Data received, but not expected. 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 + -- 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[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 = CMH[reqTable.addon][reqTable.funcId] + if func then + debugOut(func) + _G[func](sender, VarTable) + end + + -- Delete the request session cache + datacache[reqId] = nil + end +end + +function CMH.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[reqId] = nil +end + +-- Rx END + +-- Tx START + +function CMH.SendREQ(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, "C", pck.REQ, functionId, linkCount, reqId, addon) + SendAddonMessage(send, "", "WHISPER", UnitName("player")) +end + +function CMH.SendACK(reqId) + local send = string.format("%01s%01s%02d%06s", CSMHMsgPrefix, "C", pck.ACK, reqId) + SendAddonMessage(send, "", "WHISPER", UnitName("player")) +end + +function CMH.SendDAT(reqId) + -- Build data message header + local send = string.format("%01s%01s%02d%06s", CSMHMsgPrefix, "C", 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[reqId]["data"] == 0) then + SendAddonMessage(send, "", "WHISPER", UnitName("player")) + else + for _, v in pairs (datacache[reqId]["data"]) do + local payload = send..v + SendAddonMessage(payload, "", "WHISPER", UnitName("player")) + end + end + + -- all items have been sent, cache can be purged + debugOut("All data sent, cleaning up cache.") + datacache[reqId] = nil +end + +function CMH.SendNAK(reqId) + local send = string.format("%01s%01s%02d%06s", CSMHMsgPrefix, "C", pck.NAK, reqId) + SendAddonMessage(send, "", "WHISPER", UnitName("player")) +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["data"], reqId, prefix) +end + +--A API END \ No newline at end of file diff --git a/Client/CMH.toc b/Client/CMH.toc index fc21898..380bd87 100644 --- a/Client/CMH.toc +++ b/Client/CMH.toc @@ -1,7 +1,7 @@ ## Interface: 30300 ## Title: CMH ## Notes: Client & Server Message Handler framework for communication between server and client. -## Version: 1.0 +## Version: 2.0 ## Author: Foereaper, Stoneharry, Terrorblade smallfolk.lua diff --git a/Example_StatPointUI/Client/StatPointUI.lua b/Example_StatPointUI/Client/StatPointUI.lua index e459706..e96ac39 100644 --- a/Example_StatPointUI/Client/StatPointUI.lua +++ b/Example_StatPointUI/Client/StatPointUI.lua @@ -138,7 +138,7 @@ function StatPointUI.OnLoad() end function OnCacheReceived(sender, argTable) - StatPointUI.cache = argTable[2] + StatPointUI.cache = argTable[1] local rowContent = {"Strength", "Agility", "Stamina", "Intellect", "Spirit"} for i = 1, 5 do StatPointUI[rowContent[i]].Val:SetText(StatPointUI.cache[i]) diff --git a/Example_StatPointUI/Client/StatPointUI.toc b/Example_StatPointUI/Client/StatPointUI.toc index b1543a5..c9c5411 100644 --- a/Example_StatPointUI/Client/StatPointUI.toc +++ b/Example_StatPointUI/Client/StatPointUI.toc @@ -1,7 +1,7 @@ ## Interface: 30300 ## Title: StatPointUI ## Notes: CMH Stat Point UI example script. -## Version: 1.0 +## Version: 1.1 ## Author: Foereaper, Kaev ##RequiredDeps: CMH diff --git a/Example_StatPointUI/Server/StatPointUI.lua b/Example_StatPointUI/Server/StatPointUI.lua index 0758535..f3fbc04 100644 --- a/Example_StatPointUI/Server/StatPointUI.lua +++ b/Example_StatPointUI/Server/StatPointUI.lua @@ -107,8 +107,8 @@ end function OnSpendPointRequest(player, argTable) if(StatPointUI.cache[player:GetGUIDLow()][6] > 0) then -- Double check that the stat requested is actually a valid number - if(tonumber(argTable[2]) <= 5 and tonumber(argTable[2]) >= 0) then - StatPointUI.OnPointSpent(player:GetGUIDLow(), argTable[2]) + if(tonumber(argTable[1]) <= 5 and tonumber(argTable[1]) >= 0) then + StatPointUI.OnPointSpent(player:GetGUIDLow(), argTable[1]) end else player:SendBroadcastMessage("You have no points left!") diff --git a/Server/SMH.lua b/Server/SMH.lua index d314c6d..0f2ca4d 100644 --- a/Server/SMH.lua +++ b/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 \ No newline at end of file + + 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 \ No newline at end of file