Mercurial > wow > askmrrobot
comparison Libs/AceComm-3.0/ChatThrottleLib.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 -- | |
| 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 SendAddonMessage 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 -- LICENSE: ChatThrottleLib is released into the Public Domain | |
| 24 -- | |
| 25 | |
| 26 local CTL_VERSION = 23 | |
| 27 | |
| 28 local _G = _G | |
| 29 | |
| 30 if _G.ChatThrottleLib then | |
| 31 if _G.ChatThrottleLib.version >= CTL_VERSION then | |
| 32 -- There's already a newer (or same) version loaded. Buh-bye. | |
| 33 return | |
| 34 elseif not _G.ChatThrottleLib.securelyHooked then | |
| 35 print("ChatThrottleLib: Warning: There's an ANCIENT ChatThrottleLib.lua (pre-wow 2.0, <v16) in an addon somewhere. Get the addon updated or copy in a newer ChatThrottleLib.lua (>=v16) in it!") | |
| 36 -- ATTEMPT to unhook; this'll behave badly if someone else has hooked... | |
| 37 -- ... and if someone has securehooked, they can kiss that goodbye too... >.< | |
| 38 _G.SendChatMessage = _G.ChatThrottleLib.ORIG_SendChatMessage | |
| 39 if _G.ChatThrottleLib.ORIG_SendAddonMessage then | |
| 40 _G.SendAddonMessage = _G.ChatThrottleLib.ORIG_SendAddonMessage | |
| 41 end | |
| 42 end | |
| 43 _G.ChatThrottleLib.ORIG_SendChatMessage = nil | |
| 44 _G.ChatThrottleLib.ORIG_SendAddonMessage = nil | |
| 45 end | |
| 46 | |
| 47 if not _G.ChatThrottleLib then | |
| 48 _G.ChatThrottleLib = {} | |
| 49 end | |
| 50 | |
| 51 ChatThrottleLib = _G.ChatThrottleLib -- in case some addon does "local ChatThrottleLib" above us and we're copypasted (AceComm-2, sigh) | |
| 52 local ChatThrottleLib = _G.ChatThrottleLib | |
| 53 | |
| 54 ChatThrottleLib.version = CTL_VERSION | |
| 55 | |
| 56 | |
| 57 | |
| 58 ------------------ TWEAKABLES ----------------- | |
| 59 | |
| 60 ChatThrottleLib.MAX_CPS = 800 -- 2000 seems to be safe if NOTHING ELSE is happening. let's call it 800. | |
| 61 ChatThrottleLib.MSG_OVERHEAD = 40 -- Guesstimate overhead for sending a message; source+dest+chattype+protocolstuff | |
| 62 | |
| 63 ChatThrottleLib.BURST = 4000 -- WoW's server buffer seems to be about 32KB. 8KB should be safe, but seen disconnects on _some_ servers. Using 4KB now. | |
| 64 | |
| 65 ChatThrottleLib.MIN_FPS = 20 -- Reduce output CPS to half (and don't burst) if FPS drops below this value | |
| 66 | |
| 67 | |
| 68 local setmetatable = setmetatable | |
| 69 local table_remove = table.remove | |
| 70 local tostring = tostring | |
| 71 local GetTime = GetTime | |
| 72 local math_min = math.min | |
| 73 local math_max = math.max | |
| 74 local next = next | |
| 75 local strlen = string.len | |
| 76 local GetFramerate = GetFramerate | |
| 77 local strlower = string.lower | |
| 78 local unpack,type,pairs,wipe = unpack,type,pairs,wipe | |
| 79 local UnitInRaid,UnitInParty = UnitInRaid,UnitInParty | |
| 80 | |
| 81 | |
| 82 ----------------------------------------------------------------------- | |
| 83 -- Double-linked ring implementation | |
| 84 | |
| 85 local Ring = {} | |
| 86 local RingMeta = { __index = Ring } | |
| 87 | |
| 88 function Ring:New() | |
| 89 local ret = {} | |
| 90 setmetatable(ret, RingMeta) | |
| 91 return ret | |
| 92 end | |
| 93 | |
| 94 function Ring:Add(obj) -- Append at the "far end" of the ring (aka just before the current position) | |
| 95 if self.pos then | |
| 96 obj.prev = self.pos.prev | |
| 97 obj.prev.next = obj | |
| 98 obj.next = self.pos | |
| 99 obj.next.prev = obj | |
| 100 else | |
| 101 obj.next = obj | |
| 102 obj.prev = obj | |
| 103 self.pos = obj | |
| 104 end | |
| 105 end | |
| 106 | |
| 107 function Ring:Remove(obj) | |
| 108 obj.next.prev = obj.prev | |
| 109 obj.prev.next = obj.next | |
| 110 if self.pos == obj then | |
| 111 self.pos = obj.next | |
| 112 if self.pos == obj then | |
| 113 self.pos = nil | |
| 114 end | |
| 115 end | |
| 116 end | |
| 117 | |
| 118 | |
| 119 | |
| 120 ----------------------------------------------------------------------- | |
| 121 -- Recycling bin for pipes | |
| 122 -- A pipe is a plain integer-indexed queue of messages | |
| 123 -- Pipes normally live in Rings of pipes (3 rings total, one per priority) | |
| 124 | |
| 125 ChatThrottleLib.PipeBin = nil -- pre-v19, drastically different | |
| 126 local PipeBin = setmetatable({}, {__mode="k"}) | |
| 127 | |
| 128 local function DelPipe(pipe) | |
| 129 PipeBin[pipe] = true | |
| 130 end | |
| 131 | |
| 132 local function NewPipe() | |
| 133 local pipe = next(PipeBin) | |
| 134 if pipe then | |
| 135 wipe(pipe) | |
| 136 PipeBin[pipe] = nil | |
| 137 return pipe | |
| 138 end | |
| 139 return {} | |
| 140 end | |
| 141 | |
| 142 | |
| 143 | |
| 144 | |
| 145 ----------------------------------------------------------------------- | |
| 146 -- Recycling bin for messages | |
| 147 | |
| 148 ChatThrottleLib.MsgBin = nil -- pre-v19, drastically different | |
| 149 local MsgBin = setmetatable({}, {__mode="k"}) | |
| 150 | |
| 151 local function DelMsg(msg) | |
| 152 msg[1] = nil | |
| 153 -- there's more parameters, but they're very repetetive so the string pool doesn't suffer really, and it's faster to just not delete them. | |
| 154 MsgBin[msg] = true | |
| 155 end | |
| 156 | |
| 157 local function NewMsg() | |
| 158 local msg = next(MsgBin) | |
| 159 if msg then | |
| 160 MsgBin[msg] = nil | |
| 161 return msg | |
| 162 end | |
| 163 return {} | |
| 164 end | |
| 165 | |
| 166 | |
| 167 ----------------------------------------------------------------------- | |
| 168 -- ChatThrottleLib:Init | |
| 169 -- Initialize queues, set up frame for OnUpdate, etc | |
| 170 | |
| 171 | |
| 172 function ChatThrottleLib:Init() | |
| 173 | |
| 174 -- Set up queues | |
| 175 if not self.Prio then | |
| 176 self.Prio = {} | |
| 177 self.Prio["ALERT"] = { ByName = {}, Ring = Ring:New(), avail = 0 } | |
| 178 self.Prio["NORMAL"] = { ByName = {}, Ring = Ring:New(), avail = 0 } | |
| 179 self.Prio["BULK"] = { ByName = {}, Ring = Ring:New(), avail = 0 } | |
| 180 end | |
| 181 | |
| 182 -- v4: total send counters per priority | |
| 183 for _, Prio in pairs(self.Prio) do | |
| 184 Prio.nTotalSent = Prio.nTotalSent or 0 | |
| 185 end | |
| 186 | |
| 187 if not self.avail then | |
| 188 self.avail = 0 -- v5 | |
| 189 end | |
| 190 if not self.nTotalSent then | |
| 191 self.nTotalSent = 0 -- v5 | |
| 192 end | |
| 193 | |
| 194 | |
| 195 -- Set up a frame to get OnUpdate events | |
| 196 if not self.Frame then | |
| 197 self.Frame = CreateFrame("Frame") | |
| 198 self.Frame:Hide() | |
| 199 end | |
| 200 self.Frame:SetScript("OnUpdate", self.OnUpdate) | |
| 201 self.Frame:SetScript("OnEvent", self.OnEvent) -- v11: Monitor P_E_W so we can throttle hard for a few seconds | |
| 202 self.Frame:RegisterEvent("PLAYER_ENTERING_WORLD") | |
| 203 self.OnUpdateDelay = 0 | |
| 204 self.LastAvailUpdate = GetTime() | |
| 205 self.HardThrottlingBeginTime = GetTime() -- v11: Throttle hard for a few seconds after startup | |
| 206 | |
| 207 -- Hook SendChatMessage and SendAddonMessage so we can measure unpiped traffic and avoid overloads (v7) | |
| 208 if not self.securelyHooked then | |
| 209 -- Use secure hooks as of v16. Old regular hook support yanked out in v21. | |
| 210 self.securelyHooked = true | |
| 211 --SendChatMessage | |
| 212 hooksecurefunc("SendChatMessage", function(...) | |
| 213 return ChatThrottleLib.Hook_SendChatMessage(...) | |
| 214 end) | |
| 215 --SendAddonMessage | |
| 216 hooksecurefunc("SendAddonMessage", function(...) | |
| 217 return ChatThrottleLib.Hook_SendAddonMessage(...) | |
| 218 end) | |
| 219 end | |
| 220 self.nBypass = 0 | |
| 221 end | |
| 222 | |
| 223 | |
| 224 ----------------------------------------------------------------------- | |
| 225 -- ChatThrottleLib.Hook_SendChatMessage / .Hook_SendAddonMessage | |
| 226 | |
| 227 local bMyTraffic = false | |
| 228 | |
| 229 function ChatThrottleLib.Hook_SendChatMessage(text, chattype, language, destination, ...) | |
| 230 if bMyTraffic then | |
| 231 return | |
| 232 end | |
| 233 local self = ChatThrottleLib | |
| 234 local size = strlen(tostring(text or "")) + strlen(tostring(destination or "")) + self.MSG_OVERHEAD | |
| 235 self.avail = self.avail - size | |
| 236 self.nBypass = self.nBypass + size -- just a statistic | |
| 237 end | |
| 238 function ChatThrottleLib.Hook_SendAddonMessage(prefix, text, chattype, destination, ...) | |
| 239 if bMyTraffic then | |
| 240 return | |
| 241 end | |
| 242 local self = ChatThrottleLib | |
| 243 local size = tostring(text or ""):len() + tostring(prefix or ""):len(); | |
| 244 size = size + tostring(destination or ""):len() + self.MSG_OVERHEAD | |
| 245 self.avail = self.avail - size | |
| 246 self.nBypass = self.nBypass + size -- just a statistic | |
| 247 end | |
| 248 | |
| 249 | |
| 250 | |
| 251 ----------------------------------------------------------------------- | |
| 252 -- ChatThrottleLib:UpdateAvail | |
| 253 -- Update self.avail with how much bandwidth is currently available | |
| 254 | |
| 255 function ChatThrottleLib:UpdateAvail() | |
| 256 local now = GetTime() | |
| 257 local MAX_CPS = self.MAX_CPS; | |
| 258 local newavail = MAX_CPS * (now - self.LastAvailUpdate) | |
| 259 local avail = self.avail | |
| 260 | |
| 261 if now - self.HardThrottlingBeginTime < 5 then | |
| 262 -- First 5 seconds after startup/zoning: VERY hard clamping to avoid irritating the server rate limiter, it seems very cranky then | |
| 263 avail = math_min(avail + (newavail*0.1), MAX_CPS*0.5) | |
| 264 self.bChoking = true | |
| 265 elseif GetFramerate() < self.MIN_FPS then -- GetFrameRate call takes ~0.002 secs | |
| 266 avail = math_min(MAX_CPS, avail + newavail*0.5) | |
| 267 self.bChoking = true -- just a statistic | |
| 268 else | |
| 269 avail = math_min(self.BURST, avail + newavail) | |
| 270 self.bChoking = false | |
| 271 end | |
| 272 | |
| 273 avail = math_max(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. | |
| 274 | |
| 275 self.avail = avail | |
| 276 self.LastAvailUpdate = now | |
| 277 | |
| 278 return avail | |
| 279 end | |
| 280 | |
| 281 | |
| 282 ----------------------------------------------------------------------- | |
| 283 -- Despooling logic | |
| 284 -- Reminder: | |
| 285 -- - We have 3 Priorities, each containing a "Ring" construct ... | |
| 286 -- - ... made up of N "Pipe"s (1 for each destination/pipename) | |
| 287 -- - and each pipe contains messages | |
| 288 | |
| 289 function ChatThrottleLib:Despool(Prio) | |
| 290 local ring = Prio.Ring | |
| 291 while ring.pos and Prio.avail > ring.pos[1].nSize do | |
| 292 local msg = table_remove(ring.pos, 1) | |
| 293 if not ring.pos[1] then -- did we remove last msg in this pipe? | |
| 294 local pipe = Prio.Ring.pos | |
| 295 Prio.Ring:Remove(pipe) | |
| 296 Prio.ByName[pipe.name] = nil | |
| 297 DelPipe(pipe) | |
| 298 else | |
| 299 Prio.Ring.pos = Prio.Ring.pos.next | |
| 300 end | |
| 301 local didSend=false | |
| 302 local lowerDest = strlower(msg[3] or "") | |
| 303 if lowerDest == "raid" and not UnitInRaid("player") then | |
| 304 -- do nothing | |
| 305 elseif lowerDest == "party" and not UnitInParty("player") then | |
| 306 -- do nothing | |
| 307 else | |
| 308 Prio.avail = Prio.avail - msg.nSize | |
| 309 bMyTraffic = true | |
| 310 msg.f(unpack(msg, 1, msg.n)) | |
| 311 bMyTraffic = false | |
| 312 Prio.nTotalSent = Prio.nTotalSent + msg.nSize | |
| 313 DelMsg(msg) | |
| 314 didSend = true | |
| 315 end | |
| 316 -- notify caller of delivery (even if we didn't send it) | |
| 317 if msg.callbackFn then | |
| 318 msg.callbackFn (msg.callbackArg, didSend) | |
| 319 end | |
| 320 -- USER CALLBACK MAY ERROR | |
| 321 end | |
| 322 end | |
| 323 | |
| 324 | |
| 325 function ChatThrottleLib.OnEvent(this,event) | |
| 326 -- v11: We know that the rate limiter is touchy after login. Assume that it's touchy after zoning, too. | |
| 327 local self = ChatThrottleLib | |
| 328 if event == "PLAYER_ENTERING_WORLD" then | |
| 329 self.HardThrottlingBeginTime = GetTime() -- Throttle hard for a few seconds after zoning | |
| 330 self.avail = 0 | |
| 331 end | |
| 332 end | |
| 333 | |
| 334 | |
| 335 function ChatThrottleLib.OnUpdate(this,delay) | |
| 336 local self = ChatThrottleLib | |
| 337 | |
| 338 self.OnUpdateDelay = self.OnUpdateDelay + delay | |
| 339 if self.OnUpdateDelay < 0.08 then | |
| 340 return | |
| 341 end | |
| 342 self.OnUpdateDelay = 0 | |
| 343 | |
| 344 self:UpdateAvail() | |
| 345 | |
| 346 if self.avail < 0 then | |
| 347 return -- argh. some bastard is spewing stuff past the lib. just bail early to save cpu. | |
| 348 end | |
| 349 | |
| 350 -- See how many of our priorities have queued messages (we only have 3, don't worry about the loop) | |
| 351 local n = 0 | |
| 352 for prioname,Prio in pairs(self.Prio) do | |
| 353 if Prio.Ring.pos or Prio.avail < 0 then | |
| 354 n = n + 1 | |
| 355 end | |
| 356 end | |
| 357 | |
| 358 -- Anything queued still? | |
| 359 if n<1 then | |
| 360 -- Nope. Move spillover bandwidth to global availability gauge and clear self.bQueueing | |
| 361 for prioname, Prio in pairs(self.Prio) do | |
| 362 self.avail = self.avail + Prio.avail | |
| 363 Prio.avail = 0 | |
| 364 end | |
| 365 self.bQueueing = false | |
| 366 self.Frame:Hide() | |
| 367 return | |
| 368 end | |
| 369 | |
| 370 -- There's stuff queued. Hand out available bandwidth to priorities as needed and despool their queues | |
| 371 local avail = self.avail/n | |
| 372 self.avail = 0 | |
| 373 | |
| 374 for prioname, Prio in pairs(self.Prio) do | |
| 375 if Prio.Ring.pos or Prio.avail < 0 then | |
| 376 Prio.avail = Prio.avail + avail | |
| 377 if Prio.Ring.pos and Prio.avail > Prio.Ring.pos[1].nSize then | |
| 378 self:Despool(Prio) | |
| 379 -- Note: We might not get here if the user-supplied callback function errors out! Take care! | |
| 380 end | |
| 381 end | |
| 382 end | |
| 383 | |
| 384 end | |
| 385 | |
| 386 | |
| 387 | |
| 388 | |
| 389 ----------------------------------------------------------------------- | |
| 390 -- Spooling logic | |
| 391 | |
| 392 function ChatThrottleLib:Enqueue(prioname, pipename, msg) | |
| 393 local Prio = self.Prio[prioname] | |
| 394 local pipe = Prio.ByName[pipename] | |
| 395 if not pipe then | |
| 396 self.Frame:Show() | |
| 397 pipe = NewPipe() | |
| 398 pipe.name = pipename | |
| 399 Prio.ByName[pipename] = pipe | |
| 400 Prio.Ring:Add(pipe) | |
| 401 end | |
| 402 | |
| 403 pipe[#pipe + 1] = msg | |
| 404 | |
| 405 self.bQueueing = true | |
| 406 end | |
| 407 | |
| 408 function ChatThrottleLib:SendChatMessage(prio, prefix, text, chattype, language, destination, queueName, callbackFn, callbackArg) | |
| 409 if not self or not prio or not prefix or not text or not self.Prio[prio] then | |
| 410 error('Usage: ChatThrottleLib:SendChatMessage("{BULK||NORMAL||ALERT}", "prefix", "text"[, "chattype"[, "language"[, "destination"]]]', 2) | |
| 411 end | |
| 412 if callbackFn and type(callbackFn)~="function" then | |
| 413 error('ChatThrottleLib:ChatMessage(): callbackFn: expected function, got '..type(callbackFn), 2) | |
| 414 end | |
| 415 | |
| 416 local nSize = text:len() | |
| 417 | |
| 418 if nSize>255 then | |
| 419 error("ChatThrottleLib:SendChatMessage(): message length cannot exceed 255 bytes", 2) | |
| 420 end | |
| 421 | |
| 422 nSize = nSize + self.MSG_OVERHEAD | |
| 423 | |
| 424 -- Check if there's room in the global available bandwidth gauge to send directly | |
| 425 if not self.bQueueing and nSize < self:UpdateAvail() then | |
| 426 self.avail = self.avail - nSize | |
| 427 bMyTraffic = true | |
| 428 _G.SendChatMessage(text, chattype, language, destination) | |
| 429 bMyTraffic = false | |
| 430 self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize | |
| 431 if callbackFn then | |
| 432 callbackFn (callbackArg, true) | |
| 433 end | |
| 434 -- USER CALLBACK MAY ERROR | |
| 435 return | |
| 436 end | |
| 437 | |
| 438 -- Message needs to be queued | |
| 439 local msg = NewMsg() | |
| 440 msg.f = _G.SendChatMessage | |
| 441 msg[1] = text | |
| 442 msg[2] = chattype or "SAY" | |
| 443 msg[3] = language | |
| 444 msg[4] = destination | |
| 445 msg.n = 4 | |
| 446 msg.nSize = nSize | |
| 447 msg.callbackFn = callbackFn | |
| 448 msg.callbackArg = callbackArg | |
| 449 | |
| 450 self:Enqueue(prio, queueName or (prefix..(chattype or "SAY")..(destination or "")), msg) | |
| 451 end | |
| 452 | |
| 453 | |
| 454 function ChatThrottleLib:SendAddonMessage(prio, prefix, text, chattype, target, queueName, callbackFn, callbackArg) | |
| 455 if not self or not prio or not prefix or not text or not chattype or not self.Prio[prio] then | |
| 456 error('Usage: ChatThrottleLib:SendAddonMessage("{BULK||NORMAL||ALERT}", "prefix", "text", "chattype"[, "target"])', 2) | |
| 457 end | |
| 458 if callbackFn and type(callbackFn)~="function" then | |
| 459 error('ChatThrottleLib:SendAddonMessage(): callbackFn: expected function, got '..type(callbackFn), 2) | |
| 460 end | |
| 461 | |
| 462 local nSize = text:len(); | |
| 463 | |
| 464 if RegisterAddonMessagePrefix then | |
| 465 if nSize>255 then | |
| 466 error("ChatThrottleLib:SendAddonMessage(): message length cannot exceed 255 bytes", 2) | |
| 467 end | |
| 468 else | |
| 469 nSize = nSize + prefix:len() + 1 | |
| 470 if nSize>255 then | |
| 471 error("ChatThrottleLib:SendAddonMessage(): prefix + message length cannot exceed 254 bytes", 2) | |
| 472 end | |
| 473 end | |
| 474 | |
| 475 nSize = nSize + self.MSG_OVERHEAD; | |
| 476 | |
| 477 -- Check if there's room in the global available bandwidth gauge to send directly | |
| 478 if not self.bQueueing and nSize < self:UpdateAvail() then | |
| 479 self.avail = self.avail - nSize | |
| 480 bMyTraffic = true | |
| 481 _G.SendAddonMessage(prefix, text, chattype, target) | |
| 482 bMyTraffic = false | |
| 483 self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize | |
| 484 if callbackFn then | |
| 485 callbackFn (callbackArg, true) | |
| 486 end | |
| 487 -- USER CALLBACK MAY ERROR | |
| 488 return | |
| 489 end | |
| 490 | |
| 491 -- Message needs to be queued | |
| 492 local msg = NewMsg() | |
| 493 msg.f = _G.SendAddonMessage | |
| 494 msg[1] = prefix | |
| 495 msg[2] = text | |
| 496 msg[3] = chattype | |
| 497 msg[4] = target | |
| 498 msg.n = (target~=nil) and 4 or 3; | |
| 499 msg.nSize = nSize | |
| 500 msg.callbackFn = callbackFn | |
| 501 msg.callbackArg = callbackArg | |
| 502 | |
| 503 self:Enqueue(prio, queueName or (prefix..chattype..(target or "")), msg) | |
| 504 end | |
| 505 | |
| 506 | |
| 507 | |
| 508 | |
| 509 ----------------------------------------------------------------------- | |
| 510 -- Get the ball rolling! | |
| 511 | |
| 512 ChatThrottleLib:Init() | |
| 513 | |
| 514 --[[ WoWBench debugging snippet | |
| 515 if(WOWB_VER) then | |
| 516 local function SayTimer() | |
| 517 print("SAY: "..GetTime().." "..arg1) | |
| 518 end | |
| 519 ChatThrottleLib.Frame:SetScript("OnEvent", SayTimer) | |
| 520 ChatThrottleLib.Frame:RegisterEvent("CHAT_MSG_SAY") | |
| 521 end | |
| 522 ]] | |
| 523 | |
| 524 |
