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
|