yellowfive@57: --- **AceComm-3.0** allows you to send messages of unlimited length over the addon comm channels. yellowfive@57: -- It'll automatically split the messages into multiple parts and rebuild them on the receiving end.\\ yellowfive@57: -- **ChatThrottleLib** is of course being used to avoid being disconnected by the server. yellowfive@57: -- yellowfive@57: -- **AceComm-3.0** can be embeded into your addon, either explicitly by calling AceComm:Embed(MyAddon) or by yellowfive@57: -- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object yellowfive@57: -- and can be accessed directly, without having to explicitly call AceComm itself.\\ yellowfive@57: -- It is recommended to embed AceComm, otherwise you'll have to specify a custom `self` on all calls you yellowfive@57: -- make into AceComm. yellowfive@57: -- @class file yellowfive@57: -- @name AceComm-3.0 yellowfive@57: -- @release $Id: AceComm-3.0.lua 1107 2014-02-19 16:40:32Z nevcairiel $ yellowfive@57: yellowfive@57: --[[ AceComm-3.0 yellowfive@57: yellowfive@57: TODO: Time out old data rotting around from dead senders? Not a HUGE deal since the number of possible sender names is somewhat limited. yellowfive@57: yellowfive@57: ]] yellowfive@57: yellowfive@57: local MAJOR, MINOR = "AceComm-3.0", 9 yellowfive@57: yellowfive@57: local AceComm,oldminor = LibStub:NewLibrary(MAJOR, MINOR) yellowfive@57: yellowfive@57: if not AceComm then return end yellowfive@57: yellowfive@57: local CallbackHandler = LibStub:GetLibrary("CallbackHandler-1.0") yellowfive@57: local CTL = assert(ChatThrottleLib, "AceComm-3.0 requires ChatThrottleLib") yellowfive@57: yellowfive@57: -- Lua APIs yellowfive@57: local type, next, pairs, tostring = type, next, pairs, tostring yellowfive@57: local strsub, strfind = string.sub, string.find yellowfive@57: local match = string.match yellowfive@57: local tinsert, tconcat = table.insert, table.concat yellowfive@57: local error, assert = error, assert yellowfive@57: yellowfive@57: -- WoW APIs yellowfive@57: local Ambiguate = Ambiguate yellowfive@57: yellowfive@57: -- Global vars/functions that we don't upvalue since they might get hooked, or upgraded yellowfive@57: -- List them here for Mikk's FindGlobals script yellowfive@57: -- GLOBALS: LibStub, DEFAULT_CHAT_FRAME, geterrorhandler, RegisterAddonMessagePrefix yellowfive@57: yellowfive@57: AceComm.embeds = AceComm.embeds or {} yellowfive@57: yellowfive@57: -- for my sanity and yours, let's give the message type bytes some names yellowfive@57: local MSG_MULTI_FIRST = "\001" yellowfive@57: local MSG_MULTI_NEXT = "\002" yellowfive@57: local MSG_MULTI_LAST = "\003" yellowfive@57: local MSG_ESCAPE = "\004" yellowfive@57: yellowfive@57: -- remove old structures (pre WoW 4.0) yellowfive@57: AceComm.multipart_origprefixes = nil yellowfive@57: AceComm.multipart_reassemblers = nil yellowfive@57: yellowfive@57: -- the multipart message spool: indexed by a combination of sender+distribution+ yellowfive@57: AceComm.multipart_spool = AceComm.multipart_spool or {} yellowfive@57: yellowfive@57: --- Register for Addon Traffic on a specified prefix yellowfive@57: -- @param prefix A printable character (\032-\255) classification of the message (typically AddonName or AddonNameEvent), max 16 characters yellowfive@57: -- @param method Callback to call on message reception: Function reference, or method name (string) to call on self. Defaults to "OnCommReceived" yellowfive@57: function AceComm:RegisterComm(prefix, method) yellowfive@57: if method == nil then yellowfive@57: method = "OnCommReceived" yellowfive@57: end yellowfive@57: yellowfive@57: if #prefix > 16 then -- TODO: 15? yellowfive@57: error("AceComm:RegisterComm(prefix,method): prefix length is limited to 16 characters") yellowfive@57: end yellowfive@57: RegisterAddonMessagePrefix(prefix) yellowfive@57: yellowfive@57: return AceComm._RegisterComm(self, prefix, method) -- created by CallbackHandler yellowfive@57: end yellowfive@57: yellowfive@57: local warnedPrefix=false yellowfive@57: yellowfive@57: --- Send a message over the Addon Channel yellowfive@57: -- @param prefix A printable character (\032-\255) classification of the message (typically AddonName or AddonNameEvent) yellowfive@57: -- @param text Data to send, nils (\000) not allowed. Any length. yellowfive@57: -- @param distribution Addon channel, e.g. "RAID", "GUILD", etc; see SendAddonMessage API yellowfive@57: -- @param target Destination for some distributions; see SendAddonMessage API yellowfive@57: -- @param prio OPTIONAL: ChatThrottleLib priority, "BULK", "NORMAL" or "ALERT". Defaults to "NORMAL". yellowfive@57: -- @param callbackFn OPTIONAL: callback function to be called as each chunk is sent. receives 3 args: the user supplied arg (see next), the number of bytes sent so far, and the number of bytes total to send. yellowfive@57: -- @param callbackArg: OPTIONAL: first arg to the callback function. nil will be passed if not specified. yellowfive@57: function AceComm:SendCommMessage(prefix, text, distribution, target, prio, callbackFn, callbackArg) yellowfive@57: prio = prio or "NORMAL" -- pasta's reference implementation had different prio for singlepart and multipart, but that's a very bad idea since that can easily lead to out-of-sequence delivery! yellowfive@57: if not( type(prefix)=="string" and yellowfive@57: type(text)=="string" and yellowfive@57: type(distribution)=="string" and yellowfive@57: (target==nil or type(target)=="string") and yellowfive@57: (prio=="BULK" or prio=="NORMAL" or prio=="ALERT") yellowfive@57: ) then yellowfive@57: error('Usage: SendCommMessage(addon, "prefix", "text", "distribution"[, "target"[, "prio"[, callbackFn, callbackarg]]])', 2) yellowfive@57: end yellowfive@57: yellowfive@57: local textlen = #text yellowfive@57: local maxtextlen = 255 -- Yes, the max is 255 even if the dev post said 256. I tested. Char 256+ get silently truncated. /Mikk, 20110327 yellowfive@57: local queueName = prefix..distribution..(target or "") yellowfive@57: yellowfive@57: local ctlCallback = nil yellowfive@57: if callbackFn then yellowfive@57: ctlCallback = function(sent) yellowfive@57: return callbackFn(callbackArg, sent, textlen) yellowfive@57: end yellowfive@57: end yellowfive@57: yellowfive@57: local forceMultipart yellowfive@57: if match(text, "^[\001-\009]") then -- 4.1+: see if the first character is a control character yellowfive@57: -- we need to escape the first character with a \004 yellowfive@57: if textlen+1 > maxtextlen then -- would we go over the size limit? yellowfive@57: forceMultipart = true -- just make it multipart, no escape problems then yellowfive@57: else yellowfive@57: text = "\004" .. text yellowfive@57: end yellowfive@57: end yellowfive@57: yellowfive@57: if not forceMultipart and textlen <= maxtextlen then yellowfive@57: -- fits all in one message yellowfive@57: CTL:SendAddonMessage(prio, prefix, text, distribution, target, queueName, ctlCallback, textlen) yellowfive@57: else yellowfive@57: maxtextlen = maxtextlen - 1 -- 1 extra byte for part indicator in prefix(4.0)/start of message(4.1) yellowfive@57: yellowfive@57: -- first part yellowfive@57: local chunk = strsub(text, 1, maxtextlen) yellowfive@57: CTL:SendAddonMessage(prio, prefix, MSG_MULTI_FIRST..chunk, distribution, target, queueName, ctlCallback, maxtextlen) yellowfive@57: yellowfive@57: -- continuation yellowfive@57: local pos = 1+maxtextlen yellowfive@57: yellowfive@57: while pos+maxtextlen <= textlen do yellowfive@57: chunk = strsub(text, pos, pos+maxtextlen-1) yellowfive@57: CTL:SendAddonMessage(prio, prefix, MSG_MULTI_NEXT..chunk, distribution, target, queueName, ctlCallback, pos+maxtextlen-1) yellowfive@57: pos = pos + maxtextlen yellowfive@57: end yellowfive@57: yellowfive@57: -- final part yellowfive@57: chunk = strsub(text, pos) yellowfive@57: CTL:SendAddonMessage(prio, prefix, MSG_MULTI_LAST..chunk, distribution, target, queueName, ctlCallback, textlen) yellowfive@57: end yellowfive@57: end yellowfive@57: yellowfive@57: yellowfive@57: ---------------------------------------- yellowfive@57: -- Message receiving yellowfive@57: ---------------------------------------- yellowfive@57: yellowfive@57: do yellowfive@57: local compost = setmetatable({}, {__mode = "k"}) yellowfive@57: local function new() yellowfive@57: local t = next(compost) yellowfive@57: if t then yellowfive@57: compost[t]=nil yellowfive@57: for i=#t,3,-1 do -- faster than pairs loop. don't even nil out 1/2 since they'll be overwritten yellowfive@57: t[i]=nil yellowfive@57: end yellowfive@57: return t yellowfive@57: end yellowfive@57: yellowfive@57: return {} yellowfive@57: end yellowfive@57: yellowfive@57: local function lostdatawarning(prefix,sender,where) yellowfive@57: DEFAULT_CHAT_FRAME:AddMessage(MAJOR..": Warning: lost network data regarding '"..tostring(prefix).."' from '"..tostring(sender).."' (in "..where..")") yellowfive@57: end yellowfive@57: yellowfive@57: function AceComm:OnReceiveMultipartFirst(prefix, message, distribution, sender) yellowfive@57: local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender yellowfive@57: local spool = AceComm.multipart_spool yellowfive@57: yellowfive@57: --[[ yellowfive@57: if spool[key] then yellowfive@57: lostdatawarning(prefix,sender,"First") yellowfive@57: -- continue and overwrite yellowfive@57: end yellowfive@57: --]] yellowfive@57: yellowfive@57: spool[key] = message -- plain string for now yellowfive@57: end yellowfive@57: yellowfive@57: function AceComm:OnReceiveMultipartNext(prefix, message, distribution, sender) yellowfive@57: local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender yellowfive@57: local spool = AceComm.multipart_spool yellowfive@57: local olddata = spool[key] yellowfive@57: yellowfive@57: if not olddata then yellowfive@57: --lostdatawarning(prefix,sender,"Next") yellowfive@57: return yellowfive@57: end yellowfive@57: yellowfive@57: if type(olddata)~="table" then yellowfive@57: -- ... but what we have is not a table. So make it one. (Pull a composted one if available) yellowfive@57: local t = new() yellowfive@57: t[1] = olddata -- add old data as first string yellowfive@57: t[2] = message -- and new message as second string yellowfive@57: spool[key] = t -- and put the table in the spool instead of the old string yellowfive@57: else yellowfive@57: tinsert(olddata, message) yellowfive@57: end yellowfive@57: end yellowfive@57: yellowfive@57: function AceComm:OnReceiveMultipartLast(prefix, message, distribution, sender) yellowfive@57: local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender yellowfive@57: local spool = AceComm.multipart_spool yellowfive@57: local olddata = spool[key] yellowfive@57: yellowfive@57: if not olddata then yellowfive@57: --lostdatawarning(prefix,sender,"End") yellowfive@57: return yellowfive@57: end yellowfive@57: yellowfive@57: spool[key] = nil yellowfive@57: yellowfive@57: if type(olddata) == "table" then yellowfive@57: -- if we've received a "next", the spooled data will be a table for rapid & garbage-free tconcat yellowfive@57: tinsert(olddata, message) yellowfive@57: AceComm.callbacks:Fire(prefix, tconcat(olddata, ""), distribution, sender) yellowfive@57: compost[olddata] = true yellowfive@57: else yellowfive@57: -- if we've only received a "first", the spooled data will still only be a string yellowfive@57: AceComm.callbacks:Fire(prefix, olddata..message, distribution, sender) yellowfive@57: end yellowfive@57: end yellowfive@57: end yellowfive@57: yellowfive@57: yellowfive@57: yellowfive@57: yellowfive@57: yellowfive@57: yellowfive@57: ---------------------------------------- yellowfive@57: -- Embed CallbackHandler yellowfive@57: ---------------------------------------- yellowfive@57: yellowfive@57: if not AceComm.callbacks then yellowfive@57: AceComm.callbacks = CallbackHandler:New(AceComm, yellowfive@57: "_RegisterComm", yellowfive@57: "UnregisterComm", yellowfive@57: "UnregisterAllComm") yellowfive@57: end yellowfive@57: yellowfive@57: AceComm.callbacks.OnUsed = nil yellowfive@57: AceComm.callbacks.OnUnused = nil yellowfive@57: yellowfive@57: local function OnEvent(self, event, prefix, message, distribution, sender) yellowfive@57: if event == "CHAT_MSG_ADDON" then yellowfive@57: sender = Ambiguate(sender, "none") yellowfive@57: local control, rest = match(message, "^([\001-\009])(.*)") yellowfive@57: if control then yellowfive@57: if control==MSG_MULTI_FIRST then yellowfive@57: AceComm:OnReceiveMultipartFirst(prefix, rest, distribution, sender) yellowfive@57: elseif control==MSG_MULTI_NEXT then yellowfive@57: AceComm:OnReceiveMultipartNext(prefix, rest, distribution, sender) yellowfive@57: elseif control==MSG_MULTI_LAST then yellowfive@57: AceComm:OnReceiveMultipartLast(prefix, rest, distribution, sender) yellowfive@57: elseif control==MSG_ESCAPE then yellowfive@57: AceComm.callbacks:Fire(prefix, rest, distribution, sender) yellowfive@57: else yellowfive@57: -- unknown control character, ignore SILENTLY (dont warn unnecessarily about future extensions!) yellowfive@57: end yellowfive@57: else yellowfive@57: -- single part: fire it off immediately and let CallbackHandler decide if it's registered or not yellowfive@57: AceComm.callbacks:Fire(prefix, message, distribution, sender) yellowfive@57: end yellowfive@57: else yellowfive@57: assert(false, "Received "..tostring(event).." event?!") yellowfive@57: end yellowfive@57: end yellowfive@57: yellowfive@57: AceComm.frame = AceComm.frame or CreateFrame("Frame", "AceComm30Frame") yellowfive@57: AceComm.frame:SetScript("OnEvent", OnEvent) yellowfive@57: AceComm.frame:UnregisterAllEvents() yellowfive@57: AceComm.frame:RegisterEvent("CHAT_MSG_ADDON") yellowfive@57: yellowfive@57: yellowfive@57: ---------------------------------------- yellowfive@57: -- Base library stuff yellowfive@57: ---------------------------------------- yellowfive@57: yellowfive@57: local mixins = { yellowfive@57: "RegisterComm", yellowfive@57: "UnregisterComm", yellowfive@57: "UnregisterAllComm", yellowfive@57: "SendCommMessage", yellowfive@57: } yellowfive@57: yellowfive@57: -- Embeds AceComm-3.0 into the target object making the functions from the mixins list available on target:.. yellowfive@57: -- @param target target object to embed AceComm-3.0 in yellowfive@57: function AceComm:Embed(target) yellowfive@57: for k, v in pairs(mixins) do yellowfive@57: target[v] = self[v] yellowfive@57: end yellowfive@57: self.embeds[target] = true yellowfive@57: return target yellowfive@57: end yellowfive@57: yellowfive@57: function AceComm:OnEmbedDisable(target) yellowfive@57: target:UnregisterAllComm() yellowfive@57: end yellowfive@57: yellowfive@57: -- Update embeds yellowfive@57: for target, v in pairs(AceComm.embeds) do yellowfive@57: AceComm:Embed(target) yellowfive@57: end