initial commit

This commit is contained in:
Torsten Brendgen
2026-04-10 21:30:31 +02:00
commit fc5a8aa361
108 changed files with 40568 additions and 0 deletions

View File

@@ -0,0 +1,301 @@
--- **AceComm-3.0** allows you to send messages of unlimited length over the addon comm channels.
-- It'll automatically split the messages into multiple parts and rebuild them on the receiving end.\\
-- **ChatThrottleLib** is of course being used to avoid being disconnected by the server.
--
-- **AceComm-3.0** can be embeded into your addon, either explicitly by calling AceComm:Embed(MyAddon) or by
-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
-- and can be accessed directly, without having to explicitly call AceComm itself.\\
-- It is recommended to embed AceComm, otherwise you'll have to specify a custom `self` on all calls you
-- make into AceComm.
-- @class file
-- @name AceComm-3.0
-- @release $Id: AceComm-3.0.lua 1333 2024-05-05 16:24:39Z nevcairiel $
--[[ AceComm-3.0
TODO: Time out old data rotting around from dead senders? Not a HUGE deal since the number of possible sender names is somewhat limited.
]]
local CallbackHandler = LibStub("CallbackHandler-1.0")
local CTL = assert(ChatThrottleLib, "AceComm-3.0 requires ChatThrottleLib")
local MAJOR, MINOR = "AceComm-3.0", 14
local AceComm,oldminor = LibStub:NewLibrary(MAJOR, MINOR)
if not AceComm then return end
-- Lua APIs
local type, next, pairs, tostring = type, next, pairs, tostring
local strsub, strfind = string.sub, string.find
local match = string.match
local tinsert, tconcat = table.insert, table.concat
local error, assert = error, assert
-- WoW APIs
local Ambiguate = Ambiguate
AceComm.embeds = AceComm.embeds or {}
-- for my sanity and yours, let's give the message type bytes some names
local MSG_MULTI_FIRST = "\001"
local MSG_MULTI_NEXT = "\002"
local MSG_MULTI_LAST = "\003"
local MSG_ESCAPE = "\004"
-- remove old structures (pre WoW 4.0)
AceComm.multipart_origprefixes = nil
AceComm.multipart_reassemblers = nil
-- the multipart message spool: indexed by a combination of sender+distribution+
AceComm.multipart_spool = AceComm.multipart_spool or {}
--- Register for Addon Traffic on a specified prefix
-- @param prefix A printable character (\032-\255) classification of the message (typically AddonName or AddonNameEvent), max 16 characters
-- @param method Callback to call on message reception: Function reference, or method name (string) to call on self. Defaults to "OnCommReceived"
function AceComm:RegisterComm(prefix, method)
if method == nil then
method = "OnCommReceived"
end
if #prefix > 16 then -- TODO: 15?
error("AceComm:RegisterComm(prefix,method): prefix length is limited to 16 characters")
end
if C_ChatInfo then
C_ChatInfo.RegisterAddonMessagePrefix(prefix)
else
RegisterAddonMessagePrefix(prefix)
end
return AceComm._RegisterComm(self, prefix, method) -- created by CallbackHandler
end
local warnedPrefix=false
--- Send a message over the Addon Channel
-- @param prefix A printable character (\032-\255) classification of the message (typically AddonName or AddonNameEvent)
-- @param text Data to send, nils (\000) not allowed. Any length.
-- @param distribution Addon channel, e.g. "RAID", "GUILD", etc; see SendAddonMessage API
-- @param target Destination for some distributions; see SendAddonMessage API
-- @param prio OPTIONAL: ChatThrottleLib priority, "BULK", "NORMAL" or "ALERT". Defaults to "NORMAL".
-- @param callbackFn OPTIONAL: callback function to be called as each chunk is sent. receives 3 args: the user supplied arg (see next), the number of bytes sent so far, and the number of bytes total to send.
-- @param callbackArg: OPTIONAL: first arg to the callback function. nil will be passed if not specified.
function AceComm:SendCommMessage(prefix, text, distribution, target, prio, callbackFn, callbackArg)
prio = prio or "NORMAL" -- pasta's reference implementation had different prio for singlepart and multipart, but that's a very bad idea since that can easily lead to out-of-sequence delivery!
if not( type(prefix)=="string" and
type(text)=="string" and
type(distribution)=="string" and
(target==nil or type(target)=="string" or type(target)=="number") and
(prio=="BULK" or prio=="NORMAL" or prio=="ALERT")
) then
error('Usage: SendCommMessage(addon, "prefix", "text", "distribution"[, "target"[, "prio"[, callbackFn, callbackarg]]])', 2)
end
local textlen = #text
local maxtextlen = 255 -- Yes, the max is 255 even if the dev post said 256. I tested. Char 256+ get silently truncated. /Mikk, 20110327
local queueName = prefix
local ctlCallback = nil
if callbackFn then
ctlCallback = function(sent, sendResult)
return callbackFn(callbackArg, sent, textlen, sendResult)
end
end
local forceMultipart
if match(text, "^[\001-\009]") then -- 4.1+: see if the first character is a control character
-- we need to escape the first character with a \004
if textlen+1 > maxtextlen then -- would we go over the size limit?
forceMultipart = true -- just make it multipart, no escape problems then
else
text = "\004" .. text
end
end
if not forceMultipart and textlen <= maxtextlen then
-- fits all in one message
CTL:SendAddonMessage(prio, prefix, text, distribution, target, queueName, ctlCallback, textlen)
else
maxtextlen = maxtextlen - 1 -- 1 extra byte for part indicator in prefix(4.0)/start of message(4.1)
-- first part
local chunk = strsub(text, 1, maxtextlen)
CTL:SendAddonMessage(prio, prefix, MSG_MULTI_FIRST..chunk, distribution, target, queueName, ctlCallback, maxtextlen)
-- continuation
local pos = 1+maxtextlen
while pos+maxtextlen <= textlen do
chunk = strsub(text, pos, pos+maxtextlen-1)
CTL:SendAddonMessage(prio, prefix, MSG_MULTI_NEXT..chunk, distribution, target, queueName, ctlCallback, pos+maxtextlen-1)
pos = pos + maxtextlen
end
-- final part
chunk = strsub(text, pos)
CTL:SendAddonMessage(prio, prefix, MSG_MULTI_LAST..chunk, distribution, target, queueName, ctlCallback, textlen)
end
end
----------------------------------------
-- Message receiving
----------------------------------------
do
local compost = setmetatable({}, {__mode = "k"})
local function new()
local t = next(compost)
if t then
compost[t]=nil
for i=#t,3,-1 do -- faster than pairs loop. don't even nil out 1/2 since they'll be overwritten
t[i]=nil
end
return t
end
return {}
end
local function lostdatawarning(prefix,sender,where)
DEFAULT_CHAT_FRAME:AddMessage(MAJOR..": Warning: lost network data regarding '"..tostring(prefix).."' from '"..tostring(sender).."' (in "..where..")")
end
function AceComm:OnReceiveMultipartFirst(prefix, message, distribution, sender)
local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender
local spool = AceComm.multipart_spool
--[[
if spool[key] then
lostdatawarning(prefix,sender,"First")
-- continue and overwrite
end
--]]
spool[key] = message -- plain string for now
end
function AceComm:OnReceiveMultipartNext(prefix, message, distribution, sender)
local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender
local spool = AceComm.multipart_spool
local olddata = spool[key]
if not olddata then
--lostdatawarning(prefix,sender,"Next")
return
end
if type(olddata)~="table" then
-- ... but what we have is not a table. So make it one. (Pull a composted one if available)
local t = new()
t[1] = olddata -- add old data as first string
t[2] = message -- and new message as second string
spool[key] = t -- and put the table in the spool instead of the old string
else
tinsert(olddata, message)
end
end
function AceComm:OnReceiveMultipartLast(prefix, message, distribution, sender)
local key = prefix.."\t"..distribution.."\t"..sender -- a unique stream is defined by the prefix + distribution + sender
local spool = AceComm.multipart_spool
local olddata = spool[key]
if not olddata then
--lostdatawarning(prefix,sender,"End")
return
end
spool[key] = nil
if type(olddata) == "table" then
-- if we've received a "next", the spooled data will be a table for rapid & garbage-free tconcat
tinsert(olddata, message)
AceComm.callbacks:Fire(prefix, tconcat(olddata, ""), distribution, sender)
compost[olddata] = true
else
-- if we've only received a "first", the spooled data will still only be a string
AceComm.callbacks:Fire(prefix, olddata..message, distribution, sender)
end
end
end
----------------------------------------
-- Embed CallbackHandler
----------------------------------------
if not AceComm.callbacks then
AceComm.callbacks = CallbackHandler:New(AceComm,
"_RegisterComm",
"UnregisterComm",
"UnregisterAllComm")
end
AceComm.callbacks.OnUsed = nil
AceComm.callbacks.OnUnused = nil
local function OnEvent(self, event, prefix, message, distribution, sender)
if event == "CHAT_MSG_ADDON" then
sender = Ambiguate(sender, "none")
local control, rest = match(message, "^([\001-\009])(.*)")
if control then
if control==MSG_MULTI_FIRST then
AceComm:OnReceiveMultipartFirst(prefix, rest, distribution, sender)
elseif control==MSG_MULTI_NEXT then
AceComm:OnReceiveMultipartNext(prefix, rest, distribution, sender)
elseif control==MSG_MULTI_LAST then
AceComm:OnReceiveMultipartLast(prefix, rest, distribution, sender)
elseif control==MSG_ESCAPE then
AceComm.callbacks:Fire(prefix, rest, distribution, sender)
else
-- unknown control character, ignore SILENTLY (dont warn unnecessarily about future extensions!)
end
else
-- single part: fire it off immediately and let CallbackHandler decide if it's registered or not
AceComm.callbacks:Fire(prefix, message, distribution, sender)
end
else
assert(false, "Received "..tostring(event).." event?!")
end
end
AceComm.frame = AceComm.frame or CreateFrame("Frame", "AceComm30Frame")
AceComm.frame:SetScript("OnEvent", OnEvent)
AceComm.frame:UnregisterAllEvents()
AceComm.frame:RegisterEvent("CHAT_MSG_ADDON")
----------------------------------------
-- Base library stuff
----------------------------------------
local mixins = {
"RegisterComm",
"UnregisterComm",
"UnregisterAllComm",
"SendCommMessage",
}
-- Embeds AceComm-3.0 into the target object making the functions from the mixins list available on target:..
-- @param target target object to embed AceComm-3.0 in
function AceComm:Embed(target)
for k, v in pairs(mixins) do
target[v] = self[v]
end
self.embeds[target] = true
return target
end
function AceComm:OnEmbedDisable(target)
target:UnregisterAllComm()
end
-- Update embeds
for target, v in pairs(AceComm.embeds) do
AceComm:Embed(target)
end

