From 98a08522c578cbc220b83fb33e0aaabccfbc3983 Mon Sep 17 00:00:00 2001 From: Foereaper Date: Wed, 21 Jul 2021 10:16:54 +0200 Subject: [PATCH] Performance rewrite part 1 removed acking, took way too long with client delay added better debug messages --- Client/CMH.lua | 152 ++++++++++++++++++------------------------------- Server/SMH.lua | 146 +++++++++++++++++------------------------------ 2 files changed, 107 insertions(+), 191 deletions(-) diff --git a/Client/CMH.lua b/Client/CMH.lua index 8c07de9..3eebcab 100644 --- a/Client/CMH.lua +++ b/Client/CMH.lua @@ -2,15 +2,16 @@ local debug = false local CMH = {} local datacache = {} - -local CSMHMsgPrefix = "♠" -local delim = {"♥", "♚", "♛", "♜"} -local pck = {REQ = 1, ACK = 2, DAT = 3, NAK = 4} +local delim = {"♠", "♥", "♚", "♛", "♜"} +local pck = {REQ = 1, DAT = 2} -- HELPERS START -local function debugOut(msg) +local function debugOut(prefix, x, msg) + prefix = prefix or "" + msg = msg or "" + x = x or "" if(debug == true) then - print("CMH Debug: "..msg) + print("["..date("%X", time()).."][CSMH]["..x.."]["..prefix.."]: "..msg) end end @@ -26,6 +27,7 @@ local function GenerateReqId() end local function ParseMessage(str) + str = str or "" local output = {} local valTemp = {} local typeTemp = {} @@ -50,11 +52,11 @@ local function ParseMessage(str) -- Convert value to correct type for k, v in pairs(valTemp) do local varType = typeTemp[k] - if(varType == 2) then -- Ints + if(varType == 3) then -- Ints v = tonumber(v) - elseif(varType == 3) then -- Tables + elseif(varType == 4) then -- Tables v = Smallfolk.loads(v, #v) - elseif(varType == 4) then -- Booleans + elseif(varType == 5) then -- Booleans if(v == "true") then v = true else v = false end end table.insert(output, v) @@ -67,22 +69,22 @@ local function ParseMessage(str) end local function ProcessVariables(reqId, ...) - local arg = {...} local splitLength = 200 + local arg = {...} local msg = "" for _, v in pairs(arg) do if(type(v) == "string") then - msg = msg .. delim[1] - elseif(type(v) == "number") 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[3] + msg = msg .. delim[4] elseif(type(v) == "boolean") then v = tostring(v) - msg = msg .. delim[4] + msg = msg .. delim[5] end msg = msg .. v end @@ -92,8 +94,8 @@ local function ProcessVariables(reqId, ...) end 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 + datacache[reqId]["data"][datacache[reqId].count] = msg:sub(i,i+splitLength - 1) end return datacache[reqId] @@ -103,36 +105,28 @@ end -- Rx START -function CMH.OnReceive(self, event, prefix, _, Type, sender) +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, data = prefix:match("(...)(%u)(%d%d)(.+)") + 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 welll as server messages - if(pfx == CSMHMsgPrefix and source == "S") then - debugOut("Received CSMH packet, processing data.") - + -- 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 received, data: "..data) + debugOut("REQ", "Rx", "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) + debugOut("DAT", "Rx", "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") + debugOut("ERR", "Rx", "Invalid packet type, aborting") return end end @@ -144,11 +138,11 @@ CMHFrame:RegisterEvent("CHAT_MSG_ADDON") CMHFrame:SetScript("OnEvent", CMH.OnReceive) function CMH.OnREQ(sender, data) - debugOut("Processing REQ 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("Malformed REQ data, aborting.") + debugOut("REQ", "Rx", "Malformed data, aborting.") return end @@ -157,62 +151,43 @@ function CMH.OnREQ(sender, data) -- if the addon does not exist, abort if not CMH[addon] then - CMH.SendNAK(reqId) - debugOut("Invalid addon, aborting") + debugOut("REQ", "Rx", "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") + 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 - CMH.SendNAK(reqId) datacache[reqId] = nil - debugOut("Request cache already exists, aborting.") + 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 = {}} - -- 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, data) - local reqId = data:match("(%w%w%w%w%w%w)"); - if not reqId then - return - end - - -- 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) + 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, payload = data:match("(%w%w%w%w%w%w)(.*)"); - if not reqId and not payload then + 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("Data received, but not expected. Aborting.") + debugOut("DAT", "Rx", "Cache does not exist, aborting.") return end @@ -222,28 +197,32 @@ function CMH.OnDAT(sender, 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 = SMH[reqTable.addon][reqTable.funcId] + local func = CMH[reqTable.addon][reqTable.funcId] if func then - debugOut(func) _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("Received more data than expected. Aborting.") + 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) @@ -251,64 +230,42 @@ function CMH.OnDAT(sender, data) -- 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) + datacache[reqId] = nil + debugOut("DAT", "Rx", "Function "..func.." @ "..reqTable.addon.." executed, cache cleared.") end - - -- Delete the request session cache - datacache[reqId] = nil end end -function CMH.OnNAK(sender, data) - local reqId = data:match("(%w%w%w%w%w%w)"); - if not reqId then - return - end - - -- 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%03d%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")) + 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 send = string.format("%01s%01s%02d%06s", CSMHMsgPrefix, "C", pck.DAT, reqId) + 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(send, "", "WHISPER", UnitName("player")) + SendAddonMessage(header, reqId, "WHISPER", UnitName("player")) else for _, v in pairs (datacache[reqId]["data"]) do - local payload = send..v - SendAddonMessage(payload, "", "WHISPER", UnitName("player")) + local payload = reqId..v + SendAddonMessage(header, 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")) + debugOut("DAT", "Tx", "Sent all DAT for ID "..reqId..", cache cleared, closing.") end -- Tx END @@ -332,6 +289,7 @@ function SendClientRequest(prefix, functionId, ...) local varTable = ProcessVariables(reqId, ...) CMH.SendREQ(functionId, varTable.count, reqId, prefix) + CMH.SendDAT(reqId) end --A API END \ No newline at end of file diff --git a/Server/SMH.lua b/Server/SMH.lua index f8ca338..6829820 100644 --- a/Server/SMH.lua +++ b/Server/SMH.lua @@ -4,15 +4,16 @@ local debug = false local SMH = {} local datacache = {} - -local CSMHMsgPrefix = "♠" -local delim = {"♥", "♚", "♛", "♜"} -local pck = {REQ = 1, ACK = 2, DAT = 3, NAK = 4} +local delim = {"♠", "♥", "♚", "♛", "♜"} +local pck = {REQ = 1, DAT = 2} -- HELPERS START -local function debugOut(msg) +local function debugOut(prefix, x, msg) + prefix = prefix or "" + msg = msg or "" + x = x or "" if(debug == true) then - print("SMH Debug: "..msg) + print("["..os.clock().."][CSMH]["..x.."]["..prefix.."]: "..msg) end end @@ -28,6 +29,7 @@ local function GenerateReqId() end local function ParseMessage(str) + str = str or "" local output = {} local valTemp = {} local typeTemp = {} @@ -52,11 +54,11 @@ local function ParseMessage(str) -- Convert value to correct type for k, v in pairs(valTemp) do local varType = typeTemp[k] - if(varType == 2) then -- Ints + if(varType == 3) then -- Ints v = tonumber(v) - elseif(varType == 3) then -- Tables + elseif(varType == 4) then -- Tables v = smallfolk.loads(v) - elseif(varType == 4) then -- Booleans + elseif(varType == 5) then -- Booleans if(v == "true") then v = true else v = false end end table.insert(output, v) @@ -69,22 +71,22 @@ local function ParseMessage(str) end local function ProcessVariables(sender, reqId, ...) - local arg = {...} local splitLength = 200 + local arg = {...} local msg = "" for _, v in pairs(arg) do if(type(v) == "string") then - msg = msg .. delim[1] - elseif(type(v) == "number") 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[3] + msg = msg .. delim[4] elseif(type(v) == "boolean") then v = tostring(v) - msg = msg .. delim[4] + msg = msg .. delim[5] end msg = msg .. v end @@ -96,8 +98,8 @@ local function ProcessVariables(sender, reqId, ...) 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 + datacache[sender:GetGUIDLow()][reqId]["data"][datacache[sender:GetGUIDLow()][reqId].count] = msg:sub(i,i+splitLength - 1) end return datacache[sender:GetGUIDLow()][reqId] @@ -107,7 +109,7 @@ end -- Rx START -function SMH.OnReceive(event, sender, _type, prefix, _, target) +function SMH.OnReceive(event, sender, _type, header, data, 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 @@ -117,32 +119,24 @@ function SMH.OnReceive(event, sender, _type, prefix, _, target) -- 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)(.+)") + 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 == CSMHMsgPrefix and source == "C") then - debugOut("Received CSMH packet, processing data.") - + if(pfx == delim[1] and source == "C") then -- 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) + debugOut("REQ", "Rx", "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) + debugOut("DAT", "Rx", "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") + debugOut("ERR", "Rx", "Invalid packet type, aborting") return end end @@ -152,11 +146,11 @@ end RegisterServerEvent(30, SMH.OnReceive) function SMH.OnREQ(sender, data) - debugOut("Processing REQ 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("Malformed REQ data, aborting.") + debugOut("REQ", "Rx", "Malformed data, aborting.") return end @@ -165,15 +159,13 @@ function SMH.OnREQ(sender, data) -- if the addon does not exist, abort if not SMH[addon] then - SMH.SendNAK(sender, reqId) - debugOut("Invalid addon, aborting") + debugOut("REQ", "Rx", "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") + debugOut("REQ", "Rx", "Invalid addon function, aborting.") return end @@ -183,47 +175,31 @@ function SMH.OnREQ(sender, data) -- 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.") + debugOut("REQ", "Rx", "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) + debugOut("REQ", "Rx", "Header validated, cache created. Awaitng data..") end -function SMH.OnACK(sender, data) - local reqId = data:match("(%w%w%w%w%w%w)"); - if not reqId then - return - end - - -- 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) + debugOut("DAT", "Rx", "Validating data..") -- Separate REQ ID from payload and verify - local reqId, payload = data:match("(%w%w%w%w%w%w)(.*)"); + 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[sender:GetGUIDLow()][reqId] then - debugOut("Data received, but not expected. Aborting.") + debugOut("DAT", "Rx", "Cache does not exist, aborting.") return end @@ -233,28 +209,32 @@ function SMH.OnDAT(sender, 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 = SMH[reqTable.addon][reqTable.funcId] if func then - debugOut(func) _G[func](sender, {}) datacache[sender:GetGUIDLow()][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("Received more data than expected. Aborting.") + 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) @@ -262,64 +242,42 @@ function SMH.OnDAT(sender, data) -- 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) + datacache[sender:GetGUIDLow()][reqId] = nil + debugOut("DAT", "Rx", "Function "..func.." @ "..reqTable.addon.." executed, cache cleared.") end - - -- Delete the request session cache - datacache[sender:GetGUIDLow()][reqId] = nil end end -function SMH.OnNAK(sender, data) - local reqId = data:match("(%w%w%w%w%w%w)"); - if not reqId then - return - end - - -- 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%03d%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) + local header = string.format("%01s%01s%02d", delim[1], "S", pck.REQ) + local data = string.format("%02d%03d%06s%0"..tostring(#addon).."s", functionId, linkCount, reqId, addon) + sender:SendAddonMessage(header, data, 7, sender) + debugOut("REQ", "Tx", "Sent REQ with ID "..reqId..", sending DAT..") end function SMH.SendDAT(sender, reqId) -- Build data message header - local send = string.format("%01s%01s%02d%06s", CSMHMsgPrefix, "S", pck.DAT, reqId) + local header = string.format("%01s%01s%02d", delim[1], "S", 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[sender:GetGUIDLow()][reqId]["data"] == 0) then - sender:SendAddonMessage(send, "", 7, sender) + sender:SendAddonMessage(header, reqId, 7, sender) else for _, v in pairs (datacache[sender:GetGUIDLow()][reqId]["data"]) do - local payload = send..v - sender:SendAddonMessage(payload, "", 7, sender) + local payload = reqId..v + sender:SendAddonMessage(header, 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) + debugOut("DAT", "Tx", "Sent all DAT for ID "..reqId..", cache cleared, closing.") end -- Tx END @@ -344,8 +302,8 @@ end function Player:SendServerResponse(prefix, functionId, ...) local reqId = GenerateReqId() local varTable = ProcessVariables(self, reqId, ...) - SMH.SendREQ(self, functionId, varTable.count, reqId, prefix) + SMH.SendDAT(self, reqId) end -- API END \ No newline at end of file