comparison Libs/AceComm-3.0/AceComm-3.0.lua @ 57:01b63b8ed811 v21

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