vanilla-wow-addons – Blame information for rev 1
?pathlinks?
Rev | Author | Line No. | Line |
---|---|---|---|
1 | office | 1 | -- |
2 | -- ChatThrottleLib by Mikk |
||
3 | -- |
||
4 | -- Manages AddOn chat output to keep player from getting kicked off. |
||
5 | -- |
||
6 | -- ChatThrottleLib.SendChatMessage/.SendAddonMessage functions that accept |
||
7 | -- a Priority ("BULK", "NORMAL", "ALERT") as well as prefix for SendChatMessage. |
||
8 | -- |
||
9 | -- Priorities get an equal share of available bandwidth when fully loaded. |
||
10 | -- Communication channels are separated on extension+chattype+destination and |
||
11 | -- get round-robinned. (Destination only matters for whispers and channels, |
||
12 | -- obviously) |
||
13 | -- |
||
14 | -- Will install hooks for SendChatMessage and SendAdd[Oo]nMessage to measure |
||
15 | -- bandwidth bypassing the library and use less bandwidth itself. |
||
16 | -- |
||
17 | -- |
||
18 | -- Fully embeddable library. Just copy this file into your addon directory, |
||
19 | -- add it to the .toc, and it's done. |
||
20 | -- |
||
21 | -- Can run as a standalone addon also, but, really, just embed it! :-) |
||
22 | -- |
||
23 | |||
24 | local CTL_VERSION = 11 |
||
25 | |||
26 | local MAX_CPS = 1000 -- 2000 seems to be safe if NOTHING ELSE is happening. let's call it 1000. |
||
27 | local MSG_OVERHEAD = 40 -- Guesstimate overhead for sending a message; source+dest+chattype+protocolstuff |
||
28 | |||
29 | local BURST = 8000 -- WoW's server buffer seems to be about 32KB. Let's use 25% of it for our lib. |
||
30 | |||
31 | local MIN_FPS = 20 -- Reduce output CPS to half (and don't burst) if FPS drops below this value |
||
32 | |||
33 | if(ChatThrottleLib and ChatThrottleLib.version>=CTL_VERSION) then |
||
34 | -- There's already a newer (or same) version loaded. Buh-bye. |
||
35 | return; |
||
36 | end |
||
37 | |||
38 | |||
39 | |||
40 | if(not ChatThrottleLib) then |
||
41 | ChatThrottleLib = {} |
||
42 | end |
||
43 | |||
44 | ChatThrottleLib.version=CTL_VERSION; |
||
45 | |||
46 | |||
47 | ----------------------------------------------------------------------- |
||
48 | -- Double-linked ring implementation |
||
49 | |||
50 | local Ring = {} |
||
51 | local RingMeta = { __index=Ring } |
||
52 | |||
53 | function Ring:New() |
||
54 | local ret = {} |
||
55 | setmetatable(ret, RingMeta) |
||
56 | return ret; |
||
57 | end |
||
58 | |||
59 | function Ring:Add(obj) -- Append at the "far end" of the ring (aka just before the current position) |
||
60 | if(self.pos) then |
||
61 | obj.prev = self.pos.prev; |
||
62 | obj.prev.next = obj; |
||
63 | obj.next = self.pos; |
||
64 | obj.next.prev = obj; |
||
65 | else |
||
66 | obj.next = obj; |
||
67 | obj.prev = obj; |
||
68 | self.pos = obj; |
||
69 | end |
||
70 | end |
||
71 | |||
72 | function Ring:Remove(obj) |
||
73 | obj.next.prev = obj.prev; |
||
74 | obj.prev.next = obj.next; |
||
75 | if(self.pos == obj) then |
||
76 | self.pos = obj.next; |
||
77 | if(self.pos == obj) then |
||
78 | self.pos = nil; |
||
79 | end |
||
80 | end |
||
81 | end |
||
82 | |||
83 | |||
84 | |||
85 | ----------------------------------------------------------------------- |
||
86 | -- Recycling bin for pipes (kept in a linked list because that's |
||
87 | -- how they're worked with in the rotating rings; just reusing members) |
||
88 | |||
89 | ChatThrottleLib.PipeBin = { count=0 } |
||
90 | |||
91 | function ChatThrottleLib.PipeBin:Put(pipe) |
||
92 | for i=getn(pipe),1,-1 do |
||
93 | tremove(pipe, i); |
||
94 | end |
||
95 | pipe.prev = nil; |
||
96 | pipe.next = self.list; |
||
97 | self.list = pipe; |
||
98 | self.count = self.count+1; |
||
99 | end |
||
100 | |||
101 | function ChatThrottleLib.PipeBin:Get() |
||
102 | if(self.list) then |
||
103 | local ret = self.list; |
||
104 | self.list = ret.next; |
||
105 | ret.next=nil; |
||
106 | self.count = self.count - 1; |
||
107 | return ret; |
||
108 | end |
||
109 | return {}; |
||
110 | end |
||
111 | |||
112 | function ChatThrottleLib.PipeBin:Tidy() |
||
113 | if(self.count < 25) then |
||
114 | return; |
||
115 | end |
||
116 | |||
117 | if(self.count > 100) then |
||
118 | n=self.count-90; |
||
119 | else |
||
120 | n=10; |
||
121 | end |
||
122 | for i=2,n do |
||
123 | self.list = self.list.next; |
||
124 | end |
||
125 | local delme = self.list; |
||
126 | self.list = self.list.next; |
||
127 | delme.next = nil; |
||
128 | end |
||
129 | |||
130 | |||
131 | |||
132 | |||
133 | ----------------------------------------------------------------------- |
||
134 | -- Recycling bin for messages |
||
135 | |||
136 | ChatThrottleLib.MsgBin = {} |
||
137 | |||
138 | function ChatThrottleLib.MsgBin:Put(msg) |
||
139 | msg.text = nil; |
||
140 | tinsert(self, msg); |
||
141 | end |
||
142 | |||
143 | function ChatThrottleLib.MsgBin:Get() |
||
144 | local ret = tremove(self, getn(self)); |
||
145 | if(ret) then return ret; end |
||
146 | return {}; |
||
147 | end |
||
148 | |||
149 | function ChatThrottleLib.MsgBin:Tidy() |
||
150 | if(getn(self)<50) then |
||
151 | return; |
||
152 | end |
||
153 | if(getn(self)>150) then -- "can't happen" but ... |
||
154 | for n=getn(self),120,-1 do |
||
155 | tremove(self,n); |
||
156 | end |
||
157 | else |
||
158 | for n=getn(self),getn(self)-20,-1 do |
||
159 | tremove(self,n); |
||
160 | end |
||
161 | end |
||
162 | end |
||
163 | |||
164 | |||
165 | ----------------------------------------------------------------------- |
||
166 | -- ChatThrottleLib:Init |
||
167 | -- Initialize queues, set up frame for OnUpdate, etc |
||
168 | |||
169 | |||
170 | function ChatThrottleLib:Init() |
||
171 | |||
172 | -- Set up queues |
||
173 | if(not self.Prio) then |
||
174 | self.Prio = {} |
||
175 | self.Prio["ALERT"] = { ByName={}, Ring = Ring:New(), avail=0 }; |
||
176 | self.Prio["NORMAL"] = { ByName={}, Ring = Ring:New(), avail=0 }; |
||
177 | self.Prio["BULK"] = { ByName={}, Ring = Ring:New(), avail=0 }; |
||
178 | end |
||
179 | |||
180 | -- v4: total send counters per priority |
||
181 | for _,Prio in self.Prio do |
||
182 | Prio.nTotalSent = Prio.nTotalSent or 0; |
||
183 | end |
||
184 | |||
185 | self.avail = self.avail or 0; -- v5 |
||
186 | self.nTotalSent = self.nTotalSent or 0; -- v5 |
||
187 | |||
188 | |||
189 | -- Set up a frame to get OnUpdate events |
||
190 | if(not self.Frame) then |
||
191 | self.Frame = CreateFrame("Frame"); |
||
192 | self.Frame:Hide(); |
||
193 | end |
||
194 | self.Frame.Show = self.Frame.Show; -- cache for speed |
||
195 | self.Frame.Hide = self.Frame.Hide; -- cache for speed |
||
196 | self.Frame:SetScript("OnUpdate", self.OnUpdate); |
||
197 | self.Frame:SetScript("OnEvent", self.OnEvent); -- v11: Monitor P_E_W so we can throttle hard for a few seconds |
||
198 | self.Frame:RegisterEvent("PLAYER_ENTERING_WORLD"); |
||
199 | self.OnUpdateDelay=0; |
||
200 | self.LastAvailUpdate=GetTime(); |
||
201 | self.HardThrottlingBeginTime=GetTime(); -- v11: Throttle hard for a few seconds after startup |
||
202 | |||
203 | -- Hook SendChatMessage and SendAddonMessage so we can measure unpiped traffic and avoid overloads (v7) |
||
204 | if(not self.ORIG_SendChatMessage) then |
||
205 | --SendChatMessage |
||
206 | self.ORIG_SendChatMessage = SendChatMessage; |
||
207 | SendChatMessage = function(a1,a2,a3,a4) return ChatThrottleLib.Hook_SendChatMessage(a1,a2,a3,a4); end |
||
208 | --SendAdd[Oo]nMessage |
||
209 | if(SendAddonMessage or SendAddOnMessage) then -- v10: don't pretend like it doesn't exist if it doesn't! |
||
210 | self.ORIG_SendAddonMessage = SendAddonMessage or SendAddOnMessage; |
||
211 | SendAddonMessage = function(a1,a2,a3) return ChatThrottleLib.Hook_SendAddonMessage(a1,a2,a3); end |
||
212 | if(SendAddOnMessage) then -- in case Slouken changes his mind... |
||
213 | SendAddOnMessage = SendAddonMessage; |
||
214 | end |
||
215 | end |
||
216 | end |
||
217 | self.nBypass = 0; |
||
218 | end |
||
219 | |||
220 | |||
221 | ----------------------------------------------------------------------- |
||
222 | -- ChatThrottleLib.Hook_SendChatMessage / .Hook_SendAddonMessage |
||
223 | function ChatThrottleLib.Hook_SendChatMessage(text, chattype, language, destination) |
||
224 | local self = ChatThrottleLib; |
||
225 | local size = strlen(text or "") + strlen(chattype or "") + strlen(destination or "") + 40; |
||
226 | self.avail = self.avail - size; |
||
227 | self.nBypass = self.nBypass + size; |
||
228 | return self.ORIG_SendChatMessage(text, chattype, language, destination); |
||
229 | end |
||
230 | function ChatThrottleLib.Hook_SendAddonMessage(prefix, text, chattype) |
||
231 | local self = ChatThrottleLib; |
||
232 | local size = strlen(text or "") + strlen(chattype or "") + strlen(prefix or "") + 40; |
||
233 | self.avail = self.avail - size; |
||
234 | self.nBypass = self.nBypass + size; |
||
235 | return self.ORIG_SendAddonMessage(prefix, text, chattype); |
||
236 | end |
||
237 | |||
238 | |||
239 | |||
240 | ----------------------------------------------------------------------- |
||
241 | -- ChatThrottleLib:UpdateAvail |
||
242 | -- Update self.avail with how much bandwidth is currently available |
||
243 | |||
244 | function ChatThrottleLib:UpdateAvail() |
||
245 | local now = GetTime(); |
||
246 | local newavail = MAX_CPS * (now-self.LastAvailUpdate); |
||
247 | |||
248 | if(now - self.HardThrottlingBeginTime < 5) then |
||
249 | -- First 5 seconds after startup/zoning: VERY hard clamping to avoid irritating the server rate limiter, it seems very cranky then |
||
250 | self.avail = min(self.avail + (newavail*0.1), MAX_CPS*0.5); |
||
251 | elseif(GetFramerate()<MIN_FPS) then -- GetFrameRate call takes ~0.002 secs |
||
252 | newavail = newavail * 0.5; |
||
253 | self.avail = min(MAX_CPS, self.avail + newavail); |
||
254 | self.bChoking = true; -- just for stats |
||
255 | else |
||
256 | self.avail = min(BURST, self.avail + newavail); |
||
257 | self.bChoking = false; |
||
258 | end |
||
259 | |||
260 | self.avail = max(self.avail, 0-(MAX_CPS*2)); -- Can go negative when someone is eating bandwidth past the lib. but we refuse to stay silent for more than 2 seconds; if they can do it, we can. |
||
261 | self.LastAvailUpdate = now; |
||
262 | |||
263 | return self.avail; |
||
264 | end |
||
265 | |||
266 | |||
267 | ----------------------------------------------------------------------- |
||
268 | -- Despooling logic |
||
269 | |||
270 | function ChatThrottleLib:Despool(Prio) |
||
271 | local ring = Prio.Ring; |
||
272 | while(ring.pos and Prio.avail>ring.pos[1].nSize) do |
||
273 | local msg = tremove(Prio.Ring.pos, 1); |
||
274 | if(not Prio.Ring.pos[1]) then |
||
275 | local pipe = Prio.Ring.pos; |
||
276 | Prio.Ring:Remove(pipe); |
||
277 | Prio.ByName[pipe.name] = nil; |
||
278 | self.PipeBin:Put(pipe); |
||
279 | else |
||
280 | Prio.Ring.pos = Prio.Ring.pos.next; |
||
281 | end |
||
282 | Prio.avail = Prio.avail - msg.nSize; |
||
283 | msg.f(msg[1], msg[2], msg[3], msg[4]); |
||
284 | Prio.nTotalSent = Prio.nTotalSent + msg.nSize; |
||
285 | self.MsgBin:Put(msg); |
||
286 | end |
||
287 | end |
||
288 | |||
289 | |||
290 | function ChatThrottleLib:OnEvent() |
||
291 | -- v11: We know that the rate limiter is touchy after login. Assume that it's touch after zoning, too. |
||
292 | self = ChatThrottleLib; |
||
293 | if(event == "PLAYER_ENTERING_WORLD") then |
||
294 | self.HardThrottlingBeginTime=GetTime(); -- Throttle hard for a few seconds after zoning |
||
295 | self.avail = 0; |
||
296 | end |
||
297 | end |
||
298 | |||
299 | |||
300 | function ChatThrottleLib:OnUpdate() |
||
301 | self = ChatThrottleLib; |
||
302 | |||
303 | self.OnUpdateDelay = self.OnUpdateDelay + arg1; |
||
304 | if(self.OnUpdateDelay < 0.08) then |
||
305 | return; |
||
306 | end |
||
307 | self.OnUpdateDelay = 0; |
||
308 | |||
309 | self:UpdateAvail(); |
||
310 | |||
311 | if(self.avail<0) then |
||
312 | return; -- argh. some bastard is spewing stuff past the lib. just bail early to save cpu. |
||
313 | end |
||
314 | |||
315 | -- See how many of or priorities have queued messages |
||
316 | local n=0; |
||
317 | for prioname,Prio in pairs(self.Prio) do |
||
318 | if(Prio.Ring.pos or Prio.avail<0) then |
||
319 | n=n+1; |
||
320 | end |
||
321 | end |
||
322 | |||
323 | -- Anything queued still? |
||
324 | if(n<1) then |
||
325 | -- Nope. Move spillover bandwidth to global availability gauge and clear self.bQueueing |
||
326 | for prioname,Prio in pairs(self.Prio) do |
||
327 | self.avail = self.avail + Prio.avail; |
||
328 | Prio.avail = 0; |
||
329 | end |
||
330 | self.bQueueing = false; |
||
331 | self.Frame:Hide(); |
||
332 | return; |
||
333 | end |
||
334 | |||
335 | -- There's stuff queued. Hand out available bandwidth to priorities as needed and despool their queues |
||
336 | local avail= self.avail/n; |
||
337 | self.avail = 0; |
||
338 | |||
339 | for prioname,Prio in pairs(self.Prio) do |
||
340 | if(Prio.Ring.pos or Prio.avail<0) then |
||
341 | Prio.avail = Prio.avail + avail; |
||
342 | if(Prio.Ring.pos and Prio.avail>Prio.Ring.pos[1].nSize) then |
||
343 | self:Despool(Prio); |
||
344 | end |
||
345 | end |
||
346 | end |
||
347 | |||
348 | -- Expire recycled tables if needed |
||
349 | self.MsgBin:Tidy(); |
||
350 | self.PipeBin:Tidy(); |
||
351 | end |
||
352 | |||
353 | |||
354 | |||
355 | |||
356 | ----------------------------------------------------------------------- |
||
357 | -- Spooling logic |
||
358 | |||
359 | |||
360 | function ChatThrottleLib:Enqueue(prioname, pipename, msg) |
||
361 | local Prio = self.Prio[prioname]; |
||
362 | local pipe = Prio.ByName[pipename]; |
||
363 | if(not pipe) then |
||
364 | self.Frame:Show(); |
||
365 | pipe = self.PipeBin:Get(); |
||
366 | pipe.name = pipename; |
||
367 | Prio.ByName[pipename] = pipe; |
||
368 | Prio.Ring:Add(pipe); |
||
369 | end |
||
370 | |||
371 | tinsert(pipe, msg); |
||
372 | |||
373 | self.bQueueing = true; |
||
374 | end |
||
375 | |||
376 | |||
377 | |||
378 | function ChatThrottleLib:SendChatMessage(prio, prefix, text, chattype, language, destination) |
||
379 | if(not (self and prio and text and self.Prio[prio] ) ) then |
||
380 | error('Usage: ChatThrottleLib:SendChatMessage("{BULK||NORMAL||ALERT}", "prefix" or nil, "text"[, "chattype"[, "language"[, "destination"]]]', 0); |
||
381 | end |
||
382 | |||
383 | prefix = prefix or tostring(this); -- each frame gets its own queue if prefix is not given |
||
384 | |||
385 | local nSize = strlen(text) + MSG_OVERHEAD; |
||
386 | |||
387 | -- Check if there's room in the global available bandwidth gauge to send directly |
||
388 | if(not self.bQueueing and nSize < self:UpdateAvail()) then |
||
389 | self.avail = self.avail - nSize; |
||
390 | self.ORIG_SendChatMessage(text, chattype, language, destination); |
||
391 | self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize; |
||
392 | return; |
||
393 | end |
||
394 | |||
395 | -- Message needs to be queued |
||
396 | msg=self.MsgBin:Get(); |
||
397 | msg.f=self.ORIG_SendChatMessage |
||
398 | msg[1]=text; |
||
399 | msg[2]=chattype or "SAY"; |
||
400 | msg[3]=language; |
||
401 | msg[4]=destination; |
||
402 | msg.n = 4 |
||
403 | msg.nSize = nSize; |
||
404 | |||
405 | self:Enqueue(prio, string.format("%s/%s/%s", prefix, chattype, destination or ""), msg); |
||
406 | end |
||
407 | |||
408 | |||
409 | function ChatThrottleLib:SendAddonMessage(prio, prefix, text, chattype) |
||
410 | if(not (self and prio and prefix and text and chattype and self.Prio[prio] ) ) then |
||
411 | error('Usage: ChatThrottleLib:SendAddonMessage("{BULK||NORMAL||ALERT}", "prefix", "text", "chattype")', 0); |
||
412 | end |
||
413 | |||
414 | local nSize = strlen(prefix) + 1 + strlen(text) + MSG_OVERHEAD; |
||
415 | |||
416 | -- Check if there's room in the global available bandwidth gauge to send directly |
||
417 | if(not self.bQueueing and nSize < self:UpdateAvail()) then |
||
418 | self.avail = self.avail - nSize; |
||
419 | self.ORIG_SendAddonMessage(prefix, text, chattype); |
||
420 | self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize; |
||
421 | return; |
||
422 | end |
||
423 | |||
424 | -- Message needs to be queued |
||
425 | msg=self.MsgBin:Get(); |
||
426 | msg.f=self.ORIG_SendAddonMessage; |
||
427 | msg[1]=prefix; |
||
428 | msg[2]=text; |
||
429 | msg[3]=chattype; |
||
430 | msg.n = 3 |
||
431 | msg.nSize = nSize; |
||
432 | |||
433 | self:Enqueue(prio, string.format("%s/%s", prefix, chattype), msg); |
||
434 | end |
||
435 | |||
436 | |||
437 | |||
438 | |||
439 | ----------------------------------------------------------------------- |
||
440 | -- Get the ball rolling! |
||
441 | |||
442 | ChatThrottleLib:Init(); |
||
443 | |||
444 | --[[ WoWBench debugging snippet |
||
445 | if(WOWB_VER) then |
||
446 | local function SayTimer() |
||
447 | print("SAY: "..GetTime().." "..arg1); |
||
448 | end |
||
449 | ChatThrottleLib.Frame:SetScript("OnEvent", SayTimer); |
||
450 | ChatThrottleLib.Frame:RegisterEvent("CHAT_MSG_SAY"); |
||
451 | end |
||
452 | ]] |
||
453 | |||
454 |