📄 chatthrottlelib.lua
字号:
---- ChatThrottleLib by Mikk---- Manages AddOn chat output to keep player from getting kicked off.---- ChatThrottleLib.SendChatMessage/.SendAddonMessage functions that accept -- a Priority ("BULK", "NORMAL", "ALERT") as well as prefix for SendChatMessage.---- Priorities get an equal share of available bandwidth when fully loaded.-- Communication channels are separated on extension+chattype+destination and-- get round-robinned. (Destination only matters for whispers and channels,-- obviously)---- Will install hooks for SendChatMessage and SendAdd[Oo]nMessage to measure-- bandwidth bypassing the library and use less bandwidth itself.------ Fully embeddable library. Just copy this file into your addon directory,-- add it to the .toc, and it's done.---- Can run as a standalone addon also, but, really, just embed it! :-)--local CTL_VERSION = 16if ChatThrottleLib and ChatThrottleLib.version >= CTL_VERSION then -- There's already a newer (or same) version loaded. Buh-bye. returnendif not ChatThrottleLib then ChatThrottleLib = {}endChatThrottleLib.MAX_CPS = 800 -- 2000 seems to be safe if NOTHING ELSE is happening. let's call it 800.ChatThrottleLib.MSG_OVERHEAD = 40 -- Guesstimate overhead for sending a message; source+dest+chattype+protocolstuffChatThrottleLib.BURST = 4000 -- WoW's server buffer seems to be about 32KB. 8KB should be safe, but seen disconnects on _some_ servers. Using 4KB now.ChatThrottleLib.MIN_FPS = 20 -- Reduce output CPS to half (and don't burst) if FPS drops below this valuelocal ChatThrottleLib = ChatThrottleLiblocal setmetatable = setmetatablelocal table_remove = table.removelocal tostring = tostringlocal GetTime = GetTimelocal math_min = math.minlocal math_max = math.maxChatThrottleLib.version = CTL_VERSION------------------------------------------------------------------------- Double-linked ring implementationlocal Ring = {}local RingMeta = { __index = Ring }function Ring:New() local ret = {} setmetatable(ret, RingMeta) return retendfunction Ring:Add(obj) -- Append at the "far end" of the ring (aka just before the current position) if self.pos then obj.prev = self.pos.prev obj.prev.next = obj obj.next = self.pos obj.next.prev = obj else obj.next = obj obj.prev = obj self.pos = obj endendfunction Ring:Remove(obj) obj.next.prev = obj.prev obj.prev.next = obj.next if self.pos == obj then self.pos = obj.next if self.pos == obj then self.pos = nil end endend------------------------------------------------------------------------- Recycling bin for pipes (kept in a linked list because that's -- how they're worked with in the rotating rings; just reusing members)ChatThrottleLib.PipeBin = { count = 0 }function ChatThrottleLib.PipeBin:Put(pipe) for i = #pipe, 1, -1 do pipe[i] = nil end pipe.prev = nil pipe.next = self.list self.list = pipe self.count = self.count + 1endfunction ChatThrottleLib.PipeBin:Get() if self.list then local ret = self.list self.list = ret.next ret.next = nil self.count = self.count - 1 return ret end return {}endfunction ChatThrottleLib.PipeBin:Tidy() if self.count < 25 then return end local n if self.count > 100 then n = self.count-90 else n = 10 end for i = 2, n do self.list = self.list.next end local delme = self.list self.list = self.list.next delme.next = nilend------------------------------------------------------------------------- Recycling bin for messagesChatThrottleLib.MsgBin = {}function ChatThrottleLib.MsgBin:Put(msg) msg.text = nil self[#self + 1] = msgendfunction ChatThrottleLib.MsgBin:Get() return table_remove(self) or {}endfunction ChatThrottleLib.MsgBin:Tidy() if #self < 50 then return end if #self > 150 then -- "can't happen" but ... for n = #self, 120, -1 do self[n] = nil end else for n = #self, #self - 20, -1 do self[n] = nil end endend------------------------------------------------------------------------- ChatThrottleLib:Init-- Initialize queues, set up frame for OnUpdate, etcfunction ChatThrottleLib:Init() -- Set up queues if not self.Prio then self.Prio = {} self.Prio["ALERT"] = { ByName = {}, Ring = Ring:New(), avail = 0 } self.Prio["NORMAL"] = { ByName = {}, Ring = Ring:New(), avail = 0 } self.Prio["BULK"] = { ByName = {}, Ring = Ring:New(), avail = 0 } end -- v4: total send counters per priority for _, Prio in pairs(self.Prio) do Prio.nTotalSent = Prio.nTotalSent or 0 end if not self.avail then self.avail = 0 -- v5 end if not self.nTotalSent then self.nTotalSent = 0 -- v5 end -- Set up a frame to get OnUpdate events if not self.Frame then self.Frame = CreateFrame("Frame") self.Frame:Hide() end self.Frame:SetScript("OnUpdate", self.OnUpdate) self.Frame:SetScript("OnEvent", self.OnEvent) -- v11: Monitor P_E_W so we can throttle hard for a few seconds self.Frame:RegisterEvent("PLAYER_ENTERING_WORLD") self.OnUpdateDelay = 0 self.LastAvailUpdate = GetTime() self.HardThrottlingBeginTime = GetTime() -- v11: Throttle hard for a few seconds after startup -- Hook SendChatMessage and SendAddonMessage so we can measure unpiped traffic and avoid overloads (v7) if not self.ORIG_SendChatMessage then -- use secure hooks instead of insecure hooks (v16) self.securelyHooked = true --SendChatMessage self.ORIG_SendChatMessage = SendChatMessage hooksecurefunc("SendChatMessage", function(...) return ChatThrottleLib.Hook_SendChatMessage(...) end) self.ORIG_SendAddonMessage = SendAddonMessage --SendAddonMessage hooksecurefunc("SendAddonMessage", function(...) return ChatThrottleLib.Hook_SendAddonMessage(...) end) end self.nBypass = 0end------------------------------------------------------------------------- ChatThrottleLib.Hook_SendChatMessage / .Hook_SendAddonMessagefunction ChatThrottleLib.Hook_SendChatMessage(text, chattype, language, destination, ...) local self = ChatThrottleLib local size = tostring(text or ""):len() + tostring(chattype or ""):len() + tostring(destination or ""):len() + 40 self.avail = self.avail - size self.nBypass = self.nBypass + size if not self.securelyHooked then self.ORIG_SendChatMessage(text, chattype, language, destination, ...) endendfunction ChatThrottleLib.Hook_SendAddonMessage(prefix, text, chattype, destination, ...) local self = ChatThrottleLib local size = tostring(text or ""):len() + tostring(chattype or ""):len() + tostring(prefix or ""):len() + tostring(destination or ""):len() + 40 self.avail = self.avail - size self.nBypass = self.nBypass + size if not self.securelyHooked then --Sea.io.dprint(CTL_OUTGOING, GREEN_FONT_COLOR_CODE,"[",chattype,"][",prefix,"]<To:",destination,">: ",text,"|r"); self.ORIG_SendAddonMessage(prefix, text, chattype, destination, ...) endend------------------------------------------------------------------------- ChatThrottleLib:UpdateAvail-- Update self.avail with how much bandwidth is currently availablefunction ChatThrottleLib:UpdateAvail() local now = GetTime() local MAX_CPS = self.MAX_CPS; local newavail = MAX_CPS * (now - self.LastAvailUpdate) if now - self.HardThrottlingBeginTime < 5 then -- First 5 seconds after startup/zoning: VERY hard clamping to avoid irritating the server rate limiter, it seems very cranky then self.avail = math_min(self.avail + (newavail*0.1), MAX_CPS*0.5) elseif GetFramerate() < self.MIN_FPS then -- GetFrameRate call takes ~0.002 secs newavail = newavail * 0.5 self.avail = math_min(MAX_CPS, self.avail + newavail) self.bChoking = true -- just for stats else self.avail = math_min(self.BURST, self.avail + newavail) self.bChoking = false end self.avail = math_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. self.LastAvailUpdate = now return self.availend------------------------------------------------------------------------- Despooling logicfunction ChatThrottleLib:Despool(Prio) local ring = Prio.Ring while ring.pos and Prio.avail > ring.pos[1].nSize do local msg = table_remove(Prio.Ring.pos, 1) if not Prio.Ring.pos[1] then local pipe = Prio.Ring.pos Prio.Ring:Remove(pipe) Prio.ByName[pipe.name] = nil self.PipeBin:Put(pipe) else Prio.Ring.pos = Prio.Ring.pos.next end Prio.avail = Prio.avail - msg.nSize msg.f(unpack(msg, 1, msg.n)) Prio.nTotalSent = Prio.nTotalSent + msg.nSize self.MsgBin:Put(msg) endendfunction ChatThrottleLib.OnEvent() -- v11: We know that the rate limiter is touchy after login. Assume that it's touch after zoning, too. local self = ChatThrottleLib if event == "PLAYER_ENTERING_WORLD" then self.HardThrottlingBeginTime = GetTime() -- Throttle hard for a few seconds after zoning self.avail = 0 endendfunction ChatThrottleLib.OnUpdate() local self = ChatThrottleLib self.OnUpdateDelay = self.OnUpdateDelay + arg1 if self.OnUpdateDelay < 0.08 then return end self.OnUpdateDelay = 0 self:UpdateAvail() if self.avail < 0 then return -- argh. some bastard is spewing stuff past the lib. just bail early to save cpu. end -- See how many of or priorities have queued messages local n = 0 for prioname,Prio in pairs(self.Prio) do if Prio.Ring.pos or Prio.avail < 0 then n = n + 1 end end -- Anything queued still? if n<1 then -- Nope. Move spillover bandwidth to global availability gauge and clear self.bQueueing for prioname, Prio in pairs(self.Prio) do self.avail = self.avail + Prio.avail Prio.avail = 0 end self.bQueueing = false self.Frame:Hide() return end -- There's stuff queued. Hand out available bandwidth to priorities as needed and despool their queues local avail = self.avail/n self.avail = 0 for prioname, Prio in pairs(self.Prio) do if Prio.Ring.pos or Prio.avail < 0 then Prio.avail = Prio.avail + avail if Prio.Ring.pos and Prio.avail > Prio.Ring.pos[1].nSize then self:Despool(Prio) end end end -- Expire recycled tables if needed self.MsgBin:Tidy() self.PipeBin:Tidy()end------------------------------------------------------------------------- Spooling logicfunction ChatThrottleLib:Enqueue(prioname, pipename, msg) local Prio = self.Prio[prioname] local pipe = Prio.ByName[pipename] if not pipe then self.Frame:Show() pipe = self.PipeBin:Get() pipe.name = pipename Prio.ByName[pipename] = pipe Prio.Ring:Add(pipe) end pipe[#pipe + 1] = msg self.bQueueing = trueendfunction ChatThrottleLib:SendChatMessage(prio, prefix, text, chattype, language, destination) if not self or not prio or not text or not self.Prio[prio] then error('Usage: ChatThrottleLib:SendChatMessage("{BULK||NORMAL||ALERT}", "prefix" or nil, "text"[, "chattype"[, "language"[, "destination"]]]', 2) end prefix = prefix or tostring(this) -- each frame gets its own queue if prefix is not given local nSize = text:len() + self.MSG_OVERHEAD -- Check if there's room in the global available bandwidth gauge to send directly if not self.bQueueing and nSize < self:UpdateAvail() then self.avail = self.avail - nSize self.ORIG_SendChatMessage(text, chattype, language, destination) self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize return end -- Message needs to be queued local msg = self.MsgBin:Get() msg.f = self.ORIG_SendChatMessage msg[1] = text msg[2] = chattype or "SAY" msg[3] = language msg[4] = destination msg.n = 4 msg.nSize = nSize self:Enqueue(prio, ("%s/%s/%s"):format(prefix, chattype, destination or ""), msg)endfunction ChatThrottleLib:SendAddonMessage(prio, prefix, text, chattype, target) if not self or not prio or not prefix or not text or not chattype or not self.Prio[prio] then error('Usage: ChatThrottleLib:SendAddonMessage("{BULK||NORMAL||ALERT}", "prefix", "text", "chattype"[, "target"])', 0) end local nSize = prefix:len() + 1 + text:len() + self.MSG_OVERHEAD -- Check if there's room in the global available bandwidth gauge to send directly if not self.bQueueing and nSize < self:UpdateAvail() then self.avail = self.avail - nSize self.ORIG_SendAddonMessage(prefix, text, chattype, target) self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize return end -- Message needs to be queued local msg = self.MsgBin:Get() msg.f = self.ORIG_SendAddonMessage msg[1] = prefix msg[2] = text msg[3] = chattype msg[4] = target msg.n = (target~=nil) and 4 or 3; msg.nSize = nSize self:Enqueue(prio, ("%s/%s/%s"):format(prefix, chattype, target or ""), msg)end------------------------------------------------------------------------- Get the ball rolling!ChatThrottleLib:Init()--[[ WoWBench debugging snippetif(WOWB_VER) then local function SayTimer() print("SAY: "..GetTime().." "..arg1) end ChatThrottleLib.Frame:SetScript("OnEvent", SayTimer) ChatThrottleLib.Frame:RegisterEvent("CHAT_MSG_SAY")end]]
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -