annotate Libs/AceComm-3.0/AceComm-3.0.lua @ 201:3447634f0388 tip

Added tag v97 for changeset 6e8838b231d4
author Yellowfive
date Wed, 13 Jan 2021 13:12:13 -0600
parents e31b02b24488
children
rev   line source
yellowfive@57 1 --- **AceComm-3.0** allows you to send messages of unlimited length over the addon comm channels.
yellowfive@57 2 -- It'll automatically split the messages into multiple parts and rebuild them on the receiving end.\\
yellowfive@57 3 -- **ChatThrottleLib** is of course being used to avoid being disconnected by the server.
yellowfive@57 4 --
yellowfive@57 5 -- **AceComm-3.0** can be embeded into your addon, either explicitly by calling AceComm:Embed(MyAddon) or by
yellowfive@57 6 -- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
yellowfive@57 7 -- and can be accessed directly, without having to explicitly call AceComm itself.\\
yellowfive@57 8 -- It is recommended to embed AceComm, otherwise you'll have to specify a custom `self` on all calls you
yellowfive@57 9 -- make into AceComm.
yellowfive@57 10 -- @class file
yellowfive@57 11 -- @name AceComm-3.0
yellowfive@124 12 -- @release $Id: AceComm-3.0.lua 1174 2018-05-14 17:29:49Z h.leppkes@gmail.com $
yellowfive@57 13
yellowfive@57 14 --[[ AceComm-3.0
yellowfive@57 15
yellowfive@57 16 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 17
yellowfive@57 18 ]]
yellowfive@57 19
yellowfive@124 20 local CallbackHandler = LibStub("CallbackHandler-1.0")
yellowfive@124 21 local CTL = assert(ChatThrottleLib, "AceComm-3.0 requires ChatThrottleLib")
yellowfive@57 22
yellowfive@124 23 local MAJOR, MINOR = "AceComm-3.0", 12
yellowfive@57 24 local AceComm,oldminor = LibStub:NewLibrary(MAJOR, MINOR)
yellowfive@57 25
yellowfive@57 26 if not AceComm then return end
yellowfive@57 27
yellowfive@57 28 -- Lua APIs
yellowfive@57 29 local type, next, pairs, tostring = type, next, pairs, tostring
yellowfive@57 30 local strsub, strfind = string.sub, string.find
yellowfive@57 31 local match = string.match
yellowfive@57 32 local tinsert, tconcat = table.insert, table.concat
yellowfive@57 33 local error, assert = error, assert
yellowfive@57 34
yellowfive@57 35 -- WoW APIs
yellowfive@57 36 local Ambiguate = Ambiguate
yellowfive@57 37
yellowfive@57 38 -- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
yellowfive@57 39 -- List them here for Mikk's FindGlobals script
yellowfive@57 40 -- GLOBALS: LibStub, DEFAULT_CHAT_FRAME, geterrorhandler, RegisterAddonMessagePrefix
yellowfive@57 41
yellowfive@57 42 AceComm.embeds = AceComm.embeds or {}
yellowfive@57 43
yellowfive@57 44 -- for my sanity and yours, let's give the message type bytes some names
yellowfive@57 45 local MSG_MULTI_FIRST = "\001"
yellowfive@57 46 local MSG_MULTI_NEXT = "\002"
yellowfive@57 47 local MSG_MULTI_LAST = "\003"
yellowfive@57 48 local MSG_ESCAPE = "\004"
yellowfive@57 49
yellowfive@57 50 -- remove old structures (pre WoW 4.0)
yellowfive@57 51 AceComm.multipart_origprefixes = nil
yellowfive@57 52 AceComm.multipart_reassemblers = nil
yellowfive@57 53
yellowfive@57 54 -- the multipart message spool: indexed by a combination of sender+distribution+
yellowfive@57 55 AceComm.multipart_spool = AceComm.multipart_spool or {}
yellowfive@57 56
yellowfive@57 57 --- Register for Addon Traffic on a specified prefix
yellowfive@57 58 -- @param prefix A printable character (\032-\255) classification of the message (typically AddonName or AddonNameEvent), max 16 characters
yellowfive@57 59 -- @param method Callback to call on message reception: Function reference, or method name (string) to call on self. Defaults to "OnCommReceived"
yellowfive@57 60 function AceComm:RegisterComm(prefix, method)
yellowfive@57 61 if method == nil then
yellowfive@57 62 method = "OnCommReceived"
yellowfive@57 63 end
yellowfive@57 64
yellowfive@57 65 if #prefix > 16 then -- TODO: 15?
yellowfive@57 66 error("AceComm:RegisterComm(prefix,method): prefix length is limited to 16 characters")
yellowfive@57 67 end
yellowfive@124 68 if C_ChatInfo then
yellowfive@124 69 C_ChatInfo.RegisterAddonMessagePrefix(prefix)
yellowfive@124 70 else
yellowfive@124 71 RegisterAddonMessagePrefix(prefix)
yellowfive@124 72 end
yellowfive@57 73
yellowfive@57 74 return AceComm._RegisterComm(self, prefix, method) -- created by CallbackHandler
yellowfive@57 75 end
yellowfive@57 76
yellowfive@57 77 local warnedPrefix=false
yellowfive@57 78
yellowfive@57 79 --- Send a message over the Addon Channel
yellowfive@57 80 -- @param prefix A printable character (\032-\255) classification of the message (typically AddonName or AddonNameEvent)
yellowfive@57 81 -- @param text Data to send, nils (\000) not allowed. Any length.
yellowfive@57 82 -- @param distribution Addon channel, e.g. "RAID", "GUILD", etc; see SendAddonMessage API
yellowfive@57 83 -- @param target Destination for some distributions; see SendAddonMessage API
yellowfive@57 84 -- @param prio OPTIONAL: ChatThrottleLib priority, "BULK", "NORMAL" or "ALERT". Defaults to "NORMAL".
yellowfive@57 85 -- @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 86 -- @param callbackArg: OPTIONAL: first arg to the callback function. nil will be passed if not specified.
yellowfive@57 87 function AceComm:SendCommMessage(prefix, text, distribution, target, prio, callbackFn, callbackArg)
yellowfive@57 88 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 89 if not( type(prefix)=="string" and
yellowfive@57 90 type(text)=="string" and
yellowfive@57 91 type(distribution)=="string" and
yellowfive@124 92 (target==nil or type(target)=="string" or type(target)=="number") and
yellowfive@57 93 (prio=="BULK" or prio=="NORMAL" or prio=="ALERT")
yellowfive@57 94 ) then
yellowfive@57 95 error('Usage: SendCommMessage(addon, "prefix", "text", "distribution"[, "target"[, "prio"[, callbackFn, callbackarg]]])', 2)
yellowfive@57 96 end
yellowfive@57 97
yellowfive@57 98 local textlen = #text
yellowfive@57 99 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 100 local queueName = prefix..distribution..(target or "")
yellowfive@57 101
yellowfive@57 102 local ctlCallback = nil
yellowfive@57 103 if callbackFn then
yellowfive@57 104 ctlCallback = function(sent)
yellowfive@57 105 return callbackFn(callbackArg, sent, textlen)
yellowfive@57 106 end
yellowfive@57 107 end
yellowfive@57 108
yellowfive@57 109 local forceMultipart
yellowfive@57 110 if match(text, "^[\001-\009]") then -- 4.1+: see if the first character is a control character
yellowfive@57 111 -- we need to escape the first character with a \004
yellowfive@57 112 if textlen+1 > maxtextlen then -- would we go over the size limit?
yellowfive@57 113 forceMultipart = true -- just make it multipart, no escape problems then
yellowfive@57 114 else
yellowfive@57 115 text = "\004" .. text
yellowfive@57 116 end
yellowfive@57 117 end
yellowfive@57 118
yellowfive@57 119 if not forceMultipart and textlen <= maxtextlen then
yellowfive@57 120 -- fits all in one message
yellowfive@57 121 CTL:SendAddonMessage(prio, prefix, text, distribution, target, queueName, ctlCallback, textlen)
yellowfive@57 122 else
yellowfive@57 123 maxtextlen = maxtextlen - 1 -- 1 extra byte for part indicator in prefix(4.0)/start of message(4.1)
yellowfive@57 124
yellowfive@57 125 -- first part
yellowfive@57 126 local chunk = strsub(text, 1, maxtextlen)
yellowfive@57 127 CTL:SendAddonMessage(prio, prefix, MSG_MULTI_FIRST..chunk, distribution, target, queueName, ctlCallback, maxtextlen)
yellowfive@57 128
yellowfive@57 129 -- continuation
yellowfive@57 130 local pos = 1+maxtextlen
yellowfive@57 131
yellowfive@57 132 while pos+maxtextlen <= textlen do
yellowfive@57 133 chunk = strsub(text, pos, pos+maxtextlen-1)
yellowfive@57 134 CTL:SendAddonMessage(prio, prefix, MSG_MULTI_NEXT..chunk, distribution, target, queueName, ctlCallback, pos+maxtextlen-1)
yellowfive@57 135 pos = pos + maxtextlen
yellowfive@57 136 end
yellowfive@57 137
yellowfive@57 138 -- final part
yellowfive@57 139 chunk = strsub(text, pos)
yellowfive@57 140 CTL:SendAddonMessage(prio, prefix, MSG_MULTI_LAST..chunk, distribution, target, queueName, ctlCallback, textlen)
yellowfive@57 141 end
yellowfive@57 142 end
yellowfive@57 143
yellowfive@57 144
yellowfive@57 145 ----------------------------------------
yellowfive@57 146 -- Message receiving
yellowfive@57 147 ----------------------------------------
yellowfive@57 148
yellowfive@57 149 do
yellowfive@57 150 local compost = setmetatable({}, {__mode = "k"})
yellowfive@57 151 local function new()
yellowfive@57 152 local t = next(compost)
yellowfive@57 153 if t then
yellowfive@57 154 compost[t]=nil
yellowfive@57 155 for i=#t,3,-1 do -- faster than pairs loop. don't even nil out 1/2 since they'll be overwritten
yellowfive@57 156 t[i]=nil
yellowfive@57 157 end
yellowfive@57 158 return t
yellowfive@57 159 end
yellowfive@57 160
yellowfive@57 161 return {}
yellowfive@57 162 end
yellowfive@57 163
yellowfive@57 164 local function lostdatawarning(prefix,sender,where)
yellowfive@57 165 DEFAULT_CHAT_FRAME:AddMessage(MAJOR..": Warning: lost network data regarding '"..tostring(prefix).."' from '"..tostring(sender).."' (in "..where..")")
yellowfive@57 166 end
yellowfive@57 167
yellowfive@57 168 function AceComm:OnReceiveMultipartFirst(prefix, message, distribution, sender)
yellowfive@57 169 local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender
yellowfive@57 170 local spool = AceComm.multipart_spool
yellowfive@57 171
yellowfive@57 172 --[[
yellowfive@57 173 if spool[key] then
yellowfive@57 174 lostdatawarning(prefix,sender,"First")
yellowfive@57 175 -- continue and overwrite
yellowfive@57 176 end
yellowfive@57 177 --]]
yellowfive@57 178
yellowfive@57 179 spool[key] = message -- plain string for now
yellowfive@57 180 end
yellowfive@57 181
yellowfive@57 182 function AceComm:OnReceiveMultipartNext(prefix, message, distribution, sender)
yellowfive@57 183 local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender
yellowfive@57 184 local spool = AceComm.multipart_spool
yellowfive@57 185 local olddata = spool[key]
yellowfive@57 186
yellowfive@57 187 if not olddata then
yellowfive@57 188 --lostdatawarning(prefix,sender,"Next")
yellowfive@57 189 return
yellowfive@57 190 end
yellowfive@57 191
yellowfive@57 192 if type(olddata)~="table" then
yellowfive@57 193 -- ... but what we have is not a table. So make it one. (Pull a composted one if available)
yellowfive@57 194 local t = new()
yellowfive@57 195 t[1] = olddata -- add old data as first string
yellowfive@57 196 t[2] = message -- and new message as second string
yellowfive@57 197 spool[key] = t -- and put the table in the spool instead of the old string
yellowfive@57 198 else
yellowfive@57 199 tinsert(olddata, message)
yellowfive@57 200 end
yellowfive@57 201 end
yellowfive@57 202
yellowfive@57 203 function AceComm:OnReceiveMultipartLast(prefix, message, distribution, sender)
yellowfive@57 204 local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender
yellowfive@57 205 local spool = AceComm.multipart_spool
yellowfive@57 206 local olddata = spool[key]
yellowfive@57 207
yellowfive@57 208 if not olddata then
yellowfive@57 209 --lostdatawarning(prefix,sender,"End")
yellowfive@57 210 return
yellowfive@57 211 end
yellowfive@57 212
yellowfive@57 213 spool[key] = nil
yellowfive@57 214
yellowfive@57 215 if type(olddata) == "table" then
yellowfive@57 216 -- if we've received a "next", the spooled data will be a table for rapid & garbage-free tconcat
yellowfive@57 217 tinsert(olddata, message)
yellowfive@57 218 AceComm.callbacks:Fire(prefix, tconcat(olddata, ""), distribution, sender)
yellowfive@57 219 compost[olddata] = true
yellowfive@57 220 else
yellowfive@57 221 -- if we've only received a "first", the spooled data will still only be a string
yellowfive@57 222 AceComm.callbacks:Fire(prefix, olddata..message, distribution, sender)
yellowfive@57 223 end
yellowfive@57 224 end
yellowfive@57 225 end
yellowfive@57 226
yellowfive@57 227
yellowfive@57 228
yellowfive@57 229
yellowfive@57 230
yellowfive@57 231
yellowfive@57 232 ----------------------------------------
yellowfive@57 233 -- Embed CallbackHandler
yellowfive@57 234 ----------------------------------------
yellowfive@57 235
yellowfive@57 236 if not AceComm.callbacks then
yellowfive@57 237 AceComm.callbacks = CallbackHandler:New(AceComm,
yellowfive@57 238 "_RegisterComm",
yellowfive@57 239 "UnregisterComm",
yellowfive@57 240 "UnregisterAllComm")
yellowfive@57 241 end
yellowfive@57 242
yellowfive@57 243 AceComm.callbacks.OnUsed = nil
yellowfive@57 244 AceComm.callbacks.OnUnused = nil
yellowfive@57 245
yellowfive@57 246 local function OnEvent(self, event, prefix, message, distribution, sender)
yellowfive@57 247 if event == "CHAT_MSG_ADDON" then
yellowfive@57 248 sender = Ambiguate(sender, "none")
yellowfive@57 249 local control, rest = match(message, "^([\001-\009])(.*)")
yellowfive@57 250 if control then
yellowfive@57 251 if control==MSG_MULTI_FIRST then
yellowfive@57 252 AceComm:OnReceiveMultipartFirst(prefix, rest, distribution, sender)
yellowfive@57 253 elseif control==MSG_MULTI_NEXT then
yellowfive@57 254 AceComm:OnReceiveMultipartNext(prefix, rest, distribution, sender)
yellowfive@57 255 elseif control==MSG_MULTI_LAST then
yellowfive@57 256 AceComm:OnReceiveMultipartLast(prefix, rest, distribution, sender)
yellowfive@57 257 elseif control==MSG_ESCAPE then
yellowfive@57 258 AceComm.callbacks:Fire(prefix, rest, distribution, sender)
yellowfive@57 259 else
yellowfive@57 260 -- unknown control character, ignore SILENTLY (dont warn unnecessarily about future extensions!)
yellowfive@57 261 end
yellowfive@57 262 else
yellowfive@57 263 -- single part: fire it off immediately and let CallbackHandler decide if it's registered or not
yellowfive@57 264 AceComm.callbacks:Fire(prefix, message, distribution, sender)
yellowfive@57 265 end
yellowfive@57 266 else
yellowfive@57 267 assert(false, "Received "..tostring(event).." event?!")
yellowfive@57 268 end
yellowfive@57 269 end
yellowfive@57 270
yellowfive@57 271 AceComm.frame = AceComm.frame or CreateFrame("Frame", "AceComm30Frame")
yellowfive@57 272 AceComm.frame:SetScript("OnEvent", OnEvent)
yellowfive@57 273 AceComm.frame:UnregisterAllEvents()
yellowfive@57 274 AceComm.frame:RegisterEvent("CHAT_MSG_ADDON")
yellowfive@57 275
yellowfive@57 276
yellowfive@57 277 ----------------------------------------
yellowfive@57 278 -- Base library stuff
yellowfive@57 279 ----------------------------------------
yellowfive@57 280
yellowfive@57 281 local mixins = {
yellowfive@57 282 "RegisterComm",
yellowfive@57 283 "UnregisterComm",
yellowfive@57 284 "UnregisterAllComm",
yellowfive@57 285 "SendCommMessage",
yellowfive@57 286 }
yellowfive@57 287
yellowfive@57 288 -- Embeds AceComm-3.0 into the target object making the functions from the mixins list available on target:..
yellowfive@57 289 -- @param target target object to embed AceComm-3.0 in
yellowfive@57 290 function AceComm:Embed(target)
yellowfive@57 291 for k, v in pairs(mixins) do
yellowfive@57 292 target[v] = self[v]
yellowfive@57 293 end
yellowfive@57 294 self.embeds[target] = true
yellowfive@57 295 return target
yellowfive@57 296 end
yellowfive@57 297
yellowfive@57 298 function AceComm:OnEmbedDisable(target)
yellowfive@57 299 target:UnregisterAllComm()
yellowfive@57 300 end
yellowfive@57 301
yellowfive@57 302 -- Update embeds
yellowfive@57 303 for target, v in pairs(AceComm.embeds) do
yellowfive@57 304 AceComm:Embed(target)
yellowfive@57 305 end