View File

@@ -0,0 +1,5 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="ChatThrottleLib.lua"/>
<Script file="AceComm-3.0.lua"/>
</Ui>

View File

@@ -0,0 +1,701 @@
--
-- 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 SendAddonMessage 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! :-)
--
-- LICENSE: ChatThrottleLib is released into the Public Domain
--
local CTL_VERSION = 31
local _G = _G
if _G.ChatThrottleLib then
if _G.ChatThrottleLib.version >= CTL_VERSION then
-- There's already a newer (or same) version loaded. Buh-bye.
return
elseif not _G.ChatThrottleLib.securelyHooked then
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!")
-- ATTEMPT to unhook; this'll behave badly if someone else has hooked...
-- ... and if someone has securehooked, they can kiss that goodbye too... >.<
_G.SendChatMessage = _G.ChatThrottleLib.ORIG_SendChatMessage
if _G.ChatThrottleLib.ORIG_SendAddonMessage then
_G.SendAddonMessage = _G.ChatThrottleLib.ORIG_SendAddonMessage
end
end
_G.ChatThrottleLib.ORIG_SendChatMessage = nil
_G.ChatThrottleLib.ORIG_SendAddonMessage = nil
end
if not _G.ChatThrottleLib then
_G.ChatThrottleLib = {}
end
ChatThrottleLib = _G.ChatThrottleLib -- in case some addon does "local ChatThrottleLib" above us and we're copypasted (AceComm-2, sigh)
local ChatThrottleLib = _G.ChatThrottleLib
ChatThrottleLib.version = CTL_VERSION
------------------ TWEAKABLES -----------------
ChatThrottleLib.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+protocolstuff
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.
ChatThrottleLib.MIN_FPS = 20 -- Reduce output CPS to half (and don't burst) if FPS drops below this value
local setmetatable = setmetatable
local table_remove = table.remove
local tostring = tostring
local GetTime = GetTime
local math_min = math.min
local math_max = math.max
local next = next
local strlen = string.len
local GetFramerate = GetFramerate
local unpack,type,pairs,wipe = unpack,type,pairs,table.wipe
-----------------------------------------------------------------------
-- Double-linked ring implementation
local Ring = {}
local RingMeta = { __index = Ring }
function Ring:New()
local ret = {}
setmetatable(ret, RingMeta)
return ret
end
function 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
end
end
function 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
end
end
-- Note that this is local because there's no upgrade logic for existing ring
-- metatables, and this isn't present on rings created in versions older than
-- v25.
local function Ring_Link(self, other) -- Move and append all contents of another ring to this ring
if not self.pos then
-- This ring is empty, so just transfer ownership.
self.pos = other.pos
other.pos = nil
elseif other.pos then
-- Our tail should point to their head, and their tail to our head.
self.pos.prev.next, other.pos.prev.next = other.pos, self.pos
-- Our head should point to their tail, and their head to our tail.
self.pos.prev, other.pos.prev = other.pos.prev, self.pos.prev
other.pos = nil
end
end
-----------------------------------------------------------------------
-- Recycling bin for pipes
-- A pipe is a plain integer-indexed queue of messages
-- Pipes normally live in Rings of pipes (3 rings total, one per priority)
ChatThrottleLib.PipeBin = nil -- pre-v19, drastically different
local PipeBin = setmetatable({}, {__mode="k"})
local function DelPipe(pipe)
PipeBin[pipe] = true
end
local function NewPipe()
local pipe = next(PipeBin)
if pipe then
wipe(pipe)
PipeBin[pipe] = nil
return pipe
end
return {}
end
-----------------------------------------------------------------------
-- Recycling bin for messages
ChatThrottleLib.MsgBin = nil -- pre-v19, drastically different
local MsgBin = setmetatable({}, {__mode="k"})
local function DelMsg(msg)
msg[1] = nil
-- 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.
MsgBin[msg] = true
end
local function NewMsg()
local msg = next(MsgBin)
if msg then
MsgBin[msg] = nil
return msg
end
return {}
end
-----------------------------------------------------------------------
-- ChatThrottleLib:Init
-- Initialize queues, set up frame for OnUpdate, etc
function 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
if not self.BlockedQueuesDelay then
-- v25: Add blocked queues to rings to handle new client throttles.
for _, Prio in pairs(self.Prio) do
Prio.Blocked = Ring:New()
end
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.BlockedQueuesDelay = 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.securelyHooked then
-- Use secure hooks as of v16. Old regular hook support yanked out in v21.
self.securelyHooked = true
--SendChatMessage
if _G.C_ChatInfo and _G.C_ChatInfo.SendChatMessage then
hooksecurefunc(_G.C_ChatInfo, "SendChatMessage", function(...)
return ChatThrottleLib.Hook_SendChatMessage(...)
end)
else
hooksecurefunc("SendChatMessage", function(...)
return ChatThrottleLib.Hook_SendChatMessage(...)
end)
end
--SendAddonMessage
hooksecurefunc(_G.C_ChatInfo, "SendAddonMessage", function(...)
return ChatThrottleLib.Hook_SendAddonMessage(...)
end)
end
-- v26: Hook SendAddonMessageLogged for traffic logging
if not self.securelyHookedLogged then
self.securelyHookedLogged = true
hooksecurefunc(_G.C_ChatInfo, "SendAddonMessageLogged", function(...)
return ChatThrottleLib.Hook_SendAddonMessageLogged(...)
end)
end
-- v29: Hook BNSendGameData for traffic logging
if not self.securelyHookedBNGameData then
self.securelyHookedBNGameData = true
if _G.C_BattleNet and _G.C_BattleNet.SendGameData then
hooksecurefunc(_G.C_BattleNet, "SendGameData", function(...)
return ChatThrottleLib.Hook_BNSendGameData(...)
end)
else
hooksecurefunc("BNSendGameData", function(...)
return ChatThrottleLib.Hook_BNSendGameData(...)
end)
end
end
self.nBypass = 0
end
-----------------------------------------------------------------------
-- ChatThrottleLib.Hook_SendChatMessage / .Hook_SendAddonMessage
local bMyTraffic = false
function ChatThrottleLib.Hook_SendChatMessage(text, chattype, language, destination, ...)
if bMyTraffic then
return
end
local self = ChatThrottleLib
local size = strlen(tostring(text or "")) + strlen(tostring(destination or "")) + self.MSG_OVERHEAD
self.avail = self.avail - size
self.nBypass = self.nBypass + size -- just a statistic
end
function ChatThrottleLib.Hook_SendAddonMessage(prefix, text, chattype, destination, ...)
if bMyTraffic then
return
end
local self = ChatThrottleLib
local size = tostring(text or ""):len() + tostring(prefix or ""):len();
size = size + tostring(destination or ""):len() + self.MSG_OVERHEAD
self.avail = self.avail - size
self.nBypass = self.nBypass + size -- just a statistic
end
function ChatThrottleLib.Hook_SendAddonMessageLogged(prefix, text, chattype, destination, ...)
ChatThrottleLib.Hook_SendAddonMessage(prefix, text, chattype, destination, ...)
end
function ChatThrottleLib.Hook_BNSendGameData(destination, prefix, text)
ChatThrottleLib.Hook_SendAddonMessage(prefix, text, "WHISPER", destination)
end
-----------------------------------------------------------------------
-- ChatThrottleLib:UpdateAvail
-- Update self.avail with how much bandwidth is currently available
function ChatThrottleLib:UpdateAvail()
local now = GetTime()
local MAX_CPS = self.MAX_CPS;
local newavail = MAX_CPS * (now - self.LastAvailUpdate)
local avail = self.avail
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
avail = math_min(avail + (newavail*0.1), MAX_CPS*0.5)
self.bChoking = true
elseif GetFramerate() < self.MIN_FPS then -- GetFrameRate call takes ~0.002 secs
avail = math_min(MAX_CPS, avail + newavail*0.5)
self.bChoking = true -- just a statistic
else
avail = math_min(self.BURST, avail + newavail)
self.bChoking = false
end
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.
self.avail = avail
self.LastAvailUpdate = now
return avail
end
-----------------------------------------------------------------------
-- Despooling logic
-- Reminder:
-- - We have 3 Priorities, each containing a "Ring" construct ...
-- - ... made up of N "Pipe"s (1 for each destination/pipename)
-- - and each pipe contains messages
local SendAddonMessageResult = Enum.SendAddonMessageResult or {
Success = 0,
AddonMessageThrottle = 3,
NotInGroup = 5,
ChannelThrottle = 8,
GeneralError = 9,
}
local function MapToSendResult(ok, ...)
local result
if not ok then
-- The send function itself errored; don't look at anything else.
result = SendAddonMessageResult.GeneralError
else
-- Grab the last return value from the send function and remap
-- it from a boolean to an enum code. If there are no results,
-- assume success (true).
result = select(-1, true, ...)
if result == true then
result = SendAddonMessageResult.Success
elseif result == false then
result = SendAddonMessageResult.GeneralError
end
end
return result
end
local function IsThrottledSendResult(result)
return result == SendAddonMessageResult.AddonMessageThrottle
end
-- A copy of this function exists in FrameXML, but for clarity it's here too.
local function CallErrorHandler(...)
return geterrorhandler()(...)
end
local function PerformSend(sendFunction, ...)
bMyTraffic = true
local sendResult = MapToSendResult(xpcall(sendFunction, CallErrorHandler, ...))
bMyTraffic = false
return sendResult
end
function ChatThrottleLib:Despool(Prio)
local ring = Prio.Ring
while ring.pos and Prio.avail > ring.pos[1].nSize do
local pipe = ring.pos
local msg = pipe[1]
local sendResult = PerformSend(msg.f, unpack(msg, 1, msg.n))
if IsThrottledSendResult(sendResult) then
-- Message was throttled; move the pipe into the blocked ring.
Prio.Ring:Remove(pipe)
Prio.Blocked:Add(pipe)
else
-- Dequeue message after submission.
table_remove(pipe, 1)
DelMsg(msg)
if not pipe[1] then -- did we remove last msg in this pipe?
Prio.Ring:Remove(pipe)
Prio.ByName[pipe.name] = nil
DelPipe(pipe)
else
ring.pos = ring.pos.next
end
-- Update bandwidth counters on successful sends.
local didSend = (sendResult == SendAddonMessageResult.Success)
if didSend then
Prio.avail = Prio.avail - msg.nSize
Prio.nTotalSent = Prio.nTotalSent + msg.nSize
end
-- Notify caller of message submission.
if msg.callbackFn then
securecallfunction(msg.callbackFn, msg.callbackArg, didSend, sendResult)
end
end
end
end
function ChatThrottleLib.OnEvent(this,event)
-- v11: We know that the rate limiter is touchy after login. Assume that it's touchy 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
end
end
function ChatThrottleLib.OnUpdate(this,delay)
local self = ChatThrottleLib
self.OnUpdateDelay = self.OnUpdateDelay + delay
self.BlockedQueuesDelay = self.BlockedQueuesDelay + delay
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
-- Integrate blocked queues back into their rings periodically.
if self.BlockedQueuesDelay >= 0.35 then
for _, Prio in pairs(self.Prio) do
Ring_Link(Prio.Ring, Prio.Blocked)
end
self.BlockedQueuesDelay = 0
end
-- See how many of our priorities have queued messages. This is split
-- into two counters because priorities that consist only of blocked
-- queues must keep our OnUpdate alive, but shouldn't count toward
-- bandwidth distribution.
local nSendablePrios = 0
local nBlockedPrios = 0
for prioname, Prio in pairs(self.Prio) do
if Prio.Ring.pos then
nSendablePrios = nSendablePrios + 1
elseif Prio.Blocked.pos then
nBlockedPrios = nBlockedPrios + 1
end
-- Collect unused bandwidth from priorities with nothing to send.
if not Prio.Ring.pos then
self.avail = self.avail + Prio.avail
Prio.avail = 0
end
end
-- Bandwidth reclamation may take us back over the burst cap.
self.avail = math_min(self.avail, self.BURST)
-- If we can't currently send on any priorities, stop processing early.
if nSendablePrios == 0 then
-- If we're completely out of data to send, disable queue processing.
if nBlockedPrios == 0 then
self.bQueueing = false
self.Frame:Hide()
end
return
end
-- There's stuff queued. Hand out available bandwidth to priorities as needed and despool their queues
local avail = self.avail / nSendablePrios
self.avail = 0
for prioname, Prio in pairs(self.Prio) do
if Prio.Ring.pos then
Prio.avail = Prio.avail + avail
self:Despool(Prio)
end
end
end
-----------------------------------------------------------------------
-- Spooling logic
function ChatThrottleLib:Enqueue(prioname, pipename, msg)
local Prio = self.Prio[prioname]
local pipe = Prio.ByName[pipename]
if not pipe then
self.Frame:Show()
pipe = NewPipe()
pipe.name = pipename
Prio.ByName[pipename] = pipe
Prio.Ring:Add(pipe)
end
pipe[#pipe + 1] = msg
self.bQueueing = true
end
function ChatThrottleLib:SendChatMessage(prio, prefix, text, chattype, language, destination, queueName, callbackFn, callbackArg)
if not self or not prio or not prefix or not text or not self.Prio[prio] then
error('Usage: ChatThrottleLib:SendChatMessage("{BULK||NORMAL||ALERT}", "prefix", "text"[, "chattype"[, "language"[, "destination"]]]', 2)
end
if callbackFn and type(callbackFn)~="function" then
error('ChatThrottleLib:ChatMessage(): callbackFn: expected function, got '..type(callbackFn), 2)
end
local nSize = text:len()
if nSize>255 then
error("ChatThrottleLib:SendChatMessage(): message length cannot exceed 255 bytes", 2)
end
nSize = nSize + 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
local sendResult = PerformSend(_G.C_ChatInfo.SendChatMessage or _G.SendChatMessage, text, chattype, language, destination)
if not IsThrottledSendResult(sendResult) then
local didSend = (sendResult == SendAddonMessageResult.Success)
if didSend then
self.avail = self.avail - nSize
self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
end
if callbackFn then
securecallfunction(callbackFn, callbackArg, didSend, sendResult)
end
return
end
end
-- Message needs to be queued
local msg = NewMsg()
msg.f = _G.C_ChatInfo.SendChatMessage or _G.SendChatMessage
msg[1] = text
msg[2] = chattype or "SAY"
msg[3] = language
msg[4] = destination
msg.n = 4
msg.nSize = nSize
msg.callbackFn = callbackFn
msg.callbackArg = callbackArg
self:Enqueue(prio, queueName or prefix, msg)
end
local function SendAddonMessageInternal(self, sendFunction, prio, prefix, text, chattype, target, queueName, callbackFn, callbackArg)
local nSize = #text + 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
local sendResult = PerformSend(sendFunction, prefix, text, chattype, target)
if not IsThrottledSendResult(sendResult) then
local didSend = (sendResult == SendAddonMessageResult.Success)
if didSend then
self.avail = self.avail - nSize
self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
end
if callbackFn then
securecallfunction(callbackFn, callbackArg, didSend, sendResult)
end
return
end
end
-- Message needs to be queued
local msg = NewMsg()
msg.f = sendFunction
msg[1] = prefix
msg[2] = text
msg[3] = chattype
msg[4] = target
msg.n = (target~=nil) and 4 or 3;
msg.nSize = nSize
msg.callbackFn = callbackFn
msg.callbackArg = callbackArg
self:Enqueue(prio, queueName or prefix, msg)
end
function ChatThrottleLib:SendAddonMessage(prio, prefix, text, chattype, target, queueName, callbackFn, callbackArg)
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"])', 2)
elseif callbackFn and type(callbackFn)~="function" then
error('ChatThrottleLib:SendAddonMessage(): callbackFn: expected function, got '..type(callbackFn), 2)
elseif #text>255 then
error("ChatThrottleLib:SendAddonMessage(): message length cannot exceed 255 bytes", 2)
end
local sendFunction = _G.C_ChatInfo.SendAddonMessage
SendAddonMessageInternal(self, sendFunction, prio, prefix, text, chattype, target, queueName, callbackFn, callbackArg)
end
function ChatThrottleLib:SendAddonMessageLogged(prio, prefix, text, chattype, target, queueName, callbackFn, callbackArg)
if not self or not prio or not prefix or not text or not chattype or not self.Prio[prio] then
error('Usage: ChatThrottleLib:SendAddonMessageLogged("{BULK||NORMAL||ALERT}", "prefix", "text", "chattype"[, "target"])', 2)
elseif callbackFn and type(callbackFn)~="function" then
error('ChatThrottleLib:SendAddonMessageLogged(): callbackFn: expected function, got '..type(callbackFn), 2)
elseif #text>255 then
error("ChatThrottleLib:SendAddonMessageLogged(): message length cannot exceed 255 bytes", 2)
end
local sendFunction = _G.C_ChatInfo.SendAddonMessageLogged
SendAddonMessageInternal(self, sendFunction, prio, prefix, text, chattype, target, queueName, callbackFn, callbackArg)
end
local function BNSendGameDataReordered(prefix, text, _, gameAccountID)
local bnSendFunc = _G.C_BattleNet and _G.C_BattleNet.SendGameData or _G.BNSendGameData
return bnSendFunc(gameAccountID, prefix, text)
end
function ChatThrottleLib:BNSendGameData(prio, prefix, text, chattype, gameAccountID, queueName, callbackFn, callbackArg)
-- Note that this API is intentionally limited to 255 bytes of data
-- for reasons of traffic fairness, which is less than the 4078 bytes
-- BNSendGameData natively supports. Additionally, a chat type is required
-- but must always be set to 'WHISPER' to match what is exposed by the
-- receipt event.
--
-- If splitting messages, callers must also be aware that message
-- delivery over BNSendGameData is unordered.
if not self or not prio or not prefix or not text or not gameAccountID or not chattype or not self.Prio[prio] then
error('Usage: ChatThrottleLib:BNSendGameData("{BULK||NORMAL||ALERT}", "prefix", "text", "chattype", gameAccountID)', 2)
elseif callbackFn and type(callbackFn)~="function" then
error('ChatThrottleLib:BNSendGameData(): callbackFn: expected function, got '..type(callbackFn), 2)
elseif #text>255 then
error("ChatThrottleLib:BNSendGameData(): message length cannot exceed 255 bytes", 2)
elseif chattype ~= "WHISPER" then
error("ChatThrottleLib:BNSendGameData(): chat type must be 'WHISPER'", 2)
end
local sendFunction = BNSendGameDataReordered
SendAddonMessageInternal(self, sendFunction, prio, prefix, text, chattype, gameAccountID, queueName, callbackFn, callbackArg)
end
-----------------------------------------------------------------------
-- Get the ball rolling!
ChatThrottleLib:Init()
--[[ WoWBench debugging snippet
if(WOWB_VER) then
local function SayTimer()
print("SAY: "..GetTime().." "..arg1)
end
ChatThrottleLib.Frame:SetScript("OnEvent", SayTimer)
ChatThrottleLib.Frame:RegisterEvent("CHAT_MSG_SAY")
end
]]