diff --git a/Gladius/Gladius.lua b/Gladius/Gladius.lua
index 2d3c5d8..1df94ea 100644
--- a/Gladius/Gladius.lua
+++ b/Gladius/Gladius.lua
@@ -1,8 +1,8 @@
-Gladius = LibStub("AceAddon-3.0"):NewAddon("Gladius", "AceEvent-3.0", "AceConsole-3.0")
+Gladius = LibStub("AceAddon-3.0"):NewAddon("Gladius", "AceEvent-3.0", "AceConsole-3.0", "AceComm-3.0", "AceTimer-3.0", "AceSerializer-3.0")
local L = LibStub("AceLocale-3.0"):GetLocale("Gladius", true)
local LSM = LibStub("LibSharedMedia-3.0")
local LCG = LibStub("LibCustomGlow-1.0")
-local LHC = LibStub("LibHealCommArena-4.0")
+--[[ local LHC = LibStub("LibHealCommArena-4.0") ]]
local LAM = LibStub:GetLibrary("AbsorbsMonitor-1.0", true)
local LDB = LibStub ("LibDataBroker-1.1")
local LDBIcon = LibStub("LibDBIcon-1.0")
@@ -27,13 +27,13 @@ function Gladius:OnInitialize()
self.buttons = {}
self.currentBracket = nil
- LHC.UnregisterAllCallbacks(Gladius);
+ --[[ LHC.UnregisterAllCallbacks(Gladius);
LHC.RegisterCallback(Gladius, "HealCommArena_HealStarted", "HealCommArena_Heal_Update")
LHC.RegisterCallback(Gladius, "HealCommArena_HealUpdated", "HealCommArena_Heal_Update")
LHC.RegisterCallback(Gladius, "HealCommArena_HealDelayed", "HealCommArena_Heal_Update")
LHC.RegisterCallback(Gladius, "HealCommArena_HealStopped", "HealCommArena_Heal_Update")
LHC.RegisterCallback(Gladius, "HealCommArena_ModifierChanged", "HealCommArena_Modified")
- LHC.RegisterCallback(Gladius, "HealCommArena_GUIDDisappeared", "HealCommArena_Modified")
+ LHC.RegisterCallback(Gladius, "HealCommArena_GUIDDisappeared", "HealCommArena_Modified") ]]
-- Populate the arenaUnits table
for i=1, 5 do
@@ -270,7 +270,7 @@ function Gladius:JoinedArena()
LHC.RegisterCallback(Gladius, "HealCommArena_ModifierChanged", "HealCommArena_Modified")
LHC.RegisterCallback(Gladius, "HealCommArena_GUIDDisappeared", "HealCommArena_Modified")]]
- -- HealComm Events
+ -- Absorb Events
LAM.UnregisterAllCallbacks(Gladius);
LAM.RegisterCallback(Gladius, "EffectApplied");
LAM.RegisterCallback(Gladius, "EffectUpdated");
@@ -1825,7 +1825,7 @@ function Gladius:UpdateAbsorbBar(event, unit, button)
local health = UnitHealth(unit)
local maxHealth = UnitHealthMax(unit)
local _guid = UnitGUID(unit)
- local myCurrentHealAbsorb = LAM.Unit_Total(_guid)
+ local myCurrentHealAbsorb = LAM.Unit_Total(_guid) * 2 -- only enemy
--
function CompactUnitFrameUtil_UpdateFillBar(self, frame, previousTexture, health, myCurrentHealAbsorb)
@@ -1867,16 +1867,10 @@ function Gladius:UpdateAbsorbBar(event, unit, button)
end
-- HealComm (Heal Prediction)
-local function Update(self)
+--[[ local function Update(self)
local unit = self.unit
local element = self.HealCommBar
- --[[ Callback: HealthPrediction:PreUpdate(unit)
- Called before the element has been updated.
-
- * self - the HealthPrediction element
- * unit - the unit for which the update has been triggered (string)
- --]]
if element.PreUpdate then
element:PreUpdate(unit)
end
@@ -1912,27 +1906,9 @@ local function Update(self)
element.otherBar:Show()
end
- --[[ Callback: HealthPrediction:PostUpdate(unit, myIncomingHeal, otherIncomingHeal)
- Called after the element has been updated.
-
- * self - the HealthPrediction element
- * unit - the unit for which the update has been triggered (string)
- * myIncomingHeal - the amount of incoming healing done by the player (number)
- * otherIncomingHeal - the amount of incoming healing done by others (number)
- --]]
- if element.PostUpdate then
- return element:PostUpdate(unit, myIncomingHeal, otherIncomingHeal)
- end
end
local function MultiUpdate(...)
for i = 1, select("#", ...) do
- --[[ for j = 1, #enabledUF do
- local frame = enabledUF[j]
-
- if frame.unit and frame:IsVisible() and UnitGUID(frame.unit) == select(i, ...) then
- Path(frame)
- end
- end ]]
if (UnitGUID('arena1') == select(i, ...)) then
local allIncomingHeal = LHC:GetHealAmount(select(i, ...), LHC.ALL_HEALS, nil) or 0
print('updated arena1', allIncomingHeal)
@@ -1947,4 +1923,4 @@ function Gladius:HealCommArena_Heal_Update(event, casterGUID, spellID, spellType
end
function Gladius:HealCommArena_Modified(event, guid)
MultiUpdate(guid)
-end
+end ]]
diff --git a/Gladius/Gladius.toc b/Gladius/Gladius.toc
index 2bb02d1..3b15139 100644
--- a/Gladius/Gladius.toc
+++ b/Gladius/Gladius.toc
@@ -4,7 +4,7 @@
## Version: v1.2.2
## Author: Proditor, Rinu, Enhanced by Cortes
## SavedVariables: GladiusDB
-## OptionalDeps: Ace3, LibStub, LibSharedMedia-3.0, AceGUI-3.0-SharedMediaWidgets, LibCustomGlow-1.0, AbsorbsMonitor-1.0, LibDataBroker-1.1, LibDBIcon-1.0, LibHealCommArena-4.0
+## OptionalDeps: Ace3, LibStub, LibSharedMedia-3.0, AceGUI-3.0-SharedMediaWidgets, LibCustomGlow-1.0, AbsorbsMonitor-1.0, LibDataBroker-1.1, LibDBIcon-1.0
## LoadManagers: AddonLoader
## X-LoadOn-Slash: /gladius
## X-LoadOn-InterfaceOptions: Gladius
diff --git a/Gladius/embeds.xml b/Gladius/embeds.xml
index 270f514..83c717a 100644
--- a/Gladius/embeds.xml
+++ b/Gladius/embeds.xml
@@ -12,10 +12,14 @@
+
+
+
-
+
+
\ No newline at end of file
diff --git a/Gladius/frame.lua b/Gladius/frame.lua
index 582e78b..c324ef3 100644
--- a/Gladius/frame.lua
+++ b/Gladius/frame.lua
@@ -273,7 +273,7 @@ function Gladius:CreateButton(i)
absorbBar.overAbsorbGlow:SetBlendMode("ADD");
absorbBar.overAbsorbGlow:Hide()
-- Total absorb
- absorbBar.totalAbsorb = absorbBar:CreateTexture(nil, "BORDER")
+ absorbBar.totalAbsorb = absorbBar:CreateTexture(nil, "BACKGROUND")
absorbBar.totalAbsorb:Hide()
-- Total absorb overlay
absorbBar.totalAbsorbOverlay = absorbBar:CreateTexture(nil, "BORDER")
diff --git a/Gladius/libs/AceComm-3.0/AceComm-3.0.lua b/Gladius/libs/AceComm-3.0/AceComm-3.0.lua
new file mode 100644
index 0000000..ad5268f
--- /dev/null
+++ b/Gladius/libs/AceComm-3.0/AceComm-3.0.lua
@@ -0,0 +1,309 @@
+--- **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 895 2009-12-06 16:28:55Z 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 MAJOR, MINOR = "AceComm-3.0", 6
+
+local AceComm,oldminor = LibStub:NewLibrary(MAJOR, MINOR)
+
+if not AceComm then return end
+
+local CallbackHandler = LibStub:GetLibrary("CallbackHandler-1.0")
+local CTL = assert(ChatThrottleLib, "AceComm-3.0 requires ChatThrottleLib")
+
+-- Lua APIs
+local type, next, pairs, tostring = type, next, pairs, tostring
+local strsub, strfind = string.sub, string.find
+local tinsert, tconcat = table.insert, table.concat
+local error, assert = error, assert
+
+-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
+-- List them here for Mikk's FindGlobals script
+-- GLOBALS: LibStub, DEFAULT_CHAT_FRAME, geterrorhandler
+
+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"
+
+AceComm.multipart_origprefixes = AceComm.multipart_origprefixes or {} -- e.g. "Prefix\001"="Prefix", "Prefix\002"="Prefix"
+AceComm.multipart_reassemblers = AceComm.multipart_reassemblers or {} -- e.g. "Prefix\001"="OnReceiveMultipartFirst"
+
+-- 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)
+-- @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
+
+ 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") and
+ (prio=="BULK" or prio=="NORMAL" or prio=="ALERT")
+ ) then
+ error('Usage: SendCommMessage(addon, "prefix", "text", "distribution"[, "target"[, "prio"[, callbackFn, callbackarg]]])', 2)
+ end
+
+ if strfind(prefix, "[\001-\009]") then
+ if strfind(prefix, "[\001-\003]") then
+ error("SendCommMessage: Characters \\001--\\003 in prefix are reserved for AceComm metadata", 2)
+ elseif not warnedPrefix then
+ -- I have some ideas about future extensions that require more control characters /mikk, 20090808
+ geterrorhandler()("SendCommMessage: Heads-up developers: Characters \\004--\\009 in prefix are reserved for AceComm future extension")
+ warnedPrefix = true
+ end
+ end
+
+
+ local textlen = #text
+ local maxtextlen = 254 - #prefix -- 254 is the max length of prefix + text that can be sent in one message
+ local queueName = prefix..distribution..(target or "")
+
+ local ctlCallback = nil
+ if callbackFn then
+ ctlCallback = function(sent)
+ return callbackFn(callbackArg, sent, textlen)
+ end
+ end
+
+ if 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
+
+ -- 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
+ local prefix2 = prefix..MSG_MULTI_NEXT
+
+ while pos+maxtextlen <= textlen do
+ chunk = strsub(text, pos, pos+maxtextlen-1)
+ CTL:SendAddonMessage(prio, prefix2, 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
+ -- ensure that 'prefix to watch' table is consistent with registered
+ -- callbacks
+ AceComm.__prefixes = {}
+
+ AceComm.callbacks = CallbackHandler:New(AceComm,
+ "_RegisterComm",
+ "UnregisterComm",
+ "UnregisterAllComm")
+end
+
+function AceComm.callbacks:OnUsed(target, prefix)
+ AceComm.multipart_origprefixes[prefix..MSG_MULTI_FIRST] = prefix
+ AceComm.multipart_reassemblers[prefix..MSG_MULTI_FIRST] = "OnReceiveMultipartFirst"
+
+ AceComm.multipart_origprefixes[prefix..MSG_MULTI_NEXT] = prefix
+ AceComm.multipart_reassemblers[prefix..MSG_MULTI_NEXT] = "OnReceiveMultipartNext"
+
+ AceComm.multipart_origprefixes[prefix..MSG_MULTI_LAST] = prefix
+ AceComm.multipart_reassemblers[prefix..MSG_MULTI_LAST] = "OnReceiveMultipartLast"
+end
+
+function AceComm.callbacks:OnUnused(target, prefix)
+ AceComm.multipart_origprefixes[prefix..MSG_MULTI_FIRST] = nil
+ AceComm.multipart_reassemblers[prefix..MSG_MULTI_FIRST] = nil
+
+ AceComm.multipart_origprefixes[prefix..MSG_MULTI_NEXT] = nil
+ AceComm.multipart_reassemblers[prefix..MSG_MULTI_NEXT] = nil
+
+ AceComm.multipart_origprefixes[prefix..MSG_MULTI_LAST] = nil
+ AceComm.multipart_reassemblers[prefix..MSG_MULTI_LAST] = nil
+end
+
+local function OnEvent(this, event, ...)
+ if event == "CHAT_MSG_ADDON" then
+ local prefix,message,distribution,sender = ...
+ local reassemblername = AceComm.multipart_reassemblers[prefix]
+ if reassemblername then
+ -- multipart: reassemble
+ local aceCommReassemblerFunc = AceComm[reassemblername]
+ local origprefix = AceComm.multipart_origprefixes[prefix]
+ aceCommReassemblerFunc(AceComm, origprefix, message, distribution, sender)
+ 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
diff --git a/Gladius/libs/AceComm-3.0/AceComm-3.0.xml b/Gladius/libs/AceComm-3.0/AceComm-3.0.xml
new file mode 100644
index 0000000..09e8d87
--- /dev/null
+++ b/Gladius/libs/AceComm-3.0/AceComm-3.0.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/Gladius/libs/AceComm-3.0/ChatThrottleLib.lua b/Gladius/libs/AceComm-3.0/ChatThrottleLib.lua
new file mode 100644
index 0000000..b0afc92
--- /dev/null
+++ b/Gladius/libs/AceComm-3.0/ChatThrottleLib.lua
@@ -0,0 +1,503 @@
+--
+-- 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! :-)
+--
+
+local CTL_VERSION = 21
+
+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 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
+
+
+
+-----------------------------------------------------------------------
+-- 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
+-- A pipe is a plain integer-indexed queue, which also happens to be a ring member
+
+ChatThrottleLib.PipeBin = nil -- pre-v19, drastically different
+local PipeBin = setmetatable({}, {__mode="k"})
+
+local function DelPipe(pipe)
+ for i = #pipe, 1, -1 do
+ pipe[i] = nil
+ end
+ pipe.prev = nil
+ pipe.next = nil
+
+ PipeBin[pipe] = true
+end
+
+local function NewPipe()
+ local pipe = next(PipeBin)
+ if pipe then
+ 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
+
+ -- 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.securelyHooked then
+ -- Use secure hooks as of v16. Old regular hook support yanked out in v21.
+ self.securelyHooked = true
+ --SendChatMessage
+ hooksecurefunc("SendChatMessage", function(...)
+ return ChatThrottleLib.Hook_SendChatMessage(...)
+ end)
+ --SendAddonMessage
+ hooksecurefunc("SendAddonMessage", function(...)
+ return ChatThrottleLib.Hook_SendAddonMessage(...)
+ 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
+
+
+
+-----------------------------------------------------------------------
+-- 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
+
+function 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
+ DelPipe(pipe)
+ else
+ Prio.Ring.pos = Prio.Ring.pos.next
+ end
+ Prio.avail = Prio.avail - msg.nSize
+ bMyTraffic = true
+ msg.f(unpack(msg, 1, msg.n))
+ bMyTraffic = false
+ Prio.nTotalSent = Prio.nTotalSent + msg.nSize
+ DelMsg(msg)
+ if msg.callbackFn then
+ msg.callbackFn (msg.callbackArg)
+ 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
+ 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 our priorities have queued messages (we only have 3, don't worry about the loop)
+ 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)
+ -- Note: We might not get here if the user-supplied callback function errors out! Take care!
+ end
+ 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
+ self.avail = self.avail - nSize
+ bMyTraffic = true
+ _G.SendChatMessage(text, chattype, language, destination)
+ bMyTraffic = false
+ self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
+ if callbackFn then
+ callbackFn (callbackArg)
+ end
+ return
+ end
+
+ -- Message needs to be queued
+ local msg = NewMsg()
+ msg.f = _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..(chattype or "SAY")..(destination or "")), 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)
+ end
+ if callbackFn and type(callbackFn)~="function" then
+ error('ChatThrottleLib:SendAddonMessage(): callbackFn: expected function, got '..type(callbackFn), 2)
+ end
+
+ local nSize = prefix:len() + 1 + text:len();
+
+ if nSize>255 then
+ error("ChatThrottleLib:SendAddonMessage(): prefix + message length cannot exceed 254 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
+ self.avail = self.avail - nSize
+ bMyTraffic = true
+ _G.SendAddonMessage(prefix, text, chattype, target)
+ bMyTraffic = false
+ self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
+ if callbackFn then
+ callbackFn (callbackArg)
+ end
+ return
+ end
+
+ -- Message needs to be queued
+ local msg = NewMsg()
+ msg.f = _G.SendAddonMessage
+ 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..chattype..(target or "")), 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
+]]
+
+
diff --git a/Gladius/libs/AceSerializer-3.0/AceSerializer-3.0.lua b/Gladius/libs/AceSerializer-3.0/AceSerializer-3.0.lua
new file mode 100644
index 0000000..b072a2b
--- /dev/null
+++ b/Gladius/libs/AceSerializer-3.0/AceSerializer-3.0.lua
@@ -0,0 +1,281 @@
+--- **AceSerializer-3.0** can serialize any variable (except functions or userdata) into a string format,
+-- that can be send over the addon comm channel. AceSerializer was designed to keep all data intact, especially
+-- very large numbers or floating point numbers, and table structures. The only caveat currently is, that multiple
+-- references to the same table will be send individually.
+--
+-- **AceSerializer-3.0** can be embeded into your addon, either explicitly by calling AceSerializer: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 AceSerializer itself.\\
+-- It is recommended to embed AceSerializer, otherwise you'll have to specify a custom `self` on all calls you
+-- make into AceSerializer.
+-- @class file
+-- @name AceSerializer-3.0
+-- @release $Id: AceSerializer-3.0.lua 910 2010-02-11 21:54:24Z mikk $
+local MAJOR,MINOR = "AceSerializer-3.0", 3
+local AceSerializer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
+
+if not AceSerializer then return end
+
+-- Lua APIs
+local strbyte, strchar, gsub, gmatch, format = string.byte, string.char, string.gsub, string.gmatch, string.format
+local assert, error, pcall = assert, error, pcall
+local type, tostring, tonumber = type, tostring, tonumber
+local pairs, select, frexp = pairs, select, math.frexp
+local tconcat = table.concat
+
+-- quick copies of string representations of wonky numbers
+local serNaN = tostring(0/0)
+local serInf = tostring(1/0)
+local serNegInf = tostring(-1/0)
+
+
+-- Serialization functions
+
+local function SerializeStringHelper(ch) -- Used by SerializeValue for strings
+ -- We use \126 ("~") as an escape character for all nonprints plus a few more
+ local n = strbyte(ch)
+ if n==30 then -- v3 / ticket 115: catch a nonprint that ends up being "~^" when encoded... DOH
+ return "\126\122"
+ elseif n<=32 then -- nonprint + space
+ return "\126"..strchar(n+64)
+ elseif n==94 then -- value separator
+ return "\126\125"
+ elseif n==126 then -- our own escape character
+ return "\126\124"
+ elseif n==127 then -- nonprint (DEL)
+ return "\126\123"
+ else
+ assert(false) -- can't be reached if caller uses a sane regex
+ end
+end
+
+local function SerializeValue(v, res, nres)
+ -- We use "^" as a value separator, followed by one byte for type indicator
+ local t=type(v)
+
+ if t=="string" then -- ^S = string (escaped to remove nonprints, "^"s, etc)
+ res[nres+1] = "^S"
+ res[nres+2] = gsub(v,"[%c \94\126\127]", SerializeStringHelper)
+ nres=nres+2
+
+ elseif t=="number" then -- ^N = number (just tostring()ed) or ^F (float components)
+ local str = tostring(v)
+ if tonumber(str)==v or str==serNaN or str==serInf or str==serNegInf then
+ -- translates just fine, transmit as-is
+ res[nres+1] = "^N"
+ res[nres+2] = str
+ nres=nres+2
+ else
+ local m,e = frexp(v)
+ res[nres+1] = "^F"
+ res[nres+2] = format("%.0f",m*2^53) -- force mantissa to become integer (it's originally 0.5--0.9999)
+ res[nres+3] = "^f"
+ res[nres+4] = tostring(e-53) -- adjust exponent to counteract mantissa manipulation
+ nres=nres+4
+ end
+
+ elseif t=="table" then -- ^T...^t = table (list of key,value pairs)
+ nres=nres+1
+ res[nres] = "^T"
+ for k,v in pairs(v) do
+ nres = SerializeValue(k, res, nres)
+ nres = SerializeValue(v, res, nres)
+ end
+ nres=nres+1
+ res[nres] = "^t"
+
+ elseif t=="boolean" then -- ^B = true, ^b = false
+ nres=nres+1
+ if v then
+ res[nres] = "^B" -- true
+ else
+ res[nres] = "^b" -- false
+ end
+
+ elseif t=="nil" then -- ^Z = nil (zero, "N" was taken :P)
+ nres=nres+1
+ res[nres] = "^Z"
+
+ else
+ error(MAJOR..": Cannot serialize a value of type '"..t.."'") -- can't produce error on right level, this is wildly recursive
+ end
+
+ return nres
+end
+
+
+
+local serializeTbl = { "^1" } -- "^1" = Hi, I'm data serialized by AceSerializer protocol rev 1
+
+--- Serialize the data passed into the function.
+-- Takes a list of values (strings, numbers, booleans, nils, tables)
+-- and returns it in serialized form (a string).\\
+-- May throw errors on invalid data types.
+-- @param ... List of values to serialize
+-- @return The data in its serialized form (string)
+function AceSerializer:Serialize(...)
+ local nres = 1
+
+ for i=1,select("#", ...) do
+ local v = select(i, ...)
+ nres = SerializeValue(v, serializeTbl, nres)
+ end
+
+ serializeTbl[nres+1] = "^^" -- "^^" = End of serialized data
+
+ return tconcat(serializeTbl, "", 1, nres+1)
+end
+
+-- Deserialization functions
+local function DeserializeStringHelper(escape)
+ if escape<"~\122" then
+ return strchar(strbyte(escape,2,2)-64)
+ elseif escape=="~\122" then -- v3 / ticket 115: special case encode since 30+64=94 ("^") - OOPS.
+ return "\030"
+ elseif escape=="~\123" then
+ return "\127"
+ elseif escape=="~\124" then
+ return "\126"
+ elseif escape=="~\125" then
+ return "\94"
+ end
+ error("DeserializeStringHelper got called for '"..escape.."'?!?") -- can't be reached unless regex is screwed up
+end
+
+local function DeserializeNumberHelper(number)
+ if number == serNaN then
+ return 0/0
+ elseif number == serNegInf then
+ return -1/0
+ elseif number == serInf then
+ return 1/0
+ else
+ return tonumber(number)
+ end
+end
+
+-- DeserializeValue: worker function for :Deserialize()
+-- It works in two modes:
+-- Main (top-level) mode: Deserialize a list of values and return them all
+-- Recursive (table) mode: Deserialize only a single value (_may_ of course be another table with lots of subvalues in it)
+--
+-- The function _always_ works recursively due to having to build a list of values to return
+--
+-- Callers are expected to pcall(DeserializeValue) to trap errors
+
+local function DeserializeValue(iter,single,ctl,data)
+
+ if not single then
+ ctl,data = iter()
+ end
+
+ if not ctl then
+ error("Supplied data misses AceSerializer terminator ('^^')")
+ end
+
+ if ctl=="^^" then
+ -- ignore extraneous data
+ return
+ end
+
+ local res
+
+ if ctl=="^S" then
+ res = gsub(data, "~.", DeserializeStringHelper)
+ elseif ctl=="^N" then
+ res = DeserializeNumberHelper(data)
+ if not res then
+ error("Invalid serialized number: '"..tostring(data).."'")
+ end
+ elseif ctl=="^F" then -- ^F^f
+ local ctl2,e = iter()
+ if ctl2~="^f" then
+ error("Invalid serialized floating-point number, expected '^f', not '"..tostring(ctl2).."'")
+ end
+ local m=tonumber(data)
+ e=tonumber(e)
+ if not (m and e) then
+ error("Invalid serialized floating-point number, expected mantissa and exponent, got '"..tostring(m).."' and '"..tostring(e).."'")
+ end
+ res = m*(2^e)
+ elseif ctl=="^B" then -- yeah yeah ignore data portion
+ res = true
+ elseif ctl=="^b" then -- yeah yeah ignore data portion
+ res = false
+ elseif ctl=="^Z" then -- yeah yeah ignore data portion
+ res = nil
+ elseif ctl=="^T" then
+ -- ignore ^T's data, future extensibility?
+ res = {}
+ local k,v
+ while true do
+ ctl,data = iter()
+ if ctl=="^t" then break end -- ignore ^t's data
+ k = DeserializeValue(iter,true,ctl,data)
+ if k==nil then
+ error("Invalid AceSerializer table format (no table end marker)")
+ end
+ ctl,data = iter()
+ v = DeserializeValue(iter,true,ctl,data)
+ if v==nil then
+ error("Invalid AceSerializer table format (no table end marker)")
+ end
+ res[k]=v
+ end
+ else
+ error("Invalid AceSerializer control code '"..ctl.."'")
+ end
+
+ if not single then
+ return res,DeserializeValue(iter)
+ else
+ return res
+ end
+end
+
+--- Deserializes the data into its original values.
+-- Accepts serialized data, ignoring all control characters and whitespace.
+-- @param str The serialized data (from :Serialize)
+-- @return true followed by a list of values, OR false followed by an error message
+function AceSerializer:Deserialize(str)
+ str = gsub(str, "[%c ]", "") -- ignore all control characters; nice for embedding in email and stuff
+
+ local iter = gmatch(str, "(^.)([^^]*)") -- Any ^x followed by string of non-^
+ local ctl,data = iter()
+ if not ctl or ctl~="^1" then
+ -- we purposefully ignore the data portion of the start code, it can be used as an extension mechanism
+ return false, "Supplied data is not AceSerializer data (rev 1)"
+ end
+
+ return pcall(DeserializeValue, iter)
+end
+
+
+----------------------------------------
+-- Base library stuff
+----------------------------------------
+
+AceSerializer.internals = { -- for test scripts
+ SerializeValue = SerializeValue,
+ SerializeStringHelper = SerializeStringHelper,
+}
+
+local mixins = {
+ "Serialize",
+ "Deserialize",
+}
+
+AceSerializer.embeds = AceSerializer.embeds or {}
+
+function AceSerializer:Embed(target)
+ for k, v in pairs(mixins) do
+ target[v] = self[v]
+ end
+ self.embeds[target] = true
+ return target
+end
+
+-- Update embeds
+for target, v in pairs(AceSerializer.embeds) do
+ AceSerializer:Embed(target)
+end
\ No newline at end of file
diff --git a/Gladius/libs/AceSerializer-3.0/AceSerializer-3.0.xml b/Gladius/libs/AceSerializer-3.0/AceSerializer-3.0.xml
new file mode 100644
index 0000000..94924af
--- /dev/null
+++ b/Gladius/libs/AceSerializer-3.0/AceSerializer-3.0.xml
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file
diff --git a/Gladius/libs/AceTimer-3.0/AceTimer-3.0.lua b/Gladius/libs/AceTimer-3.0/AceTimer-3.0.lua
new file mode 100644
index 0000000..fdb2cff
--- /dev/null
+++ b/Gladius/libs/AceTimer-3.0/AceTimer-3.0.lua
@@ -0,0 +1,473 @@
+--- **AceTimer-3.0** provides a central facility for registering timers.
+-- AceTimer supports one-shot timers and repeating timers. All timers are stored in an efficient
+-- data structure that allows easy dispatching and fast rescheduling. Timers can be registered, rescheduled
+-- or canceled at any time, even from within a running timer, without conflict or large overhead.\\
+-- AceTimer is currently limited to firing timers at a frequency of 0.1s. This constant may change
+-- in the future, but for now it seemed like a good compromise in efficiency and accuracy.
+--
+-- All `:Schedule` functions will return a handle to the current timer, which you will need to store if you
+-- need to cancel or reschedule the timer you just registered.
+--
+-- **AceTimer-3.0** can be embeded into your addon, either explicitly by calling AceTimer: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 AceTimer itself.\\
+-- It is recommended to embed AceTimer, otherwise you'll have to specify a custom `self` on all calls you
+-- make into AceTimer.
+-- @class file
+-- @name AceTimer-3.0
+-- @release $Id: AceTimer-3.0.lua 895 2009-12-06 16:28:55Z nevcairiel $
+
+--[[
+ Basic assumptions:
+ * In a typical system, we do more re-scheduling per second than there are timer pulses per second
+ * Regardless of timer implementation, we cannot guarantee timely delivery due to FPS restriction (may be as low as 10)
+
+ This implementation:
+ CON: The smallest timer interval is constrained by HZ (currently 1/10s).
+ PRO: It will still correctly fire any timer slower than HZ over a length of time, e.g. 0.11s interval -> 90 times over 10 seconds
+ PRO: In lag bursts, the system simly skips missed timer intervals to decrease load
+ CON: Algorithms depending on a timer firing "N times per minute" will fail
+ PRO: (Re-)scheduling is O(1) with a VERY small constant. It's a simple linked list insertion in a hash bucket.
+ CAUTION: The BUCKETS constant constrains how many timers can be efficiently handled. With too many hash collisions, performance will decrease.
+
+ Major assumptions upheld:
+ - ALLOWS scheduling multiple timers with the same funcref/method
+ - ALLOWS scheduling more timers during OnUpdate processing
+ - ALLOWS unscheduling ANY timer (including the current running one) at any time, including during OnUpdate processing
+]]
+
+local MAJOR, MINOR = "AceTimer-3.0", 5
+local AceTimer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
+
+if not AceTimer then return end -- No upgrade needed
+
+AceTimer.hash = AceTimer.hash or {} -- Array of [0..BUCKET-1] = linked list of timers (using .next member)
+ -- Linked list gets around ACE-88 and ACE-90.
+AceTimer.selfs = AceTimer.selfs or {} -- Array of [self]={[handle]=timerobj, [handle2]=timerobj2, ...}
+AceTimer.frame = AceTimer.frame or CreateFrame("Frame", "AceTimer30Frame")
+
+-- Lua APIs
+local assert, error, loadstring = assert, error, loadstring
+local setmetatable, rawset, rawget = setmetatable, rawset, rawget
+local select, pairs, type, next, tostring = select, pairs, type, next, tostring
+local floor, max, min = math.floor, math.max, math.min
+local tconcat = table.concat
+
+-- WoW APIs
+local GetTime = GetTime
+
+-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
+-- List them here for Mikk's FindGlobals script
+-- GLOBALS: DEFAULT_CHAT_FRAME, geterrorhandler
+
+-- Simple ONE-SHOT timer cache. Much more efficient than a full compost for our purposes.
+local timerCache = nil
+
+--[[
+ Timers will not be fired more often than HZ-1 times per second.
+ Keep at intended speed PLUS ONE or we get bitten by floating point rounding errors (n.5 + 0.1 can be n.599999)
+ If this is ever LOWERED, all existing timers need to be enforced to have a delay >= 1/HZ on lib upgrade.
+ If this number is ever changed, all entries need to be rehashed on lib upgrade.
+ ]]
+local HZ = 11
+
+--[[
+ Prime for good distribution
+ If this number is ever changed, all entries need to be rehashed on lib upgrade.
+]]
+local BUCKETS = 131
+
+local hash = AceTimer.hash
+for i=1,BUCKETS do
+ hash[i] = hash[i] or false -- make it an integer-indexed array; it's faster than hashes
+end
+
+--[[
+ xpcall safecall implementation
+]]
+local xpcall = xpcall
+
+local function errorhandler(err)
+ return geterrorhandler()(err)
+end
+
+local function CreateDispatcher(argCount)
+ local code = [[
+ local xpcall, eh = ... -- our arguments are received as unnamed values in "..." since we don't have a proper function declaration
+ local method, ARGS
+ local function call() return method(ARGS) end
+
+ local function dispatch(func, ...)
+ method = func
+ if not method then return end
+ ARGS = ...
+ return xpcall(call, eh)
+ end
+
+ return dispatch
+ ]]
+
+ local ARGS = {}
+ for i = 1, argCount do ARGS[i] = "arg"..i end
+ code = code:gsub("ARGS", tconcat(ARGS, ", "))
+ return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(xpcall, errorhandler)
+end
+
+local Dispatchers = setmetatable({}, {
+ __index=function(self, argCount)
+ local dispatcher = CreateDispatcher(argCount)
+ rawset(self, argCount, dispatcher)
+ return dispatcher
+ end
+})
+Dispatchers[0] = function(func)
+ return xpcall(func, errorhandler)
+end
+
+local function safecall(func, ...)
+ return Dispatchers[select('#', ...)](func, ...)
+end
+
+local lastint = floor(GetTime() * HZ)
+
+-- --------------------------------------------------------------------
+-- OnUpdate handler
+--
+-- traverse buckets, always chasing "now", and fire timers that have expired
+
+local function OnUpdate()
+ local now = GetTime()
+ local nowint = floor(now * HZ)
+
+ -- Have we passed into a new hash bucket?
+ if nowint == lastint then return end
+
+ local soon = now + 1 -- +1 is safe as long as 1 < HZ < BUCKETS/2
+
+ -- Pass through each bucket at most once
+ -- Happens on e.g. instance loads, but COULD happen on high local load situations also
+ for curint = (max(lastint, nowint - BUCKETS) + 1), nowint do -- loop until we catch up with "now", usually only 1 iteration
+ local curbucket = (curint % BUCKETS)+1
+ -- Yank the list of timers out of the bucket and empty it. This allows reinsertion in the currently-processed bucket from callbacks.
+ local nexttimer = hash[curbucket]
+ hash[curbucket] = false -- false rather than nil to prevent the array from becoming a hash
+
+ while nexttimer do
+ local timer = nexttimer
+ nexttimer = timer.next
+ local when = timer.when
+
+ if when < soon then
+ -- Call the timer func, either as a method on given object, or a straight function ref
+ local callback = timer.callback
+ if type(callback) == "string" then
+ safecall(timer.object[callback], timer.object, timer.arg)
+ elseif callback then
+ safecall(callback, timer.arg)
+ else
+ -- probably nilled out by CancelTimer
+ timer.delay = nil -- don't reschedule it
+ end
+
+ local delay = timer.delay -- NOW make a local copy, can't do it earlier in case the timer cancelled itself in the callback
+
+ if not delay then
+ -- single-shot timer (or cancelled)
+ AceTimer.selfs[timer.object][tostring(timer)] = nil
+ timerCache = timer
+ else
+ -- repeating timer
+ local newtime = when + delay
+ if newtime < now then -- Keep lag from making us firing a timer unnecessarily. (Note that this still won't catch too-short-delay timers though.)
+ newtime = now + delay
+ end
+ timer.when = newtime
+
+ -- add next timer execution to the correct bucket
+ local bucket = (floor(newtime * HZ) % BUCKETS) + 1
+ timer.next = hash[bucket]
+ hash[bucket] = timer
+ end
+ else -- if when>=soon
+ -- reinsert (yeah, somewhat expensive, but shouldn't be happening too often either due to hash distribution)
+ timer.next = hash[curbucket]
+ hash[curbucket] = timer
+ end -- if whenhandle->timer registry
+ local handle = tostring(timer)
+
+ local selftimers = AceTimer.selfs[self]
+ if not selftimers then
+ selftimers = {}
+ AceTimer.selfs[self] = selftimers
+ end
+ selftimers[handle] = timer
+ selftimers.__ops = (selftimers.__ops or 0) + 1
+
+ return handle
+end
+
+--- Schedule a new one-shot timer.
+-- The timer will fire once in `delay` seconds, unless canceled before.
+-- @param callback Callback function for the timer pulse (funcref or method name).
+-- @param delay Delay for the timer, in seconds.
+-- @param arg An optional argument to be passed to the callback function.
+-- @usage
+-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("TimerTest", "AceTimer-3.0")
+--
+-- function MyAddon:OnEnable()
+-- self:ScheduleTimer("TimerFeedback", 5)
+-- end
+--
+-- function MyAddon:TimerFeedback()
+-- print("5 seconds passed")
+-- end
+function AceTimer:ScheduleTimer(callback, delay, arg)
+ return Reg(self, callback, delay, arg)
+end
+
+--- Schedule a repeating timer.
+-- The timer will fire every `delay` seconds, until canceled.
+-- @param callback Callback function for the timer pulse (funcref or method name).
+-- @param delay Delay for the timer, in seconds.
+-- @param arg An optional argument to be passed to the callback function.
+-- @usage
+-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("TimerTest", "AceTimer-3.0")
+--
+-- function MyAddon:OnEnable()
+-- self.timerCount = 0
+-- self.testTimer = self:ScheduleRepeatingTimer("TimerFeedback", 5)
+-- end
+--
+-- function MyAddon:TimerFeedback()
+-- self.timerCount = self.timerCount + 1
+-- print(("%d seconds passed"):format(5 * self.timerCount))
+-- -- run 30 seconds in total
+-- if self.timerCount == 6 then
+-- self:CancelTimer(self.testTimer)
+-- end
+-- end
+function AceTimer:ScheduleRepeatingTimer(callback, delay, arg)
+ return Reg(self, callback, delay, arg, true)
+end
+
+--- Cancels a timer with the given handle, registered by the same addon object as used for `:ScheduleTimer`
+-- Both one-shot and repeating timers can be canceled with this function, as long as the `handle` is valid
+-- and the timer has not fired yet or was canceled before.
+-- @param handle The handle of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
+-- @param silent If true, no error is raised if the timer handle is invalid (expired or already canceled)
+-- @return True if the timer was successfully cancelled.
+function AceTimer:CancelTimer(handle, silent)
+ if not handle then return end -- nil handle -> bail out without erroring
+ if type(handle) ~= "string" then
+ error(MAJOR..": CancelTimer(handle): 'handle' - expected a string", 2) -- for now, anyway
+ end
+ local selftimers = AceTimer.selfs[self]
+ local timer = selftimers and selftimers[handle]
+ if silent then
+ if timer then
+ timer.callback = nil -- don't run it again
+ timer.delay = nil -- if this is the currently-executing one: don't even reschedule
+ -- The timer object is removed in the OnUpdate loop
+ end
+ return not not timer -- might return "true" even if we double-cancel. we'll live.
+ else
+ if not timer then
+ geterrorhandler()(MAJOR..": CancelTimer(handle[, silent]): '"..tostring(handle).."' - no such timer registered")
+ return false
+ end
+ if not timer.callback then
+ geterrorhandler()(MAJOR..": CancelTimer(handle[, silent]): '"..tostring(handle).."' - timer already cancelled or expired")
+ return false
+ end
+ timer.callback = nil -- don't run it again
+ timer.delay = nil -- if this is the currently-executing one: don't even reschedule
+ return true
+ end
+end
+
+--- Cancels all timers registered to the current addon object ('self')
+function AceTimer:CancelAllTimers()
+ if not(type(self) == "string" or type(self) == "table") then
+ error(MAJOR..": CancelAllTimers(): 'self' - must be a string or a table",2)
+ end
+ if self == AceTimer then
+ error(MAJOR..": CancelAllTimers(): supply a meaningful 'self'", 2)
+ end
+
+ local selftimers = AceTimer.selfs[self]
+ if selftimers then
+ for handle,v in pairs(selftimers) do
+ if type(v) == "table" then -- avoid __ops, etc
+ AceTimer.CancelTimer(self, handle, true)
+ end
+ end
+ end
+end
+
+--- Returns the time left for a timer with the given handle, registered by the current addon object ('self').
+-- This function will raise a warning when the handle is invalid, but not stop execution.
+-- @param handle The handle of the timer, as returned by `:ScheduleTimer` or `:ScheduleRepeatingTimer`
+-- @return The time left on the timer, or false if the handle is invalid.
+function AceTimer:TimeLeft(handle)
+ if not handle then return end
+ if type(handle) ~= "string" then
+ error(MAJOR..": TimeLeft(handle): 'handle' - expected a string", 2) -- for now, anyway
+ end
+ local selftimers = AceTimer.selfs[self]
+ local timer = selftimers and selftimers[handle]
+ if not timer then
+ geterrorhandler()(MAJOR..": TimeLeft(handle): '"..tostring(handle).."' - no such timer registered")
+ return false
+ end
+ return timer.when - GetTime()
+end
+
+
+-- ---------------------------------------------------------------------
+-- PLAYER_REGEN_ENABLED: Run through our .selfs[] array step by step
+-- and clean it out - otherwise the table indices can grow indefinitely
+-- if an addon starts and stops a lot of timers. AceBucket does this!
+--
+-- See ACE-94 and tests/AceTimer-3.0-ACE-94.lua
+
+local lastCleaned = nil
+
+local function OnEvent(this, event)
+ if event~="PLAYER_REGEN_ENABLED" then
+ return
+ end
+
+ -- Get the next 'self' to process
+ local selfs = AceTimer.selfs
+ local self = next(selfs, lastCleaned)
+ if not self then
+ self = next(selfs)
+ end
+ lastCleaned = self
+ if not self then -- should only happen if .selfs[] is empty
+ return
+ end
+
+ -- Time to clean it out?
+ local list = selfs[self]
+ if (list.__ops or 0) < 250 then -- 250 slosh indices = ~10KB wasted (max!). For one 'self'.
+ return
+ end
+
+ -- Create a new table and copy all members over
+ local newlist = {}
+ local n=0
+ for k,v in pairs(list) do
+ newlist[k] = v
+ n=n+1
+ end
+ newlist.__ops = 0 -- Reset operation count
+
+ -- And since we now have a count of the number of live timers, check that it's reasonable. Emit a warning if not.
+ if n>BUCKETS then
+ DEFAULT_CHAT_FRAME:AddMessage(MAJOR..": Warning: The addon/module '"..tostring(self).."' has "..n.." live timers. Surely that's not intended?")
+ end
+
+ selfs[self] = newlist
+end
+
+-- ---------------------------------------------------------------------
+-- Embed handling
+
+AceTimer.embeds = AceTimer.embeds or {}
+
+local mixins = {
+ "ScheduleTimer", "ScheduleRepeatingTimer",
+ "CancelTimer", "CancelAllTimers",
+ "TimeLeft"
+}
+
+function AceTimer:Embed(target)
+ AceTimer.embeds[target] = true
+ for _,v in pairs(mixins) do
+ target[v] = AceTimer[v]
+ end
+ return target
+end
+
+-- AceTimer:OnEmbedDisable( target )
+-- target (object) - target object that AceTimer is embedded in.
+--
+-- cancel all timers registered for the object
+function AceTimer:OnEmbedDisable( target )
+ target:CancelAllTimers()
+end
+
+
+for addon in pairs(AceTimer.embeds) do
+ AceTimer:Embed(addon)
+end
+
+-- ---------------------------------------------------------------------
+-- Debug tools (expose copies of internals to test suites)
+AceTimer.debug = AceTimer.debug or {}
+AceTimer.debug.HZ = HZ
+AceTimer.debug.BUCKETS = BUCKETS
+
+-- ---------------------------------------------------------------------
+-- Finishing touchups
+
+AceTimer.frame:SetScript("OnUpdate", OnUpdate)
+AceTimer.frame:SetScript("OnEvent", OnEvent)
+AceTimer.frame:RegisterEvent("PLAYER_REGEN_ENABLED")
+
+-- In theory, we should hide&show the frame based on there being timers or not.
+-- However, this job is fairly expensive, and the chance that there will
+-- actually be zero timers running is diminuitive to say the lest.
diff --git a/Gladius/libs/AceTimer-3.0/AceTimer-3.0.xml b/Gladius/libs/AceTimer-3.0/AceTimer-3.0.xml
new file mode 100644
index 0000000..38e9021
--- /dev/null
+++ b/Gladius/libs/AceTimer-3.0/AceTimer-3.0.xml
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file
diff --git a/Gladius/libs/SpecializedAbsorbs-1.0/SpecializedAbsorbs-1.0.lua b/Gladius/libs/SpecializedAbsorbs-1.0/SpecializedAbsorbs-1.0.lua
new file mode 100644
index 0000000..247a887
--- /dev/null
+++ b/Gladius/libs/SpecializedAbsorbs-1.0/SpecializedAbsorbs-1.0.lua
@@ -0,0 +1,2352 @@
+------------------------------------------------------------------------
+-- SpecializedAbsorbs
+------------------------------------------------------------------------
+
+local MAJOR, MINOR = "SpecializedAbsorbs-1.0", 4
+local lib, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
+if not lib then return end
+local Core
+
+local error = error
+local pairs, select = pairs, select
+local min, max, floor = math.min, math.max, math.floor
+local setmetatable, getmetatable = setmetatable, getmetatable
+local tinsert, tremove, tsort = table.insert, table.remove, table.sort
+local UnitBuff, UnitHealthMax, UnitAttackPower = UnitBuff, UnitHealthMax, UnitAttackPower
+local UnitExists, UnitGUID, UnitClass, UnitLevel = UnitExists, UnitGUID, UnitClass, UnitLevel
+local UnitFactionGroup, UnitInBattleground, GetTalentInfo = UnitFactionGroup, UnitInBattleground, GetTalentInfo
+local GetNumRaidMembers, GetNumPartyMembers = GetNumRaidMembers, GetNumPartyMembers
+
+lib.CheckFlags = lib.CheckFlags or true
+lib.NoErrors = lib.NoErrors or true
+
+---------------------
+-- Install/Upgrade --
+---------------------
+-- We have to upgrade from a previous version
+if oldminor then
+ Core = lib.Core
+ if lib.Enabled then
+ Core.Disable()
+ end
+else
+ lib.Core = {callbacks = LibStub:GetLibrary("CallbackHandler-1.0"):New(lib), Events = {}}
+ Core = lib.Core
+
+ local frame = CreateFrame("Frame", "SpecializedAbsorbs_Events")
+ local events = Core.Events
+ frame:SetScript("OnEvent", function(self, event, ...) events[event](...) end)
+ Core.Frame = frame
+ LibStub("AceComm-3.0"):Embed(Core)
+ LibStub("AceTimer-3.0"):Embed(Core)
+ LibStub("AceSerializer-3.0"):Embed(Core)
+end
+
+---------------------
+-- Local Variables --
+---------------------
+
+local callbacks = Core.callbacks
+local Events = Core.Events
+
+local playerid, playerclass
+
+-- Specifies the channel any AddOn message should be
+-- sent to, nil if silent
+-- Can also be used to get the last known party state
+local curChatChannel = nil
+
+-- Hold the group type and number of members
+local groupCount = 0
+
+-- Always hold the timestamp from the last COMBAT_LOG_EVENT_UNFILTERED fired
+local lastCombatLogEvent = 0.0
+
+-- Table of all active absorb effects indexed by GUID and then spellid
+-- at spellid == -1 there is a numeric entry with the total remaining value,
+-- at spellid == -2 there is a numeric entry with the total quality (that is, the minimal quality)
+-- The priority is also in this table for performance reasons during sort
+-- [GUID] = { [spellid] = {spellid, priority, remainingValue, maxValue, quality, durationTimerHandle, extra} }
+-- Shortcut to Core.activeEffects.bySpell
+local activeEffectsBySpell
+
+-- Table of all active absorb effects indexed by GUID and then a list in the order in which
+-- they will be used
+-- Shortcut to Core.activeEffects.byPriority
+local activeEffectsByPriority
+
+-- Table of all active area absorb effects indexed by triggerGUID
+-- same format as an entry in activeEffectsBySpell with two extra fields at the end
+-- {..., triggerGUID, refcount}
+-- Note that in this approach, a trigger can have only one absorb effect up at the same time.
+-- This should be a reasonable assumption for quite some time, considering only Anti-Magic Zone
+-- and (soon) Power Word: Barrier use this feature anyway
+-- Shortcut to Core.activeEffects.Area
+local activeAreaEffects
+
+-- Table of current unit charges
+-- A charge is a variant value a custom trigger can put on any unit with a limited lifetime
+-- It is organized as a (very simple) queue (FIFO) for use with Divine Aegis e.g. to save the
+-- critical heal value and then apply it on the aura gain or with Val'anyr.
+-- [GUID] = { [spellid] = { charge1, charge2, ... } }
+-- Shortcut to Core.activeCharges
+local activeCharges
+
+-- Table of known spells that cause an absorb effects
+-- priority: active effects above 2 are neither used in total value nor displayed (e.g. Anti-Magic Shell)
+-- [spellid] = {priority, duration, createFunc, hitFunc}
+-- Shortcut to Core.EffectInfo
+local Effects
+
+-- Table of spells that cause an area effect
+-- [spellid (trigger)] = spellid (absorb effect)
+-- Shortcut to Core.AreaTriggers
+local AreaTriggers
+
+-- Table of additional callbacks on combat log events for proc-based and other non-generic absorb effects.
+-- Shortcuts to entries of Core.CombatTriggers
+local CombatTriggersOnHeal
+local CombatTriggersOnHealCrit
+local CombatTriggersOnAuraApplied
+local CombatTriggersOnAuraRemoved
+
+-- Table of all unit stats relevant to absorb effects like attack power and spell power
+-- (mastery rating later on)
+-- [GUID] = { class, AttackPower, SpellPower, quality }
+-- Shortcut to Core.UnitStats
+local UnitStatsTable
+
+-- Table of all scaling factors to absorb effects like talents, items, set boni, buffs
+-- If there is no mechanism in Cataclysm to obtain the correct absorb amount by any effect
+-- this entries are meant to be distributed among a group, raid, etc
+-- [GUID] = { [scaling_Name] = scaling_Value }
+-- A numerical scaling_name above 10 should ONLY be used if it is the spellid of the affected effect,
+-- since it is sometimes used as a very quick way to check for a unit's class
+-- Scaling factors that are only relevant to the local user like priest talents and do not
+-- need to be distributed but are needed for calculation of the public one's are private ones,
+-- found at index -1
+-- Shortcuts to Core.Scaling and its entries
+local Scaling
+local privateScaling
+local playerScaling
+
+-- Table of all unitGUIDs that have a Heal Absorb spell effect, with shield amount as value
+-- Health threshold until it gets fully absorbed
+local GUIDtoAbsorbHealSpells
+
+-- Class-specific callbacks
+local OnEnableClass = {}
+local OnScalingDecode = setmetatable({}, {__index = function(table, class) return table.DEFAULT end})
+
+-- Shortcut to the most important core functions
+local ApplySingularEffect
+local ApplyAreaEffect
+local CreateAreaTrigger
+local HitUnit
+local RemoveActiveEffect
+
+-- Constants
+local LOW_VALUE_TOLERANCE = 50
+local ZONE_MODIFIER = 1
+
+-- addon comm prefixes
+local COMM_UNITSTATS = "SpecializedAbsorbs_UnitStats"
+local COMM_SCALING = "SpecializedAbsorbs_Scaling"
+
+-- for players using AbsorbsMonitor-1.0
+local COMM_UNITSTATS_ALT = "Absorbs_UnitStats"
+local COMM_SCALING_ALT = "Absorbs_Scaling"
+
+----------------------
+-- Helper functions --
+----------------------
+
+local CommStatsCooldown = false
+local function ClearCommStatsCooldown()
+ CommStatsCooldown = false
+end
+
+local CommScalingCooldown = false
+local function ClearCommScalingCooldown()
+ CommScalingCooldown = false
+end
+
+local function DeepTableCopy(src)
+ local res = {}
+
+ for k, v in pairs(src) do
+ if type(k) == "table" then
+ k = DeepTableCopy(k)
+ end
+ if type(v) == "table" then
+ v = DeepTableCopy(v)
+ end
+ res[k] = v
+ end
+ setmetatable(res, getmetatable(src))
+ return res
+end
+
+local function NoOp() end
+
+local SortEffects
+do
+ local mage_fire_ward = {543, 8457, 8458, 10223, 10225, 27128, 43010}
+ local mage_frost_ward = {6143, 8461, 8462, 10177, 28609, 32796, 43012}
+ local mage_ice_barrier = {11426, 13031, 13032, 13033, 27134, 33405, 43038, 43039}
+ local warlock_shadow_ward = {6229, 11739, 11740, 28610, 47890, 47891}
+ local warlock_sacrifice = {7812, 19438, 19440, 19441, 19442, 19443, 27273, 47985, 47986}
+
+ function SortEffects(a, b)
+ -- use timestamp in case of the same id
+ if a[1] == b[1] then
+ if a[8] == nil then
+ return true
+ elseif b[8] == nil then
+ return false
+ else
+ return (a[8] < b[8])
+ end
+ end
+
+ -- Twin Val'kyr
+ if a[1] == 65686 then
+ return true
+ end
+ if b[1] == 65686 then
+ return false
+ end
+ if a[1] == 65684 then
+ return true
+ end
+ if b[1] == 65684 then
+ return false
+ end
+
+ -- Frost Ward
+ if tContains(mage_frost_ward, a[1]) then
+ return true
+ end
+ if tContains(mage_frost_ward, b[1]) then
+ return false
+ end
+
+ -- Fire Ward
+ if tContains(mage_fire_ward, a[1]) then
+ return true
+ end
+ if tContains(mage_fire_ward, b[1]) then
+ return false
+ end
+
+ -- Shadow Ward
+ if tContains(warlock_shadow_ward, a[1]) then
+ return true
+ end
+ if tContains(warlock_shadow_ward, b[1]) then
+ return false
+ end
+
+ -- Sacred Shield
+ if a[1] == 58597 then
+ return true
+ end
+ if b[1] == 58597 then
+ return false
+ end
+
+ -- Fel Blossom
+ if a[1] == 58597 then
+ return true
+ end
+ if b[1] == 58597 then
+ return false
+ end
+
+ -- Divine Aegis
+ if a[1] == 47753 then
+ return true
+ end
+ if b[1] == 47753 then
+ return false
+ end
+
+ -- Ice Barrier
+ if tContains(mage_ice_barrier, a[1]) then
+ return true
+ end
+ if tContains(mage_ice_barrier, b[1]) then
+ return false
+ end
+
+ -- Sacrifice
+ if tContains(warlock_sacrifice, a[1]) then
+ return true
+ end
+ if tContains(warlock_sacrifice, b[1]) then
+ return false
+ end
+
+ -- same priority
+ -- if a[2] == b[2] then
+ -- return (a[3] < b[3])
+ -- elseif a[2] and a[2] then
+ -- return (a[3] > b[3])
+ -- end
+
+ -- latest.
+ return (a[7] < b[7])
+ end
+end
+
+-- Tries to get a working unitId
+local function GetUnitId(guid, name)
+ if name and UnitGUID(name) then
+ return name
+ end
+ if UnitGUID("target") == guid then
+ return "target"
+ end
+ if UnitGUID("focus") == guid then
+ return "focus"
+ end
+
+ -- group
+ local prefix
+ if curChatChannel == "BATTLEGROUND" or curChatChannel == "RAID" then
+ prefix = "raid"
+ elseif curChatChannel == "PARTY" then
+ prefix = "party"
+ end
+
+ if prefix then
+ for i = 1, groupCount do
+ local unit = (i == 0) and "player" or prefix .. i
+ if UnitExists(unit) then
+ if UnitGUID(unit) == guid then
+ return unit
+ end
+ if UnitExists(unit .. "pet") and UnitGUID(unit .. "pet") == guid then
+ return unit .. "pet"
+ end
+ end
+ end
+ end
+ -- solo
+ if UnitGUID("player") == guid then
+ return "player"
+ end
+ if UnitExists("playerpet") and UnitGUID("playerpet") == guid then
+ return "playerpet"
+ end
+end
+
+-- Tries to get a working unitId and calls the given UnitXXX() function
+local function UnitDispatch(func, guid, name)
+ local result = func(name)
+ if not result then
+ local unit = GetUnitId(guid, name)
+ if unit then
+ result = func(unit)
+ end
+ end
+ return result
+end
+
+--------------------
+-- Core functions --
+--------------------
+
+function Core.Error(...)
+ if lib.NoErrors then return end
+ error(...)
+end
+
+function Core.Enable()
+ playerclass = select(2, UnitClass("player"))
+ playerid = UnitGUID("player")
+
+ Core.activeEffects = {bySpell = {}, byPriority = {}, Area = {}}
+
+ activeEffectsBySpell = Core.activeEffects.bySpell
+ activeEffectsByPriority = Core.activeEffects.byPriority
+ activeAreaEffects = Core.activeEffects.Area
+
+ Core.activeCharges = {}
+ activeCharges = Core.activeCharges
+
+ Effects = Core.Effects
+ AreaTriggers = Core.AreaTriggers
+
+ CombatTriggersOnHeal = Core.CombatTriggers.OnHeal
+ CombatTriggersOnHealCrit = Core.CombatTriggers.OnHealCrit
+ CombatTriggersOnAuraApplied = Core.CombatTriggers.OnAuraApplied
+ CombatTriggersOnAuraRemoved = Core.CombatTriggers.OnAuraRemoved
+
+ Core.UnitStatsTable = {[playerid] = {playerclass, 0, 0, 1.0}}
+ UnitStatsTable = Core.UnitStatsTable
+
+ Core.Scaling = {[-1] = {}, [playerid] = {}}
+ Scaling = Core.Scaling
+
+ playerScaling = Scaling[playerid]
+ privateScaling = Scaling[-1]
+
+ Core.GUIDtoAbsorbHealSpells = {}
+ GUIDtoAbsorbHealSpells = Core.GUIDtoAbsorbHealSpells
+
+ Core.RegisterEvent("ZONE_CHANGED_NEW_AREA")
+ Events.ZONE_CHANGED_NEW_AREA()
+
+ if playerclass == "DEATHKNIGHT" then
+ Core.RegisterEvent("UNIT_ATTACK_POWER")
+ elseif playerclass == "DRUID" then
+ Core.RegisterEvent("UNIT_ATTACK_POWER")
+ elseif playerclass == "MAGE" then
+ Core.RegisterEvent("PLAYER_DAMAGE_DONE_MODS")
+ elseif playerclass == "PALADIN" then
+ Core.RegisterEvent("PLAYER_DAMAGE_DONE_MODS")
+ elseif playerclass == "PRIEST" then
+ Core.RegisterEvent("PLAYER_DAMAGE_DONE_MODS")
+ elseif playerclass == "WARLOCK" then
+ Core.RegisterEvent("PLAYER_DAMAGE_DONE_MODS")
+ end
+
+ Events.STATS_CHANGED()
+
+ -- This has to happen before class init, else we get no initial scaling broadcast
+ if not Core.Silent then
+ Core.SetVerbose()
+ end
+
+ if OnEnableClass[playerclass] then
+ OnEnableClass[playerclass]()
+ end
+
+ if Events.PLAYER_LEVEL_UP then
+ Core.RegisterEvent("PLAYER_LEVEL_UP")
+ end
+
+ if Events.PLAYER_TALENT_UPDATE then
+ Core.RegisterEvent("PLAYER_TALENT_UPDATE")
+ end
+
+ if Events.GLYPH_UPDATED then
+ Core.RegisterEvent("GLYPH_UPDATED")
+ end
+
+ if Events.PLAYER_EQUIPMENT_CHANGED then
+ Core.RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
+ end
+
+ Core:ScheduleRepeatingTimer(Events.OnPeriodicBroadcast, 300)
+
+ Core:RegisterComm(COMM_UNITSTATS, Events.OnUnitStatsReceived)
+ Core:RegisterComm(COMM_UNITSTATS_ALT, Events.OnUnitStatsReceived)
+
+ Core:RegisterComm(COMM_SCALING, Events.OnScalingReceived)
+ Core:RegisterComm(COMM_SCALING_ALT, Events.OnScalingReceived)
+
+ if not lib.Passive then
+ Core:SetActive()
+ end
+
+ lib.Enabled = true
+end
+
+-- These function has to clear any memory this version of the library may
+-- have accumulated. It will be called in case this library version gets
+-- replaced by a new one
+function Core.Disable()
+ for guid, effects in pairs(activeEffectsBySpell) do
+ callbacks:Fire("UnitCleared", guid)
+ end
+
+ wipe(Core.activeEffects)
+ wipe(Core.activeCharges)
+
+ wipe(Core.Effects)
+ wipe(Core.CombatTriggers)
+ wipe(Core.AreaTriggers)
+
+ wipe(Core.UnitStatsTable)
+ wipe(Core.Scaling)
+
+ wipe(Core.GUIDtoAbsorbHealSpells)
+
+ wipe(Core.Events)
+ Core.Frame:UnregisterAllEvents()
+
+ Core:UnregisterAllComm()
+ Core:CancelAllTimers()
+
+ lib.Enabled = false
+
+ collectgarbage("collect")
+end
+
+function Core.RegisterEvent(name)
+ Core.Frame:RegisterEvent(name)
+end
+
+function Core.UnregisterEvent(name)
+ Core.Frame:UnregisterEvent(name)
+end
+
+local Debug = false
+function Core.Debug(str)
+ if not Debug then return end
+ Core.Print(str)
+end
+
+function Core.Print(str)
+ DEFAULT_CHAT_FRAME:AddMessage("|cff33ff99AbsMon|r: " .. str)
+end
+
+function Core.ApplySingularEffect(timestamp, srcGUID, srcName, dstGUID, dstName, spellid, spellschool)
+ local destEffects = activeEffectsBySpell[dstGUID]
+ local effectInfo = Effects[spellid]
+ local effectEntry
+
+ local value, quality = effectInfo[3](srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ if value == nil then return end
+
+ -- No entry yet for this unit
+ if not destEffects then
+ -- Not this specific effect yet
+ effectEntry = {spellid, effectInfo[1], value, value, quality, 0, timestamp}
+ destEffects = {[-1] = 0, [-2] = 1.0, [spellid] = effectEntry}
+ activeEffectsBySpell[dstGUID] = destEffects
+ activeEffectsByPriority[dstGUID] = {effectEntry}
+ callbacks:Fire("EffectApplied", srcGUID, srcName, dstGUID, dstName, spellid, spellschool, value, quality, effectInfo[2])
+ elseif not destEffects[spellid] then
+ -- Effect exists already
+ effectEntry = {spellid, effectInfo[1], value, value, quality, 0, timestamp}
+ destEffects[spellid] = effectEntry
+ tinsert(activeEffectsByPriority[dstGUID], effectEntry)
+ tsort(activeEffectsByPriority[dstGUID], SortEffects)
+ callbacks:Fire("EffectApplied", srcGUID, srcName, dstGUID, dstName, spellid, spellschool, value, quality, effectInfo[2])
+ else
+ effectEntry = destEffects[spellid]
+ local prevAmount = effectEntry[3]
+
+ effectEntry[3] = value
+ effectEntry[4] = value
+ effectEntry[5] = quality
+ effectEntry[7] = timestamp
+
+ tsort(activeEffectsByPriority[dstGUID], SortEffects)
+ callbacks:Fire("EffectUpdated", dstGUID, spellid, value, quality, effectInfo[2])
+
+ -- Adjust value in case this is a visible absorb to get the difference
+ value = value - prevAmount
+
+ -- Cancel the exting duration timeout timer
+ Core:CancelTimer(effectEntry[6], true)
+ end
+
+ if quality < destEffects[-2] then
+ destEffects[-2] = quality
+ end
+
+ if effectInfo[1] < 2 then
+ destEffects[-1] = destEffects[-1] + value
+ callbacks:Fire("UnitUpdated", dstGUID, destEffects[-1], destEffects[-2])
+ end
+
+ if effectInfo[2] then
+ -- We add a 5s grace period for latency and all kind of stuff
+ -- This duration timeout should only be needed if the unit moved out of combat log
+ -- reporting range anyway
+ effectEntry[6] = Core:ScheduleTimer(Events.OnSingularTimeout, effectInfo[2] + 5, {dstGUID, spellid})
+ else
+ effectEntry[6] = Core:ScheduleRepeatingTimer(Events.OnSingularActivityCheck, 8, {dstGUID, spellid})
+ end
+end
+
+function Core.ApplyAreaEffect(timestamp, triggerGUID, triggerName, dstGUID, dstName, spellid, spellschool)
+ if not activeAreaEffects[triggerGUID] then
+ return ApplySingularEffect(timestamp, triggerGUID, triggerName, dstGUID, dstName, 0, spellschool)
+ end
+
+ local effectEntry = activeAreaEffects[triggerGUID]
+ local destEffects = activeEffectsBySpell[dstGUID]
+
+ if not destEffects then
+ -- At the moment it is impossible for such an effect to be refreshed
+ -- Since it is created by a summoned unit radiating it, it either
+ -- gets removed/reapplied or removed/applied by a different unit.
+ -- Quality of 1.1 to enforce message
+ destEffects = {[-1] = 0, [-2] = 1.1}
+
+ activeEffectsBySpell[dstGUID] = destEffects
+ activeEffectsByPriority[dstGUID] = {}
+ elseif destEffects[spellid] then
+ Core.Error("Called ApplyAreaEffect on refreshed aura")
+ return
+ end
+
+ destEffects[spellid] = effectEntry
+ effectEntry[9] = effectEntry[9] + 1 -- increase refcount
+
+ -- While we keep the trigger itself as a normal afflicted unit to keep
+ -- track when the area effect breaks, we do not broadcast it nor have to
+ -- keep a sorted priority list
+ if triggerGUID ~= dstGUID then
+ tinsert(activeEffectsByPriority[dstGUID], effectEntry)
+ tsort(activeEffectsByPriority[dstGUID], SortEffects)
+
+ -- Note that we CANNOT use nil as an amount, since external addons can rely on this value being non-nil
+ -- for sorting. We're using -1 here that usually represents infinite values
+ callbacks:Fire("EffectApplied", triggerGUID, triggerName, dstGUID, dstName, spellid, spellschool, -1, effectEntry[5], nil)
+
+ -- Update quality if needed
+ if effectEntry[5] < destEffects[-2] then
+ destEffects[-2] = effectEntry[5]
+ callbacks:Fire("UnitUpdated", dstGUID, destEffects[-1], destEffects[-2])
+ end
+ end
+end
+
+function Core.CreateAreaTrigger(timestamp, srcGUID, srcName, triggerGUID, triggerName, spellid, spellschool)
+ if activeAreaEffects[triggerGUID] then
+ Core.Error("Trying to create new area trigger on existing one, triggerGUID: " .. triggerGUID .. ", existing spellid: " .. activeAreaEffects[triggerGUID][1])
+ return
+ end
+
+ local effectInfo = Effects[spellid]
+ local value, quality = effectInfo[3](srcGUID, srcName, triggerGUID, triggerName, spellid, nil)
+ if value == nil then return end
+
+ local effectEntry = {spellid, -1 * effectInfo[1], value, value, quality, 0, timestamp, triggerGUID, 0}
+ effectEntry[6] = Core:ScheduleTimer(Events.OnAreaTimeout, effectInfo[2] + 5, effectEntry)
+ activeAreaEffects[triggerGUID] = effectEntry
+ callbacks:Fire("AreaCreated", srcGUID, srcName, triggerGUID, spellid, spellschool, value, quality)
+end
+
+local Fire = callbacks.Fire
+function Core.HitUnit(guid, absorbedTotal, overkill, spellschool)
+ local guidEffects = activeEffectsBySpell[guid]
+ if not guidEffects then return end
+
+ local absorbedRemaining = absorbedTotal
+
+ local absorbed = 0
+ local keepEffect, visibleAbsorb = false, false
+ local i = 1
+ local effectEntry
+
+ -- This loop lasts as long as there is still an absorb value that
+ -- no effect could account for, but it will break once the list of
+ -- available effects got used completely.
+ while absorbedRemaining > 0 do
+ effectEntry = activeEffectsByPriority[guid][i]
+
+ -- Sometimes there can be holes in this list since we don't re-sort
+ -- after removing an effect
+ if effectEntry == nil then break end
+
+ -- Only absorb effects with exactly zero are ignored, negative ones
+ -- are treated as infinite (that is, no addon should display their value)
+ if effectEntry[3] ~= 0 then
+ -- Hit the abosrb effect
+ absorbed, keepEffect = Effects[effectEntry[1]][4](effectEntry, absorbedRemaining, overkill, spellschool)
+
+ if absorbed > 0 then
+ -- Reduce the value of this effect and the remaining absorb value
+ -- to be accounted for
+ effectEntry[3] = effectEntry[3] - absorbed
+ absorbedRemaining = absorbedRemaining - absorbed
+
+ if effectEntry[8] then
+ Fire(callbacks, "AreaUpdated", effectEntry[8], effectEntry[3])
+ else
+ -- If it should be visible (priority < 2), correct the total value
+ if effectEntry[2] < 2 then
+ guidEffects[-1] = guidEffects[-1] - absorbed
+ -- Shows us that at least one visible effect got hit
+ visibleAbsorb = true
+ end
+
+ Fire(callbacks, "EffectUpdated", guid, effectEntry[1], effectEntry[3])
+ end
+ end
+
+ -- If the hit-function told us to remove the effect, do so
+ -- Note that only RemoveActiveEffect is allowed to remove it from the
+ -- list, we just set the value to zero, so it gets ignored on any hit.
+ -- This is do not come into any desync issues with the events, and to
+ -- keep the proper clean-up code in one place
+ if not keepEffect then
+ effectEntry[3] = 0
+ end
+ end
+
+ i = i + 1
+ end
+
+ -- There are two possibilities when things are going wrong
+ --
+ -- a) we guessed an absorb value too high, in that case it will
+ -- automatically be corrected when it breaks
+ --
+ -- b) we guessed an absorb value too low, so we end up with an
+ -- amount to absorb when all effects seem to be gone
+ -- (absorbedRemaining > 0)
+ -- Note that we cannot rely on SPELL_AURA_REMOVED to check this,
+ -- since it may happen completely out of order, but it will
+ -- clear this unit soon or did so already.
+ -- we reduce the quality to zero, since any absorb now happening
+ -- cannot be accounted for.
+ -- Since we may have rounding errors from scanning the spellbook
+ -- and calculating the value thereafter (does Blizzard round on
+ -- EVERY step?!?), we accept a small threshold
+ if absorbedRemaining > LOW_VALUE_TOLERANCE then
+ guidEffects[-1] = guidEffects[-1] - absorbedRemaining
+ guidEffects[-2] = 0.0
+ visibleAbsorb = true
+ end
+
+ if visibleAbsorb then
+ Fire(Core, "UnitUpdated", guid, guidEffects[-1], guidEffects[-2])
+ Fire(Core, "UnitAbsorbed", guid, absorbedTotal)
+ end
+end
+
+-- Note that this method should NOT be called on non-existing effects or units
+-- There are no exist checks within it.
+function Core.RemoveActiveEffect(guid, spellid)
+ local guidEffects = activeEffectsBySpell and activeEffectsBySpell[guid]
+ if not guidEffects then return end
+
+ local effectEntry = guidEffects[spellid]
+
+ -- This is a shared effect with a triggerGUID
+ if effectEntry[8] then
+ effectEntry[9] = effectEntry[9] - 1
+
+ if guid ~= effectEntry[8] then
+ callbacks:Fire("EffectRemoved", guid, spellid)
+ end
+
+ if effectEntry[9] == 0 then
+ if effectEntry[6] then
+ Core:CancelTimer(effectEntry[6], true)
+ end
+ callbacks:Fire("AreaCleared", effectEntry[8])
+ end
+ else
+ if effectEntry[2] < 2 then
+ guidEffects[-1] = guidEffects[-1] - effectEntry[3]
+ callbacks:Fire("UnitUpdated", guid, guidEffects[-1], guidEffects[-2])
+ end
+
+ Core:CancelTimer(effectEntry[6], true)
+ callbacks:Fire("EffectRemoved", guid, spellid)
+ end
+
+ if #(activeEffectsByPriority[guid]) == 1 then
+ activeEffectsBySpell[guid] = nil
+ activeEffectsByPriority[guid] = nil
+ callbacks:Fire("UnitCleared", guid)
+ else
+ guidEffects[spellid] = nil
+ for k, v in pairs(activeEffectsByPriority[guid]) do
+ if v[1] == spellid then
+ tremove(activeEffectsByPriority[guid], k)
+ break
+ end
+ end
+ tsort(activeEffectsByPriority[guid], SortEffects)
+ end
+end
+
+-- This is uses a _very_ simple queue implementation with tinsert and tremove.
+-- It will not scale very well for large values, but we're talking of a maximum
+-- of ~3 entries per GUID at any given time. A proper implementation
+-- with linked list would probably not be any faster.
+function Core.PushCharge(guid, spellid, amount, lifetime)
+ local guidCharges = activeCharges[guid]
+
+ if not guidCharges then
+ activeCharges[guid] = {[spellid] = {lastCombatLogEvent + lifetime, amount}}
+ elseif not guidCharges[spellid] then
+ guidCharges[spellid] = {lastCombatLogEvent + lifetime, amount}
+ else
+ tinsert(guidCharges[spellid], lastCombatLogEvent + lifetime)
+ tinsert(guidCharges[spellid], amount)
+ end
+end
+
+function Core.PopCharge(guid, spellid)
+ local guidCharges = activeCharges[guid]
+
+ if guidCharges then
+ local queue = guidCharges[spellid]
+
+ -- For some weird reason, it will fail (true even on empty array)
+ -- if checked for queue[1] ?!?
+ if queue and queue[2] then
+ local chargeAmount
+ local chargeExpire
+
+ -- This loop will not be able to run infinitely
+ while true do
+ -- In this order we might save one table reshuffle
+ chargeAmount = tremove(queue, 2)
+ if not chargeAmount then return 0 end
+
+ chargeExpire = tremove(queue, 1)
+ if chargeExpire > lastCombatLogEvent then
+ return chargeAmount
+ end
+ end
+ end
+ end
+ return 0
+end
+
+function Core.AddCombatTrigger(target, event, func)
+ local eventTriggers = Core.CombatTriggers[event]
+ local oldTrigger = eventTriggers[target]
+
+ if not oldTrigger then
+ eventTriggers[target] = func
+ else
+ local listIndex = target .. "_list"
+ local funcList = eventTriggers[listIndex]
+
+ -- There is already a list of callbacks, so we just add this one
+ if funcList then
+ -- We used a direct call so far, create a list and set up a handler
+ for k, v in pairs(funcList) do
+ if v == func then
+ return
+ end
+ end
+ tinsert(funcList, func)
+ else
+ if oldTrigger == func then
+ return
+ end
+
+ funcList = {oldTrigger, func}
+ eventTriggers[listIndex] = funcList
+
+ local handler = function(...)
+ for k, v in pairs(funcList) do
+ v(...)
+ end
+ end
+
+ eventTriggers[target] = handler
+ end
+ end
+end
+
+function Core.RemoveCombatTrigger(target, event, func)
+ local eventTriggers = Core.CombatTriggers[event]
+
+ local listIndex = target .. "_list"
+ local funcList = eventTriggers[listIndex]
+
+ -- We have a list of callbacks, reduce if possible
+ if (funcList) then
+ -- It was a direct call anyway
+ if (#funcList == 2) then
+ eventTriggers[target] = (funcList[1] == func) and funcList[2] or funcList[1]
+ eventTriggers[listIndex] = nil
+ else
+ -- ATTENTION: We have to keep the table in place
+ -- because the handler references this table
+ local old_funcList = DeepTableCopy(funcList)
+ wipe(funcList)
+
+ for k, v in pairs(old_funcList) do
+ if v ~= func then
+ tinsert(funcList, v)
+ end
+ end
+ end
+ else
+ eventTriggers[target] = nil
+ end
+end
+
+local lastAP, lastSP = 0, 0
+function Core.SendUnitStats()
+ if curChatChannel then
+ local curAP, curSP = UnitStatsTable[playerid][2], UnitStatsTable[playerid][3]
+ if (curAP ~= lastAP) or (curSP ~= lastSP) then
+ Core:SendCommMessage(COMM_UNITSTATS, Core:Serialize(playerid, playerclass, curAP, curSP), curChatChannel)
+ Core:SendCommMessage(COMM_UNITSTATS_ALT, Core:Serialize(playerid, playerclass, curAP, curSP), curChatChannel)
+
+ lastAP, lastSP = curAP, curSP
+ CommStatsCooldown = true
+ Core:ScheduleTimer(ClearCommStatsCooldown, 15)
+ end
+ end
+end
+
+function Core.SendScaling()
+ if curChatChannel then
+ Core:SendCommMessage(COMM_SCALING, Core:Serialize(playerid, playerclass, Events.OnScalingEncode()), curChatChannel)
+ Core:SendCommMessage(COMM_SCALING_ALT, Core:Serialize(playerid, playerclass, Events.OnScalingEncode()), curChatChannel)
+
+ CommScalingCooldown = true
+ Core:ScheduleTimer(ClearCommStatsCooldown, 30)
+ end
+end
+
+-- An extension of AceTimer to schedule a one-shot timer that will not
+-- be scheduled twice if scheduled again before it's fired.
+local activeTimers = {}
+function Core:ScheduleUniqueTimer(id, callback, delay, arg)
+ if not activeTimers[id] then
+ self:ScheduleTimer(function()
+ callback(arg)
+ activeTimers[id] = nil
+ end, delay)
+ activeTimers[id] = 1
+ end
+end
+
+function Core.SetActive()
+ Core.RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
+ lib.Passive = false
+end
+
+function Core.SetPassive()
+ Core.UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
+ lib.Passive = true
+end
+
+function Core.SetVerbose()
+ Core.RegisterEvent("PARTY_MEMBERS_CHANGED")
+ Core.RegisterEvent("RAID_ROSTER_UPDATE")
+ lib.Silent = false
+ Events.GROUPING_CHANGED()
+end
+
+function Core.SetSilent()
+ Core.UnregisterEvent("PARTY_MEMBERS_CHANGED")
+ Core.UnregisterEvent("RAID_ROSTER_UPDATE")
+ lib.Silent = true
+ curChatChannel = nil
+end
+
+ApplySingularEffect = Core.ApplySingularEffect
+ApplyAreaEffect = Core.ApplyAreaEffect
+CreateAreaTrigger = Core.CreateAreaTrigger
+HitUnit = Core.HitUnit
+RemoveActiveEffect = Core.RemoveActiveEffect
+
+---------------------
+-- Event functions --
+---------------------
+
+function Events.PLAYER_ENTERING_WORLD()
+ if not GetTalentInfo(1, 1) then
+ Core.RegisterEvent("PLAYER_ALIVE")
+ else
+ Core.Available = true
+ Core.Enable()
+ Core:ScheduleTimer(Events.STATS_CHANGED, 5)
+ end
+ Core.UnregisterEvent("PLAYER_ENTERING_WORLD")
+end
+
+function Events.PLAYER_ALIVE()
+ Core.Available = true
+ Core.Enable()
+ Core.UnregisterEvent("PLAYER_ALIVE")
+end
+
+local BITMASK_GROUP = COMBATLOG_OBJECT_AFFILIATION_MINE + COMBATLOG_OBJECT_AFFILIATION_PARTY + COMBATLOG_OBJECT_AFFILIATION_RAID
+local function CheckFlags(srcFlags, dstFlags)
+ return (srcFlags and bit.band(srcFlags, BITMASK_GROUP) ~= 0) or (dstFlags and bit.band(dstFlags, BITMASK_GROUP) ~= 0)
+end
+
+------------------
+-- Heal Absorbs --
+------------------
+
+local absorbHealSpells = {
+ [66237] = 30000, -- Incinerate Flesh (10 Normal)
+ [67049] = 60000, -- Incinerate Flesh (25 Normal)
+ [67050] = 40000, -- Incinerate Flesh (10 Heroic)
+ [67051] = 85000, -- Incinerate Flesh (25 Heroic)
+ [66236] = 30000, -- Incinerate Flesh
+ [70659] = 9000, -- Necrotic Strike (10 Normal)
+ [71951] = 15000, -- Necrotic Strike
+ [72490] = 14000, -- Necrotic Strike (25 Normal)
+ [72491] = 14000, -- Necrotic Strike (10 Heroic)
+ [72492] = 20000, -- Necrotic Strike (25 Heroic)
+}
+
+local environmentSchools = {
+ FALLING = SCHOOL_MASK_PHYSICAL,
+ DROWNING = SCHOOL_MASK_PHYSICAL,
+ FATIGUE = SCHOOL_MASK_PHYSICAL,
+ FIRE = SCHOOL_MASK_FIRE,
+ LAVA = SCHOOL_MASK_FIRE,
+ SLIME = SCHOOL_MASK_NATURE
+}
+
+function Events.COMBAT_LOG_EVENT_UNFILTERED(timestamp, etype, srcGUID, srcName, srcFlags, dstGUID, dstName, dstFlags, ...)
+ if lib.CheckFlags and not CheckFlags(srcFlags, dstFlags) then return end
+
+ lastCombatLogEvent = timestamp
+ if etype == "SWING_DAMAGE" then
+ local amount, _, _, _, _, absorbed = ...
+ if not absorbed then return end
+ HitUnit(dstGUID, absorbed, amount, SCHOOL_MASK_PHYSICAL)
+ elseif etype == "RANGE_DAMAGE" or etype == "SPELL_DAMAGE" or etype == "SPELL_PERIODIC_DAMAGE" then
+ local _, _, spellschool, amount, _, _, _, _, absorbed = ...
+ if not absorbed then return end
+ HitUnit(dstGUID, absorbed, amount, spellschool)
+ elseif etype == "ENVIRONMENTAL_DAMAGE" then
+ local envtype, amount, _, _, _, _, absorbed = ...
+ if not absorbed then return end
+ HitUnit(dstGUID, absorbed, amount, environmentSchools[envtype] or SCHOOL_MASK_PHYSICAL)
+ elseif etype == "SWING_MISSED" then
+ local misstype, amount = ...
+ if misstype ~= "ABSORB" then return end
+ HitUnit(dstGUID, amount, 0, SCHOOL_MASK_PHYSICAL)
+ elseif etype == "RANGE_MISSED" or etype == "SPELL_MISSED" or etype == "SPELL_PERIODIC_MISSED" then
+ local _, _, spellschool, misstype, amount = ...
+ if misstype ~= "ABSORB" then return end
+ HitUnit(dstGUID, amount, 0, spellschool)
+ elseif etype == "SPELL_HEAL" or etype == "SPELL_PERIODIC_HEAL" then
+ local spellid, _, _, amount, overheal, absorbed, critical = ...
+ if CombatTriggersOnHeal[srcGUID] then
+ CombatTriggersOnHeal[srcGUID](srcGUID, srcName, dstGUID, dstName, spellid, amount, overheal)
+ end
+ if absorbed and Core.GUIDtoAbsorbHealSpells[dstGUID] then
+ Core.GUIDtoAbsorbHealSpells[dstGUID] = Core.GUIDtoAbsorbHealSpells[dstGUID] - absorbed
+ end
+ if critical and CombatTriggersOnHealCrit[srcGUID] then
+ CombatTriggersOnHealCrit[srcGUID](srcGUID, srcName, dstGUID, dstName, spellid, amount, overheal)
+ end
+ elseif etype == "SPELL_AURA_APPLIED" or etype == "SPELL_AURA_REFRESH" then
+ local spellid, _, spellschool = ...
+ if Effects[spellid] then
+ if Effects[spellid][1] > 0 then
+ ApplySingularEffect(timestamp, srcGUID, srcName, dstGUID, dstName, spellid, spellschool)
+ else
+ ApplyAreaEffect(timestamp, srcGUID, srcName, dstGUID, dstName, spellid, spellschool)
+ end
+ end
+ if CombatTriggersOnAuraApplied[spellid] then
+ CombatTriggersOnAuraApplied[spellid](srcGUID, srcName, dstGUID, dstName, spellid)
+ end
+ if absorbHealSpells[spellid] then
+ Core.GUIDtoAbsorbHealSpells[dstGUID] = absorbHealSpells[spellid]
+ end
+ elseif etype == "SPELL_AURA_REMOVED" then
+ local spellid, _, spellschool = ...
+ if Effects[spellid] then
+ if activeEffectsBySpell[dstGUID] and activeEffectsBySpell[dstGUID][spellid] then
+ RemoveActiveEffect(dstGUID, spellid)
+ end
+ end
+ if CombatTriggersOnAuraRemoved[spellid] then
+ CombatTriggersOnAuraRemoved[spellid](srcGUID, srcName, dstGUID, dstName, spellid, spellschool)
+ end
+ if absorbHealSpells[spellid] then
+ Core.GUIDtoAbsorbHealSpells[dstGUID] = nil
+ end
+ elseif etype == "SPELL_SUMMON" then
+ local spellid, _, spellschool = ...
+ if AreaTriggers[spellid] then
+ CreateAreaTrigger(timestamp, srcGUID, srcName, dstGUID, dstName, AreaTriggers[spellid], spellschool)
+ end
+ end
+end
+
+function Events.GROUPING_CHANGED()
+ -- Note that the order here is VERY important
+ if UnitInBattleground("player") then
+ curChatChannel = "BATTLEGROUND"
+ groupCount = GetNumRaidMembers()
+ elseif GetNumRaidMembers() > 0 then
+ curChatChannel = "RAID"
+ groupCount = GetNumRaidMembers()
+ elseif GetNumPartyMembers() > 0 then
+ curChatChannel = "PARTY"
+ groupCount = GetNumPartyMembers()
+ else
+ curChatChannel = nil
+ groupCount = 0
+ end
+
+ Core:ScheduleTimer(Events.STATS_CHANGED, 5)
+ Events.STATS_CHANGED()
+end
+
+function Events.ZONE_CHANGED_NEW_AREA()
+ if UnitInBattleground("player") then
+ ZONE_MODIFIER = 1.17
+ elseif IsActiveBattlefieldArena() then
+ ZONE_MODIFIER = 0.9
+ elseif GetCurrentMapAreaID() == 502 then
+ ZONE_MODIFIER = 1.17
+ elseif GetCurrentMapAreaID() == 605 then
+ ZONE_MODIFIER = (UnitBuff("player", GetSpellInfo(73822)) or UnitBuff("player", GetSpellInfo(73828))) and 1.3 or 1
+ else
+ ZONE_MODIFIER = 1
+ end
+
+ Core:ScheduleTimer(Events.STATS_CHANGED, 5)
+end
+
+function Events.STATS_CHANGED()
+ local baseAP, plusAP, minusAP = UnitAttackPower("player")
+
+ UnitStatsTable[playerid][2] = baseAP + plusAP - minusAP
+ -- TODO: What about spell power ~= healing spell power?
+ UnitStatsTable[playerid][3] = GetSpellBonusHealing()
+ if curChatChannel then
+ Core:ScheduleUniqueTimer("comm_stats", Core.SendUnitStats, CommStatsCooldown and 15 or 5)
+ Core:ScheduleUniqueTimer("comm_scaling", Core.SendScaling, CommScalingCooldown and 30 or 5)
+ end
+end
+
+function Events.OnUnitStatsReceived(prefix, text, distribution, target)
+ if not text then return end
+
+ local success, guid, class, ap, sp = Core:Deserialize(text)
+ if not (success and guid and class and ap and sp) then return end
+ if guid == playerid then return end
+
+ if not UnitStatsTable[guid] then
+ UnitStatsTable[guid] = {class, ap, sp, 1.0}
+ else
+ UnitStatsTable[guid][2] = ap
+ UnitStatsTable[guid][3] = sp
+ UnitStatsTable[guid][4] = 1.0
+ end
+end
+
+function Events.OnScalingEncode()
+ return Scaling[playerid]
+end
+
+function OnScalingDecode.DEFAULT(guid, guidScaling)
+ Scaling[guid] = guidScaling
+end
+
+function Events.OnScalingReceived(prefix, text, distribution, target)
+ if not text then return end
+
+ local success, guid, class, inScaling = Core:Deserialize(text)
+ if not (success and guid and class and inScaling) then return end
+ if guid == playerid then return end
+ (OnScalingDecode[class])(guid, inScaling)
+end
+
+function Events.OnPeriodicBroadcast()
+ if not curChatChannel then return end
+
+ local playerStats = UnitStatsTable[playerid]
+ local playerScaling = Scaling[playerid]
+
+ if not CommStatsCooldown then
+ Core:ScheduleUniqueTimer("comm_stats", Core.SendUnitStats, 5)
+ end
+
+ if not CommScalingCooldown then
+ Core:ScheduleUniqueTimer("comm_scaling", Core.SendScaling, 5)
+ end
+end
+
+function Events.OnSingularTimeout(args)
+ local guid, spellid = args[1], args[2]
+ if activeEffectsBySpell[guid] and activeEffectsBySpell[guid][spellid] then
+ RemoveActiveEffect(guid, spellid)
+ end
+end
+
+function Events.OnSingularActivityCheck(args)
+ local guid, spellid = args[1], args[2]
+
+ if activeEffectsBySpell[guid] and activeEffectsBySpell[guid][spellid] then
+ local name = select(6, GetPlayerInfoByGUID(guid))
+
+ -- We cannot track whether it's still on, remove it
+ if not name then
+ Core:CancelTimer(activeEffectsBySpell[guid][spellid][6])
+ RemoveActiveEffect(guid, spellid)
+ end
+
+ local stillActive = false
+
+ for i = 1, 40 do
+ local buffId = select(11, UnitBuff(name, i))
+ if not buffId then break end
+ if buffId == spellid then
+ stillActive = true
+ break
+ end
+ end
+
+ if not stillActive then
+ Core:CancelTimer(activeEffectsBySpell[guid][spellid][6])
+ RemoveActiveEffect(guid, spellid)
+ end
+ else
+ -- Make sure to remove the timer
+ Core:CancelTimer(activeEffectsBySpell[guid][spellid][6])
+ end
+end
+
+-- This should be called in even less cases than the normal timeout
+function Events.OnAreaTimeout(areaEntry)
+ -- Disable the timer handle entry already
+ areaEntry[6] = nil
+
+ for guid, guidEffects in pairs(activeEffectsBySpell) do
+ if areaEntry[9] == 0 then return end
+ for spellid, effectEntry in pairs(guidEffects) do
+ if effectEntry == areaEntry then
+ RemoveActiveEffect(guid, spellid)
+ end
+ end
+ end
+
+ -- We're only here if we didn't reduce the refcount to zero
+ Core.Error("Positive refcount " .. areaEntry[9] .. " remained for area effect " .. areaEntry[1] .. " by trigger " .. areaEntry[8])
+end
+
+-- Map client events to our callbacks
+Events.PARTY_MEMBERS_CHANGED = Events.GROUPING_CHANGED
+Events.RAID_ROSTER_UPDATE = Events.GROUPING_CHANGED
+Events.PLAYER_DAMAGE_DONE_MODS = Events.STATS_CHANGED
+Events.UNIT_ATTACK_POWER = Events.STATS_CHANGED
+
+----------------------
+-- Public functions --
+----------------------
+
+function lib.RegisterEffectCallbacks(self, funcApplied, funcUpdated, funcRemoved)
+ lib.RegisterCallback(self, "EffectApplied", funcApplied)
+ lib.RegisterCallback(self, "EffectUpdated", funcUpdated or funcApplied)
+ lib.RegisterCallback(self, "EffectRemoved", funcRemoved or funcApplied)
+end
+
+function lib.RegisterUnitCallbacks(self, funcUpdated, funcCleared)
+ lib.RegisterCallback(self, "UnitUpdated", funcUpdated)
+ lib.RegisterCallback(self, "UnitCleared", funcCleared or funcUpdated)
+end
+
+function lib.RegisterAreaCallbacks(self, funcCreated, funcUpdated, funcCleared)
+ lib.RegisterCallback(self, "AreaCreated", funcCreated)
+ lib.RegisterCallback(self, "AreaUpdated", funcUpdated or funcCreated)
+ lib.RegisterCallback(self, "AreaCleared", funcCleared or funcCreated)
+end
+
+function lib.GetLowValueTolerance()
+ return LOW_VALUE_TOLERANE
+end
+
+function lib.SetLowValueTolerance(value)
+ LOW_VALUE_TOLERANCE = tonumber(value)
+end
+
+function lib.PrintProfiling()
+ if GetCVar("scriptProfile") ~= "1" then
+ Core.Print("CPU profiling disabled")
+ return
+ end
+
+ UpdateAddOnCPUUsage()
+ UpdateAddOnMemoryUsage()
+
+ Core.Print("Mem: " .. format("%.3f", GetAddOnMemoryUsage("SpecializedAbsorbs")) .. " kB")
+ Core.Print("Time: " .. format("%.3f", GetAddOnCPUUsage("SpecializedAbsorbs")) .. " ms")
+ Core.Print("--- critical code paths (all times in ms) ---")
+
+ local funcTable
+
+ funcTable = {
+ ["ApplySingularEffect"] = ApplySingularEffect,
+ ["HitUnit"] = HitUnit,
+ ["RemoveActiveEffect"] = RemoveActiveEffect,
+ ["OnCombatLogEvent"] = COMBAT_LOG_EVENT_UNFILTERED,
+ ["SortEffects"] = SortEffects
+ }
+
+ local v_type
+ local time_self, time_combined, count
+
+ for k, v in pairs(funcTable) do
+ v_type = type(v)
+
+ if v_type == "function" then
+ time_self, count = GetFunctionCPUUsage(v, false)
+ time_combined = GetFunctionCPUUsage(v, true)
+ Core.Print(k .. " (#" .. count .. "): " .. format("%.4f", time_self) .. " / " .. format("%.4f", time_combined))
+ end
+ end
+end
+
+function lib.UnitTotal(guid)
+ local total = 0
+ if activeEffectsBySpell and activeEffectsBySpell[guid] then
+ total = activeEffectsBySpell[guid][-1]
+ end
+ return total
+end
+
+function lib.UnitEffect(guid, spellid)
+ local guidEffects = activeEffectsBySpell and activeEffectsBySpell[guid]
+ if guidEffects and guidEffects[spellid] then
+ return guidEffects[spellid][3]
+ end
+ return 0
+end
+
+function lib.UnitStats(guid, missingQuality)
+ local guidStats = UnitStatsTable and UnitStatsTable[guid]
+ if guidStats then
+ return guidStats[2], guidStats[3], guidStats[4]
+ end
+ return 0, 0, missingQuality
+end
+
+function lib.UnitScaling(guid, defaultScaling, defaultQuality)
+ local guidScaling = Scaling and Scaling[guid]
+ if guidScaling then
+ return guidScaling, 1.0
+ end
+ return defaultScaling, defaultQuality
+end
+
+-- Optimized method to save one function call on creation, since a lot of spells
+-- actually require stats and scaling
+function lib.UnitStatsAndScaling(guid, missingQuality, defaultScaling, defaultQuality)
+ local guidStats = UnitStatsTable and UnitStatsTable[guid]
+ local guidScaling = Scaling and Scaling[guid]
+ if guidStats then
+ if guidScaling then
+ return guidStats[2], guidStats[3], guidStats[4], guidScaling, 1.0
+ end
+ return guidStats[2], guidStats[3], guidStats[4], defaultScaling, defaultQuality
+ else
+ if guidScaling then
+ return 0, 0, missingQuality, guidScaling, 1.0
+ end
+ return 0, 0, missingQuality, defaultScaling, defaultQuality
+ end
+end
+
+function lib.UnitEffectsMap(guid)
+ return activeEffectsBySpell and activeEffectsBySpell[guid]
+end
+
+function lib.UnitEffectsList(guid)
+ return activeEffectsByPriority and activeEffectsByPriority[guid]
+end
+
+function lib.UnitActiveEffect(guid)
+ local guidEffects = lib.UnitEffectsList(guid)
+ return guidEffects and guidEffects[1]
+end
+
+function lib.ScheduleScalingBroadcast()
+ if curChatChannel then
+ Core:ScheduleUniqueTimer("comm_scaling", Core.SendScaling, CommScalingCooldown and 30 or 5)
+ end
+end
+
+function lib.UnitTotalHealAbsorbs(guid)
+ if GUIDtoAbsorbHealSpells then
+ local guidHealAbsorbs = guid and GUIDtoAbsorbHealSpells[guid]
+ return guidHealAbsorbs
+ end
+end
+
+------------------------------
+-- Generic Effect functions --
+------------------------------
+
+local PushCharge = Core.PushCharge
+local PopCharge = Core.PopCharge
+local UnitStats = lib.UnitStats
+local UnitScaling = lib.UnitScaling
+local UnitStatsAndScaling = lib.UnitStatsAndScaling
+
+--- Generic Create function (only for documentary purposes)
+-- @param srcGUID guid of the originating unit
+-- @param srcName name of the originating unit
+-- @param dstGUID guid of the affected unit
+-- @param dstName name of the affected unit
+-- @param spellid the spellid of this absorb effect
+-- @param destEffects activeEffectsBySpell[dstGUID]
+-- @return total value of this absorb effect
+-- @return quality (that is accuracy) of this value
+local function generic_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+end
+
+-- Generic Create function for constant effects pulled from a table
+-- Expects the table at effect[5] indexed by spellid with base values
+local function generic_ConstantByTable_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ return Effects[spellid][5][spellid], 1.0
+end
+
+-- Generic Create function for effects simply scaling with spellpower and a fixed
+-- coefficient.
+-- Expects at effect[5] a table indexed by spellid with the base values and at
+-- effect[6] the spellpower coefficient
+local function generic_SpellScalingByTable_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local effectInfo = Effects[spellid]
+ local _, sp, quality = UnitStats(srcGUID, 0.1)
+ return floor(effectInfo[5][spellid] + (sp * effectInfo[6])), quality
+end
+
+--- Generic Hit function suitable for most absorb effects
+-- Note that this function is only responsible for determining the amount this
+-- particular absorb effect will take, NOT to handle its consequences like updating
+-- the data structures
+-- @param effectEntry activeEffectsBySpell[guid][spellid]
+-- @param absorbedRemaining absorb value left to be accounted for on this unit
+-- @param overkill amount of damage done on top of the absorb
+-- @param spellschool spell school for this hit
+-- @return absorb value this effect can account fors
+-- @return whether this absorb was broken by this hit
+local function generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if absorbedRemaining > effectEntry[3] then
+ -- dirty but efficient
+ absorbedRemaining = effectEntry[3]
+ overkill = 1
+ end
+ return absorbedRemaining, (overkill == 0)
+end
+
+local function Physical_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= SCHOOL_MASK_PHYSICAL then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+local function Holy_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= SCHOOL_MASK_HOLY then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+local function Fire_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= SCHOOL_MASK_FIRE then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+local function Nature_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= SCHOOL_MASK_NATURE then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+local function Frost_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= SCHOOL_MASK_FROST then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+local function FrostFire_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= (SCHOOL_MASK_FIRE + SCHOOL_MASK_FROST) then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+local function Shadow_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= SCHOOL_MASK_SHADOW then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+local function Arcane_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= SCHOOL_MASK_ARCANE then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+---------------------------
+-- Effects: Death Knight --
+---------------------------
+
+local deathknight_MS_Ranks = {[0] = 0, [1] = 0.08, [2] = 0.16, [3] = 0.25}
+
+-- Public Scaling: { [MagicSuppression] }
+local deathknight_defaultScaling = {0}
+
+local function deathknight_AntiMagicShell_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local maxHealth = UnitDispatch(UnitHealthMax, dstGUID, dstName)
+ if maxHealth == 0 then
+ return 0, 0.0
+ end
+
+ local sourceScaling, quality = UnitScaling(srcGUID, deathknight_defaultScaling, 0.5)
+ return floor(maxHealth * 0.5), quality, (0.75 + sourceScaling[1])
+end
+
+local function deathknight_AntiMagicShell_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ -- TODO: what happens to mixed school attacks?
+ if spellschool == SCHOOL_MASK_PHYSICAL then
+ return 0, true
+ end
+
+ local maxAbsorb = floor((absorbedRemaining + overkill) * effectEntry[7])
+ if effectEntry[3] < maxAbsorb then
+ return effectEntry[3], false
+ end
+ return maxAbsorb, true
+end
+
+local function deathknight_AntiMagicZone_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local ap, _, quality = UnitStats(srcGUID, 0.3)
+ return (10000 + 2 * ap), quality
+end
+
+local function deathknight_AntiMagicZone_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ -- TODO: what happens to mixed school attacks?
+ if spellschool == SCHOOL_MASK_PHYSICAL then
+ return 0, true
+ end
+
+ local maxAbsorb = floor((absorbedRemaining + overkill) * 0.75)
+
+ if effectEntry[3] < maxAbsorb then
+ return effectEntry[3], false
+ end
+ return maxAbsorb, true
+end
+
+local function deathknight_WoN_Hit1(effectEntry, absorbedRemaining, overkill, spellschool)
+ local maxAbsorb = floor((absorbedRemaining + overkill) * 0.05)
+ if effectEntry[3] < maxAbsorb then
+ return effectEntry[3], false
+ end
+ return maxAbsorb, true
+end
+
+local function deathknight_WoN_Hit2(effectEntry, absorbedRemaining, overkill, spellschool)
+ local maxAbsorb = floor((absorbedRemaining + overkill) * 0.1)
+ if effectEntry[3] < maxAbsorb then
+ return effectEntry[3], false
+ end
+ return maxAbsorb, true
+end
+
+local function deathknight_WoN_Hit3(effectEntry, absorbedRemaining, overkill, spellschool)
+ local maxAbsorb = floor((absorbedRemaining + overkill) * 0.15)
+ if effectEntry[3] < maxAbsorb then
+ return effectEntry[3], false
+ end
+ return maxAbsorb, true
+end
+
+local function deathknight_OnTalentUpdate()
+ -- Magic Suppression
+ local t = select(5, GetTalentInfo(3, 18))
+ playerScaling[1] = deathknight_MS_Ranks[t]
+ lib.ScheduleScalingBroadcast()
+end
+
+function OnEnableClass.DEATHKNIGHT()
+ Events.PLAYER_TALENT_UPDATE = deathknight_OnTalentUpdate
+ deathknight_OnTalentUpdate()
+end
+
+--------------------
+-- Effects: Druid --
+--------------------
+
+local function druid_SavageDefense_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local ap, _, quality = UnitStats(srcGUID, 0.0)
+ return floor(ap * 0.25), quality
+end
+
+local function druid_SavageDefense_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ -- TODO: what happens to mixed school attacks?
+ if spellschool == SCHOOL_MASK_PHYSICAL then
+ return min(effectEntry[3], absorbedRemaining), false
+ end
+ return 0, true
+end
+
+-------------------
+-- Effects: Mage --
+-------------------
+
+-- Table for base values of
+-- Fire Ward, Frost Ward, Ice Barrier, Mana Shield
+-- TODO: base leveling increase
+local mage_Absorb_Spells = {
+ -- Fire Ward
+ [543] = 165,
+ [8457] = 290,
+ [8458] = 470,
+ [10223] = 675,
+ [10225] = 875,
+ [27218] = 1125,
+ [43010] = 1950,
+ -- Frost Ward
+ [6143] = 165,
+ [8461] = 290,
+ [8462] = 470,
+ [10177] = 675,
+ [28609] = 875,
+ [32796] = 1125,
+ [43012] = 1950,
+ -- Ice Barrier
+ [11426] = 438,
+ [13031] = 549,
+ [13032] = 678,
+ [13033] = 818,
+ [27134] = 925,
+ [33405] = 1075,
+ [43038] = 2800,
+ [43039] = 3300,
+ -- Mana Shield
+ [1463] = 120,
+ [8494] = 210,
+ [8495] = 300,
+ [10191] = 390,
+ [10192] = 480,
+ [10193] = 570,
+ [27131] = 715,
+ [43019] = 1080,
+ [43020] = 1330
+}
+
+-- Public Scaling: { [GlyphOfIceBarrier] }
+local mage_defaultScaling = {1.0}
+
+-- No Downranking support here
+local function mage_IceBarrier_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local _, sp, quality1, sourceScaling, quality2 = UnitStatsAndScaling(srcGUID, 0.3, mage_defaultScaling, 0.4)
+ return floor((mage_Absorb_Spells[spellid] + (sp * 0.8053)) * sourceScaling[1]), min(quality1, quality2)
+end
+
+local function mage_FireWard_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= SCHOOL_MASK_FIRE then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+local function mage_FrostWard_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= SCHOOL_MASK_FROST then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+local function mage_OnGlyphUpdated()
+ local glyphSpellId
+
+ playerScaling[1] = 1.0
+
+ for i = 1, 6 do
+ glyphSpellId = select(3, GetGlyphSocketInfo(i))
+ -- Glyph of Ice Barrier
+ if glyphSpellId and glyphSpellId == 63095 then
+ playerScaling[1] = 1.3
+ break
+ end
+ end
+ lib.ScheduleScalingBroadcast()
+end
+
+function OnEnableClass.MAGE()
+ Events.GLYPH_UPDATED = mage_OnGlyphUpdated
+ mage_OnGlyphUpdated()
+end
+
+----------------------
+-- Effects: Paladin --
+----------------------
+
+-- Public Scaling: { [DivineGuardian] }
+local paladin_defaultScaling = {1.0}
+
+-- The base value is always 500
+local function paladin_SacredShield_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local _, sp, quality1, sourceScaling, quality2 = UnitStatsAndScaling(srcGUID, 0.1, paladin_defaultScaling, 0.2)
+ return floor((500 + (sp * 0.75)) * (sourceScaling[1] or paladin_defaultScaling[1]) * ZONE_MODIFIER), min(quality1, quality2)
+end
+
+local function paladin_OnTalentUpdate()
+ -- No need to do it before
+ if UnitLevel("player") < 80 then return end
+
+ -- Divine Guardian
+ local t = select(5, GetTalentInfo(2, 9))
+ playerScaling[1] = 1 + (t * 0.1)
+ lib.ScheduleScalingBroadcast()
+end
+
+function OnEnableClass.PALADIN()
+ Events.PLAYER_TALENT_UPDATE = paladin_OnTalentUpdate
+ paladin_OnTalentUpdate()
+end
+
+---------------------
+-- Effects: Priest --
+---------------------
+
+PRIEST_DIVINEAEGIS_SPELLID = 47753
+
+-- [rank] = {spellid, level, baseValue, incValue}
+local priest_PWS_Ranks = {
+ [1] = {17, 6, 44, 4},
+ [2] = {592, 12, 88, 6},
+ [3] = {600, 18, 158, 8},
+ [4] = {3747, 24, 234, 10},
+ [5] = {6065, 30, 301, 11},
+ [6] = {6066, 36, 381, 13},
+ [7] = {10898, 42, 484, 15},
+ [8] = {10899, 48, 605, 17},
+ [9] = {10900, 54, 763, 19},
+ [10] = {10901, 60, 942, 21},
+ [11] = {25217, 65, 1125, 18},
+ [12] = {25218, 70, 1265, 20},
+ [13] = {48065, 75, 1920, 30},
+ [14] = {48066, 80, 2230, 0}
+}
+
+-- Public Scaling:
+-- Power Word: Shield: [spellid] = {base, spFactor}
+-- Divine Aegis: [47753] = healFactor
+local priest_defaultScaling = {[PRIEST_DIVINEAEGIS_SPELLID] = 0}
+do
+ for k, v in pairs(priest_PWS_Ranks) do
+ priest_defaultScaling[v[1]] = {v[3], 0.809}
+ end
+end
+
+-- Private Scaling
+-- Talents: "TwinDisc", "ImpPWS", "FocusedPower", "DivineAegis", "BorrowedTime", "SpiritualHealing"
+-- Gear: "4pcRaid9", "4pcRaid10"
+-- Computed: base, sp, DA
+
+local function priest_PowerWordShield_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local _, sp, quality1, sourceScaling, quality2 = UnitStatsAndScaling(srcGUID, 0.1, priest_defaultScaling, 0.1)
+ sourceScaling[spellid] = sourceScaling[spellid] or priest_defaultScaling[spellid]
+ if sourceScaling[spellid] then
+ return floor((sourceScaling[spellid][1] + sp * sourceScaling[spellid][2]) * ZONE_MODIFIER), min(quality1, quality2)
+ end
+end
+
+local function priest_DivineAegis_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local existing = 0
+
+ if destEffects and destEffects[spellid] then
+ existing = destEffects[spellid][3]
+ end
+
+ local charge = PopCharge(dstGUID, spellid)
+
+ if charge == 0 then
+ return existing, 0.0
+ end
+
+ local destLevel = UnitDispatch(UnitLevel, dstGUID, dstName)
+ local quality = 1.0
+
+ if destLevel == 0 then
+ destLevel = 80
+ quality = 0.4
+ end
+
+ return min(destLevel * 125, existing + charge), quality
+end
+
+-- I officially HATE Divine Aegis (and Val'anyr for that matter) from now.
+-- After extensive testing and parsing/filtering a few hours of combat log, I found the following facts:
+-- * In MOST cases, every critical heal will trigger an AURA_APPLIED/AURA_REFRESHED event, even on multiple crits on
+-- one penance and with both AURA events being triggered after all critical heals. In rare cases, there is only one...
+-- * If on your side the aura get removed before the next one is applied (double penance crit, both critical heals
+-- are first in your combat log), then AURA_APPLIED -> AURA_REMOVED by damage -> AURA_APPLIED for 2nd crit
+-- * There are occasions where 2 Discipline priests apply two Divine Aegis auras. In general, the source of Divine Aegis
+-- is completely fucked up in those cases. If two priests channel penance at the same time, both crit, you can never say
+-- who will get the AURA_APPLIED event credited (yes, I found both the first-hitting priest as well as the second-hitting
+-- priest there!) and who the following AURA_REFRESHED events
+local function priest_DivineAegis_OnHealCrit(srcGUID, srcName, dstGUID, dstName, spellid, amount)
+ -- We can do a direct access to Scaling here, since the callback is only in place if it was present
+ -- in the first place
+ PushCharge(dstGUID, PRIEST_DIVINEAEGIS_SPELLID, floor(amount * Scaling[srcGUID][PRIEST_DIVINEAEGIS_SPELLID]), 5.0)
+end
+
+local function priest_ApplyScaling(guid, level, baseFactor, spFactor, daFactor)
+ local guidScaling
+
+ if not Scaling[guid] then
+ guidScaling = {}
+ Scaling[guid] = guidScaling
+ else
+ guidScaling = Scaling[guid]
+ end
+
+ guidScaling[PRIEST_DIVINEAEGIS_SPELLID] = daFactor
+
+ if daFactor > 0 then
+ Core.AddCombatTrigger(guid, "OnHealCrit", priest_DivineAegis_OnHealCrit)
+ else
+ Core.RemoveCombatTrigger(guid, "OnHealCrit", priest_DivineAegis_OnHealCrit)
+ end
+
+ local rankValue, rankSP
+
+ for k, v in pairs(priest_PWS_Ranks) do
+ if v[2] <= level then
+ if level == 80 then
+ rankValue = v[3] + v[4]
+ else
+ -- TODO
+ rankValue = v[3]
+ end
+
+ -- Based on the assumption that the decrease in sp coefficient is linear,
+ -- only tested for level 80 though so far
+ -- Cataclysm will help us get rid of this crap again
+ if v[2] < (level - 5) then
+ if level == 80 then
+ rankSP = max(spFactor * ((v[2] * 0.045228921) - 2.381389768), 0)
+ else
+ rankSP = 0
+ end
+ else
+ rankSP = spFactor
+ end
+
+ guidScaling[v[1]] = {rankValue * baseFactor, rankSP}
+ end
+ end
+end
+
+local function priest_UpdatePlayerScaling()
+ privateScaling.base = ((1.0 + (privateScaling["TwinDisc"] * 0.01) + (privateScaling["FocusedPower"] * 0.02) + (privateScaling["SpiritualHealing"] * 0.02)) * (1.0 + ((privateScaling["ImpPWS"] + privateScaling["4pcRaid10"]) * 0.05)))
+
+ local spFactor = 0.807
+ spFactor = spFactor + (privateScaling["BorrowedTime"] * 0.08)
+ spFactor = spFactor * (1.0 + (privateScaling["TwinDisc"] * 0.01) + (privateScaling["FocusedPower"] * 0.02) + (privateScaling["SpiritualHealing"] * 0.02)) * (1.0 + ((privateScaling["ImpPWS"] + privateScaling["4pcRaid10"]) * 0.05))
+ privateScaling.sp = spFactor
+ privateScaling.DA = (privateScaling["DivineAegis"] * 0.1) * (1 + (privateScaling["4pcRaid9"] * 0.03))
+ priest_ApplyScaling(playerid, UnitLevel("player"), privateScaling.base, privateScaling.sp, privateScaling.DA)
+ lib.ScheduleScalingBroadcast()
+end
+
+local function priest_ScanTalents()
+ -- Twin Disciplines
+ local t = select(5, GetTalentInfo(1, 2))
+ privateScaling["TwinDisc"] = t
+
+ -- Improved Power Word: Shield
+ t = select(5, GetTalentInfo(1, 9))
+ privateScaling["ImpPWS"] = t
+
+ -- Focused Power
+ t = select(5, GetTalentInfo(1, 16))
+ privateScaling["FocusedPower"] = t
+
+ -- Divine Aegis
+ t = select(5, GetTalentInfo(1, 24))
+ privateScaling["DivineAegis"] = t
+
+ -- Borrowed Time
+ t = select(5, GetTalentInfo(1, 27))
+ privateScaling["BorrowedTime"] = t
+
+ -- Spiritual Healing
+ t = select(5, GetTalentInfo(2, 16))
+ privateScaling["SpiritualHealing"] = t
+end
+
+local function priest_ScanEquipment()
+ local n = 0
+
+ -- Crimson Acolyte Raiment's 4-piece Bonus
+ if IsEquippedItem(50765) or IsEquippedItem(51178) or IsEquippedItem(51261) then
+ n = 1
+ end
+ if IsEquippedItem(50767) or IsEquippedItem(51175) or IsEquippedItem(51264) then
+ n = n + 1
+ end
+ if IsEquippedItem(50768) or IsEquippedItem(51176) or IsEquippedItem(51263) then
+ n = n + 1
+ end
+ if IsEquippedItem(50766) or IsEquippedItem(51179) or IsEquippedItem(51260) then
+ n = n + 1
+ end
+ if IsEquippedItem(50769) or IsEquippedItem(51177) or IsEquippedItem(51262) then
+ n = n + 1
+ end
+
+ if n >= 4 then
+ privateScaling["4pcRaid10"] = 1
+
+ -- no way to have 4pcRaid9 now
+ privateScaling["4pcRaid9"] = 0
+ return
+ else
+ privateScaling["4pcRaid10"] = 0
+ end
+
+ n = 0
+
+ -- Velen's/Zabra's Raiment 4-piece Bonus
+
+ -- UnitFactionGroup's first return value is NOT localized
+ if UnitFactionGroup("player") == "Alliance" then
+ if IsEquippedItem(47914) or IsEquippedItem(47984) or IsEquippedItem(48035) then
+ n = 1
+ end
+ if IsEquippedItem(47981) or IsEquippedItem(47987) or IsEquippedItem(48029) then
+ n = n + 1
+ end
+ if IsEquippedItem(47936) or IsEquippedItem(47986) or IsEquippedItem(48031) then
+ n = n + 1
+ end
+ if IsEquippedItem(47982) or IsEquippedItem(47983) or IsEquippedItem(48037) then
+ n = n + 1
+ end
+ if IsEquippedItem(47980) or IsEquippedItem(47985) or IsEquippedItem(48033) then
+ n = n + 1
+ end
+ else
+ if IsEquippedItem(48068) or IsEquippedItem(48065) or IsEquippedItem(48058) then
+ n = 1
+ end
+ if IsEquippedItem(48071) or IsEquippedItem(48062) or IsEquippedItem(48061) then
+ n = n + 1
+ end
+ if IsEquippedItem(48070) or IsEquippedItem(48063) or IsEquippedItem(48060) then
+ n = n + 1
+ end
+ if IsEquippedItem(48067) or IsEquippedItem(48066) or IsEquippedItem(48057) then
+ n = n + 1
+ end
+ if IsEquippedItem(48069) or IsEquippedItem(48064) or IsEquippedItem(48059) then
+ n = n + 1
+ end
+ end
+
+ if n >= 4 then
+ privateScaling["4pcRaid9"] = 1
+ else
+ privateScaling["4pcRaid9"] = 0
+ end
+end
+
+local function priest_OnLevelUp()
+ priest_UpdatePlayerScaling()
+end
+
+local function priest_OnTalentUpdate()
+ priest_ScanTalents()
+ priest_UpdatePlayerScaling()
+end
+
+local function priest_OnEquipmentChangedDelayed()
+ priest_ScanEquipment()
+ priest_UpdatePlayerScaling()
+end
+
+local function priest_OnEquipmentChanged()
+ Core:ScheduleUniqueTimer("priest_equip", priest_OnEquipmentChangedDelayed, 0.7)
+end
+
+local function priest_OnScalingEncode()
+ return {UnitLevel("player"), privateScaling.base, privateScaling.sp, privateScaling.DA}
+end
+
+function OnScalingDecode.PRIEST(guid, in_guidScaling)
+ if #in_guidScaling ~= 4 then return end
+ priest_ApplyScaling(guid, unpack(in_guidScaling))
+end
+
+function OnEnableClass.PRIEST()
+ Events.PLAYER_LEVEL_UP = priest_OnLevelUp
+ Events.PLAYER_TALENT_UPDATE = priest_OnTalentUpdate
+ Events.PLAYER_EQUIPMENT_CHANGED = priest_OnEquipmentChanged
+ Events.OnScalingEncode = priest_OnScalingEncode
+
+ priest_ScanTalents()
+ priest_ScanEquipment()
+ priest_UpdatePlayerScaling()
+end
+
+---------------------
+-- Effects: Shaman --
+---------------------
+
+-- Public Scaling: { [AstralShift] }
+-- We default to 0.3, because quite frankly nobody will use less points except while leveling
+local shaman_defaultScaling = {0.3}
+
+local function shaman_AstralShift_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local sourceScaling, quality = UnitScaling(srcGUID, shaman_defaultScaling, 0.7)
+ return -1, quality, sourceScaling[1]
+end
+
+local function shaman_AstralShift_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ local total = absorbedRemaining + overkill
+
+ return floor(total * effectEntry[7]), true
+end
+
+local function shaman_OnTalentUpdate()
+ -- Astral Shift
+ local t = select(5, GetTalentInfo(1, 21))
+ playerScaling[1] = t * 0.1
+ lib.ScheduleScalingBroadcast()
+end
+
+function OnEnableClass.SHAMAN()
+ Events.PLAYER_TALENT_UPDATE = shaman_OnTalentUpdate
+ shaman_OnTalentUpdate()
+end
+
+----------------------
+-- Effects: Warlock --
+----------------------
+
+-- TODO: base leveling increase
+local warlock_Sacrifice_Spells = {
+ [7812] = 305,
+ [19438] = 510,
+ [19440] = 770,
+ [19441] = 1095,
+ [19442] = 1470,
+ [19443] = 1905,
+ [27273] = 2855,
+ [47985] = 6750,
+ [47986] = 8365
+}
+
+local warlock_ShadowWard_Spells = {
+ [6229] = 290,
+ [11739] = 470,
+ [11740] = 675,
+ [28610] = 875,
+ [47890] = 2750,
+ [47891] = 3300
+}
+
+-- Public Scaling: { [DemonicBrutality] }
+local warlock_defaultScaling = {1.0}
+
+-- No downranking support here
+local function warlock_Sacrifice_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ -- Note that the source is the voidwalker, so dest is the warlock!
+ local sourceScaling, quality = UnitScaling(dstGUID, warlock_defaultScaling, 0.4)
+ return floor(warlock_Sacrifice_Spells[spellid] * sourceScaling[1]), quality
+end
+
+local function warlock_ShadowWard_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+ if spellschool ~= SCHOOL_MASK_SHADOW then
+ return 0, true
+ end
+ return generic_Hit(effectEntry, absorbedRemaining, overkill, spellschool)
+end
+
+local function warlock_OnTalentUpdate()
+ -- Demonic Brutality
+ local t = select(5, GetTalentInfo(2, 6))
+ playerScaling[1] = 1 + (t * 0.1)
+ lib.ScheduleScalingBroadcast()
+end
+
+function OnEnableClass.WARLOCK()
+ Events.PLAYER_TALENT_UPDATE = warlock_OnTalentUpdate
+ warlock_OnTalentUpdate()
+end
+
+--------------------
+-- Effects: Items --
+--------------------
+
+local function items_EssenceOfGossamer_Hit(effectEntry)
+ if effectEntry[3] < 140 then
+ return effectEntry[3], false
+ end
+ return 140, true
+end
+
+local function items_ArgussianCompass_Hit(effectEntry)
+ if effectEntry[3] < 68 then
+ return effectEntry[3], false
+ end
+ return 68, true
+end
+
+local function items_Valanyr_OnHeal(srcGUID, srcName, dstGUID, dstName, spellid, amount)
+ PushCharge(dstGUID, 64413, floor(amount * 0.15), 5.0)
+end
+
+local function items_Valanyr_OnAuraApplied(srcGUID, srcName, dstGUID, dstName, spellid)
+ Core.AddCombatTrigger(srcGUID, "OnHeal", items_Valanyr_OnHeal)
+end
+
+local function items_Valanyr_OnAuraRemoved(srcGUID, srcName, dstGUID, dstName, spellid)
+ Core.RemoveCombatTrigger(srcGUID, "OnHeal", items_Valanyr_OnHeal)
+end
+
+local function items_Valanyr_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local existing = 0
+
+ if destEffects and destEffects[spellid] then
+ existing = destEffects[spellid][3]
+ end
+
+ local charge = PopCharge(dstGUID, spellid)
+ if charge == 0 then
+ return existing, 0.0
+ end
+
+ -- According to the blue post explaining the Val'anyr effect on introduction, all units are
+ -- contributing to the same bubble with a cap of 20.000
+ return min(20000, existing + charge), 1.0
+end
+
+local function items_Stoicism_Create(srcGUID, srcName, dstGUID, dstName, spellid, destEffects)
+ local maxHealth = UnitDispatch(UnitHealthMax, dstGUID, dstName)
+ if maxHealth == 0 then
+ return 0, 0.0
+ end
+ return floor(maxHealth * 0.2), 1.0
+end
+
+-----------------
+-- Data Tables --
+-----------------
+
+local mage_FireWard_Entry = {2.0, 30, generic_SpellScalingByTable_Create, mage_FireWard_Hit, mage_Absorb_Spells, 0.8053}
+local mage_FrostWard_Entry = {2.0, 30, generic_SpellScalingByTable_Create, mage_FrostWard_Hit, mage_Absorb_Spells, 0.8053}
+local mage_IceBarrier_Entry = {1.0, 60, mage_IceBarrier_Create, generic_Hit}
+local mage_ManaShield_Entry = {1.0, 60, generic_SpellScalingByTable_Create, generic_Hit, mage_Absorb_Spells, 0.8053}
+local priest_PWS_Entry = {1.0, 30, priest_PowerWordShield_Create, generic_Hit}
+local warlock_Sacrifice_Entry = {1.0, 30, generic_ConstantByTable_Create, generic_Hit, warlock_Sacrifice_Spells}
+local warlock_ShadowWard_Entry = {2.0, 30, generic_SpellScalingByTable_Create, warlock_ShadowWard_Hit, warlock_ShadowWard_Spells, 0.8053}
+
+-- INCOMPLETE
+Core.Effects = {
+ -- Unknown Effect
+ -- This is used when a known effect is applied, but it is impossible to properly account for it,
+ -- for example if an AREA effect is applied with an unknown trigger
+ [0] = {1.0, 0, function() return 0, 0.0 end, nil},
+ [48707] = {3.0, 5, deathknight_AntiMagicShell_Create, deathknight_AntiMagicShell_Hit}, -- Anti-Magic Shell
+ [50461] = {-3.0, 10, deathknight_AntiMagicZone_Create, deathknight_AntiMagicZone_Hit}, -- Anti-Magic Zone
+
+ [52284] = {1.0, nil, function() return 0, 0.0 end, deathknight_WoN_Hit1}, -- Will of the Necropolis (Rank 1)
+ [52285] = {1.0, nil, function() return 0, 0.0 end, deathknight_WoN_Hit2}, -- Will of the Necropolis (Rank 2)
+ [52286] = {1.0, nil, function() return 0, 0.0 end, deathknight_WoN_Hit3}, -- Will of the Necropolis (Rank 3)
+
+ [62606] = {1.1, 10, druid_SavageDefense_Create, druid_SavageDefense_Hit}, -- Savage Defense
+ [543] = mage_FireWard_Entry, -- Fire Ward (rank 1)
+ [8457] = mage_FireWard_Entry, -- Fire Ward (rank 2)
+ [8458] = mage_FireWard_Entry, -- Fire Ward (rank 3)
+ [10223] = mage_FireWard_Entry, -- Fire Ward (rank 4)
+ [10225] = mage_FireWard_Entry, -- Fire Ward (rank 5)
+ [27218] = mage_FireWard_Entry, -- Fire Ward (rank 6)
+ [43010] = mage_FireWard_Entry, -- Fire Ward (rank 7)
+ [6143] = mage_FrostWard_Entry, -- Frost Ward (rank 1)
+ [8461] = mage_FrostWard_Entry, -- Frost Ward (rank 2)
+ [8462] = mage_FrostWard_Entry, -- Frost Ward (rank 3)
+ [10177] = mage_FrostWard_Entry, -- Frost Ward (rank 4)
+ [28609] = mage_FrostWard_Entry, -- Frost Ward (rank 5)
+ [32796] = mage_FrostWard_Entry, -- Frost Ward (rank 6)
+ [43012] = mage_FrostWard_Entry, -- Frost Ward (rank 7)
+ [11426] = mage_IceBarrier_Entry, -- Ice Barrier (rank 1)
+ [13031] = mage_IceBarrier_Entry, -- Ice Barrier (rank 2)
+ [13032] = mage_IceBarrier_Entry, -- Ice Barrier (rank 3)
+ [13033] = mage_IceBarrier_Entry, -- Ice Barrier (rank 4)
+ [27134] = mage_IceBarrier_Entry, -- Ice Barrier (rank 5)
+ [33405] = mage_IceBarrier_Entry, -- Ice Barrier (rank 6)
+ [43038] = mage_IceBarrier_Entry, -- Ice Barrier (rank 7)
+ [43039] = mage_IceBarrier_Entry, -- Ice Barrier (rank 8)
+ [1463] = mage_ManaShield_Entry, -- Mana shield (rank 1)
+ [8494] = mage_ManaShield_Entry, -- Mana shield (rank 2)
+ [8495] = mage_ManaShield_Entry, -- Mana shield (rank 3)
+ [10191] = mage_ManaShield_Entry, -- Mana shield (rank 4)
+ [10192] = mage_ManaShield_Entry, -- Mana shield (rank 5)
+ [10193] = mage_ManaShield_Entry, -- Mana shield (rank 6)
+ [27131] = mage_ManaShield_Entry, -- Mana shield (rank 7)
+ [43019] = mage_ManaShield_Entry, -- Mana shield (rank 8)
+ [43020] = mage_ManaShield_Entry, -- Mana shield (rank 9)
+ [58597] = {1.0, 6, paladin_SacredShield_Create, generic_Hit}, -- Sacred Shield
+ [17] = priest_PWS_Entry, -- Power Word: Shield (rank 1)
+ [592] = priest_PWS_Entry, -- Power Word: Shield (rank 2)
+ [600] = priest_PWS_Entry, -- Power Word: Shield (rank 3)
+ [3747] = priest_PWS_Entry, -- Power Word: Shield (rank 4)
+ [6065] = priest_PWS_Entry, -- Power Word: Shield (rank 5)
+ [6066] = priest_PWS_Entry, -- Power Word: Shield (rank 6)
+ [10898] = priest_PWS_Entry, -- Power Word: Shield (rank 7)
+ [10899] = priest_PWS_Entry, -- Power Word: Shield (rank 8)
+ [10900] = priest_PWS_Entry, -- Power Word: Shield (rank 9)
+ [10901] = priest_PWS_Entry, -- Power Word: Shield (rank 10)
+ [25217] = priest_PWS_Entry, -- Power Word: Shield (rank 11)
+ [25218] = priest_PWS_Entry, -- Power Word: Shield (rank 12)
+ [48065] = priest_PWS_Entry, -- Power Word: Shield (rank 13)
+ [48066] = priest_PWS_Entry, -- Power Word: Shield (rank 14)
+ [47753] = {1.0, 12, priest_DivineAegis_Create, generic_Hit}, -- Divine Aegis
+ [52179] = {2.5, nil, shaman_AstralShift_Create, shaman_AstralShift_Hit}, -- Astral Shift
+ [7812] = warlock_Sacrifice_Entry, -- Sacrifice (rank 1)
+ [19438] = warlock_Sacrifice_Entry, -- Sacrifice (rank 2)
+ [19440] = warlock_Sacrifice_Entry, -- Sacrifice (rank 3)
+ [19441] = warlock_Sacrifice_Entry, -- Sacrifice (rank 4)
+ [19442] = warlock_Sacrifice_Entry, -- Sacrifice (rank 5)
+ [19443] = warlock_Sacrifice_Entry, -- Sacrifice (rank 6)
+ [27273] = warlock_Sacrifice_Entry, -- Sacrifice (rank 7)
+ [47985] = warlock_Sacrifice_Entry, -- Sacrifice (rank 8)
+ [47986] = warlock_Sacrifice_Entry, -- Sacrifice (rank 9)
+ [6229] = warlock_ShadowWard_Entry, -- Shadow Ward (rank 1)
+ [11739] = warlock_ShadowWard_Entry, -- Shadow Ward (rank 1)
+ [11740] = warlock_ShadowWard_Entry, -- Shadow Ward (rank 2)
+ [28610] = warlock_ShadowWard_Entry, -- Shadow Ward (rank 3)
+ [47890] = warlock_ShadowWard_Entry, -- Shadow Ward (rank 4)
+ [47891] = warlock_ShadowWard_Entry, -- Shadow Ward (rank 5)
+ [64413] = {1.0, 8, items_Valanyr_Create, generic_Hit}, -- Val'anyr (spellid of the created absorb effect)
+ [60218] = {5.0, 10, function() return 4000, 1.0 end, items_EssenceOfGossamer_Hit}, -- Essence of Gossamer
+ [71586] = {1.0, 10, function() return 6400, 1.0 end, generic_Hit}, -- Corroded Skeleton Key
+ [36481] = {1.0, 4, function() return 100000, 1.0 end, generic_Hit}, -- Phaseshift Bulwark
+ [57350] = {1.0, 6, function() return 1500, 1.0 end, generic_Hit}, -- Darkmoon Card: Illusion
+ [17252] = {1.0, 1800, function() return 500, 1.0 end, generic_Hit}, -- Mark of the Dragon Lord
+ [29506] = {1.0, 20, function() return 900, 1.0 end, generic_Hit}, -- The Burrower's Shell
+ [31771] = {1.0, 20, function() return 440, 1.0 end, generic_Hit}, -- Runed Fungalcap
+ [9800] = {1.0, 60, function() return 175, 1.0 end, generic_Hit}, -- Truesilver Champion
+ [13234] = {1.0, 600, function() return 500, 1.0 end, generic_Hit}, -- Gnomish Harm Prevention Belt
+ [30458] = {1.0, 8, function() return 4000, 1.0 end, generic_Hit}, -- Nigh Invulnerability Belt
+ [27779] = {1.0, 30, function() return 350, 1.0 end, generic_Hit}, -- Divine Protection (Priest Dungeon Set 1/2 4pc bonus)
+ [28810] = {1.0, 30, function() return 500, 1.0 end, generic_Hit}, -- Armor of Faith (Priest Raid Set 3 4pc bonus)
+ [29674] = {1.0, nil, function() return 1000, 1.0 end, generic_Hit}, -- Lesser Ward of Shielding
+ [29719] = {1.0, nil, function() return 4000, 1.0 end, generic_Hit}, -- Greater Ward of Shielding
+ [29701] = {1.0, nil, function() return 4000, 1.0 end, generic_Hit}, -- Greater Ward of Shielding
+ [28538] = {1.0, 120, function() return 3400, 1.0 end, Holy_Hit}, -- Major Holy Protection Potion
+ [28537] = {1.0, 120, function() return 3400, 1.0 end, Shadow_Hit}, -- Major Shadow Protection Potion
+ [28536] = {1.0, 120, function() return 3400, 1.0 end, Arcane_Hit}, -- Major Arcane Protection Potion
+ [28513] = {1.0, 120, function() return 3400, 1.0 end, Nature_Hit}, -- Major Nature Protection Potion
+ [28512] = {1.0, 120, function() return 3400, 1.0 end, Frost_Hit}, -- Major Frost Protection Potion
+ [28511] = {1.0, 120, function() return 3400, 1.0 end, Fire_Hit}, -- Major Fire Protection Potion
+ [7233] = {1.0, 120, function() return 1300, 1.0 end, Fire_Hit}, -- Fire Protection Potion
+ [7239] = {1.0, 120, function() return 1300, 1.0 end, Frost_Hit}, -- Frost Protection Potion
+ [7242] = {1.0, 120, function() return 1300, 1.0 end, Shadow_Hit}, -- Shadow Protection Potion
+ [7245] = {1.0, 120, function() return 1300, 1.0 end, Holy_Hit}, -- Holy Protection Potion
+ [7254] = {1.0, 120, function() return 1300, 1.0 end, Nature_Hit}, -- Nature Protection Potion
+ [53915] = {1.0, 120, function() return 3100, 1.0 end, Shadow_Hit}, -- Mighty Shadow Protection Potion
+ [53914] = {1.0, 120, function() return 3100, 1.0 end, Nature_Hit}, -- Mighty Nature Protection Potion
+ [53913] = {1.0, 120, function() return 3100, 1.0 end, Frost_Hit}, -- Mighty Frost Protection Potion
+ [53911] = {1.0, 120, function() return 3100, 1.0 end, Fire_Hit}, -- Mighty Fire Protection Potion
+ [53910] = {1.0, 120, function() return 3100, 1.0 end, Arcane_Hit}, -- Mighty Arcane Protection Potion
+ [17548] = {1.0, 120, function() return 2600, 1.0 end, Shadow_Hit}, -- Greater Shadow Protection Potion
+ [17546] = {1.0, 120, function() return 2600, 1.0 end, Shadow_Hit}, -- Greater Nature Protection Potion
+ [17545] = {1.0, 120, function() return 2600, 1.0 end, Shadow_Hit}, -- Greater Holy Protection Potion
+ [17544] = {1.0, 120, function() return 2600, 1.0 end, Shadow_Hit}, -- Greater Frost Protection Potion
+ [17543] = {1.0, 120, function() return 2600, 1.0 end, Shadow_Hit}, -- Greater Fire Protection Potion
+ [17549] = {1.0, 120, function() return 2600, 1.0 end, Shadow_Hit}, -- Greater Arcane Protection Potion
+ [28527] = {1.0, 15, function() return 1000, 1.0 end, generic_Hit}, -- Fel Blossom
+ [29432] = {1.0, 3600, function() return 2000, 1.0 end, Fire_Hit}, -- Frozen Rune
+ [25750] = {1.0, 15, function() return 151, 1.0 end, Physical_Hit}, -- Defiler's Talisman/Talisman of Arathor
+ [25747] = {1.0, 15, function() return 344, 1.0 end, Physical_Hit}, -- Defiler's Talisman/Talisman of Arathor
+ [25746] = {1.0, 15, function() return 394, 1.0 end, Physical_Hit}, -- Defiler's Talisman/Talisman of Arathor
+ [23991] = {1.0, 15, function() return 550, 1.0 end, Physical_Hit}, -- Defiler's Talisman/Talisman of Arathor
+ [30997] = {1.0, 300, function() return 1800, 1.0 end, Fire_Hit}, -- Pendant of Frozen Flame Usage
+ [31002] = {1.0, 300, function() return 1800, 1.0 end, Arcane_Hit}, -- Pendant of the Null Rune
+ [30999] = {1.0, 300, function() return 1800, 1.0 end, Nature_Hit}, -- Pendant of Withering
+ [30994] = {1.0, 300, function() return 1800, 1.0 end, Frost_Hit}, -- Pendant of Thawing
+ [31000] = {1.0, 300, function() return 1800, 1.0 end, Shadow_Hit}, -- Pendant of Shadow's End
+ [23506] = {1.0, 20, function() return 1000, 1.0 end, generic_Hit}, -- Arena Grand Master
+ [12561] = {1.0, 60, function() return 400, 1.0 end, Fire_Hit}, -- Goblin Construction Helmet
+ [21956] = {1.0, 15, function() return 250, 1.0 end, Physical_Hit}, -- Mark of Resolution
+ [4057] = {1.0, 60, function() return 250, 1.0 end, Fire_Hit}, -- Flame Deflector
+ [4077] = {1.0, 60, function() return 300, 1.0 end, generic_Hit}, -- Ice Deflector
+ [39228] = {1.0, 20, function() return 609, 1.0 end, generic_Hit}, -- Argussian Compass (may not be an actual absorb)
+ [11657] = {1.0, 20, function() return 70, 1.0 end, generic_Hit}, -- Jang'thraze (Zul Farrak)
+ [10368] = {1.0, 15, function() return 1000, 1.0 end, generic_Hit}, -- Uther's Strength
+ [37515] = {1.0, 15, function() return 1000, 1.0 end, generic_Hit}, -- Warbringer Armor Proc
+ [42137] = {1.0, 86400, function() return 1000, 1.0 end, generic_Hit}, -- Greater Rune of Warding Proc
+ [26467] = {1.0, 30, function() return 1000, 1.0 end, generic_Hit}, -- Scarab Brooch
+ [26470] = {1.0, 8, function() return 1000, 1.0 end, generic_Hit}, -- Scarab Brooch
+ [27539] = {1.0, 6, function() return 1000, 1.0 end, generic_Hit}, -- Thick Obsidian Breatplate
+ [54808] = {1.0, 12, function() return 1000, 1.0 end, generic_Hit}, -- Noise Machine Sonic Shield
+ [55019] = {1.0, 12, function() return 1000, 1.0 end, generic_Hit}, -- Sonic Shield
+ [70845] = {1.0, 10, items_Stoicism_Create, generic_Hit}, -- Stoicism (Warrior Raid Set 10 4pc bonus)
+ [65686] = {1.0, 0, function() return 0, 0.0 end, nil}, -- Twin Val'kyr: Light Essence
+ [65684] = {1.0, 0, function() return 0, 0.0 end, nil} -- Twin Val'kyr: Dark Essence
+}
+
+Core.AreaTriggers = {
+ [51052] = 50461, -- Anti-Magic Zone
+ [62618] = 81781 -- Power Word: Barrier
+}
+
+Core.CombatTriggers = {
+ OnAuraApplied = {
+ [64411] = items_Valanyr_OnAuraApplied
+ },
+ OnAuraRemoved = {
+ [64411] = items_Valanyr_OnAuraRemoved
+ },
+ OnHeal = {},
+ OnHealCrit = {}
+}
+
+----------------
+-- Initialize --
+----------------
+
+if not Core.Available then
+ Core.RegisterEvent("PLAYER_ENTERING_WORLD")
+else
+ Core.Enable()
+end
+
+------------------------
+-- Callback Reference --
+------------------------
+
+-- EffectApplied
+-- (srcGUID, srcName, dstGUID, dstName, spellid, value, quality, duration)
+-- The effect-individual messages get sent on visible and non-visible effects
+
+-- EffectUpdated
+-- (guid, spellid, value, [duration, only if refreshed])
+
+-- EffectRemoved
+-- (guid, spellid)
+
+-- Whenever the unit that radiates an AREA effect is created (visible/non-visible)
+-- Note that the actual effect on the unit that absorbs damage casues an
+-- EffectApplied/EffectRemoved message, but not EffectUpdated (instead AreaUpdated)
+-- The rationale behind this is performance, since we cannot update every unit afflicted
+-- by the area effect on every hit. Therefore, we have the shared entry in the activeEffects
+-- table of each unit, and will handle it the same way when exporting - separately from each
+-- others.
+
+-- AreaCreated
+-- (srcGUID, srcName, triggerGUID, spellid, value, quality)
+
+-- AreaUpdated
+-- (triggerGUID, value)
+
+-- AreaCleared
+-- (triggerGUID)
+
+-- UnitUpdated
+-- (guid, value, quality)
+-- Only for VISIBLE changes on the total amount
+
+-- UnitCleared
+-- (guid)
+-- Everytime a unit gets cleared from all absorb effects (quality reset)
+-- including non-visible effects
\ No newline at end of file
diff --git a/Gladius/libs/SpecializedAbsorbs-1.0/SpecializedAbsorbs-1.0.xml b/Gladius/libs/SpecializedAbsorbs-1.0/SpecializedAbsorbs-1.0.xml
new file mode 100644
index 0000000..9d1a9f0
--- /dev/null
+++ b/Gladius/libs/SpecializedAbsorbs-1.0/SpecializedAbsorbs-1.0.xml
@@ -0,0 +1,4 @@
+
+
+
\ No newline at end of file