Tercio@4: -- Tercio@4: -- ChatThrottleLib by Mikk Tercio@4: -- Tercio@4: -- Manages AddOn chat output to keep player from getting kicked off. Tercio@4: -- Tercio@4: -- ChatThrottleLib:SendChatMessage/:SendAddonMessage functions that accept Tercio@4: -- a Priority ("BULK", "NORMAL", "ALERT") as well as prefix for SendChatMessage. Tercio@4: -- Tercio@4: -- Priorities get an equal share of available bandwidth when fully loaded. Tercio@4: -- Communication channels are separated on extension+chattype+destination and Tercio@4: -- get round-robinned. (Destination only matters for whispers and channels, Tercio@4: -- obviously) Tercio@4: -- Tercio@4: -- Will install hooks for SendChatMessage and SendAddonMessage to measure Tercio@4: -- bandwidth bypassing the library and use less bandwidth itself. Tercio@4: -- Tercio@4: -- Tercio@4: -- Fully embeddable library. Just copy this file into your addon directory, Tercio@4: -- add it to the .toc, and it's done. Tercio@4: -- Tercio@4: -- Can run as a standalone addon also, but, really, just embed it! :-) Tercio@4: -- Tercio@4: -- LICENSE: ChatThrottleLib is released into the Public Domain Tercio@4: -- Tercio@4: Tercio@58: local CTL_VERSION = 24 Tercio@4: Tercio@4: local _G = _G Tercio@4: Tercio@4: if _G.ChatThrottleLib then Tercio@4: if _G.ChatThrottleLib.version >= CTL_VERSION then Tercio@4: -- There's already a newer (or same) version loaded. Buh-bye. Tercio@4: return Tercio@4: elseif not _G.ChatThrottleLib.securelyHooked then Tercio@4: print("ChatThrottleLib: Warning: There's an ANCIENT ChatThrottleLib.lua (pre-wow 2.0, =v16) in it!") Tercio@4: -- ATTEMPT to unhook; this'll behave badly if someone else has hooked... Tercio@4: -- ... and if someone has securehooked, they can kiss that goodbye too... >.< Tercio@4: _G.SendChatMessage = _G.ChatThrottleLib.ORIG_SendChatMessage Tercio@4: if _G.ChatThrottleLib.ORIG_SendAddonMessage then Tercio@4: _G.SendAddonMessage = _G.ChatThrottleLib.ORIG_SendAddonMessage Tercio@4: end Tercio@4: end Tercio@4: _G.ChatThrottleLib.ORIG_SendChatMessage = nil Tercio@4: _G.ChatThrottleLib.ORIG_SendAddonMessage = nil Tercio@4: end Tercio@4: Tercio@4: if not _G.ChatThrottleLib then Tercio@4: _G.ChatThrottleLib = {} Tercio@4: end Tercio@4: Tercio@4: ChatThrottleLib = _G.ChatThrottleLib -- in case some addon does "local ChatThrottleLib" above us and we're copypasted (AceComm-2, sigh) Tercio@4: local ChatThrottleLib = _G.ChatThrottleLib Tercio@4: Tercio@4: ChatThrottleLib.version = CTL_VERSION Tercio@4: Tercio@4: Tercio@4: Tercio@4: ------------------ TWEAKABLES ----------------- Tercio@4: Tercio@4: ChatThrottleLib.MAX_CPS = 800 -- 2000 seems to be safe if NOTHING ELSE is happening. let's call it 800. Tercio@4: ChatThrottleLib.MSG_OVERHEAD = 40 -- Guesstimate overhead for sending a message; source+dest+chattype+protocolstuff Tercio@4: Tercio@4: 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. Tercio@4: Tercio@4: ChatThrottleLib.MIN_FPS = 20 -- Reduce output CPS to half (and don't burst) if FPS drops below this value Tercio@4: Tercio@4: Tercio@4: local setmetatable = setmetatable Tercio@4: local table_remove = table.remove Tercio@4: local tostring = tostring Tercio@4: local GetTime = GetTime Tercio@4: local math_min = math.min Tercio@4: local math_max = math.max Tercio@4: local next = next Tercio@4: local strlen = string.len Tercio@4: local GetFramerate = GetFramerate Tercio@4: local strlower = string.lower Tercio@4: local unpack,type,pairs,wipe = unpack,type,pairs,wipe Tercio@4: local UnitInRaid,UnitInParty = UnitInRaid,UnitInParty Tercio@4: Tercio@4: Tercio@4: ----------------------------------------------------------------------- Tercio@4: -- Double-linked ring implementation Tercio@4: Tercio@4: local Ring = {} Tercio@4: local RingMeta = { __index = Ring } Tercio@4: Tercio@4: function Ring:New() Tercio@4: local ret = {} Tercio@4: setmetatable(ret, RingMeta) Tercio@4: return ret Tercio@4: end Tercio@4: Tercio@4: function Ring:Add(obj) -- Append at the "far end" of the ring (aka just before the current position) Tercio@4: if self.pos then Tercio@4: obj.prev = self.pos.prev Tercio@4: obj.prev.next = obj Tercio@4: obj.next = self.pos Tercio@4: obj.next.prev = obj Tercio@4: else Tercio@4: obj.next = obj Tercio@4: obj.prev = obj Tercio@4: self.pos = obj Tercio@4: end Tercio@4: end Tercio@4: Tercio@4: function Ring:Remove(obj) Tercio@4: obj.next.prev = obj.prev Tercio@4: obj.prev.next = obj.next Tercio@4: if self.pos == obj then Tercio@4: self.pos = obj.next Tercio@4: if self.pos == obj then Tercio@4: self.pos = nil Tercio@4: end Tercio@4: end Tercio@4: end Tercio@4: Tercio@4: Tercio@4: Tercio@4: ----------------------------------------------------------------------- Tercio@4: -- Recycling bin for pipes Tercio@4: -- A pipe is a plain integer-indexed queue of messages Tercio@4: -- Pipes normally live in Rings of pipes (3 rings total, one per priority) Tercio@4: Tercio@4: ChatThrottleLib.PipeBin = nil -- pre-v19, drastically different Tercio@4: local PipeBin = setmetatable({}, {__mode="k"}) Tercio@4: Tercio@4: local function DelPipe(pipe) Tercio@4: PipeBin[pipe] = true Tercio@4: end Tercio@4: Tercio@4: local function NewPipe() Tercio@4: local pipe = next(PipeBin) Tercio@4: if pipe then Tercio@4: wipe(pipe) Tercio@4: PipeBin[pipe] = nil Tercio@4: return pipe Tercio@4: end Tercio@4: return {} Tercio@4: end Tercio@4: Tercio@4: Tercio@4: Tercio@4: Tercio@4: ----------------------------------------------------------------------- Tercio@4: -- Recycling bin for messages Tercio@4: Tercio@4: ChatThrottleLib.MsgBin = nil -- pre-v19, drastically different Tercio@4: local MsgBin = setmetatable({}, {__mode="k"}) Tercio@4: Tercio@4: local function DelMsg(msg) Tercio@4: msg[1] = nil Tercio@4: -- 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. Tercio@4: MsgBin[msg] = true Tercio@4: end Tercio@4: Tercio@4: local function NewMsg() Tercio@4: local msg = next(MsgBin) Tercio@4: if msg then Tercio@4: MsgBin[msg] = nil Tercio@4: return msg Tercio@4: end Tercio@4: return {} Tercio@4: end Tercio@4: Tercio@4: Tercio@4: ----------------------------------------------------------------------- Tercio@4: -- ChatThrottleLib:Init Tercio@4: -- Initialize queues, set up frame for OnUpdate, etc Tercio@4: Tercio@4: Tercio@4: function ChatThrottleLib:Init() Tercio@4: Tercio@4: -- Set up queues Tercio@4: if not self.Prio then Tercio@4: self.Prio = {} Tercio@4: self.Prio["ALERT"] = { ByName = {}, Ring = Ring:New(), avail = 0 } Tercio@4: self.Prio["NORMAL"] = { ByName = {}, Ring = Ring:New(), avail = 0 } Tercio@4: self.Prio["BULK"] = { ByName = {}, Ring = Ring:New(), avail = 0 } Tercio@4: end Tercio@4: Tercio@4: -- v4: total send counters per priority Tercio@4: for _, Prio in pairs(self.Prio) do Tercio@4: Prio.nTotalSent = Prio.nTotalSent or 0 Tercio@4: end Tercio@4: Tercio@4: if not self.avail then Tercio@4: self.avail = 0 -- v5 Tercio@4: end Tercio@4: if not self.nTotalSent then Tercio@4: self.nTotalSent = 0 -- v5 Tercio@4: end Tercio@4: Tercio@4: Tercio@4: -- Set up a frame to get OnUpdate events Tercio@4: if not self.Frame then Tercio@4: self.Frame = CreateFrame("Frame") Tercio@4: self.Frame:Hide() Tercio@4: end Tercio@4: self.Frame:SetScript("OnUpdate", self.OnUpdate) Tercio@4: self.Frame:SetScript("OnEvent", self.OnEvent) -- v11: Monitor P_E_W so we can throttle hard for a few seconds Tercio@4: self.Frame:RegisterEvent("PLAYER_ENTERING_WORLD") Tercio@4: self.OnUpdateDelay = 0 Tercio@4: self.LastAvailUpdate = GetTime() Tercio@4: self.HardThrottlingBeginTime = GetTime() -- v11: Throttle hard for a few seconds after startup Tercio@4: Tercio@4: -- Hook SendChatMessage and SendAddonMessage so we can measure unpiped traffic and avoid overloads (v7) Tercio@4: if not self.securelyHooked then Tercio@4: -- Use secure hooks as of v16. Old regular hook support yanked out in v21. Tercio@4: self.securelyHooked = true Tercio@4: --SendChatMessage Tercio@4: hooksecurefunc("SendChatMessage", function(...) Tercio@4: return ChatThrottleLib.Hook_SendChatMessage(...) Tercio@4: end) Tercio@4: --SendAddonMessage Tercio@58: if _G.C_ChatInfo then Tercio@58: hooksecurefunc(_G.C_ChatInfo, "SendAddonMessage", function(...) Tercio@58: return ChatThrottleLib.Hook_SendAddonMessage(...) Tercio@58: end) Tercio@58: else Tercio@58: hooksecurefunc("SendAddonMessage", function(...) Tercio@58: return ChatThrottleLib.Hook_SendAddonMessage(...) Tercio@58: end) Tercio@58: end Tercio@4: end Tercio@4: self.nBypass = 0 Tercio@4: end Tercio@4: Tercio@4: Tercio@4: ----------------------------------------------------------------------- Tercio@4: -- ChatThrottleLib.Hook_SendChatMessage / .Hook_SendAddonMessage Tercio@4: Tercio@4: local bMyTraffic = false Tercio@4: Tercio@4: function ChatThrottleLib.Hook_SendChatMessage(text, chattype, language, destination, ...) Tercio@4: if bMyTraffic then Tercio@4: return Tercio@4: end Tercio@4: local self = ChatThrottleLib Tercio@4: local size = strlen(tostring(text or "")) + strlen(tostring(destination or "")) + self.MSG_OVERHEAD Tercio@4: self.avail = self.avail - size Tercio@4: self.nBypass = self.nBypass + size -- just a statistic Tercio@4: end Tercio@4: function ChatThrottleLib.Hook_SendAddonMessage(prefix, text, chattype, destination, ...) Tercio@4: if bMyTraffic then Tercio@4: return Tercio@4: end Tercio@4: local self = ChatThrottleLib Tercio@4: local size = tostring(text or ""):len() + tostring(prefix or ""):len(); Tercio@4: size = size + tostring(destination or ""):len() + self.MSG_OVERHEAD Tercio@4: self.avail = self.avail - size Tercio@4: self.nBypass = self.nBypass + size -- just a statistic Tercio@4: end Tercio@4: Tercio@4: Tercio@4: Tercio@4: ----------------------------------------------------------------------- Tercio@4: -- ChatThrottleLib:UpdateAvail Tercio@4: -- Update self.avail with how much bandwidth is currently available Tercio@4: Tercio@4: function ChatThrottleLib:UpdateAvail() Tercio@4: local now = GetTime() Tercio@4: local MAX_CPS = self.MAX_CPS; Tercio@4: local newavail = MAX_CPS * (now - self.LastAvailUpdate) Tercio@4: local avail = self.avail Tercio@4: Tercio@4: if now - self.HardThrottlingBeginTime < 5 then Tercio@4: -- First 5 seconds after startup/zoning: VERY hard clamping to avoid irritating the server rate limiter, it seems very cranky then Tercio@4: avail = math_min(avail + (newavail*0.1), MAX_CPS*0.5) Tercio@4: self.bChoking = true Tercio@4: elseif GetFramerate() < self.MIN_FPS then -- GetFrameRate call takes ~0.002 secs Tercio@4: avail = math_min(MAX_CPS, avail + newavail*0.5) Tercio@4: self.bChoking = true -- just a statistic Tercio@4: else Tercio@4: avail = math_min(self.BURST, avail + newavail) Tercio@4: self.bChoking = false Tercio@4: end Tercio@4: Tercio@4: 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. Tercio@4: Tercio@4: self.avail = avail Tercio@4: self.LastAvailUpdate = now Tercio@4: Tercio@4: return avail Tercio@4: end Tercio@4: Tercio@4: Tercio@4: ----------------------------------------------------------------------- Tercio@4: -- Despooling logic Tercio@4: -- Reminder: Tercio@4: -- - We have 3 Priorities, each containing a "Ring" construct ... Tercio@4: -- - ... made up of N "Pipe"s (1 for each destination/pipename) Tercio@4: -- - and each pipe contains messages Tercio@4: Tercio@4: function ChatThrottleLib:Despool(Prio) Tercio@4: local ring = Prio.Ring Tercio@4: while ring.pos and Prio.avail > ring.pos[1].nSize do Tercio@4: local msg = table_remove(ring.pos, 1) Tercio@4: if not ring.pos[1] then -- did we remove last msg in this pipe? Tercio@4: local pipe = Prio.Ring.pos Tercio@4: Prio.Ring:Remove(pipe) Tercio@4: Prio.ByName[pipe.name] = nil Tercio@4: DelPipe(pipe) Tercio@4: else Tercio@4: Prio.Ring.pos = Prio.Ring.pos.next Tercio@4: end Tercio@4: local didSend=false Tercio@4: local lowerDest = strlower(msg[3] or "") Tercio@4: if lowerDest == "raid" and not UnitInRaid("player") then Tercio@4: -- do nothing Tercio@4: elseif lowerDest == "party" and not UnitInParty("player") then Tercio@4: -- do nothing Tercio@4: else Tercio@4: Prio.avail = Prio.avail - msg.nSize Tercio@4: bMyTraffic = true Tercio@4: msg.f(unpack(msg, 1, msg.n)) Tercio@4: bMyTraffic = false Tercio@4: Prio.nTotalSent = Prio.nTotalSent + msg.nSize Tercio@4: DelMsg(msg) Tercio@4: didSend = true Tercio@4: end Tercio@4: -- notify caller of delivery (even if we didn't send it) Tercio@4: if msg.callbackFn then Tercio@4: msg.callbackFn (msg.callbackArg, didSend) Tercio@4: end Tercio@4: -- USER CALLBACK MAY ERROR Tercio@4: end Tercio@4: end Tercio@4: Tercio@4: Tercio@4: function ChatThrottleLib.OnEvent(this,event) Tercio@4: -- v11: We know that the rate limiter is touchy after login. Assume that it's touchy after zoning, too. Tercio@4: local self = ChatThrottleLib Tercio@4: if event == "PLAYER_ENTERING_WORLD" then Tercio@4: self.HardThrottlingBeginTime = GetTime() -- Throttle hard for a few seconds after zoning Tercio@4: self.avail = 0 Tercio@4: end Tercio@4: end Tercio@4: Tercio@4: Tercio@4: function ChatThrottleLib.OnUpdate(this,delay) Tercio@4: local self = ChatThrottleLib Tercio@4: Tercio@4: self.OnUpdateDelay = self.OnUpdateDelay + delay Tercio@4: if self.OnUpdateDelay < 0.08 then Tercio@4: return Tercio@4: end Tercio@4: self.OnUpdateDelay = 0 Tercio@4: Tercio@4: self:UpdateAvail() Tercio@4: Tercio@4: if self.avail < 0 then Tercio@4: return -- argh. some bastard is spewing stuff past the lib. just bail early to save cpu. Tercio@4: end Tercio@4: Tercio@4: -- See how many of our priorities have queued messages (we only have 3, don't worry about the loop) Tercio@4: local n = 0 Tercio@4: for prioname,Prio in pairs(self.Prio) do Tercio@4: if Prio.Ring.pos or Prio.avail < 0 then Tercio@4: n = n + 1 Tercio@4: end Tercio@4: end Tercio@4: Tercio@4: -- Anything queued still? Tercio@4: if n<1 then Tercio@4: -- Nope. Move spillover bandwidth to global availability gauge and clear self.bQueueing Tercio@4: for prioname, Prio in pairs(self.Prio) do Tercio@4: self.avail = self.avail + Prio.avail Tercio@4: Prio.avail = 0 Tercio@4: end Tercio@4: self.bQueueing = false Tercio@4: self.Frame:Hide() Tercio@4: return Tercio@4: end Tercio@4: Tercio@4: -- There's stuff queued. Hand out available bandwidth to priorities as needed and despool their queues Tercio@4: local avail = self.avail/n Tercio@4: self.avail = 0 Tercio@4: Tercio@4: for prioname, Prio in pairs(self.Prio) do Tercio@4: if Prio.Ring.pos or Prio.avail < 0 then Tercio@4: Prio.avail = Prio.avail + avail Tercio@4: if Prio.Ring.pos and Prio.avail > Prio.Ring.pos[1].nSize then Tercio@4: self:Despool(Prio) Tercio@4: -- Note: We might not get here if the user-supplied callback function errors out! Take care! Tercio@4: end Tercio@4: end Tercio@4: end Tercio@4: Tercio@4: end Tercio@4: Tercio@4: Tercio@4: Tercio@4: Tercio@4: ----------------------------------------------------------------------- Tercio@4: -- Spooling logic Tercio@4: Tercio@4: function ChatThrottleLib:Enqueue(prioname, pipename, msg) Tercio@4: local Prio = self.Prio[prioname] Tercio@4: local pipe = Prio.ByName[pipename] Tercio@4: if not pipe then Tercio@4: self.Frame:Show() Tercio@4: pipe = NewPipe() Tercio@4: pipe.name = pipename Tercio@4: Prio.ByName[pipename] = pipe Tercio@4: Prio.Ring:Add(pipe) Tercio@4: end Tercio@4: Tercio@4: pipe[#pipe + 1] = msg Tercio@4: Tercio@4: self.bQueueing = true Tercio@4: end Tercio@4: Tercio@4: function ChatThrottleLib:SendChatMessage(prio, prefix, text, chattype, language, destination, queueName, callbackFn, callbackArg) Tercio@4: if not self or not prio or not prefix or not text or not self.Prio[prio] then Tercio@4: error('Usage: ChatThrottleLib:SendChatMessage("{BULK||NORMAL||ALERT}", "prefix", "text"[, "chattype"[, "language"[, "destination"]]]', 2) Tercio@4: end Tercio@4: if callbackFn and type(callbackFn)~="function" then Tercio@4: error('ChatThrottleLib:ChatMessage(): callbackFn: expected function, got '..type(callbackFn), 2) Tercio@4: end Tercio@4: Tercio@4: local nSize = text:len() Tercio@4: Tercio@4: if nSize>255 then Tercio@4: error("ChatThrottleLib:SendChatMessage(): message length cannot exceed 255 bytes", 2) Tercio@4: end Tercio@4: Tercio@4: nSize = nSize + self.MSG_OVERHEAD Tercio@4: Tercio@4: -- Check if there's room in the global available bandwidth gauge to send directly Tercio@4: if not self.bQueueing and nSize < self:UpdateAvail() then Tercio@4: self.avail = self.avail - nSize Tercio@4: bMyTraffic = true Tercio@4: _G.SendChatMessage(text, chattype, language, destination) Tercio@4: bMyTraffic = false Tercio@4: self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize Tercio@4: if callbackFn then Tercio@4: callbackFn (callbackArg, true) Tercio@4: end Tercio@4: -- USER CALLBACK MAY ERROR Tercio@4: return Tercio@4: end Tercio@4: Tercio@4: -- Message needs to be queued Tercio@4: local msg = NewMsg() Tercio@4: msg.f = _G.SendChatMessage Tercio@4: msg[1] = text Tercio@4: msg[2] = chattype or "SAY" Tercio@4: msg[3] = language Tercio@4: msg[4] = destination Tercio@4: msg.n = 4 Tercio@4: msg.nSize = nSize Tercio@4: msg.callbackFn = callbackFn Tercio@4: msg.callbackArg = callbackArg Tercio@4: Tercio@4: self:Enqueue(prio, queueName or (prefix..(chattype or "SAY")..(destination or "")), msg) Tercio@4: end Tercio@4: Tercio@4: Tercio@4: function ChatThrottleLib:SendAddonMessage(prio, prefix, text, chattype, target, queueName, callbackFn, callbackArg) Tercio@4: if not self or not prio or not prefix or not text or not chattype or not self.Prio[prio] then Tercio@4: error('Usage: ChatThrottleLib:SendAddonMessage("{BULK||NORMAL||ALERT}", "prefix", "text", "chattype"[, "target"])', 2) Tercio@4: end Tercio@4: if callbackFn and type(callbackFn)~="function" then Tercio@4: error('ChatThrottleLib:SendAddonMessage(): callbackFn: expected function, got '..type(callbackFn), 2) Tercio@4: end Tercio@4: Tercio@4: local nSize = text:len(); Tercio@4: Tercio@58: if C_ChatInfo or RegisterAddonMessagePrefix then Tercio@4: if nSize>255 then Tercio@4: error("ChatThrottleLib:SendAddonMessage(): message length cannot exceed 255 bytes", 2) Tercio@4: end Tercio@4: else Tercio@4: nSize = nSize + prefix:len() + 1 Tercio@4: if nSize>255 then Tercio@4: error("ChatThrottleLib:SendAddonMessage(): prefix + message length cannot exceed 254 bytes", 2) Tercio@4: end Tercio@4: end Tercio@4: Tercio@4: nSize = nSize + self.MSG_OVERHEAD; Tercio@4: Tercio@4: -- Check if there's room in the global available bandwidth gauge to send directly Tercio@4: if not self.bQueueing and nSize < self:UpdateAvail() then Tercio@4: self.avail = self.avail - nSize Tercio@4: bMyTraffic = true Tercio@58: if _G.C_ChatInfo then Tercio@58: _G.C_ChatInfo.SendAddonMessage(prefix, text, chattype, target) Tercio@58: else Tercio@58: _G.SendAddonMessage(prefix, text, chattype, target) Tercio@58: end Tercio@4: bMyTraffic = false Tercio@4: self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize Tercio@4: if callbackFn then Tercio@4: callbackFn (callbackArg, true) Tercio@4: end Tercio@4: -- USER CALLBACK MAY ERROR Tercio@4: return Tercio@4: end Tercio@4: Tercio@4: -- Message needs to be queued Tercio@4: local msg = NewMsg() Tercio@58: msg.f = _G.C_ChatInfo and _G.C_ChatInfo.SendAddonMessage or _G.SendAddonMessage Tercio@4: msg[1] = prefix Tercio@4: msg[2] = text Tercio@4: msg[3] = chattype Tercio@4: msg[4] = target Tercio@4: msg.n = (target~=nil) and 4 or 3; Tercio@4: msg.nSize = nSize Tercio@4: msg.callbackFn = callbackFn Tercio@4: msg.callbackArg = callbackArg Tercio@4: Tercio@4: self:Enqueue(prio, queueName or (prefix..chattype..(target or "")), msg) Tercio@4: end Tercio@4: Tercio@4: Tercio@4: Tercio@4: Tercio@4: ----------------------------------------------------------------------- Tercio@4: -- Get the ball rolling! Tercio@4: Tercio@4: ChatThrottleLib:Init() Tercio@4: Tercio@4: --[[ WoWBench debugging snippet Tercio@4: if(WOWB_VER) then Tercio@4: local function SayTimer() Tercio@4: print("SAY: "..GetTime().." "..arg1) Tercio@4: end Tercio@4: ChatThrottleLib.Frame:SetScript("OnEvent", SayTimer) Tercio@4: ChatThrottleLib.Frame:RegisterEvent("CHAT_MSG_SAY") Tercio@4: end Tercio@4: ]] Tercio@4: Tercio@4: