diff --git a/aux-addon.toc b/aux-addon.toc index 919f718..67a58e0 100644 --- a/aux-addon.toc +++ b/aux-addon.toc @@ -7,6 +7,7 @@ libs\package.lua libs\T.lua +libs\ChatThrottleLib.lua aux-addon.lua util.lua diff --git a/core/history.lua b/core/history.lua index 443b072..f41ca37 100644 --- a/core/history.lua +++ b/core/history.lua @@ -99,7 +99,7 @@ AUX_data_sharer:SetScript("OnEvent", function() end end) - function M.process_auction(auction_record, pages) +function M.process_auction(auction_record, pages) local item_record = read_record(auction_record.item_key) local unit_buyout_price = ceil(auction_record.buyout_price / auction_record.aux_quantity) local item_key = auction_record.item_key @@ -108,12 +108,12 @@ AUX_data_sharer:SetScript("OnEvent", function() write_record(auction_record.item_key, item_record) --AuxAddon:SendCommMessage("GUILD", item_key, unit_buyout_price) relies on acecomm if aux.account_data.sharing == true then - if pages < 15 then --sometimes pages is nil, not entirely sure why but we'll assume it's nothing worrying xd - if GetChannelName("LFT") ~= 0 then - SendChatMessage("AuxData," .. item_key .."," .. unit_buyout_price , "CHANNEL", nil, GetChannelName("LFT")) - --print("sent") - end - end + if pages < 15 then --to avoid sharing data when people do searches without a keyword "full scans" + if GetChannelName("LFT") ~= 0 then + ChatThrottleLib:SendChatMessage("ALERT", nil, "AuxData," .. item_key .."," .. unit_buyout_price , "CHANNEL", nil, GetChannelName("LFT")) --According to Candor prioritization is wrong and ALERT gets sent last. ChatThrottleLib fixed for turtle by Candor https://github.com/trumpetx/ChatLootBidder/blob/master/ChatThrottleLib.lua + --print("sent") + end + end end end end diff --git a/libs/ChatThrottleLib.lua b/libs/ChatThrottleLib.lua new file mode 100644 index 0000000..71e713c --- /dev/null +++ b/libs/ChatThrottleLib.lua @@ -0,0 +1,500 @@ +-- +-- 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! :-) +-- + +-- +-- ChangeLog and notes for this version: +-- There is no historic CTL version 14 and this would supersceed all other Vanilla era +-- versions (<=13) while also not stepping on private server TBC versions (15+) +-- +-- Modifications for this version are simply to throttle raw chat lines per second and +-- have nothing to do with the bytes sent. Chat to CHANNEL, RAID, etc all count toward +-- this limit. Otherwise, this is the same as version 13 as it relates to throttling +-- raw bytes sent. To be clear, this update doesn't further throttle raw add-on messages. +-- +local CTL_VERSION = 14 + +local MAX_CPS = 800 -- 2000 seems to be safe if NOTHING ELSE is happening. let's call it 800. +local MSG_OVERHEAD = 40 -- Guesstimate overhead for sending a message; source+dest+chattype+protocolstuff + +local BURST = 4000 -- WoW's server buffer seems to be about 32KB. 8KB should be safe, but seen disconnects on _some_ servers. Using 4KB now. + +local MIN_FPS = 20 -- Reduce output CPS to half (and don't burst) if FPS drops below this value + +local TURTLE_MAX_CHAT_LINES_PER_SECOND = 6 -- Turtle seems to allow > 6 lines per second in some situations; but for pure spam throughput, this appears to be the limit before the soft mute kicks in. + +if(ChatThrottleLib and ChatThrottleLib.version>=CTL_VERSION) then + -- There's already a newer (or same) version loaded. Buh-bye. + return; +end + + + +if(not ChatThrottleLib) then + ChatThrottleLib = {} +end + +local ChatThrottleLib = ChatThrottleLib +local strlen = strlen +local setmetatable = setmetatable +local getn = getn +local tremove = tremove +local tinsert = tinsert +local tostring = tostring +local GetTime = GetTime +local format = format + +ChatThrottleLib.version=CTL_VERSION; + + +----------------------------------------------------------------------- +-- 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 + + + +----------------------------------------------------------------------- +-- 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=getn(pipe),1,-1 do + tremove(pipe, i); + end + pipe.prev = nil; + pipe.next = self.list; + self.list = pipe; + self.count = self.count+1; +end + +function 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 {}; +end + +function ChatThrottleLib.PipeBin:Tidy() + if(self.count < 25) then + return; + end + + 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 = nil; +end + + + + +----------------------------------------------------------------------- +-- Recycling bin for messages + +ChatThrottleLib.MsgBin = {} + +function ChatThrottleLib.MsgBin:Put(msg) + msg.text = nil; + tinsert(self, msg); +end + +function ChatThrottleLib.MsgBin:Get() + local ret = tremove(self, getn(self)); + if(ret) then return ret; end + return {}; +end + +function ChatThrottleLib.MsgBin:Tidy() + if(getn(self)<50) then + return; + end + if(getn(self)>150) then -- "can't happen" but ... + for n=getn(self),120,-1 do + tremove(self,n); + end + else + for n=getn(self),getn(self)-20,-1 do + tremove(self,n); + end + end +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 + + -- v4: total send counters per priority + for _,Prio in pairs(self.Prio) do + Prio.nTotalSent = Prio.nTotalSent or 0; + end + + self.avail = self.avail or 0; -- v5 + self.nTotalSent = self.nTotalSent or 0; -- v5 + + + -- Set up a frame to get OnUpdate events + if(not self.Frame) then + self.Frame = CreateFrame("Frame"); + self.Frame:Hide(); + end + self.Frame.Show = self.Frame.Show; -- cache for speed + self.Frame.Hide = self.Frame.Hide; -- cache for speed + 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.TurtleChatLinesAvailable=TURTLE_MAX_CHAT_LINES_PER_SECOND; + 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 + --SendChatMessage + self.ORIG_SendChatMessage = SendChatMessage; + SendChatMessage = function(a1,a2,a3,a4) return ChatThrottleLib.Hook_SendChatMessage(a1,a2,a3,a4); end + --SendAdd[Oo]nMessage + if(SendAddonMessage or SendAddOnMessage) then -- v10: don't pretend like it doesn't exist if it doesn't! + self.ORIG_SendAddonMessage = SendAddonMessage or SendAddOnMessage; + SendAddonMessage = function(a1,a2,a3) return ChatThrottleLib.Hook_SendAddonMessage(a1,a2,a3); end + if(SendAddOnMessage) then -- in case Slouken changes his mind... + SendAddOnMessage = SendAddonMessage; + end + end + end + self.nBypass = 0; +end + + +----------------------------------------------------------------------- +-- ChatThrottleLib.Hook_SendChatMessage / .Hook_SendAddonMessage +function ChatThrottleLib.Hook_SendChatMessage(text, chattype, language, destination) + local self = ChatThrottleLib; + local size = strlen(tostring(text or "")) + strlen(tostring(chattype or "")) + strlen(tostring(destination or "")) + 40; + self.avail = self.avail - size; + self.nBypass = self.nBypass + size; + self.TurtleSendChat() + return self.ORIG_SendChatMessage(text, chattype, language, destination); +end +function ChatThrottleLib.Hook_SendAddonMessage(prefix, text, chattype) + local self = ChatThrottleLib; + local size = strlen(tostring(text or "")) + strlen(tostring(chattype or "")) + strlen(tostring(prefix or "")) + 40; + self.avail = self.avail - size; + self.nBypass = self.nBypass + size; + return self.ORIG_SendAddonMessage(prefix, text, chattype); +end + + + +----------------------------------------------------------------------- +-- ChatThrottleLib:UpdateAvail +-- Update self.avail with how much bandwidth is currently available + +function ChatThrottleLib:UpdateAvail() + local now = GetTime(); + 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 = min(self.avail + (newavail*0.1), MAX_CPS*0.5); + elseif(GetFramerate() 1 then + return true + end + -- print("Chat Throttled") + return false +end + +function ChatThrottleLib:Despool(Prio) + local ring = Prio.Ring; + while(ring.pos and Prio.avail>ring.pos[1].nSize and self.IsTurtleSendChatReady()) do + local msg = tremove(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(msg[1], msg[2], msg[3], msg[4]); + if msg.type == "chat" then self.TurtleSendChat() end + Prio.nTotalSent = Prio.nTotalSent + msg.nSize; + self.MsgBin:Put(msg); + end +end + + +function ChatThrottleLib.OnEvent() + -- v11: We know that the rate limiter is touchy after login. Assume that it's touch after zoning, too. + 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() + self = ChatThrottleLib; + + self.OnUpdateDelay = self.OnUpdateDelay + arg1; + if(self.OnUpdateDelay < 0.08) then + return; + end + if self.TurtleChatLinesAvailable < TURTLE_MAX_CHAT_LINES_PER_SECOND then + self.TurtleChatLinesAvailable = math.min(self.TurtleChatLinesAvailable + self.OnUpdateDelay * TURTLE_MAX_CHAT_LINES_PER_SECOND, TURTLE_MAX_CHAT_LINES_PER_SECOND) + 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 and self.TurtleChatLinesAvailable >= TURTLE_MAX_CHAT_LINES_PER_SECOND) 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 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 = self.PipeBin:Get(); + pipe.name = pipename; + Prio.ByName[pipename] = pipe; + Prio.Ring:Add(pipe); + end + + tinsert(pipe, msg); + + self.bQueueing = true; +end + + + +function ChatThrottleLib:SendChatMessage(prio, prefix, text, chattype, language, destination) + if(not (self and prio and text and 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 = strlen(text) + MSG_OVERHEAD; + + -- Check if there's room in the global available bandwidth gauge to send directly + if(not self.bQueueing and nSize < self:UpdateAvail() and self.IsTurtleSendChatReady()) then + self.avail = self.avail - nSize; + self.ORIG_SendChatMessage(text, chattype, language, destination); + self.TurtleSendChat() + self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize; + return; + end + + -- Message needs to be queued + msg=self.MsgBin:Get(); + msg.f=self.ORIG_SendChatMessage + msg.type="chat"; + msg[1]=text; + msg[2]=chattype or "SAY"; + msg[3]=language; + msg[4]=destination; + msg.n = 4 + msg.nSize = nSize; + + self:Enqueue(prio, format("%s/%s/%s", prefix, chattype, destination or ""), msg); +end + + +function ChatThrottleLib:SendAddonMessage(prio, prefix, text, chattype) + if(not (self and prio and prefix and text and chattype and self.Prio[prio] ) ) then + error('Usage: ChatThrottleLib:SendAddonMessage("{BULK||NORMAL||ALERT}", "prefix", "text", "chattype")', 0); + end + + local nSize = strlen(prefix) + 1 + strlen(text) + 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); + self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize; + return; + end + + -- Message needs to be queued + msg=self.MsgBin:Get(); + msg.f=self.ORIG_SendAddonMessage; + msg.type="addon"; + msg[1]=prefix; + msg[2]=text; + msg[3]=chattype; + msg.n = 3 + msg.nSize = nSize; + + self:Enqueue(prio, format("%s/%s", prefix, chattype), msg); +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 +]] + +