vanilla-wow-addons – Blame information for rev 1

Subversion Repositories:
Rev:
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