WoWInterface SVN PhanxConfigWidgets

Compare Revisions

  • This comparison shows the changes necessary to convert path
    /branches
    from Rev 17 to Rev 19
    Reverse comparison

Rev 17 → Rev 19

LibWrapperHealComm-1.0/LibWrapperHealComm-1.0.lua New file
0,0 → 1,286
local major = "LibWrapperHealComm-1.0"
local minor = 8
assert(LibStub, string.format("%s requires LibStub.", major))
 
local CommWrapper = LibStub:NewLibrary(major, minor)
if( not CommWrapper ) then return end
 
local HealComm40 = LibStub:GetLibrary("LibHealComm-4.0", true)
assert(HealComm40, string.format("%s-%d requires LibHealComm-4.0 to run.", major, minor))
 
local playerName = UnitName("player")
local playerGUID, distribution
 
CommWrapper.blacklisted = CommWrapper.blacklisted or {}
CommWrapper.alreadySaw = CommWrapper.alreadySaw or {}
CommWrapper.lastCast = CommWrapper.lastCast or {}
 
-- Not 100% since it doesn't do specific rank conversion, but good enough. Map of LHC-3.0 supported heals -> their ID
CommWrapper.spellNameToID = CommWrapper.spellNameToID or {
[GetSpellInfo(5185)] = 5185,
[GetSpellInfo(8936)] = 8936,
[GetSpellInfo(50464)] = 50464,
[GetSpellInfo(635)] = 635,
[GetSpellInfo(19750)] = 19750,
[GetSpellInfo(596)] = 596,
[GetSpellInfo(32546)] = 32546,
[GetSpellInfo(2061)] = 2061,
[GetSpellInfo(2060)] = 2060,
[GetSpellInfo(2054)] = 2054,
[GetSpellInfo(2050)] = 2050,
[GetSpellInfo(1064)] = 1064,
[GetSpellInfo(331)] = 331,
[GetSpellInfo(8004)] = 8004,
}
 
-- Only public API there is for this, nice and easy
function CommWrapper:DisableSendingData(flag)
self.disableSending30 = flag
self:Update()
end
 
local function sendMessage(msg)
if( distribution ) then
SendAddonMessage("HealComm", msg, distribution)
end
end
 
-- If the player has HC3.0 then any sending options are automatically overriden, easier to let LHC-3.0 rather than trying to disable LHC-3.0
function CommWrapper:ADDON_LOADED(addon)
if( not self.playerHasHC and LibStub:GetLibrary("LibHealComm-3.0", true) ) then
self.playerHasHC = true
self.frame:UnregisterEvent("ADDON_LOADED")
self:Update()
end
end
 
-- Handles registering and unregistering the callbacks to support sending heals in LHC-3.0 format
function CommWrapper:Update()
playerGUID = UnitGUID("player")
HealComm40.UnregisterAllCallbacks(CommWrapper)
 
if( not self.disableSending30 and not self.playerHasHC and self.groupHasHC30 ) then
HealComm40.RegisterCallback(CommWrapper, "HealComm_HealStarted")
HealComm40.RegisterCallback(CommWrapper, "HealComm_HealStopped")
end
end
 
-- Send heals to HC
local function getName(unit)
if( not unit ) then return nil end
 
local name, server = UnitName(unit)
if( distribution == "BATTLEGROUND" and server and server ~= "" ) then
return string.format("%s-%s", name, server)
end
 
return name
end
 
local PrayerofHealing = GetSpellInfo(596)
local BindingHeal = GetSpellInfo(32546)
function CommWrapper:HealComm_HealStarted(event, casterGUID, spellID, healType, endTime, ...)
if( healType ~= HealComm40.DIRECT_HEALS or casterGUID ~= playerGUID ) then return end
 
local spellName = GetSpellInfo(spellID)
if( spellName == PrayerofHealing ) then
local targetList
for i=1, select("#", ...) do
local unit = HealComm40.guidToUnit[select(i, ...)]
if( unit ) then
if( targetList ) then
targetList = targetList .. ":" .. getName(unit)
else
targetList = getName(unit)
end
end
end
 
-- Grab the first heal amount it finds
local healAmount = HealComm40.pendingHeals[casterGUID][spellName][2]
if( targetList and healAmount ) then
sendMessage(string.format("003%05d%s", healAmount, targetList))
end
elseif( spellName == BindingHeal ) then
-- Grab the first heal amount it finds
local healAmount = HealComm40.pendingHeals[casterGUID][spellName][2]
local target = getName(HealComm40.guidToUnit[select(1, ...)])
 
if( healAmount and target ) then
sendMessage(string.format("002%05d%s", healAmount, target))
end
else
-- Grab the first heal amount it finds
local healAmount = HealComm40.pendingHeals[casterGUID][spellName][2]
local target = getName(HealComm40.guidToUnit[select(1, ...)])
 
if( healAmount and target ) then
sendMessage(string.format("000%05d%s", healAmount, target))
end
end
end
 
function CommWrapper:HealComm_HealStopped(event, casterGUID, spellID, healType, endTime, ...)
if( healType ~= HealComm40.DIRECT_HEALS or casterGUID ~= playerGUID ) then return end
sendMessage("001S")
end
 
-- When we leave a group need to deactive HC support regardless
local function updateDistribution()
local instanceType = select(2, IsInInstance())
 
if( instanceType == "pvp" or instanceType == "arena" ) then
distribution = "BATTLEGROUND"
elseif( GetNumRaidMembers() > 0 ) then
distribution = "RAID"
elseif( GetNumPartyMembers() > 0 ) then
distribution = "PARTY"
else
distribution = nil
end
end
 
local isGrouped
function CommWrapper:PARTY_MEMBERS_CHANGED()
if( GetNumRaidMembers() == 0 and GetNumPartyMembers() == 0 ) then
self.groupHasHC30 = nil
self:Update()
 
isGrouped = nil
 
-- Release to GC
self.lastCast = {}
self.alreadySaw = {}
 
updateDistribution()
-- Need to get the initial versions from players so we know if the compatibility is necsesary
elseif( not self.playerHasSC and not isGrouped and ( GetNumRaidMembers() > 0 or GetNumPartyMembers() > 0 ) ) then
updateDistribution()
isGrouped = true
 
sendMessage("999888888")
end
end
 
CommWrapper.RAID_ROSTER_UPDATE = CommWrapper.PARTY_MEMBERS_CHANGED
 
-- Yoinked from LHC-3
local function extractRealm(fullName)
return select(2, string.split('-', fullName, 2))
end
 
-- Convert a remotely generated fully qualified name to a local fully qualified name.
local playerRealm = GetRealmName()
 
local function convertRealm(fullName, remoteRealm)
if( not remoteRealm ) then return fullName end
 
local name, realm = string.split('-', fullName, 2)
if( not realm ) then
-- Apply remote realm if there is no realm on the target
return fullName.."-"..remoteRealm
elseif(realm == playerRealm) then
-- Strip realm if it is equal to the local realm
return name
end
 
-- Sender and target realms are both different from local realm
return fullName
end
 
local function loadPlayers(sender, ...)
local players
local senderRealm = extractRealm(sender)
for i=1, select("#", ...) do
local guid = UnitGUID(convertRealm(select(i, ...), senderRealm))
if( guid ) then
if( players ) then
players = players .. "," .. HealComm40.compressGUID[guid]
else
players = HealComm40.compressGUID[guid]
end
end
end
 
return players
end
 
-- Handle parsing all the fun stuff
function CommWrapper:CHAT_MSG_ADDON(prefix, message, channel, sender)
if( sender == playerName ) then return end
 
-- Alright, we got HealComm data let us do fun parsing stuff
if( prefix == "HealComm" and not self.blacklisted[sender] and message ~= "999888888" ) then
local commType = tonumber(string.sub(message, 1, 3))
 
-- Got a HC-3.0 message so we know someone is using it
if( not self.groupHasHC30 ) then
self.groupHasHC30 = true
self:Update()
end
 
-- This will ignore the absolute first heal message by someone per session, this isn't a perfect solution
-- but it's meant to reduce any bugouts by someone using LHC-4.0 and LHC-3.0 together
if( commType <= 3 ) then
if( not self.alreadySaw[sender] ) then
self.alreadySaw[sender] = true
return
end
end
 
local casterGUID = UnitGUID(sender)
if( not casterGUID ) then return end
 
-- Heal start
if( commType == 0 ) then
local heal = tonumber(string.sub(message, 4, 8))
local target = string.sub(message, 9, -1)
local spellName = UnitCastingInfo(sender)
local guid = UnitGUID(convertRealm(target, extractRealm(sender)))
 
if( heal and target and guid and spellName ) then
self.lastCast[casterGUID] = spellName
 
--table.insert(TestLog, {"Heal started", message, casterGUID, spellName, heal, target, guid, HealComm40.compressGUID[guid]})
HealComm40.parseDirectHeal(casterGUID, CommWrapper.spellNameToID[spellName] or 0, heal, HealComm40.compressGUID[guid])
end
 
-- Multi target heal started
elseif( commType == 2 or commType == 3 ) then
local heal = tonumber(string.sub(message, 4, 8))
local targets = string.sub(message, 9, -1)
local spellName = UnitCastingInfo(sender)
 
if( heal and targets and spellName ) then
local players = loadPlayers(sender, string.split(":", targets))
if( not players ) then return end
 
self.lastCast[casterGUID] = spellName
 
--table.insert(TestLog, {"Heal started", message, casterGUID, spellName, heal, targets, players})
HealComm40.parseDirectHeal(casterGUID, self.spellNameToID[spellName] or 0, heal, players)
end
-- Heal stopped
elseif( commType == 1 and self.lastCast[casterGUID] ) then
--table.insert(TestLog, {"Heal stopped", message, casterGUID, self.lastCast[casterGUID]})
HealComm40.parseHealEnd(casterGUID, nil, "name", self.spellNameToID[self.lastCast[casterGUID]] or 0, string.sub(message, 4, 4) ~= "S")
end
 
-- If we see any message from LHC-4.0, automatically blacklist that user and ignore any data they sent from HC
elseif( prefix == "LHC40" ) then
self.blacklisted[sender] = true
end
end
 
local function OnEvent(self, event, ...)
CommWrapper[event](CommWrapper, ...)
end
 
local frame = CreateFrame("Frame")
frame:RegisterEvent("RAID_ROSTER_UPDATE")
frame:RegisterEvent("PARTY_MEMBERS_CHANGED")
frame:RegisterEvent("ADDON_LOADED")
frame:RegisterEvent("CHAT_MSG_ADDON")
frame:SetScript("OnEvent", OnEvent)
 
CommWrapper.frame = frame
Property changes : Added: svn:eol-style + native
LibHealComm-4.0/LibHealComm-4.0.lua New file
0,0 → 1,2729
local major = "LibHealComm-4.0"
local minor = 50
assert(LibStub, string.format("%s requires LibStub.", major))
 
local HealComm = LibStub:NewLibrary(major, minor)
if( not HealComm ) then return end
 
-- API CONSTANTS
--local ALL_DATA = 0x0f
local DIRECT_HEALS = 0x01
local CHANNEL_HEALS = 0x02
local HOT_HEALS = 0x04
--local ABSORB_SHIELDS = 0x08
local BOMB_HEALS = 0x10
local ALL_HEALS = bit.bor(DIRECT_HEALS, CHANNEL_HEALS, HOT_HEALS, BOMB_HEALS)
local CASTED_HEALS = bit.bor(DIRECT_HEALS, CHANNEL_HEALS)
local OVERTIME_HEALS = bit.bor(HOT_HEALS, CHANNEL_HEALS)
 
HealComm.ALL_HEALS, HealComm.CHANNEL_HEALS, HealComm.DIRECT_HEALS, HealComm.HOT_HEALS, HealComm.CASTED_HEALS, HealComm.ABSORB_SHIELDS, HealComm.ALL_DATA, HealComm.BOMB_HEALS = ALL_HEALS, CHANNEL_HEALS, DIRECT_HEALS, HOT_HEALS, CASTED_HEALS, ABSORB_SHIELDS, ALL_DATA, BOMB_HEALS
 
local COMM_PREFIX = "LHC40"
local playerGUID, playerName, playerLevel
local IS_BUILD30300 = tonumber((select(4, GetBuildInfo()))) >= 30300
 
HealComm.callbacks = HealComm.callbacks or LibStub:GetLibrary("CallbackHandler-1.0"):New(HealComm)
HealComm.spellData = HealComm.spellData or {}
HealComm.hotData = HealComm.hotData or {}
HealComm.talentData = HealComm.talentData or {}
HealComm.itemSetsData = HealComm.itemSetsData or {}
HealComm.glyphCache = HealComm.glyphCache or {}
HealComm.equippedSetCache = HealComm.equippedSetCache or {}
HealComm.playerModifiers = HealComm.playerModifiers or {}
HealComm.guidToGroup = HealComm.guidToGroup or {}
HealComm.guidToUnit = HealComm.guidToUnit or {}
HealComm.pendingHeals = HealComm.pendingHeals or {}
HealComm.tempPlayerList = HealComm.tempPlayerList or {}
HealComm.activePets = HealComm.activePets or {}
HealComm.activeHots = HealComm.activeHots or {}
 
if( not HealComm.unitToPet ) then
HealComm.unitToPet = {["player"] = "pet"}
for i=1, MAX_PARTY_MEMBERS do HealComm.unitToPet["party" .. i] = "partypet" .. i end
for i=1, MAX_RAID_MEMBERS do HealComm.unitToPet["raid" .. i] = "raidpet" .. i end
end
 
local spellData, hotData, tempPlayerList, pendingHeals = HealComm.spellData, HealComm.hotData, HealComm.tempPlayerList, HealComm.pendingHeals
local equippedSetCache, itemSetsData, talentData = HealComm.equippedSetCache, HealComm.itemSetsData, HealComm.talentData
local activeHots, activePets = HealComm.activeHots, HealComm.activePets
 
-- Figure out what they are now since a few things change based off of this
local playerClass = select(2, UnitClass("player"))
local isHealerClass = playerClass == "DRUID" or playerClass == "PRIEST" or playerClass == "SHAMAN" or playerClass == "PALADIN"
 
-- Stolen from Threat-2.0, compresses GUIDs from 18 characters to around 8 - 9, 50%/55% savings
-- 44 = , / 58 = : / 255 = \255 / 0 = line break? / 64 = @
if( not HealComm.compressGUID ) then
local map = {[58] = "\254\250", [64] = "\254\251", [44] = "\254\252", [255] = "\254\253", [0] = "\255"}
local function guidCompressHelper(x)
local a = tonumber(x, 16)
return map[a] or string.char(a)
end
 
local dfmt = "0x%02X%02X%02X%02X%02X%02X%02X%02X"
local function unescape(str)
str = string.gsub(str, "\255", "\000")
str = string.gsub(str, "\254\250", "\058")
str = string.gsub(str, "\254\251", "\064")
str = string.gsub(str, "\254\252", "\044")
return string.gsub(str, "\254\253", "\255")
end
 
HealComm.compressGUID = setmetatable({}, {
__index = function(tbl, guid)
local cguid = string.match(guid, "0x(.*)")
local str = string.gsub(cguid, "(%x%x)", guidCompressHelper)
 
rawset(tbl, guid, str)
return str
end})
 
HealComm.decompressGUID = setmetatable({}, {
__index = function(tbl, str)
if( not str ) then return nil end
local usc = unescape(str)
local a, b, c, d, e, f, g, h = string.byte(usc, 1, 8)
 
-- Failed to decompress, silently exit
if( not a or not b or not c or not d or not e or not f or not g or not h ) then
return ""
end
 
local guid = string.format(dfmt, a, b, c, d, e, f, g, h)
 
rawset(tbl, str, guid)
return guid
end})
end
 
local compressGUID, decompressGUID = HealComm.compressGUID, HealComm.decompressGUID
 
-- Handles caching of tables for variable tick spells, like Wild Growth
if( not HealComm.tableCache ) then
HealComm.tableCache = setmetatable({}, {__mode = "k"})
function HealComm:RetrieveTable()
return table.remove(HealComm.tableCache, 1) or {}
end
 
function HealComm:DeleteTable(tbl)
table.wipe(tbl)
table.insert(HealComm.tableCache, tbl)
end
end
 
-- Validation for passed arguments
if( not HealComm.tooltip ) then
local tooltip = CreateFrame("GameTooltip")
tooltip:SetOwner(UIParent, "ANCHOR_NONE")
tooltip.TextLeft1 = tooltip:CreateFontString()
tooltip.TextRight1 = tooltip:CreateFontString()
tooltip:AddFontStrings(tooltip.TextLeft1, tooltip.TextRight1)
 
HealComm.tooltip = tooltip
end
 
-- So I don't have to keep matching the same numbers every time or create a local copy of every rank -> # map for locals
if( not HealComm.rankNumbers ) then
HealComm.rankNumbers = setmetatable({}, {
__index = function(tbl, index)
local number = tonumber(string.match(index, "(%d+)")) or 1
 
rawset(tbl, index, number)
return number
end,
})
end
 
-- Find the spellID by the name/rank combination
-- Need to swap this to a double table metatable something like [spellName][spellRank] so I can reduce the garbage created
if( not HealComm.spellToID ) then
HealComm.spellToID = setmetatable({}, {
__index = function(tbl, index)
-- Find the spell from the spell book and cache the results!
local offset, numSpells = select(3, GetSpellTabInfo(GetNumSpellTabs()))
for id=1, (offset + numSpells) do
-- Match, yay!
local spellName, spellRank = GetSpellName(id, BOOKTYPE_SPELL)
local name = spellName .. spellRank
if( index == name ) then
HealComm.tooltip:SetSpell(id, BOOKTYPE_SPELL)
local spellID = select(3, HealComm.tooltip:GetSpell())
if( spellID ) then
rawset(tbl, index, spellID)
return spellID
end
end
end
 
rawset(tbl, index, false)
return false
end,
})
end
 
-- This gets filled out after data has been loaded, this is only for casted heals. Hots just directly pull from the averages as they do not increase in power with level, Cataclysm will change this though.
if( HealComm.averageHealMT and not HealComm.fixedAverage ) then
HealComm.averageHealMT = nil
end
 
HealComm.fixedAverage = true
HealComm.averageHeal = HealComm.averageHeal or {}
HealComm.averageHealMT = HealComm.averageHealMT or {
__index = function(tbl, index)
local rank = HealComm.rankNumbers[index]
local spellData = HealComm.spellData[rawget(tbl, "spell")]
local spellLevel = spellData.levels[rank]
 
-- No increase, it doesn't scale with levely
if( not spellData.increase or UnitLevel("player") <= spellLevel ) then
rawset(tbl, index, spellData.averages[rank])
return spellData.averages[rank]
end
 
local average = spellData.averages[rank]
if( UnitLevel("level") >= MAX_PLAYER_LEVEL ) then
average = average + spellData.increase[rank]
-- Here's how this works: If a spell increases 1,000 between 70 and 80, the player is level 75 the spell is 70
-- it's 1000 / (80 - 70) so 100, the player learned the spell 5 levels ago which means that the spell average increases by 500
-- This figures out how much it increases per level and how ahead of the spells level they are to figure out how much to add
else
average = average + (UnitLevel("player") - spellLevel) * (spellData.increase[rank] / (MAX_PLAYER_LEVEL - spellLevel))
end
 
rawset(tbl, index, average)
return average
end}
 
-- Record management, because this is getting more complicted to deal with
local function updateRecord(pending, guid, amount, stack, endTime, ticksLeft)
if( pending[guid] ) then
local id = pending[guid]
 
pending[id] = guid
pending[id + 1] = amount
pending[id + 2] = stack
pending[id + 3] = endTime or 0
pending[id + 4] = ticksLeft or 0
else
pending[guid] = #(pending) + 1
table.insert(pending, guid)
table.insert(pending, amount)
table.insert(pending, stack)
table.insert(pending, endTime or 0)
table.insert(pending, ticksLeft or 0)
 
if( pending.bitType == HOT_HEALS ) then
activeHots[guid] = (activeHots[guid] or 0) + 1
HealComm.hotMonitor:Show()
end
end
end
 
local function getRecord(pending, guid)
local id = pending[guid]
if( not id ) then return nil end
 
-- amount, stack, endTime, ticksLeft
return pending[id + 1], pending[id + 2], pending[id + 3], pending[id + 4]
end
 
local function removeRecord(pending, guid)
local id = pending[guid]
if( not id ) then return nil end
 
-- ticksLeft, endTime, stack, amount, guid
table.remove(pending, id + 4)
table.remove(pending, id + 3)
table.remove(pending, id + 2)
local amount = table.remove(pending, id + 1)
table.remove(pending, id)
pending[guid] = nil
 
-- Release the table
if( type(amount) == "table" ) then HealComm:DeleteTable(amount) end
 
if( pending.bitType == HOT_HEALS and activeHots[guid] ) then
activeHots[guid] = activeHots[guid] - 1
activeHots[guid] = activeHots[guid] > 0 and activeHots[guid] or nil
end
 
-- Shift any records after this ones index down 5 to account for the removal
for i=1, #(pending), 5 do
local guid = pending[i]
if( pending[guid] > id ) then
pending[guid] = pending[guid] - 5
end
end
end
 
local function removeRecordList(pending, inc, comp, ...)
for i=1, select("#", ...), inc do
local guid = select(i, ...)
guid = comp and decompressGUID[guid] or guid
 
local id = pending[guid]
-- ticksLeft, endTime, stack, amount, guid
table.remove(pending, id + 4)
table.remove(pending, id + 3)
table.remove(pending, id + 2)
local amount = table.remove(pending, id + 1)
table.remove(pending, id)
pending[guid] = nil
 
-- Release the table
if( type(amount) == "table" ) then HealComm:DeleteTable(amount) end
end
 
-- Redo all the id maps
for i=1, #(pending), 5 do
pending[pending[i]] = i
end
end
 
-- Removes every mention to the given GUID
local function removeAllRecords(guid)
local changed
for _, spells in pairs(pendingHeals) do
for _, pending in pairs(spells) do
if( pending.bitType and pending[guid] ) then
local id = pending[guid]
 
-- ticksLeft, endTime, stack, amount, guid
table.remove(pending, id + 4)
table.remove(pending, id + 3)
table.remove(pending, id + 2)
local amount = table.remove(pending, id + 1)
table.remove(pending, id)
pending[guid] = nil
 
-- Release the table
if( type(amount) == "table" ) then HealComm:DeleteTable(amount) end
 
-- Shift everything back
if( #(pending) > 0 ) then
for i=1, #(pending), 5 do
local guid = pending[i]
if( pending[guid] > id ) then
pending[guid] = pending[guid] - 5
end
end
else
table.wipe(pending)
end
 
changed = true
end
end
end
 
activeHots[guid] = nil
 
if( changed ) then
HealComm.callbacks:Fire("HealComm_GUIDDisappeared", guid)
end
end
 
-- These are not public APIs and are purely for the wrapper to use
HealComm.removeRecordList = removeRecordList
HealComm.removeRecord = removeRecord
HealComm.getRecord = getRecord
HealComm.updateRecord = updateRecord
 
-- Removes all pending heals, if it's a group that is causing the clear then we won't remove the players heals on themselves
local function clearPendingHeals()
for casterGUID, spells in pairs(pendingHeals) do
for _, pending in pairs(spells) do
if( pending.bitType ) then
table.wipe(tempPlayerList)
for i=#(pending), 1, -5 do table.insert(tempPlayerList, pending[i - 4]) end
 
if( #(tempPlayerList) > 0 ) then
local spellID, bitType = pending.spellID, pending.bitType
table.wipe(pending)
 
HealComm.callbacks:Fire("HealComm_HealStopped", casterGUID, spellID, bitType, true, unpack(tempPlayerList))
end
end
end
end
end
 
-- APIs
-- Returns the current healing modifier for the GUID
function HealComm:GetHealModifier(guid)
return HealComm.currentModifiers[guid] or 1
end
 
-- Returns whether or not the GUID has casted a heal
function HealComm:GUIDHasHealed(guid)
return pendingHeals[guid] and true or nil
end
 
-- Returns the guid to unit table
function HealComm:GetGUIDUnitMapTable()
if( not HealComm.protectedMap ) then
HealComm.protectedMap = setmetatable({}, {
__index = function(tbl, key) return HealComm.guidToUnit[key] end,
__newindex = function() error("This is a read only table and cannot be modified.", 2) end,
__metatable = false
})
end
 
return HealComm.protectedMap
end
 
-- Gets the next heal landing on someone using the passed filters
function HealComm:GetNextHealAmount(guid, bitFlag, time, ignoreGUID)
local healTime, healAmount, healFrom
local currentTime = GetTime()
 
for casterGUID, spells in pairs(pendingHeals) do
for _, pending in pairs(spells) do
if( pending.bitType and bit.band(pending.bitType, bitFlag) > 0 ) then
for i=1, #(pending), 5 do
local guid = pending[i]
if( not ignoreGUID or ignoreGUID ~= guid ) then
local amount = pending[i + 1]
local stack = pending[i + 2]
local endTime = pending[i + 3]
endTime = endTime > 0 and endTime or pending.endTime
 
-- Direct heals are easy, if they match the filter then return them
if( ( pending.bitType == DIRECT_HEALS or pending.bitType == BOMB_HEALS ) and ( not time or endTime <= time ) ) then
if( not healTime or endTime < healTime ) then
healTime = endTime
healAmount = amount * stack
healFrom = casterGUID
end
 
-- Channeled heals and hots, have to figure out how many times it'll tick within the given time band
elseif( ( pending.bitType == CHANNEL_HEALS or pending.bitType == HOT_HEALS ) and ( not pending.hasVariableTicks or pending.hasVariableTicks and amount[1] ) ) then
local secondsLeft = time and time - currentTime or endTime - currentTime
local nextTick = currentTime + (secondsLeft % pending.tickInterval)
if( not healTime or nextTick < healTime ) then
healTime = nextTick
healAmount = not pending.hasVariableTicks and amount * stack or amount[1] * stack
healFrom = casterGUID
end
end
end
end
end
end
end
 
return healTime, healFrom, healAmount
end
 
-- Get the healing amount that matches the passed filters
local function filterData(spells, filterGUID, bitFlag, time, ignoreGUID)
local healAmount = 0
local currentTime = GetTime()
 
for _, pending in pairs(spells) do
if( pending.bitType and bit.band(pending.bitType, bitFlag) > 0 ) then
for i=1, #(pending), 5 do
local guid = pending[i]
if( guid == filterGUID or ignoreGUID ) then
local amount = pending[i + 1]
local stack = pending[i + 2]
local endTime = pending[i + 3]
endTime = endTime > 0 and endTime or pending.endTime
 
-- Direct heals are easy, if they match the filter then return them
if( ( pending.bitType == DIRECT_HEALS or pending.bitType == BOMB_HEALS ) and ( not time or endTime <= time ) ) then
healAmount = healAmount + amount * stack
-- Channeled heals and hots, have to figure out how many times it'll tick within the given time band
elseif( ( pending.bitType == CHANNEL_HEALS or pending.bitType == HOT_HEALS ) and endTime > currentTime ) then
local ticksLeft = pending[i + 4]
if( not time or time >= endTime ) then
if( not pending.hasVariableTicks ) then
healAmount = healAmount + (amount * stack) * ticksLeft
else
for _, heal in pairs(amount) do
healAmount = healAmount + (heal * stack)
end
end
else
local secondsLeft = endTime - currentTime
local bandSeconds = time - currentTime
local ticks = math.floor(math.min(bandSeconds, secondsLeft) / pending.tickInterval)
local nextTickIn = secondsLeft % pending.tickInterval
local fractionalBand = bandSeconds % pending.tickInterval
if( nextTickIn > 0 and nextTickIn < fractionalBand ) then
ticks = ticks + 1
end
 
if( not pending.hasVariableTicks ) then
healAmount = healAmount + (amount * stack) * math.min(ticks, ticksLeft)
else
for i=1, math.min(ticks, #(amount)) do
healAmount = healAmount + (amount[i] * stack)
end
end
end
end
end
end
end
end
 
return healAmount
end
 
-- Gets healing amount using the passed filters
function HealComm:GetHealAmount(guid, bitFlag, time, casterGUID)
local amount = 0
if( casterGUID and pendingHeals[casterGUID] ) then
amount = filterData(pendingHeals[casterGUID], guid, bitFlag, time)
elseif( not casterGUID ) then
for _, spells in pairs(pendingHeals) do
amount = amount + filterData(spells, guid, bitFlag, time)
end
end
 
return amount > 0 and amount or nil
end
 
-- Gets healing amounts for everyone except the player using the passed filters
function HealComm:GetOthersHealAmount(guid, bitFlag, time)
local amount = 0
for casterGUID, spells in pairs(pendingHeals) do
if( casterGUID ~= playerGUID ) then
amount = amount + filterData(spells, guid, bitFlag, time)
end
end
 
return amount > 0 and amount or nil
end
 
function HealComm:GetCasterHealAmount(guid, bitFlag, time)
local amount = pendingHeals[guid] and filterData(pendingHeals[guid], nil, bitFlag, time, true) or 0
return amount > 0 and amount or nil
end
 
-- Healing class data
-- Thanks to Gagorian (DrDamage) for letting me steal his formulas and such
local playerHealModifier, playerCurrentRelic = 1
 
local playerModifiers, averageHeal, rankNumbers = HealComm.playerModifiers, HealComm.averageHeal, HealComm.rankNumbers
local guidToUnit, guidToGroup, glyphCache = HealComm.guidToUnit, HealComm.guidToGroup, HealComm.glyphCache
 
-- UnitBuff priortizes our buffs over everyone elses when there is a name conflict, so yay for that
local function unitHasAura(unit, name)
return select(8, UnitBuff(unit, name)) == "player"
end
 
-- Note because I always forget on the order:
-- Talents that effective the coeffiency of spell power to healing are first and are tacked directly onto the coeffiency (Empowered Rejuvenation)
-- Penalty modifiers (downranking/spell level too low) are applied directly to the spell power
-- Spell power modifiers are then applied to the spell power
-- Heal modifiers are applied after all of that
-- Crit modifiers are applied after
-- Any other modifiers such as Mortal Strike or Avenging Wrath are applied after everything else
local function calculateGeneralAmount(level, amount, spellPower, spModifier, healModifier)
-- Apply downranking penalities for spells below 20
local penalty = level > 20 and 1 or (1 - ((20 - level) * 0.0375))
 
-- Apply further downranking penalities
spellPower = spellPower * (penalty * math.min(1, math.max(0, 1 - (playerLevel - level - 11) * 0.05)))
 
-- Do the general factoring
return healModifier * (amount + (spellPower * spModifier))
end
 
-- For spells like Wild Growth, it's a waste to do the calculations for each tick, easier to calculate spell power now and then manually calculate it all after
local function calculateSpellPower(level, spellPower)
-- Apply downranking penalities for spells below 20
local penalty = level > 20 and 1 or (1 - ((20 - level) * 0.0375))
 
-- Apply further downranking penalities
return spellPower * (penalty * math.min(1, math.max(0, 1 - (playerLevel - level - 11) * 0.05)))
end
 
-- Yes silly function, just cleaner to look at
local function avg(a, b)
return (a + b) / 2
end
 
--[[
What the different callbacks do:
 
AuraHandler: Specific aura tracking needed for this class, who has Beacon up on them and such
 
ResetChargeData: Due to spell "queuing" you can't always rely on aura data for buffs that last one or two casts, for example Divine Favor (+100% crit, one spell)
if you cast Holy Light and queue Flash of Light the library would still see they have Divine Favor and give them crits on both spells. The reset means that the flag that indicates
they have the aura can be killed and if they interrupt the cast then it will call this and let you reset the flags.
 
What happens in terms of what the client thinks and what actually is, is something like this:
 
UNIT_SPELLCAST_START, Holy Light -> Divine Favor up
UNIT_SPELLCAST_SUCCEEDED, Holy Light -> Divine Favor up (But it was really used)
UNIT_SPELLCAST_START, Flash of Light -> Divine Favor up (It's not actually up but auras didn't update)
UNIT_AURA -> Divine Favor up (Split second where it still thinks it's up)
UNIT_AURA -> Divine Favor faded (Client catches up and realizes it's down)
 
CalculateHealing: Calculates the healing value, does all the formula calculations talent modifiers and such
 
CalculateHotHealing: Used specifically for calculating the heals of hots
 
GetHealTargets: Who the heal is going to hit, used for setting extra targets for Beacon of Light + Paladin heal or Prayer of Healing.
The returns should either be:
 
"compressedGUID1,compressedGUID2,compressedGUID3,compressedGUID4", healthAmount
Or if you need to set specific healing values for one GUID it should be
"compressedGUID1,healthAmount1,compressedGUID2,healAmount2,compressedGUID3,healAmount3", -1
 
The latter is for cases like Glyph of Healing Wave where you need a heal for 1,000 on A and a heal for 200 on the player for B without sending 2 events.
The -1 tells the library to look in the GUId list for the heal amounts
 
**NOTE** Any GUID returned from GetHealTargets must be compressed through a call to compressGUID[guid]
]]
 
local CalculateHealing, GetHealTargets, AuraHandler, CalculateHotHealing, ResetChargeData, LoadClassData
 
-- DRUIDS
-- All data is accurate as of 3.2.2 (build 10392)
if( playerClass == "DRUID" ) then
LoadClassData = function()
-- Rejuvenation
local Rejuvenation = GetSpellInfo(774)
hotData[Rejuvenation] = {interval = 3,
levels = {4, 10, 16, 22, 28, 34, 40, 46, 52, 58, 60, 63, 69, 75, 80}, averages = {32, 56, 116, 180, 244, 304, 388, 488, 608, 756, 888, 932, 1060, 1192, 1690}}
-- Regrowth
local Regrowth = GetSpellInfo(8936)
hotData[Regrowth] = {interval = 3, ticks = 7, coeff = 1.316,
levels = {12, 18, 24, 30, 36, 42, 48, 54, 60, 65, 71, 77}, averages = {98, 175, 259, 343, 427, 546, 686, 861, 1064, 1274, 1792, 2345}}
-- Lifebloom
local Lifebloom = GetSpellInfo(33763)
hotData[Lifebloom] = {interval = 1, ticks = 7, coeff = 0.66626, dhCoeff = 0.34324 * 0.8, levels = {64, 72, 80}, averages = {224, 287, 371}, bomb = {480, 616, 776}}
-- Wild Growth
local WildGrowth = GetSpellInfo(48438)
hotData[WildGrowth] = {interval = 1, ticks = 7, coeff = 0.8056, levels = {60, 70, 75, 80}, averages = {686, 861, 1239, 1442}}
 
-- Regrowth
spellData[Regrowth] = {coeff = 0.2867,
levels = hotData[Regrowth].levels,
averages = {avg(84, 98), avg(164, 188), avg(240, 274), avg(318, 360), avg(405, 457), avg(511, 575), avg(646, 724), avg(809, 905), avg(1003, 1119), avg(1215, 1355), avg(1710, 1908), avg(2234, 2494)},
increase = {122, 155, 173, 180, 180, 178, 169, 156, 136, 115, 97, 23}}
-- Healing Touch
local HealingTouch = GetSpellInfo(5185)
spellData[HealingTouch] = {
levels = {1, 8, 14, 20, 26, 32, 38, 44, 50, 56, 60, 62, 69, 74, 79},
averages = {avg(37, 51), avg(88, 112), avg(195, 243), avg(363, 445), avg(490, 594), avg(636, 766), avg(802, 960), avg(1199, 1427), avg(1299, 1539), avg(1620, 1912), avg(1944, 2294), avg(2026, 2392), avg(2321, 2739), avg(3223, 3805), avg(3750, 4428)}}
-- Nourish
local Nourish = GetSpellInfo(50464)
spellData[Nourish] = {coeff = 0.358005, levels = {80}, averages = {avg(1883, 2187)}}
-- Tranquility
local Tranquility = GetSpellInfo(740)
spellData[Tranquility] = {coeff = 1.144681, ticks = 4, levels = {30, 40, 50, 60, 70, 75, 80}, averages = {351, 515, 765, 1097, 1518, 2598, 3035}}
 
-- Talent data, these are filled in later and modified on talent changes
-- Master Shapeshifter (Multi)
local MasterShapeshifter = GetSpellInfo(48411)
talentData[MasterShapeshifter] = {mod = 0.02, current = 0}
-- Gift of Nature (Add)
local GiftofNature = GetSpellInfo(17104)
talentData[GiftofNature] = {mod = 0.02, current = 0}
-- Empowered Touch (Add, increases spell power HT/Nourish gains)
local EmpoweredTouch = GetSpellInfo(33879)
talentData[EmpoweredTouch] = {mod = 0.2, current = 0}
-- Empowered Rejuvenation (Multi, this ups both the direct heal and the hot)
local EmpoweredRejuv = GetSpellInfo(33886)
talentData[EmpoweredRejuv] = {mod = 0.04, current = 0}
-- Genesis (Add)
local Genesis = GetSpellInfo(57810)
talentData[Genesis] = {mod = 0.01, current = 0}
-- Improved Rejuvenation (Add)
local ImprovedRejuv = GetSpellInfo(17111)
talentData[ImprovedRejuv] = {mod = 0.05, current = 0}
-- Nature's Splendor (+3s Rejuv/+6s Regrowth/+2s Lifebloom)
local NaturesSplendor = GetSpellInfo(57865)
talentData[NaturesSplendor] = {mod = 1, current = 0}
 
local TreeofLife = GetSpellInfo(33891)
local Innervate = GetSpellInfo(29166)
 
-- Set data
-- 2 piece, +6 seconds to Regrowth
itemSetsData["T5 Resto"] = {30216, 30217, 30219, 30220, 30221}
-- +5% more healing to Nourish per hot
itemSetsData["T7 Resto"] = {40460, 40461, 40462, 40463, 40465, 39531, 39538, 39539, 39542, 39543}
--itemSetsData["T8 Resto"] = {46183, 46184, 46185, 46186, 46187, 45345, 45346, 45347, 45348, 45349}
--itemSetsData["T9 Resto"] = {48102, 48129, 48130, 48131, 48132, 48153, 48154, 48155, 48156, 48157, 48133, 48134, 48135, 48136, 48137, 48142, 48141, 48140, 48139, 48138, 48152, 48151, 48150, 48149, 48148, 48143, 48144, 48145, 48146, 48147}
-- 2 piece, 30% less healing lost on WG
itemSetsData["T10 Resto"] = {50106, 50107, 50108, 50109, 50113, 51139, 51138, 51137, 51136, 51135}
 
local hotTotals, hasRegrowth = {}, {}
AuraHandler = function(unit, guid)
hotTotals[guid] = 0
if( unitHasAura(unit, Rejuvenation) ) then hotTotals[guid] = hotTotals[guid] + 1 end
if( unitHasAura(unit, Lifebloom) ) then hotTotals[guid] = hotTotals[guid] + 1 end
if( unitHasAura(unit, WildGrowth) ) then hotTotals[guid] = hotTotals[guid] + 1 end
if( unitHasAura(unit, Regrowth) ) then
hasRegrowth[guid] = true
hotTotals[guid] = hotTotals[guid] + 1
else
hasRegrowth[guid] = nil
end
end
 
GetHealTargets = function(bitType, guid, healAmount, spellName, hasVariableTicks)
-- Tranquility pulses on everyone within 30 yards, if they are in range of Innervate they'll get Tranquility
if( spellName == Tranquility ) then
local targets = compressGUID[playerGUID]
local playerGroup = guidToGroup[playerGUID]
 
for groupGUID, id in pairs(guidToGroup) do
if( id == playerGroup and playerGUID ~= groupGUID and IsSpellInRange(Innervate, guidToUnit[groupGUID]) == 1 ) then
targets = targets .. "," .. compressGUID[groupGUID]
end
end
 
return targets, healAmount
elseif( hasVariableTicks ) then
healAmount = table.concat(healAmount, "@")
end
 
return compressGUID[guid], healAmount
end
 
-- Calculate hot heals
local wgTicks = {}
CalculateHotHealing = function(guid, spellID)
local spellName, spellRank = GetSpellInfo(spellID)
local rank = HealComm.rankNumbers[spellRank]
local healAmount = hotData[spellName].averages[rank]
local spellPower = GetSpellBonusHealing()
local healModifier, spModifier = playerHealModifier, 1
local bombAmount, totalTicks
 
healModifier = healModifier + talentData[GiftofNature].current
healModifier = healModifier + talentData[Genesis].current
 
-- Master Shapeshifter does not apply directly when using Lifebloom
if( unitHasAura("player", TreeofLife) ) then
healModifier = healModifier * (1 + talentData[MasterShapeshifter].current)
 
-- 32387 - Idol of the Raven Godess, +44 SP while in TOL
if( playerCurrentRelic == 32387 ) then
spellPower = spellPower + 44
end
end
 
-- Rejuvenation
if( spellName == Rejuvenation ) then
healModifier = healModifier + talentData[ImprovedRejuv].current
 
-- 25643 - Harold's Rejuvenation Broach, +86 Rejuv SP
if( playerCurrentRelic == 25643 ) then
spellPower = spellPower + 86
-- 22398 - Idol of Rejuvenation, +50 SP to Rejuv
elseif( playerCurrentRelic == 22398 ) then
spellPower = spellPower + 50
end
 
local duration, ticks
if( IS_BUILD30300 ) then
duration = 15
ticks = 5
totalTicks = 5
else
duration = rank > 14 and 15 or 12
ticks = duration / hotData[spellName].interval
totalTicks = ticks
end
 
spellPower = spellPower * (((duration / 15) * 1.88) * (1 + talentData[EmpoweredRejuv].current))
spellPower = spellPower / ticks
healAmount = healAmount / ticks
 
--38366 - Idol of Pure Thoughts, +33 SP base per tick
if( playerCurrentRelic == 38366 ) then
spellPower = spellPower + 33
end
 
-- Nature's Splendor, +6 seconds
if( talentData[NaturesSplendor].mod >= 1 ) then totalTicks = totalTicks + 1 end
 
-- Regrowth
elseif( spellName == Regrowth ) then
spellPower = spellPower * (hotData[spellName].coeff * (1 + talentData[EmpoweredRejuv].current))
spellPower = spellPower / hotData[spellName].ticks
healAmount = healAmount / hotData[spellName].ticks
 
totalTicks = 7
-- Nature's Splendor, +6 seconds
if( talentData[NaturesSplendor].mod >= 1 ) then totalTicks = totalTicks + 2 end
-- T5 Resto, +6 seconds
if( equippedSetCache["T5 Resto"] >= 2 ) then totalTicks = totalTicks + 2 end
 
-- Lifebloom
elseif( spellName == Lifebloom ) then
-- Figure out the bomb heal, apparently Gift of Nature double dips and will heal 10% for the HOT + 10% again for the direct heal
local bombSpell = spellPower * (hotData[spellName].dhCoeff * 1.88)
bombAmount = math.ceil(calculateGeneralAmount(hotData[spellName].levels[rank], hotData[spellName].bomb[rank], bombSpell, spModifier, healModifier + talentData[GiftofNature].current))
 
-- Figure out the hot tick healing
spellPower = spellPower * (hotData[spellName].coeff * (1 + talentData[EmpoweredRejuv].current))
spellPower = spellPower / hotData[spellName].ticks
healAmount = healAmount / hotData[spellName].ticks
 
-- Idol of Lush Moss, +125 SP per tick
if( playerCurrentRelic == 40711 ) then
spellPower = spellPower + 125
-- Idol of the Emerald Queen, +47 SP per tick
elseif( playerCurrentRelic == 27886 ) then
spellPower = spellPower + 47
end
 
-- Figure out total ticks
totalTicks = 7
 
-- Glyph of Lifebloom, +1 second
if( glyphCache[54826] ) then totalTicks = totalTicks + 1 end
-- Nature's Splendor, +2 seconds
if( talentData[NaturesSplendor].mod >= 1 ) then totalTicks = totalTicks + 1 end
 
-- Wild Growth
elseif( spellName == WildGrowth ) then
spellPower = spellPower * (hotData[spellName].coeff * (1 + talentData[EmpoweredRejuv].current))
spellPower = spellPower / hotData[spellName].ticks
spellPower = calculateSpellPower(hotData[spellName].levels[rank], spellPower)
healAmount = healAmount / hotData[spellName].ticks
 
--if( equippedSetCache["T10 Resto"] >= 2 ) then
 
table.wipe(wgTicks)
local tickModifier = healAmount / hotData[spellName].ticks
for i=1, hotData[spellName].ticks do
table.insert(wgTicks, math.ceil(healModifier * ((healAmount + tickModifier * (4 - i)) + (spellPower * spModifier))))
end
 
return HOT_HEALS, wgTicks, hotData[spellName].ticks, hotData[spellName].interval, nil, true
end
 
healAmount = calculateGeneralAmount(hotData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
return HOT_HEALS, math.ceil(healAmount), totalTicks, hotData[spellName].interval, bombAmount
end
 
-- Calcualte direct and channeled heals
CalculateHealing = function(guid, spellName, spellRank)
local healAmount = averageHeal[spellName][spellRank]
local spellPower = GetSpellBonusHealing()
local healModifier, spModifier = playerHealModifier, 1
local rank = HealComm.rankNumbers[spellRank]
 
-- Gift of Nature
healModifier = healModifier + talentData[GiftofNature].current
 
-- Master Shapeshifter does not apply directly when using Lifebloom
if( unitHasAura("player", TreeofLife) ) then
healModifier = healModifier * (1 + talentData[MasterShapeshifter].current)
 
-- 32387 - Idol of the Raven Godess, +44 SP while in TOL
if( playerCurrentRelic == 32387 ) then
spellPower = spellPower + 44
end
end
 
-- Regrowth
if( spellName == Regrowth ) then
-- Glyph of Regrowth - +20% if target has Regrowth
if( glyphCache[54743] and hasRegrowth[guid] ) then
healModifier = healModifier * 1.20
end
 
spellPower = spellPower * ((spellData[spellName].coeff * 1.88) * (1 + talentData[EmpoweredRejuv].current))
-- Nourish
elseif( spellName == Nourish ) then
-- 46138 - Idol of Flourishing Life, +187 Nourish SP
if( playerCurrentRelic == 46138 ) then
spellPower = spellPower + 187
end
 
-- Apply any hot specific bonuses
local hots = hotTotals[guid]
if( hots and hots > 0 ) then
local bonus = 1.20
 
-- T7 Resto, +5% healing per each of the players hot on their target
if( equippedSetCache["T7 Resto"] >= 2 ) then
bonus = bonus + 0.05 * hots
end
 
-- Glyph of Nourish - 6% per HoT
if( glyphCache[62971] ) then
bonus = bonus + 0.06 * hots
end
 
healModifier = healModifier * bonus
end
 
spellPower = spellPower * ((spellData[spellName].coeff * 1.88) + talentData[EmpoweredTouch].spent * 0.10)
-- Healing Touch
elseif( spellName == HealingTouch ) then
-- Glyph of Healing Touch, -50% healing
if( glyphCache[54825] ) then
healModifier = healModifier - 0.50
end
 
-- Idol of the Avian Heart, +136 baseh ealing
if( playerCurrentRelic == 28568 ) then
healAmount = healAmount + 136
-- Idol of Health, +100 base healing
elseif( playerCurrentRelic == 22399 ) then
healAmount = healAmount + 100
end
 
-- Rank 1 - 3: 1.5/2/2.5 cast time, Rank 4+: 3 cast time
local castTime = rank > 3 and 3 or rank == 3 and 2.5 or rank == 2 and 2 or 1.5
spellPower = spellPower * (((castTime / 3.5) * 1.88) + talentData[EmpoweredTouch].current)
 
-- Tranquility
elseif( spellName == Tranquility ) then
healModifier = healModifier + talentData[Genesis].current
 
spellPower = spellPower * ((spellData[spellName].coeff * 1.88) * (1 + talentData[EmpoweredRejuv].current))
spellPower = spellPower / spellData[spellName].ticks
end
 
healAmount = calculateGeneralAmount(spellData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
 
-- 100% chance to crit with Nature, this mostly just covers fights like Loatheb where you will basically have 100% crit
if( GetSpellCritChance(4) >= 100 ) then
healAmount = healAmount * 1.50
end
 
if( spellData[spellName].ticks ) then
return CHANNEL_HEALS, math.ceil(healAmount), spellData[spellName].ticks
end
 
return DIRECT_HEALS, math.ceil(healAmount)
end
end
end
 
-- PALADINS
-- All data is accurate as of 3.2.2 (build 10392)
if( playerClass == "PALADIN" ) then
LoadClassData = function()
-- Hot data, this is just so it realizes that FoL can be a hot so it will call the calculator
--local FlashofLight = GetSpellInfo(19750)
--hotData[FlashofLight] = true
 
-- Spell data
-- Holy Light
local HolyLight = GetSpellInfo(635)
spellData[HolyLight] = {coeff = 2.5 / 3.5 * 1.25,
levels = {1, 6, 14, 22, 30, 38, 46, 54, 60, 62, 70, 75, 80},
averages = {avg(50, 60), avg(96, 116), avg(203, 239), avg(397, 455), avg(628, 708), avg(894, 998), avg(1209, 1349), avg(1595, 1777), avg(2034, 2266), avg(2232, 2486), avg(2818, 3138), avg(4199, 4677), avg(4888, 5444)},
increase = {63, 81, 112, 139, 155, 159, 156, 135, 116, 115, 70, 52, 0}}
-- Flash of Light
local FlashofLight = GetSpellInfo(19750)
spellData[FlashofLight] = {coeff = 1.5 / 3.5 * 1.25,
levels = {20, 26, 34, 42, 50, 58, 66, 74, 79},
averages = {avg(81, 93), avg(124, 144), avg(189, 211), avg(256, 288), avg(346, 390), avg(445, 499), avg(588, 658), avg(682, 764), avg(785, 879)},
increase = {60, 70, 73, 72, 66, 57, 42, 20, 3}}
 
-- Talent data
-- Need to figure out a way of supporting +6% healing from imp devo aura, might not be able to
-- Healing Light (Add)
local HealingLight = GetSpellInfo(20237)
talentData[HealingLight] = {mod = 0.04, current = 0}
-- Divinity (Add)
local Divinity = GetSpellInfo(63646)
talentData[Divinity] = {mod = 0.01, current = 0}
-- Touched by the Light (Add?)
local TouchedbytheLight = GetSpellInfo(53592)
talentData[TouchedbytheLight] = {mod = 0.10, current = 0}
-- 100% of your heal on someone within range of your beacon heals the beacon target too
local BeaconofLight = GetSpellInfo(53563)
-- 100% chance to crit
local DivineFavor = GetSpellInfo(20216)
-- Seal of Light + Glyph = 5% healing
local SealofLight = GetSpellInfo(20165)
 
local flashLibrams = {[42615] = 375, [42614] = 331, [42613] = 293, [42612] = 204, [28592] = 89, [25644] = 79, [23006] = 43, [23201] = 28}
local holyLibrams = {[45436] = 160, [40268] = 141, [28296] = 47}
 
-- Holy Shock crits put a hot that heals for 15% of the HS over 9s
--itemSetsData["T8 Holy"] = { 45370, 45371, 45372, 45373, 45374, 46178, 46179, 46180, 46181, 46182 }
-- +100% to the hot when using Flash of Light + Sacred Shield
--itemSetsData["T9 Holy"] = { 48595, 48596, 48597, 48598, 48599, 48564, 48566, 48568, 48572, 48574, 48593, 48591, 48592, 48590, 48594, 48588, 48586, 48587, 48585, 48589, 48576, 48578, 48577, 48579, 48575, 48583, 48581, 48582, 48580, 48584}
 
-- Need the GUID of whoever has beacon on them so we can make sure they are visible to us and so we can check the mapping
local activeBeaconGUID, hasDivineFavor
AuraHandler = function(unit, guid)
if( unitHasAura(unit, BeaconofLight) ) then
activeBeaconGUID = guid
elseif( activeBeaconGUID == guid ) then
activeBeaconGUID = nil
end
 
-- Check Divine Favor
if( unit == "player" ) then
hasDivineFavor = unitHasAura("player", DivineFavor)
end
end
 
ResetChargeData = function(guid)
hasDivineFavor = unitHasAura("player", DivineFavor)
end
 
-- Check for beacon when figuring out who to heal
GetHealTargets = function(bitType, guid, healAmount, spellName, hasVariableTicks)
if( activeBeaconGUID and activeBeaconGUID ~= guid and guidToUnit[activeBeaconGUID] and UnitIsVisible(guidToUnit[activeBeaconGUID]) ) then
return string.format("%s,%s", compressGUID[guid], compressGUID[activeBeaconGUID]), healAmount
elseif( hasVariableTicks ) then
healAmount = table.concat(healAmount, "@")
end
 
return compressGUID[guid], healAmount
end
 
-- If only every other class was as easy as Paladins
CalculateHealing = function(guid, spellName, spellRank)
local healAmount = averageHeal[spellName][spellRank]
local spellPower = GetSpellBonusHealing()
local healModifier, spModifier = playerHealModifier, 1
local rank = HealComm.rankNumbers[spellRank]
 
-- Glyph of Seal of Light, +5% healing if the player has Seal of Light up
if( glyphCache[54943] and unitHasAura("player", SealofLight) ) then
healModifier = healModifier * 1.05
end
 
healModifier = healModifier * (1 + talentData[Divinity].current)
healModifier = healModifier * (1 + talentData[HealingLight].current)
 
-- Apply extra spell power based on libram
if( playerCurrentRelic ) then
if( spellName == HolyLight and holyLibrams[playerCurrentRelic] ) then
spellPower = spellPower + holyLibrams[playerCurrentRelic]
elseif( spellName == FlashofLight and flashLibrams[playerCurrentRelic] ) then
spellPower = spellPower + flashLibrams[playerCurrentRelic]
end
end
 
-- Normal calculations
spellPower = spellPower * (spellData[spellName].coeff * 1.88)
healAmount = calculateGeneralAmount(spellData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
 
-- Divine Favor, 100% chance to crit
if( hasDivineFavor ) then
hasDivineFavor = nil
healAmount = healAmount * (1.50 + talentData[TouchedbytheLight].current)
-- Or the player has over a 100% chance to crit with Holy spells
elseif( GetSpellCritChance(2) >= 100 ) then
healAmount = healAmount * (1.50 + talentData[TouchedbytheLight].current)
end
 
return DIRECT_HEALS, math.ceil(healAmount)
end
end
end
 
-- PRIESTS
-- Accurate as of 3.2.2 (build 10392)
if( playerClass == "PRIEST" ) then
LoadClassData = function()
-- Hot data
local Renew = GetSpellInfo(139)
hotData[Renew] = {coeff = 1, interval = 3, ticks = 5, levels = {8, 14, 20, 26, 32, 38, 44, 50, 56, 60, 65, 70, 75, 80}, averages = {45, 100, 175, 245, 315, 400, 510, 650, 810, 970, 1010, 1110, 1235, 1400}}
--local GlyphofPoH = GetSpellInfo(56161)
--hotData[GlyphofPoH] = {isMulti = true, interval = 3}
 
-- Spell data
-- Greater Heal
local GreaterHeal = GetSpellInfo(2060)
spellData[GreaterHeal] = {coeff = 3 / 3.5, levels = {40, 46, 52, 58, 60, 63, 68, 73, 78}, increase = {204, 197, 184, 165, 162, 142, 111, 92, 30},
averages = {avg(899, 1013), avg(1149, 1289), avg(1437, 1609), avg(1798, 2006), avg(1966, 2194), avg(2074, 2410), avg(2394, 2784), avg(3395, 3945), avg(3950, 4590)}}
-- Prayer of Healing
local PrayerofHealing = GetSpellInfo(596)
spellData[PrayerofHealing] = {coeff = 0.2798, levels = {30, 40, 50, 60, 60, 68, 76}, increase = {65, 64, 60, 48, 50, 33, 18},
averages = {avg(301, 321), avg(444, 472), avg(657, 695), avg(939, 991), avg(997, 1053), avg(1246, 1316), avg(2091, 2209)}}
-- Flash Heal
local FlashHeal = GetSpellInfo(2061)
spellData[FlashHeal] = {coeff = 1.5 / 3.5, levels = {20, 26, 32, 38, 44, 52, 58, 61, 67, 73, 79}, increase = {114, 118, 120, 117, 118, 111, 100, 89, 67, 56, 9},
averages = {avg(193, 237), avg(258, 314), avg(327, 393), avg(400, 478), avg(518, 616), avg(644, 764), avg(812, 958), avg(913, 1059), avg(1101, 1279), avg(1578, 1832), avg(1887, 2198)}}
-- Binding Heal
local BindingHeal = GetSpellInfo(32546)
spellData[BindingHeal] = {coeff = 1.5 / 3.5, levels = {64, 72, 78}, averages = {avg(1042, 1338), avg(1619, 2081), avg(1952, 2508)}, increase = {30, 24, 7}}
-- Penance
local Penance = GetSpellInfo(53007)
spellData[Penance] = {coeff = 0.857, ticks = 3, levels = {60, 70, 75, 80}, averages = {avg(670, 756), avg(805, 909), avg(1278, 1442), avg(1484, 1676)}}
-- Heal
local Heal = GetSpellInfo(2054)
spellData[Heal] = {coeff = 3 / 3.5, levels = {16, 22, 28, 34}, averages = {avg(295, 341), avg(429, 491), avg(566, 642), avg(712, 804)}, increase = {153, 185, 208, 207}}
-- Lesser Heal
local LesserHeal = GetSpellInfo(2050)
spellData[LesserHeal] = {levels = {1, 4, 10}, averages = {avg(46, 56), avg(71, 85), avg(135, 157)}, increase = {71, 83, 112}}
 
-- Talent data
local Grace = GetSpellInfo(47517)
-- Spiritual Healing (Add)
local SpiritualHealing = GetSpellInfo(14898)
talentData[SpiritualHealing] = {mod = 0.02, current = 0}
-- Empowered Healing (Add, also 0.04 for FH/BH)
local EmpoweredHealing = GetSpellInfo(33158)
talentData[EmpoweredHealing] = {mod = 0.08, current = 0}
-- Blessed Resilience (Add)
local BlessedResilience = GetSpellInfo(33142)
talentData[BlessedResilience] = {mod = 0.01, current = 0}
-- Focused Power (Add)
local FocusedPower = GetSpellInfo(33190)
talentData[FocusedPower] = {mod = 0.02, current = 0}
-- Divine Providence (Add)
local DivineProvidence = GetSpellInfo(47567)
talentData[DivineProvidence] = {mod = 0.02, current = 0}
-- Improved Renew (Add)
local ImprovedRenew = GetSpellInfo(14908)
talentData[ImprovedRenew] = {mod = 0.05, current = 0}
-- Empowered Renew (Multi, spell power)
local EmpoweredRenew = GetSpellInfo(63534)
talentData[EmpoweredRenew] = {mod = 0.05, current = 0}
-- Twin Disciplines (Add)
local TwinDisciplines = GetSpellInfo(47586)
talentData[TwinDisciplines] = {mod = 0.01, current = 0}
 
-- Keep track of who has grace on them
local activeGraceGUID, activeGraceModifier
AuraHandler = function(unit, guid)
local stack, _, _, _, caster = select(4, UnitBuff(unit, Grace))
if( caster == "player" ) then
activeGraceModifier = stack * 0.03
activeGraceGUID = guid
elseif( activeGraceGUID == guid ) then
activeGraceGUID = nil
end
end
 
-- Check for beacon when figuring out who to heal
GetHealTargets = function(bitType, guid, healAmount, spellName, hasVariableTicks)
if( spellName == BindingHeal ) then
return string.format("%s,%s", compressGUID[guid], compressGUID[playerGUID]), healAmount
elseif( spellName == PrayerofHealing ) then
local targets = compressGUID[guid]
local group = guidToGroup[guid]
 
for groupGUID, id in pairs(guidToGroup) do
if( id == group and guid ~= groupGUID and UnitIsVisible(guidToUnit[groupGUID]) ) then
targets = targets .. "," .. compressGUID[groupGUID]
end
end
 
return targets, healAmount
elseif( hasVariableTicks ) then
healAmount = table.concat(healAmount, "@")
end
 
return compressGUID[guid], healAmount
end
 
CalculateHotHealing = function(guid, spellID)
local spellName, spellRank = GetSpellInfo(spellID)
local rank = HealComm.rankNumbers[spellRank]
local healAmount = hotData[spellName].averages[rank]
local spellPower = GetSpellBonusHealing()
local healModifier, spModifier = playerHealModifier, 1
local totalTicks
 
-- Add grace if it's active on them
if( activeGraceGUID == guid and activeGraceModifier ) then
healModifier = healModifier + activeGraceModifier
end
 
healModifier = healModifier + talentData[FocusedPower].current
healModifier = healModifier + talentData[BlessedResilience].current
healModifier = healModifier + talentData[SpiritualHealing].current
 
if( spellName == Renew ) then
healModifier = healModifier + talentData[ImprovedRenew].current
healModifier = healModifier + talentData[TwinDisciplines].current
 
-- Glyph of Renew, one less tick for +25% healing per tick. As this heals the same just faster, it has to be a flat 25% modifier
if( glyphCache[55674] ) then
healModifier = healModifier + 0.25
totalTicks = 4
else
totalTicks = 5
end
 
spellPower = spellPower * ((hotData[spellName].coeff * 1.88) * (1 + (talentData[EmpoweredRenew].current)))
spellPower = spellPower / hotData[spellName].ticks
healAmount = healAmount / hotData[spellName].ticks
 
end
 
healAmount = calculateGeneralAmount(hotData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
return HOT_HEALS, math.ceil(healAmount), totalTicks, hotData[spellName].interval
end
 
-- If only every other class was as easy as Paladins
CalculateHealing = function(guid, spellName, spellRank)
local healAmount = averageHeal[spellName][spellRank]
local rank = HealComm.rankNumbers[spellRank]
local spellPower = GetSpellBonusHealing()
local healModifier, spModifier = playerHealModifier, 1
 
-- Add grace if it's active on them
if( activeGraceGUID == guid ) then
healModifier = healModifier + activeGraceModifier
end
 
healModifier = healModifier + talentData[FocusedPower].current
healModifier = healModifier + talentData[BlessedResilience].current
healModifier = healModifier + talentData[SpiritualHealing].current
 
-- Greater Heal
if( spellName == GreaterHeal ) then
spellPower = spellPower * ((spellData[spellName].coeff * 1.88) * (1 + talentData[EmpoweredHealing].current))
-- Flash Heal
elseif( spellName == FlashHeal ) then
spellPower = spellPower * ((spellData[spellName].coeff * 1.88) * (1 + talentData[EmpoweredHealing].spent * 0.04))
-- Binding Heal
elseif( spellName == BindingHeal ) then
healModifier = healModifier + talentData[DivineProvidence].current
spellPower = spellPower * ((spellData[spellName].coeff * 1.88) * (1 + talentData[EmpoweredHealing].spent * 0.04))
-- Penance
elseif( spellName == Penance ) then
spellPower = spellPower * (spellData[spellName].coeff * 1.88)
spellPower = spellPower / spellData[spellName].ticks
-- Prayer of Heaing
elseif( spellName == PrayerofHealing ) then
healModifier = healModifier + talentData[DivineProvidence].current
spellPower = spellPower * (spellData[spellName].coeff * 1.88)
-- Heal
elseif( spellName == Heal ) then
spellPower = spellPower * (spellData[spellName].coeff * 1.88)
-- Lesser Heal
elseif( spellName == LesserHeal ) then
local castTime = rank > 3 and 2.5 or rank == 2 and 2 or 1.5
spellPower = spellPower * ((castTime / 3.5) * 1.88)
end
 
healAmount = calculateGeneralAmount(spellData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
 
-- Player has over a 100% chance to crit with Holy spells
if( GetSpellCritChance(2) >= 100 ) then
healAmount = healAmount * 1.50
end
 
-- Penance ticks 3 times, but the first one is instant and as it will land before the comm get there, pretend that
-- it only has two ticks.
if( spellName == Penance ) then
return CHANNEL_HEALS, math.ceil(healAmount), 2
end
 
return DIRECT_HEALS, math.ceil(healAmount)
end
end
end
 
-- SHAMANS
-- All spells accurate as of 3.2.2 (build 10392)
if( playerClass == "SHAMAN" ) then
LoadClassData = function()
-- Hot data
-- Riptide
local Riptide = GetSpellInfo(61295)
hotData[Riptide] = {interval = 3, ticks = 5, coeff = 0.50, levels = {60, 70, 75, 80}, averages = {665, 885, 1435, 1670}}
-- Earthliving Weapon proc
local Earthliving = GetSpellInfo(52000)
hotData[Earthliving] = {interval = 3, ticks = 4, coeff = 0.80, levels = {30, 40, 50, 60, 70, 80}, averages = {116, 160, 220, 348, 456, 652}}
 
-- Spell data
-- Chain Heal
local ChainHeal = GetSpellInfo(1064)
spellData[ChainHeal] = {coeff = 2.5 / 3.5, levels = {40, 46, 54, 61, 68, 74, 80}, increase = {100, 95, 85, 72, 45, 22, 0},
averages = {avg(320, 368), avg(405, 465), avg(551, 629), avg(605, 691), avg(826, 942), avg(906, 1034), avg(1055, 1205)}}
-- Healing Wave
local HealingWave = GetSpellInfo(331)
spellData[HealingWave] = {levels = {1, 6, 12, 18, 24, 32, 40, 48, 56, 60, 63, 70, 75, 80},
averages = {avg(34, 44), avg(64, 78), avg(129, 155), avg(268, 316), avg(376, 440), avg(536, 622), avg(740, 854), avg(1017, 1167), avg(1367, 1561), avg(1620, 1850), avg(1725, 1969), avg(2134, 2438), avg(2624, 2996), avg(3034, 3466)},
increase = {55, 74, 102, 142, 151, 158, 156, 150, 132, 110, 107, 71, 40, 0}}
-- Lesser Healing Wave
local LesserHealingWave = GetSpellInfo(8004)
spellData[LesserHealingWave] = {coeff = 1.5 / 3.5, levels = {20, 28, 36, 44, 52, 60, 66, 72, 77}, increase = {102, 109, 110, 108, 100, 84, 58, 40, 18},
averages = {avg(162, 186), avg(247, 281), avg(337, 381), avg(458, 514), avg(631, 05), avg(832, 928), avg(1039, 1185), avg(1382, 1578), avg(1606, 1834)}}
 
-- Talent data
local EarthShield = GetSpellInfo(49284)
-- Improved Chain Heal (Multi)
local ImpChainHeal = GetSpellInfo(30872)
talentData[ImpChainHeal] = {mod = 0.10, current = 0}
-- Tidal Waves (Add, this is a buff)
local TidalWaves = GetSpellInfo(51566)
talentData[TidalWaves] = {mod = 0.04, current = 0}
-- Healing Way (Multi, this goes from 8 -> 16 -> 25 so have to manually do the conversion)
local HealingWay = GetSpellInfo(29206)
talentData[HealingWay] = {mod = 0, current = 0}
-- Purification (Add)
local Purification = GetSpellInfo(16178)
talentData[Purification] = {mod = 0.02, current = 0}
 
-- Set bonuses
-- T7 Resto 4 piece, +5% healing on Chain Heal and Healing Wave
itemSetsData["T7 Resto"] = {40508, 40509, 40510, 40512, 40513, 39583, 39588, 39589, 39590, 39591}
-- T9 Resto 2 piece, +20% healing to Riptide
itemSetsData["T9 Resto"] = {48280, 48281, 48282, 48283, 48284, 48295, 48296, 48297, 48298, 48299, 48301, 48302, 48303, 48304, 48300, 48306, 48307, 48308, 48309, 48305, 48286, 48287, 48288, 48289, 48285, 48293, 48292, 48291, 48290, 48294}
 
-- Totems
local lhwTotems = {[42598] = 320, [42597] = 267, [42596] = 236, [42595] = 204, [25645] = 79, [22396] = 80, [23200] = 53}
local chTotems = {[45114] = 243, [38368] = 102, [28523] = 87}
 
-- Keep track of who has riptide on them
local riptideData, earthshieldList = {}, {}
AuraHandler = function(unit, guid)
riptideData[guid] = unitHasAura(unit, Riptide) and true or nil
 
-- Currently, Glyph of Lesser Healing Wave + Any Earth Shield increase the healing not just the players own
if( UnitBuff(unit, EarthShield) ) then
earthshieldList[guid] = true
elseif( earthshieldList[guid] ) then
earthshieldList[guid] = nil
end
end
 
-- Cast was interrupted, recheck if we still have the auras up
ResetChargeData = function(guid)
riptideData[guid] = guidToUnit[guid] and unitHasAura(guidToUnit[guid], Riptide) and true or nil
end
 
-- Lets a specific override on how many people this will hit
GetHealTargets = function(bitType, guid, healAmount, spellName, hasVariableTicks)
-- Glyph of Healing Wave, heals you for 20% of your heal when you heal someone else
if( glyphCache[55440] and guid ~= playerGUID and spellName == HealingWave ) then
return string.format("%s,%d,%s,%d", compressGUID[guid], healAmount, compressGUID[playerGUID], healAmount * 0.20), -1
elseif( hasVariableTicks ) then
healAmount = table.concat(healAmount, "@")
end
 
return compressGUID[guid], healAmount
end
 
CalculateHotHealing = function(guid, spellID)
local spellName, spellRank = GetSpellInfo(spellID)
local rank = HealComm.rankNumbers[spellRank]
local healAmount = hotData[spellName].averages[rank]
local spellPower = GetSpellBonusHealing()
local healModifier, spModifier = playerHealModifier, 1
local totalTicks
 
healModifier = healModifier + talentData[Purification].current
 
-- Riptide
if( spellName == Riptide ) then
if( equippedSetCache["T9 Resto"] >= 2 ) then
spModifier = spModifier * 1.20
end
 
spellPower = spellPower * (hotData[spellName].coeff * 1.88)
spellPower = spellPower / hotData[spellName].ticks
healAmount = healAmount / hotData[spellName].ticks
 
totalTicks = hotData[spellName].ticks
-- Glyph of Riptide, +6 seconds
if( glyphCache[63273] ) then totalTicks = totalTicks + 2 end
 
-- Earthliving Weapon
elseif( spellName == Earthliving ) then
spellPower = (spellPower * (hotData[spellName].coeff * 1.88) * 0.45)
spellPower = spellPower / hotData[spellName].ticks
healAmount = healAmount / hotData[spellName].ticks
 
totalTicks = hotData[spellName].ticks
end
 
healAmount = calculateGeneralAmount(hotData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
return HOT_HEALS, healAmount, totalTicks, hotData[spellName].interval
end
 
 
-- If only every other class was as easy as Paladins
CalculateHealing = function(guid, spellName, spellRank)
local healAmount = averageHeal[spellName][spellRank]
local rank = HealComm.rankNumbers[spellRank]
local spellPower = GetSpellBonusHealing()
local healModifier, spModifier = playerHealModifier, 1
 
healModifier = healModifier + talentData[Purification].current
 
-- Chain Heal
if( spellName == ChainHeal ) then
healModifier = healModifier * (1 + talentData[ImpChainHeal].current)
healAmount = healAmount + (playerCurrentRelic and chTotems[playerCurrentRelic] or 0)
 
if( equippedSetCache["T7 Resto"] >= 4 ) then
healModifier = healModifier * 1.05
end
 
-- Add +25% from Riptide being up and reset the flag
if( riptideData[guid] ) then
healModifier = healModifier * 1.25
riptideData[guid] = nil
end
 
spellPower = spellPower * (spellData[spellName].coeff * 1.88)
-- Heaing Wave
elseif( spellName == HealingWave ) then
healModifier = healModifier * (talentData[HealingWay].spent == 3 and 1.25 or talentData[HealingWay].spent == 2 and 1.16 or talentData[HealingWay].spent == 1 and 1.08 or 1)
 
if( equippedSetCache["T7 Resto"] >= 4 ) then
healModifier = healModifier * 1.05
end
 
-- Totem of Spontaneous Regrowth, +88 Spell Power to Healing Wave
if( playerCurrentRelic == 27544 ) then
spellPower = spellPower + 88
end
 
local castTime = rank > 3 and 3 or rank == 3 and 2.5 or rank == 2 and 2 or 1.5
spellPower = spellPower * (((castTime / 3.5) * 1.88) + talentData[TidalWaves].current)
 
-- Lesser Healing Wave
elseif( spellName == LesserHealingWave ) then
-- Glyph of Lesser Healing Wave, +20% healing on LHW if target has ES up
if( glyphCache[55438] and earthshieldList[guid] ) then
healModifier = healModifier * 1.20
end
 
spellPower = spellPower + (playerCurrentRelic and lhwTotems[playerCurrentRelic] or 0)
spellPower = spellPower * ((spellData[spellName].coeff * 1.88) + talentData[TidalWaves].spent * 0.02)
end
 
healAmount = calculateGeneralAmount(spellData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
 
-- Player has over a 100% chance to crit with Nature spells
if( GetSpellCritChance(4) >= 100 ) then
healAmount = healAmount * 1.50
end
 
-- Apply the final modifier of any MS or self heal increasing effects
return DIRECT_HEALS, math.ceil(healAmount)
end
end
end
 
local function getName(spellID)
local name = GetSpellInfo(spellID)
--[===[@debug@
if( not name ) then
print(string.format("%s-r%s: Failed to find spellID %d", major, minor, spellID))
end
--@end-debug@]===]
return name or ""
end
 
-- Healing modifiers
HealComm.currentModifiers = HealComm.currentModifiers or {}
 
HealComm.selfModifiers = HealComm.selfModifiers or {
[64850] = 0.50, -- Unrelenting Assault
[65925] = 0.50, -- Unrelenting Assault
[54428] = 0.50, -- Divine Plea
[32346] = 0.50, -- Stolen Soul
[64849] = 0.75, -- Unrelenting Assault
[72221] = 1.05, -- Luck of the Draw
[70873] = 1.10, -- Emerald Vigor (Valithria Dreamwalker)
[31884] = 1.20, -- Avenging Wrath
}
 
HealComm.selfModifiers[getName(72390)] = 0.75 -- Hopelessness
 
-- The only spell in the game with a name conflict is Ray of Pain from the Nagrand Void Walkers
HealComm.healingModifiers = HealComm.healingModifiers or {
[getName(30843)] = 0.00, -- Enfeeble
[getName(41292)] = 0.00, -- Aura of Suffering
[getName(59513)] = 0.00, -- Embrace of the Vampyr
[getName(55593)] = 0.00, -- Necrotic Aura
[getName(34625)] = 0.25, -- Demolish
[getName(19716)] = 0.25, -- Gehennas' Curse
[getName(24674)] = 0.25, -- Veil of Shadow
[getName(69633)] = 0.25, -- Veil of Shadow, in German this is translated differently from the one above
-- Wound Poison still uses a unique spellID/spellName despite the fact that it's a static 50% reduction.
[getName(13218)] = 0.50, -- 1
[getName(13222)] = 0.50, -- 2
[getName(13223)] = 0.50, -- 3
[getName(13224)] = 0.50, -- 4
[getName(27189)] = 0.50, -- 5
[getName(57974)] = 0.50, -- 6
[getName(57975)] = 0.50, -- 7
[getName(20900)] = 0.50, -- Aimed Shot
[getName(44534)] = 0.50, -- Wretched Strike
[getName(21551)] = 0.50, -- Mortal Strike
[getName(40599)] = 0.50, -- Arcing Smash
[getName(36917)] = 0.50, -- Magma-Throwser's Curse
[getName(23169)] = 0.50, -- Brood Affliction: Green
[getName(22859)] = 0.50, -- Mortal Cleave
[getName(36023)] = 0.50, -- Deathblow
[getName(13583)] = 0.50, -- Curse of the Deadwood
[getName(32378)] = 0.50, -- Filet
[getName(35189)] = 0.50, -- Solar Strike
[getName(32315)] = 0.50, -- Soul Strike
[getName(60084)] = 0.50, -- The Veil of Shadow
[getName(45885)] = 0.50, -- Shadow Spike
[getName(69674)] = 0.50, -- Mutated Infection (Rotface)
[getName(63038)] = 0.75, -- Dark Volley
[getName(52771)] = 0.75, -- Wounding Strike
[getName(48291)] = 0.75, -- Fetid Healing
[getName(34366)] = 0.75, -- Ebon Poison
[getName(54525)] = 0.80, -- Shroud of Darkness (This might be wrong)
[getName(48301)] = 0.80, -- Mind Trauma (Improved Mind Blast)
[getName(68391)] = 0.80, -- Permafrost, the debuff is generic no way of seeing 7/13/20, go with 20
[getName(52645)] = 0.80, -- Hex of Weakness
[getName(34073)] = 0.85, -- Curse of the Bleeding Hollow
[getName(43410)] = 0.90, -- Chop
[getName(70588)] = 0.90, -- Suppression (Valithria Dreamwalker NPCs?)
[getName(34123)] = 1.06, -- Tree of Life
[getName(64844)] = 1.10, -- Divine Hymn
[getName(47788)] = 1.40, -- Guardian Spirit
[getName(38387)] = 1.50, -- Bane of Infinity
[getName(31977)] = 1.50, -- Curse of Infinity
[getName(41350)] = 2.00, -- Aura of Desire
}
 
HealComm.healingStackMods = HealComm.healingStackMods or {
-- Tenacity
[getName(58549)] = function(name, rank, icon, stacks) return icon == "Interface\\Icons\\Ability_Warrior_StrengthOfArms" and stacks ^ 1.18 or 1 end,
-- Focused Will
[getName(45242)] = function(name, rank, icon, stacks) return 1 + (stacks * (0.02 + rankNumbers[rank] / 100)) end,
-- Nether Portal - Dominance
[getName(30423)] = function(name, rank, icon, stacks) return 1 + stacks * 0.01 end,
-- Dark Touched
[getName(45347)] = function(name, rank, icon, stacks) return 1 - stacks * 0.04 end,
-- Necrotic Strike
[getName(60626)] = function(name, rank, icon, stacks) return 1 - stacks * 0.10 end,
-- Mortal Wound
[getName(28467)] = function(name, rank, icon, stacks) return 1 - stacks * 0.10 end,
-- Furious Strikes
[getName(56112)] = function(name, rank, icon, stacks) return 1 - stacks * 0.25 end,
}
 
local healingStackMods, selfModifiers = HealComm.healingStackMods, HealComm.selfModifiers
local healingModifiers, currentModifiers = HealComm.healingModifiers, HealComm.currentModifiers
 
local distribution
local CTL = ChatThrottleLib
local function sendMessage(msg)
if( distribution ) then
CTL:SendAddonMessage("BULK", COMM_PREFIX, msg, distribution)
end
end
 
-- Keep track of where all the data should be going
local instanceType
local function updateDistributionChannel()
local lastChannel = distribution
if( instanceType == "pvp" or instanceType == "arena" ) then
distribution = "BATTLEGROUND"
elseif( GetNumRaidMembers() > 0 ) then
distribution = "RAID"
elseif( GetNumPartyMembers() > 0 ) then
distribution = "PARTY"
else
distribution = nil
end
 
if( distribution == lastChannel ) then return end
 
-- If the player is not a healer, some events can be disabled until the players grouped.
if( distribution ) then
HealComm.eventFrame:RegisterEvent("CHAT_MSG_ADDON")
if( not isHealerClass ) then
HealComm.eventFrame:RegisterEvent("UNIT_AURA")
HealComm.eventFrame:RegisterEvent("UNIT_SPELLCAST_DELAYED")
HealComm.eventFrame:RegisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")
HealComm.eventFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
end
else
HealComm.eventFrame:UnregisterEvent("CHAT_MSG_ADDON")
if( not isHealerClass ) then
HealComm.eventFrame:UnregisterEvent("UNIT_AURA")
HealComm.eventFrame:UnregisterEvent("UNIT_SPELLCAST_DELAYED")
HealComm.eventFrame:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")
HealComm.eventFrame:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
end
end
end
 
-- Figure out where we should be sending messages and wipe some caches
function HealComm:ZONE_CHANGED_NEW_AREA()
local type = select(2, IsInInstance())
if( type ~= instanceType ) then
instanceType = type
 
updateDistributionChannel()
clearPendingHeals()
table.wipe(activeHots)
 
-- Changes the value of Necrotic Poison based on zone type, if there are more difficulty type MS's I'll support those too
-- Heroic = 90%, Non-Heroic = 75%
-- Not all versions of WoW are on 3.2, as such only check difficulty if they are, otherwise default to the heroic version
if( GetRaidDifficulty and ( GetRaidDifficulty() == 2 or GetRaidDifficulty() == 4 ) ) then
healingModifiers[GetSpellInfo(53121)] = 0.25
else
healingModifiers[GetSpellInfo(53121)] = 0.10
end
end
 
instanceType = type
end
 
-- Figure out the modifier for the players healing in general
-- Because Unrelenting Assault can be applied while casting, it should probably fire a heal changed if modifier changes
-- while a cast is going on
local function recalculatePlayerModifiers()
local increase, decrease = 1, 1
for _, modifier in pairs(playerModifiers) do
if( modifier >= 1 ) then
increase = increase * modifier
else
decrease = math.min(decrease, modifier)
end
end
 
playerHealModifier = increase * decrease
end
 
local alreadyAdded = {}
function HealComm:UNIT_AURA(unit)
local guid = UnitGUID(unit)
if( not guidToUnit[guid] ) then return end
local increase, decrease = 1, 1
 
-- Scan buffs
local id = 1
while( true ) do
local name, rank, icon, stack = UnitAura(unit, id, "HELPFUL")
if( not name ) then break end
-- Prevent buffs like Tree of Life that have the same name for the shapeshift/healing increase from being calculated twice
if( not alreadyAdded[name] ) then
alreadyAdded[name] = true
 
if( healingModifiers[name] ) then
increase = increase * healingModifiers[name]
elseif( healingStackMods[name] ) then
increase = increase * healingStackMods[name](name, rank, icon, stack)
end
end
 
id = id + 1
end
 
-- Scan debuffs
id = 1
while( true ) do
local name, rank, icon, stack = UnitAura(unit, id, "HARMFUL")
if( not name ) then break end
if( healingModifiers[name] ) then
decrease = math.min(decrease, healingModifiers[name])
elseif( healingStackMods[name] ) then
decrease = math.min(decrease, healingStackMods[name](name, rank, icon, stack))
end
 
id = id + 1
end
 
-- Check if modifier changed
local modifier = increase * decrease
if( modifier ~= currentModifiers[guid] ) then
if( currentModifiers[guid] or modifier ~= 1 ) then
currentModifiers[guid] = modifier
self.callbacks:Fire("HealComm_ModifierChanged", guid, modifier)
else
currentModifiers[guid] = modifier
end
end
 
table.wipe(alreadyAdded)
 
-- Class has a specific monitor it needs for auras
if( AuraHandler ) then
AuraHandler(unit, guid)
end
end
 
-- Monitor glyph changes
function HealComm:GlyphsUpdated(id)
local spellID = glyphCache[id]
 
-- Invalidate the old cache value
if( spellID ) then
glyphCache[spellID] = nil
glyphCache[id] = nil
end
 
-- Cache the new one if any
local enabled, _, glyphID = GetGlyphSocketInfo(id)
if( enabled and glyphID ) then
glyphCache[glyphID] = true
glyphCache[id] = glyphID
end
end
 
HealComm.GLYPH_ADDED = HealComm.GlyphsUpdated
HealComm.GLYPH_REMOVED = HealComm.GlyphsUpdated
HealComm.GLYPH_UPDATED = HealComm.GlyphsUpdated
 
-- Invalidate he average cache to recalculate for spells that increase in power due to leveling up (but not training new ranks)
function HealComm:PLAYER_LEVEL_UP(level)
for spell, average in pairs(averageHeal) do
table.wipe(average)
 
average.spell = spell
end
 
-- WoWProgramming says this is a string, why this is a string I do not know.
playerLevel = tonumber(level) or UnitLevel("player")
end
 
-- Cache player talent data for spells we need
function HealComm:PLAYER_TALENT_UPDATE()
for tabIndex=1, GetNumTalentTabs() do
for i=1, GetNumTalents(tabIndex) do
local name, _, _, _, spent = GetTalentInfo(tabIndex, i)
if( name and talentData[name] ) then
talentData[name].current = talentData[name].mod * spent
talentData[name].spent = spent
end
end
end
end
 
-- Save the currently equipped range weapon
local RANGED_SLOT = GetInventorySlotInfo("RangedSlot")
function HealComm:PLAYER_EQUIPMENT_CHANGED()
-- Caches set bonus info, as you can't reequip set bonus gear in combat no sense in checking it
if( not InCombatLockdown() ) then
for name, items in pairs(itemSetsData) do
equippedSetCache[name] = 0
for _, itemID in pairs(items) do
if( IsEquippedItem(itemID) ) then
equippedSetCache[name] = equippedSetCache[name] + 1
end
end
end
end
 
-- Check relic
local relic = GetInventoryItemLink("player", RANGED_SLOT)
playerCurrentRelic = relic and tonumber(string.match(relic, "item:(%d+):")) or nil
end
 
-- COMM CODE
local function loadHealAmount(...)
local tbl = HealComm:RetrieveTable()
for i=1, select("#", ...) do
tbl[i] = tonumber((select(i, ...)))
end
 
return tbl
end
 
-- Direct heal started
local function loadHealList(pending, amount, stack, endTime, ticksLeft, ...)
table.wipe(tempPlayerList)
 
-- For the sake of consistency, even a heal doesn't have multiple end times like a hot, it'll be treated as such in the DB
if( amount ~= -1 and amount ~= "-1" ) then
amount = not pending.hasVariableTicks and amount or loadHealAmount(string.split("@", amount))
 
for i=1, select("#", ...) do
local guid = select(i, ...)
if( guid ) then
updateRecord(pending, decompressGUID[guid], amount, stack, endTime, ticksLeft)
table.insert(tempPlayerList, decompressGUID[guid])
end
end
else
for i=1, select("#", ...), 2 do
local guid = select(i, ...)
local amount = not pending.hasVariableTicks and tonumber((select(i + 1, ...))) or loadHealAmount(string.split("@", amount))
if( guid and amount ) then
updateRecord(pending, decompressGUID[guid], amount, stack, endTime, ticksLeft)
table.insert(tempPlayerList, decompressGUID[guid])
end
end
end
end
 
local function parseDirectHeal(casterGUID, spellID, amount, ...)
local spellName = GetSpellInfo(spellID)
local unit = guidToUnit[casterGUID]
if( not unit or not spellName or not amount or select("#", ...) == 0 ) then return end
 
local endTime = select(6, UnitCastingInfo(unit))
if( not endTime ) then return end
 
pendingHeals[casterGUID] = pendingHeals[casterGUID] or {}
pendingHeals[casterGUID][spellName] = pendingHeals[casterGUID][spellName] or {}
 
local pending = pendingHeals[casterGUID][spellName]
table.wipe(pending)
pending.endTime = endTime / 1000
pending.spellID = spellID
pending.bitType = DIRECT_HEALS
 
loadHealList(pending, amount, 1, 0, nil, ...)
 
HealComm.callbacks:Fire("HealComm_HealStarted", casterGUID, spellID, pending.bitType, pending.endTime, unpack(tempPlayerList))
end
 
HealComm.parseDirectHeal = parseDirectHeal
 
-- Channeled heal started
local function parseChannelHeal(casterGUID, spellID, amount, totalTicks, ...)
local spellName = GetSpellInfo(spellID)
local unit = guidToUnit[casterGUID]
if( not unit or not spellName or not totalTicks or not amount or select("#", ...) == 0 ) then return end
 
local startTime, endTime = select(5, UnitChannelInfo(unit))
if( not startTime or not endTime ) then return end
 
pendingHeals[casterGUID] = pendingHeals[casterGUID] or {}
pendingHeals[casterGUID][spellName] = pendingHeals[casterGUID][spellname] or {}
 
local inc = amount == -1 and 2 or 1
local pending = pendingHeals[casterGUID][spellName]
table.wipe(pending)
pending.startTime = startTime / 1000
pending.endTime = endTime / 1000
pending.duration = math.max(pending.duration or 0, pending.endTime - pending.startTime)
pending.totalTicks = totalTicks
pending.tickInterval = (pending.endTime - pending.startTime) / totalTicks
pending.spellID = spellID
pending.isMultiTarget = (select("#", ...) / inc) > 1
pending.bitType = CHANNEL_HEALS
 
loadHealList(pending, amount, 1, 0, math.ceil(pending.duration / pending.tickInterval), ...)
 
HealComm.callbacks:Fire("HealComm_HealStarted", casterGUID, spellID, pending.bitType, pending.endTime, unpack(tempPlayerList))
end
 
-- Hot heal started
-- When the person is within visible range of us, the aura is available by the time the message reaches the target
-- as such, we can rely that at least one person is going to have the aura data on them (and that it won't be different, at least for this cast)
local function findAura(casterGUID, spellName, spellRank, inc, ...)
for i=1, select("#", ...), inc do
local guid = decompressGUID[select(i, ...)]
local unit = guid and guidToUnit[guid]
if( unit and UnitIsVisible(unit) ) then
local id = 1
while( true ) do
local name, rank, _, stack, _, duration, endTime, caster = UnitBuff(unit, id)
if( not name ) then break end
 
if( name == spellName and spellRank == rank and caster and UnitGUID(caster) == casterGUID ) then
return (stack and stack > 0 and stack or 1), duration, endTime
end
 
id = id + 1
end
end
end
end
 
local function parseHotHeal(casterGUID, wasUpdated, spellID, tickAmount, totalTicks, tickInterval, ...)
local spellName, spellRank = GetSpellInfo(spellID)
-- If the user is on 3.3, then anything without a total ticks attached to it is rejected
if( ( IS_BUILD30300 and not totalTicks ) or not tickAmount or not spellName or select("#", ...) == 0 ) then return end
 
-- Retrieve the hot information
local inc = ( tickAmount == -1 or tickAmount == "-1" ) and 2 or 1
local stack, duration, endTime = findAura(casterGUID, spellName, spellRank, inc, ...)
if( not stack or not duration or not endTime ) then return end
 
pendingHeals[casterGUID] = pendingHeals[casterGUID] or {}
pendingHeals[casterGUID][spellID] = pendingHeals[casterGUID][spellID] or {}
 
local pending = pendingHeals[casterGUID][spellID]
pending.duration = duration
pending.endTime = endTime
pending.stack = stack
pending.totalTicks = totalTicks or duration / tickInterval
pending.tickInterval = totalTicks and duration / totalTicks or tickInterval
pending.spellID = spellID
pending.hasVariableTicks = type(tickAmount) == "string"
pending.isMutliTarget = (select("#", ...) / inc) > 1
pending.bitType = HOT_HEALS
 
-- As you can't rely on a hot being the absolutely only one up, have to apply the total amount now :<
local ticksLeft = math.ceil((endTime - GetTime()) / pending.tickInterval)
loadHealList(pending, tickAmount, stack, endTime, ticksLeft, ...)
 
if( not wasUpdated ) then
HealComm.callbacks:Fire("HealComm_HealStarted", casterGUID, spellID, pending.bitType, endTime, unpack(tempPlayerList))
else
HealComm.callbacks:Fire("HealComm_HealUpdated", casterGUID, spellID, pending.bitType, endTime, unpack(tempPlayerList))
end
end
 
local function parseHotBomb(casterGUID, wasUpdated, spellID, amount, ...)
local spellName, spellRank = GetSpellInfo(spellID)
if( not amount or not spellName or select("#", ...) == 0 ) then return end
 
-- If we don't have a pending hot then there is no bomb as far as were concerned
local hotPending = pendingHeals[casterGUID] and pendingHeals[casterGUID][spellID]
if( not hotPending or not hotPending.bitType ) then return end
hotPending.hasBomb = true
 
pendingHeals[casterGUID][spellName] = pendingHeals[casterGUID][spellName] or {}
 
local pending = pendingHeals[casterGUID][spellName]
pending.endTime = hotPending.endTime
pending.spellID = spellID
pending.bitType = BOMB_HEALS
pending.stack = hotPending.stack
 
loadHealList(pending, amount, pending.stack, pending.endTime, nil, ...)
 
if( not wasUpdated ) then
HealComm.callbacks:Fire("HealComm_HealStarted", casterGUID, spellID, pending.bitType, pending.endTime, unpack(tempPlayerList))
else
HealComm.callbacks:Fire("HealComm_HealUpdated", casterGUID, spellID, pending.bitType, pending.endTime, unpack(tempPlayerList))
end
end
 
-- Heal finished
local function parseHealEnd(casterGUID, pending, checkField, spellID, interrupted, ...)
local spellName = GetSpellInfo(spellID)
if( not spellName or not pendingHeals[casterGUID] ) then return end
 
-- Hots use spell IDs while everything else uses spell names. Avoids naming conflicts for multi-purpose spells such as Lifebloom or Regrowth
if( not pending ) then
pending = checkField == "id" and pendingHeals[casterGUID][spellID] or pendingHeals[casterGUID][spellName]
end
if( not pending or not pending.bitType ) then return end
 
table.wipe(tempPlayerList)
 
if( select("#", ...) == 0 ) then
for i=#(pending), 1, -5 do
table.insert(tempPlayerList, pending[i - 4])
removeRecord(pending, pending[i - 4])
end
else
for i=1, select("#", ...) do
local guid = decompressGUID[select(i, ...)]
 
table.insert(tempPlayerList, guid)
removeRecord(pending, guid)
end
end
 
-- Double check and make sure we actually removed at least one person
if( #(tempPlayerList) == 0 ) then return end
 
-- Heals that also have a bomb associated to them have to end at this point, they will fire there own callback too
local bombPending = pending.hasBomb and pendingHeals[casterGUID][spellName]
if( bombPending and bombPending.bitType ) then
parseHealEnd(casterGUID, bombPending, "name", spellID, interrupted, ...)
end
 
local bitType = pending.bitType
-- Clear data if we're done
if( #(pending) == 0 ) then table.wipe(pending) end
 
HealComm.callbacks:Fire("HealComm_HealStopped", casterGUID, spellID, bitType, interrupted, unpack(tempPlayerList))
end
 
HealComm.parseHealEnd = parseHealEnd
 
-- Heal delayed
local function parseHealDelayed(casterGUID, startTime, endTime, spellName)
local pending = pendingHeals[casterGUID][spellName]
-- It's possible to get duplicate interrupted due to raid1 = party1, player = raid# etc etc, just block it here
if( pending.endTime == endTime and pending.startTime == startTime ) then return end
 
-- Casted heal
if( pending.bitType == DIRECT_HEALS ) then
pending.startTime = startTime
pending.endTime = endTime
-- Channel heal
elseif( pending.bitType == CHANNEL_HEALS ) then
pending.startTime = startTime
pending.endTime = endTime
pending.tickInterval = (pending.endTime - pending.startTime)
else
return
end
 
table.wipe(tempPlayerList)
for i=1, #(pending), 5 do
table.insert(tempPlayerList, pending[i])
end
 
HealComm.callbacks:Fire("HealComm_HealDelayed", casterGUID, pending.spellID, pending.bitType, pending.endTime, unpack(tempPlayerList))
end
 
-- After checking around 150-200 messages in battlegrounds, server seems to always be passed (if they are from another server)
-- Channels use tick total because the tick interval varies by haste
-- Hots use tick interval because the total duration varies but the tick interval stays the same
function HealComm:CHAT_MSG_ADDON(prefix, message, channel, sender)
if( prefix ~= COMM_PREFIX or channel ~= distribution or sender == playerName ) then return end
 
local commType, extraArg, spellID, arg1, arg2, arg3, arg4, arg5, arg6 = string.split(":", message)
local casterGUID = UnitGUID(sender)
spellID = tonumber(spellID)
 
if( not commType or not spellID or not casterGUID ) then return end
 
-- New direct heal - D:<extra>:<spellID>:<amount>:target1,target2...
if( commType == "D" and arg1 and arg2 ) then
parseDirectHeal(casterGUID, spellID, tonumber(arg1), string.split(",", arg2))
-- New channel heal - C:<extra>:<spellID>:<amount>:<totalTicks>:target1,target2...
elseif( commType == "C" and arg1 and arg3 ) then
parseChannelHeal(casterGUID, spellID, tonumber(arg1), tonumber(arg2), string.split(",", arg3))
-- New hot with a "bomb" component - B:<totalTicks>:<spellID>:<bombAmount>:target1,target2:<amount>:<isMulti>:<tickInterval>:target1,target2...
elseif( commType == "B" and arg1 and arg6 ) then
parseHotHeal(casterGUID, false, spellID, tonumber(arg3), tonumber(extraArg), tonumber(arg5), string.split(",", arg6))
parseHotBomb(casterGUID, false, spellID, tonumber(arg1), string.split(",", arg2))
-- New hot - H:<totalTicks>:<spellID>:<amount>:<isMulti>:<tickInterval>:target1,target2...
elseif( commType == "H" and arg1 and arg4 ) then
parseHotHeal(casterGUID, false, spellID, tonumber(arg1), tonumber(extraArg), tonumber(arg3), string.split(",", arg4))
-- New updated heal somehow before ending - U:<totalTicks>:<spellID>:<amount>:<tickInterval>:target1,target2...
elseif( commtype == "U" and arg1 and arg3 ) then
parseHotHeal(casterGUID, true, spellID, tonumber(arg1), tonumber(extraArg), tonumber(arg2), string.split(",", arg3))
-- New variable tick hot - VH::<spellID>:<amount>:<isMulti>:<tickInterval>:target1,target2...
elseif( commType == "VH" and arg1 and arg4 ) then
parseHotHeal(casterGUID, false, spellID, arg1, tonumber(arg3), nil, string.split(",", arg4))
-- New updated variable tick hot - U::<spellID>:amount1@amount2@amount3:<tickTotal>:target1,target2...
elseif( commtype == "VU" and arg1 and arg3 ) then
parseHotHeal(casterGUID, true, spellID, arg1, tonumber(arg2), nil, string.split(",", arg3))
-- New updated bomb hot - UB:<totalTicks>:<spellID>:<bombAmount>:target1,target2:<amount>:<tickInterval>:target1,target2...
elseif( commtype == "UB" and arg1 and arg5 ) then
parseHotHeal(casterGUID, true, spellID, tonumber(arg3), tonumber(extraArg), tonumber(arg4), string.split(",", arg5))
parseHotBomb(casterGUID, true, spellID, tonumber(arg1), string.split(",", arg2))
-- Heal stopped - S:<extra>:<spellID>:<ended early: 0/1>:target1,target2...
elseif( commType == "S" or commType == "HS" ) then
local interrupted = arg1 == "1" and true or false
local type = commType == "HS" and "id" or "name"
 
if( arg2 and arg2 ~= "" ) then
parseHealEnd(casterGUID, nil, type, spellID, interrupted, string.split(",", arg2))
else
parseHealEnd(casterGUID, nil, type, spellID, interrupted)
end
end
end
 
-- Bucketing reduces the number of events triggered for heals such as Tranquility that hit multiple targets
-- instead of firing 5 events * ticks it will fire 1 (maybe 2 depending on lag) events
HealComm.bucketHeals = HealComm.bucketHeals or {}
local bucketHeals = HealComm.bucketHeals
local BUCKET_FILLED = 0.30
 
HealComm.bucketFrame = HealComm.bucketFrame or CreateFrame("Frame")
HealComm.bucketFrame:Hide()
 
HealComm.bucketFrame:SetScript("OnUpdate", function(self, elapsed)
local totalLeft = 0
for casterGUID, spells in pairs(bucketHeals) do
for id, data in pairs(spells) do
if( data.timeout ) then
data.timeout = data.timeout - elapsed
 
if( data.timeout <= 0 ) then
-- This shouldn't happen, on the offhand chance it does then don't bother sending an event
if( #(data) == 0 or not data.spellID or not data.spellName ) then
table.wipe(data)
-- We're doing a bucket for a tick heal like Tranquility or Wild Growth
elseif( data.type == "tick" ) then
local pending = pendingHeals[casterGUID] and ( pendingHeals[casterGUID][data.spellID] or pendingHeals[casterGUID][data.spellName] )
if( pending and pending.bitType ) then
local endTime = select(3, getRecord(pending, data[1]))
HealComm.callbacks:Fire("HealComm_HealUpdated", casterGUID, pending.spellID, pending.bitType, endtime, unpack(data))
end
 
table.wipe(data)
-- We're doing a bucket for a cast thats a multi-target heal like Wild Growth or Prayer of Healing
elseif( data.type == "heal" ) then
local type, amount, totalTicks, tickInterval, _, hasVariableTicks = CalculateHotHealing(data[1], data.spellID)
if( type ) then
local targets, amount = GetHealTargets(type, data[1], hasVariableTicks and amount or math.max(amount, 0), data.spellName, data, hasVariableTicks)
parseHotHeal(playerGUID, false, data.spellID, amount, totalTicks, tickInterval, string.split(",", targets))
 
if( not hasVariableTicks ) then
sendMessage(string.format("H:%d:%d:%d::%d:%s", totalTicks, data.spellID, amount, tickInterval, targets))
else
sendMessage(string.format("VH::%d:%s::%d:%s", data.spellID, amount, totalTicks, targets))
end
end
 
table.wipe(data)
end
else
totalLeft = totalLeft + 1
end
end
end
end
 
if( totalLeft <= 0 ) then
self:Hide()
end
end)
 
-- Monitor aura changes as well as new hots being cast
local eventRegistered = {["SPELL_HEAL"] = true, ["SPELL_PERIODIC_HEAL"] = true}
if( isHealerClass ) then
eventRegistered["SPELL_AURA_REMOVED"] = true
eventRegistered["SPELL_AURA_APPLIED"] = true
eventRegistered["SPELL_AURA_REFRESH"] = true
eventRegistered["SPELL_AURA_APPLIED_DOSE"] = true
eventRegistered["SPELL_AURA_REMOVED_DOSE"] = true
end
 
local COMBATLOG_OBJECT_AFFILIATION_MINE = COMBATLOG_OBJECT_AFFILIATION_MINE
local CAN_HEAL = bit.bor(COMBATLOG_OBJECT_REACTION_FRIENDLY, COMBATLOG_OBJECT_REACTION_NEUTRAL)
function HealComm:COMBAT_LOG_EVENT_UNFILTERED(timestamp, eventType, sourceGUID, sourceName, sourceFlags, destGUID, destName, destFlags, ...)
if( not eventRegistered[eventType] ) then return end
 
-- Heal or hot ticked that the library is tracking
-- It's more efficient/accurate to have the library keep track of this locally, spamming the comm channel would not be a very good thing especially when a single player can have 4 - 8 hots/channels going on them.
if( eventType == "SPELL_HEAL" or eventType == "SPELL_PERIODIC_HEAL" ) then
local spellID, spellName, spellSchool = ...
local pending = sourceGUID and pendingHeals[sourceGUID] and (pendingHeals[sourceGUID][spellID] or pendingHeals[sourceGUID][spellName])
if( pending and pending[destGUID] and pending.bitType and bit.band(pending.bitType, OVERTIME_HEALS) > 0 ) then
local amount, stack, endTime, ticksLeft = getRecord(pending, destGUID)
ticksLeft = ticksLeft - 1
endTime = GetTime() + pending.tickInterval * ticksLeft
if( pending.hasVariableTicks ) then table.remove(amount, 1) end
 
updateRecord(pending, destGUID, amount, stack, endTime, ticksLeft)
 
if( pending.isMultiTarget ) then
bucketHeals[sourceGUID] = bucketHeals[sourceGUID] or {}
bucketHeals[sourceGUID][spellID] = bucketHeals[sourceGUID][spellID] or {}
 
local spellBucket = bucketHeals[sourceGUID][spellID]
if( not spellBucket[destGUID] ) then
spellBucket.timeout = BUCKET_FILLED
spellBucket.type = "tick"
spellBucket.spellName = spellName
spellBucket.spellID = spellID
spellBucket[destGUID] = true
table.insert(spellBucket, destGUID)
 
self.bucketFrame:Show()
end
else
HealComm.callbacks:Fire("HealComm_HealUpdated", sourceGUID, spellID, pending.bitType, endTime, destGUID)
end
end
 
-- New hot was applied
elseif( ( eventType == "SPELL_AURA_APPLIED" or eventType == "SPELL_AURA_REFRESH" or eventType == "SPELL_AURA_APPLIED_DOSE" ) and bit.band(sourceFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) == COMBATLOG_OBJECT_AFFILIATION_MINE ) then
local spellID, spellName, spellSchool, auraType = ...
if( eventType == "SPELL_AURA_APPLIED" and selfModifiers[spellID] ) then
playerModifiers[spellID] = selfModifiers[spellID]
recalculatePlayerModifiers()
end
 
if( hotData[spellName] ) then
-- Multi target heal so put it in the bucket
if( hotData[spellName].isMulti ) then
bucketHeals[sourceGUID] = bucketHeals[sourceGUID] or {}
bucketHeals[sourceGUID][spellName] = bucketHeals[sourceGUID][spellName] or {}
 
-- For some reason, Glyph of Prayer of Healing fires a SPELL_AURA_APPLIED then a SPELL_AURA_REFRESH right after
local spellBucket = bucketHeals[sourceGUID][spellName]
if( not spellBucket[destGUID] ) then
spellBucket.timeout = BUCKET_FILLED
spellBucket.type = "heal"
spellBucket.spellName = spellName
spellBucket.spellID = spellID
spellBucket[destGUID] = true
table.insert(spellBucket, destGUID)
 
self.bucketFrame:Show()
end
return
end
 
-- Single target so we can just send it off now thankfully
local type, amount, totalTicks, tickInterval, bombAmount, hasVariableTicks = CalculateHotHealing(destGUID, spellID)
if( type ) then
local targets, amount = GetHealTargets(type, destGUID, hasVariableTicks and amount or math.max(amount, 0), spellName, hasVariableTicks)
parseHotHeal(sourceGUID, false, spellID, amount, totalTicks, tickInterval, string.split(",", targets))
 
-- Hot with a bomb!
if( bombAmount ) then
local bombTargets, bombAmount = GetHealTargets(BOMB_HEALS, destGUID, math.max(bombAmount, 0), spellName)
parseHotBomb(sourceGUID, false, spellID, bombAmount, string.split(",", bombTargets))
sendMessage(string.format("B:%d:%d:%d:%s:%d::%d:%s", totalTicks, spellID, bombAmount, bombTargets, amount, tickInterval, targets))
elseif( hasVariableTicks ) then
sendMessage(string.format("VH::%d:%s::%d:%s", spellID, amount, totalTicks, targets))
-- Normal hot, nothing fancy
else
sendMessage(string.format("H:%d:%d:%d::%d:%s", totalTicks, spellID, amount, tickInterval, targets))
end
end
end
-- Single stack of a hot was removed, this only applies when going from 2 -> 1, when it goes from 1 -> 0 it fires SPELL_AURA_REMOVED
elseif( eventType == "SPELL_AURA_REMOVED_DOSE" and bit.band(sourceFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) == COMBATLOG_OBJECT_AFFILIATION_MINE ) then
local spellID, spellName, spellSchool, auraType, stacks = ...
local pending = sourceGUID and pendingHeals[sourceGUID] and pendingHeals[sourceGUID][spellID]
if( pending and pending.bitType ) then
local amount = getRecord(pending, destGUID)
if( amount ) then
parseHotHeal(sourceGUID, true, spellID, amount, pending.totalTicks, pending.tickInterval, compressGUID[destGUID])
 
-- Plant the bomb
local bombPending = pending.hasBomb and pendingHeals[sourceGUID][spellName]
if( bombPending and bombPending.bitType ) then
local bombAmount = getRecord(bombPending, destGUID)
if( bombAmount ) then
parseHotBomb(sourceGUID, true, spellID, bombAmount, compressGUID[destGUID])
 
sendMessage(string.format("UB:%s:%d:%d:%s:%d:%d:%s", pending.totalTicks, spellID, bombAmount, compressGUID[destGUID], amount, pending.tickInterval, compressGUID[destGUID]))
return
end
end
 
-- Failed to find any sort of bomb-y info we needed or it doesn't have a bomb anyway
if( pending.hasVariableTicks ) then
sendMessage(string.format("VU::%d:%s:%d:%s", spellID, amount, pending.totalTicks, compressGUID[destGUID]))
else
sendMessage(string.format("U:%s:%d:%d:%d:%s", spellID, amount, pending.totalTicks, pending.tickInterval, compressGUID[destGUID]))
end
end
end
 
-- Aura faded
elseif( eventType == "SPELL_AURA_REMOVED" ) then
local spellID, spellName, spellSchool, auraType = ...
 
-- It was applied to the player so it might be a modifierhopthal
if( playerModifiers[spellID] and bit.band(destFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) == COMBATLOG_OBJECT_AFFILIATION_MINE ) then
playerModifiers[spellID] = nil
recalculatePlayerModifiers()
end
 
-- Hot faded that we cast
if( hotData[spellName] and bit.band(sourceFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) == COMBATLOG_OBJECT_AFFILIATION_MINE ) then
parseHealEnd(sourceGUID, nil, "id", spellID, false, compressGUID[destGUID])
sendMessage(string.format("HS::%d::%s", spellID, compressGUID[destGUID]))
end
end
end
 
-- Spell cast magic
-- When auto self cast is on, the UNIT_SPELLCAST_SENT event will always come first followed by the funciton calls
-- Otherwise either SENT comes first then function calls, or some function calls then SENT then more function calls
local castTarget, castID, mouseoverGUID, mouseoverName, hadTargetingCursor, lastSentID, lastTargetGUID, lastTargetName
local lastFriendlyGUID, lastFriendlyName, lastGUID, lastName, lastIsFriend
local castGUIDs, guidPriorities = {}, {}
 
-- Deals with the fact that functions are called differently
-- Why a table when you can only cast one spell at a time you ask? When you factor in lag and mash clicking it's possible to:
-- cast A, interrupt it, cast B and have A fire SUCEEDED before B does, the tables keeps it from bugging out
local function setCastData(priority, name, guid)
if( not guid or not lastSentID ) then return end
if( guidPriorities[lastSentID] and guidPriorities[lastSentID] >= priority ) then return end
 
-- This is meant as a way of locking a cast in because which function has accurate data can be called into question at times, one of them always does though
-- this means that as soon as it finds a name match it locks the GUID in until another SENT is fired. Technically it's possible to get a bad GUID but it first requires
-- the functions to return different data and it requires the messed up call to be for another name conflict.
if( castTarget and castTarget == name ) then priority = 99 end
 
castGUIDs[lastSentID] = guid
guidPriorities[lastSentID] = priority
end
 
-- When the game tries to figure out the UnitID from the name it will prioritize players over non-players
-- if there are conflicts in names it will pull the one with the least amount of current health
function HealComm:UNIT_SPELLCAST_SENT(unit, spellName, spellRank, castOn)
if( unit ~= "player" or not spellData[spellName] or not averageHeal[spellName][spellRank] ) then return end
 
castTarget = string.gsub(castOn, "(.-)%-(.*)$", "%1")
lastSentID = spellName .. spellRank
 
-- Self cast is off which means it's possible to have a spell waiting for a target.
-- It's possible that it's the mouseover unit, but if a Target, TargetLast or AssistUnit call comes right after it means it's casting on that instead instead.
if( hadTargetingCursor ) then
hadTargetingCursor = nil
self.resetFrame:Show()
 
guidPriorities[lastSentID] = nil
setCastData(5, mouseoverName, mouseoverGUID)
else
-- If the player is ungrouped and healing, you can't take advantage of the name -> "unit" map, look in the UnitIDs that would most likely contain the information that's needed.
local guid = UnitGUID(castOn)
if( not guid ) then
guid = UnitName("target") == castTarget and UnitGUID("target") or UnitName("focus") == castTarget and UnitGUID("focus") or UnitName("mouseover") == castTarget and UnitGUID("mouseover") or UnitName("targettarget") == castTarget and UnitGUID("target") or UnitName("focustarget") == castTarget and UnitGUID("focustarget")
end
 
guidPriorities[lastSentID] = nil
setCastData(0, nil, guid)
end
end
 
function HealComm:UNIT_SPELLCAST_START(unit, spellName, spellRank, id)
if( unit ~= "player" or not spellData[spellName] or not averageHeal[spellName][spellRank] ) then return end
local nameID = spellName .. spellRank
local castGUID = castGUIDs[nameID]
 
castID = id
 
-- Figure out who we are healing and for how much
local type, amount, ticks = CalculateHealing(castGUID, spellName, spellRank)
local targets, amount = GetHealTargets(type, castGUID, math.max(amount, 0), spellName)
 
if( type == DIRECT_HEALS ) then
parseDirectHeal(playerGUID, self.spellToID[nameID], amount, string.split(",", targets))
sendMessage(string.format("D::%d:%d:%s", self.spellToID[nameID] or 0, amount or "", targets))
elseif( type == CHANNEL_HEALS ) then
parseChannelHeal(playerGUID, self.spellToID[nameID], amount, ticks, string.split(",", targets))
sendMessage(string.format("C::%d:%d:%s:%s", self.spellToID[nameID] or 0, amount, ticks, targets))
end
end
 
HealComm.UNIT_SPELLCAST_CHANNEL_START = HealComm.UNIT_SPELLCAST_START
 
function HealComm:UNIT_SPELLCAST_SUCCEEDED(unit, spellName, spellRank, id)
if( unit ~= "player" or not spellData[spellName] or id ~= castID or id == 0 ) then return end
castID = nil
 
parseHealEnd(playerGUID, nil, "name", self.spellToID[spellName .. spellRank], false)
sendMessage(string.format("S::%d:0", self.spellToID[spellName .. spellRank] or 0))
end
 
function HealComm:UNIT_SPELLCAST_STOP(unit, spellName, spellRank, id)
if( unit ~= "player" or not spellData[spellName] or id ~= castID ) then return end
local nameID = spellName .. spellRank
 
castID = nil
parseHealEnd(playerGUID, nil, "name", self.spellToID[spellName .. spellRank], true)
sendMessage(string.format("S::%d:1", self.spellToID[spellName .. spellRank] or 0))
end
 
function HealComm:UNIT_SPELLCAST_CHANNEL_STOP(unit, spellName, spellRank, id)
if( unit ~= "player" or not spellData[spellName] or id ~= castID ) then return end
local nameID = spellName .. spellRank
 
castID = nil
parseHealEnd(playerGUID, nil, "name", self.spellToID[nameID], false)
sendMessage(string.format("S::%d:0", self.spellToID[nameID] or 0))
end
 
-- Cast didn't go through, recheck any charge data if necessary
function HealComm:UNIT_SPELLCAST_INTERRUPTED(unit, spellName, spellRank, id)
if( unit ~= "player" or not spellData[spellName] or castID ~= id ) then return end
 
ResetChargeData(castGUIDs[spellName .. spellRank], spellName, spellRank)
end
 
-- It's faster to do heal delays locally rather than through syncing, as it only has to go from WoW -> Player instead of Caster -> WoW -> Player
function HealComm:UNIT_SPELLCAST_DELAYED(unit, spellName, spellRank, id)
local casterGUID = UnitGUID(unit)
if( unit == "focus" or unit == "target" or not pendingHeals[casterGUID] or not pendingHeals[casterGUID][spellName] ) then return end
 
-- Direct heal delayed
if( pendingHeals[casterGUID][spellName].bitType == DIRECT_HEALS ) then
local startTime, endTime = select(5, UnitCastingInfo(unit))
if( startTime and endTime ) then
parseHealDelayed(casterGUID, startTime / 1000, endTime / 1000, spellName)
end
-- Channel heal delayed
elseif( pendingHeals[casterGUID][spellName].bitType == CHANNEL_HEALS ) then
local startTime, endTime = select(5, UnitChannelInfo(unit))
if( startTime and endTime ) then
parseHealDelayed(casterGUID, startTime / 1000, endTime / 1000, spellName)
end
end
end
 
HealComm.UNIT_SPELLCAST_CHANNEL_UPDATE = HealComm.UNIT_SPELLCAST_DELAYED
 
-- Need to keep track of mouseover as it can change in the split second after/before casts
function HealComm:UPDATE_MOUSEOVER_UNIT()
mouseoverGUID = UnitCanAssist("player", "mouseover") and UnitGUID("mouseover")
mouseoverName = UnitCanAssist("player", "mouseover") and UnitName("mouseover")
end
 
-- Keep track of our last target/friendly target for the sake of /targetlast and /targetlastfriend
function HealComm:PLAYER_TARGET_CHANGED()
if( lastGUID and lastName ) then
if( lastIsFriend ) then
lastFriendlyGUID, lastFriendlyName = lastGUID, lastName
end
 
lastTargetGUID, lastTargetName = lastGUID, lastName
end
 
-- Despite the fact that it's called target last friend, UnitIsFriend won't actually work
lastGUID = UnitGUID("target")
lastName = UnitName("target")
lastIsFriend = UnitCanAssist("player", "target")
end
 
-- Unit was targeted through a function
function HealComm:Target(unit)
if( self.resetFrame:IsShown() and UnitCanAssist("player", unit) ) then
setCastData(6, UnitName(unit), UnitGUID(unit))
end
 
self.resetFrame:Hide()
hadTargetingCursor = nil
end
 
-- This is only needed when auto self cast is off, in which case this is called right after UNIT_SPELLCAST_SENT
-- because the player got a waiting-for-cast icon up and they pressed a key binding to target someone
HealComm.TargetUnit = HealComm.Target
 
-- Works the same as the above except it's called when you have a cursor icon and you click on a secure frame with a target attribute set
HealComm.SpellTargetUnit = HealComm.Target
 
-- Used in /assist macros
function HealComm:AssistUnit(unit)
if( self.resetFrame:IsShown() and UnitCanAssist("player", unit .. "target") ) then
setCastData(6, UnitName(unit .. "target"), UnitGUID(unit .. "target"))
end
 
self.resetFrame:Hide()
hadTargetingCursor = nil
end
 
-- Target last was used, the only reason this is called with reset frame being shown is we're casting on a valid unit
-- don't have to worry about the GUID no longer being invalid etc
function HealComm:TargetLast(guid, name)
if( name and guid and self.resetFrame:IsShown() ) then
setCastData(6, name, guid)
end
 
self.resetFrame:Hide()
hadTargetingCursor = nil
end
 
function HealComm:TargetLastFriend()
self:TargetLast(lastFriendlyGUID, lastFriendlyName)
end
 
function HealComm:TargetLastTarget()
self:TargetLast(lastTargetGUID, lastTargetName)
end
 
-- Spell was cast somehow
function HealComm:CastSpell(arg, unit)
-- If the spell is waiting for a target and it's a spell action button then we know that the GUID has to be mouseover or a key binding cast.
if( unit and UnitCanAssist("player", unit) ) then
setCastData(4, UnitName(unit), UnitGUID(unit))
-- No unit, or it's a unit we can't assist
elseif( not SpellIsTargeting() ) then
if( UnitCanAssist("player", "target") ) then
setCastData(4, UnitName("target"), UnitGUID("target"))
else
setCastData(4, playerName, playerGUID)
end
 
hadTargetingCursor = nil
else
hadTargetingCursor = true
end
end
 
HealComm.CastSpellByName = HealComm.CastSpell
HealComm.CastSpellByID = HealComm.CastSpell
HealComm.UseAction = HealComm.CastSpell
 
-- Make sure we don't have invalid units in this
local function sanityCheckMapping()
for guid, unit in pairs(guidToUnit) do
-- Unit no longer exists, remove all healing for them
if( not UnitExists(unit) ) then
-- Check for (and remove) any active heals
if( pendingHeals[guid] ) then
for id, pending in pairs(pendingHeals[guid]) do
if( pending.bitType ) then
parseHealEnd(guid, pending, nil, pending.spellID, true)
end
end
 
pendingHeals[guid] = nil
end
 
-- Remove any heals that are on them
removeAllRecords(guid)
 
guidToUnit[guid] = nil
guidToGroup[guid] = nil
end
end
end
 
-- 5s poll that tries to solve the problem of X running out of range while a HoT is ticking
-- this is not really perfect far from it in fact. If I can find a better solution I will switch to that.
if( not HealComm.hotMonitor ) then
HealComm.hotMonitor = CreateFrame("Frame")
HealComm.hotMonitor:Hide()
HealComm.hotMonitor.timeElapsed = 0
HealComm.hotMonitor:SetScript("OnUpdate", function(self, elapsed)
self.timeElapsed = self.timeElapsed + elapsed
if( self.timeElapsed < 5 ) then return end
self.timeElapsed = self.timeElapsed - 5
 
-- For the time being, it will only remove them if they don't exist and it found a valid unit
-- units that leave the raid are automatically removed
local found
for guid in pairs(activeHots) do
if( guidToUnit[guid] and not UnitIsVisible(guidToUnit[guid]) ) then
removeAllRecords(guid)
else
found = true
end
end
 
if( not found ) then
self:Hide()
end
end)
end
 
-- After the player leaves a group, tables are wiped out or released for GC
local wasInParty, wasInRaid
local function clearGUIDData()
clearPendingHeals()
 
table.wipe(compressGUID)
table.wipe(decompressGUID)
table.wipe(activePets)
 
playerGUID = playerGUID or UnitGUID("player")
HealComm.guidToUnit = {[playerGUID] = "player"}
guidToUnit = HealComm.guidToUnit
 
HealComm.guidToGroup = {}
guidToGroup = HealComm.guidToGroup
 
HealComm.activeHots = {}
activeHots = HealComm.activeHots
 
HealComm.pendingHeals = {}
pendingHeals = HealComm.pendingHeals
 
HealComm.bucketHeals = {}
bucketHeals = HealComm.bucketHeals
 
wasInParty, wasInRaid = nil, nil
end
 
-- Keeps track of pet GUIDs, as pets are considered vehicles this will also map vehicle GUIDs to unit
function HealComm:UNIT_PET(unit)
local pet = self.unitToPet[unit]
local guid = pet and UnitGUID(pet)
if( guid ) then
guidToUnit[guid] = pet
guidToGroup[guid] = guidToGroup[UnitGUID(unit)]
activePets[unit] = guid
-- This used to be an active pet, need to invalidate pending heals
elseif( activePets[unit] and guidToUnit[guid] ) then
removeAllRecords(activePets[unit])
 
guidToUnit[guid] = nil
guidToGroup[guid] = nil
activePets[unit] = nil
end
end
 
-- Keep track of party GUIDs, ignored in raids as RRU will handle that mapping
function HealComm:PARTY_MEMBERS_CHANGED()
if( GetNumRaidMembers() > 0 ) then return end
updateDistributionChannel()
 
if( GetNumPartyMembers() == 0 ) then
if( wasInParty ) then
clearGUIDData()
end
return
end
 
-- Parties are not considered groups in terms of API, so fake it and pretend they are all in group 0
guidToGroup[playerGUID or UnitGUID("player")] = 0
if( not wasInParty ) then self:UNIT_PET("player") end
 
for i=1, MAX_PARTY_MEMBERS do
local unit = "party" .. i
if( UnitExists(unit) ) then
local lastGroup = guidToGroup[guid]
local guid = UnitGUID(unit)
guidToUnit[guid] = unit
guidToGroup[guid] = 0
 
if( not wasInParty or lastGroup ~= guidToGroup[guid] ) then
self:UNIT_PET(unit)
end
end
end
 
sanityCheckMapping()
wasInParty = true
end
 
-- Keep track of raid GUIDs
function HealComm:RAID_ROSTER_UPDATE()
updateDistributionChannel()
 
-- Left raid, clear any cache we had
if( GetNumRaidMembers() == 0 ) then
if( wasInRaid ) then
clearGUIDData()
end
return
end
 
-- Add new members
for i=1, MAX_RAID_MEMBERS do
local unit = "raid" .. i
if( UnitExists(unit) ) then
local lastGroup = guidToGroup[guid]
local guid = UnitGUID(unit)
guidToUnit[guid] = unit
guidToGroup[guid] = select(3, GetRaidRosterInfo(i))
 
-- If the pets owners group changed then the pets group should be updated too
if( not wasInRaid or guidToGroup[guid] ~= lastGroup ) then
self:UNIT_PET(unit)
end
end
end
 
sanityCheckMapping()
wasInRaid = true
end
 
-- PLAYER_ALIVE = got talent data
function HealComm:PLAYER_ALIVE()
self:PLAYER_TALENT_UPDATE()
self.eventFrame:UnregisterEvent("PLAYER_ALIVE")
end
 
-- Initialize the library
function HealComm:OnInitialize()
-- If another instance already loaded then the tables should be wiped to prevent old data from persisting
-- in case of a spell being removed later on, only can happen if a newer LoD version is loaded
table.wipe(spellData)
table.wipe(hotData)
table.wipe(itemSetsData)
table.wipe(talentData)
table.wipe(averageHeal)
 
-- Load all of the classes formulas and such
LoadClassData()
 
-- Setup the metatables for average healing
for spell in pairs(spellData) do
averageHeal[spell] = setmetatable({spell = spell}, self.averageHealMT)
end
 
-- Cache glyphs initially
for id=1, GetNumGlyphSockets() do
local enabled, _, glyphID = GetGlyphSocketInfo(id)
if( enabled and glyphID ) then
glyphCache[glyphID] = true
glyphCache[id] = glyphID
end
end
 
self:PLAYER_EQUIPMENT_CHANGED()
 
-- When first logging in talent data isn't available until at least PLAYER_ALIVE, so if we don't have data
-- will wait for that event otherwise will just cache it right now
if( GetNumTalentTabs() == 0 ) then
self.eventFrame:RegisterEvent("PLAYER_ALIVE")
else
self:PLAYER_TALENT_UPDATE()
end
 
if( ResetChargeData ) then
HealComm.eventFrame:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
end
 
-- Finally, register it all
self.eventFrame:RegisterEvent("UNIT_SPELLCAST_SENT")
self.eventFrame:RegisterEvent("UNIT_SPELLCAST_START")
self.eventFrame:RegisterEvent("UNIT_SPELLCAST_STOP")
self.eventFrame:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")
self.eventFrame:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START")
self.eventFrame:RegisterEvent("UNIT_SPELLCAST_DELAYED")
self.eventFrame:RegisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")
self.eventFrame:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
self.eventFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
self.eventFrame:RegisterEvent("PLAYER_TALENT_UPDATE")
self.eventFrame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
self.eventFrame:RegisterEvent("PLAYER_TARGET_CHANGED")
self.eventFrame:RegisterEvent("UPDATE_MOUSEOVER_UNIT")
self.eventFrame:RegisterEvent("PLAYER_LEVEL_UP")
self.eventFrame:RegisterEvent("GLYPH_ADDED")
self.eventFrame:RegisterEvent("GLYPH_REMOVED")
self.eventFrame:RegisterEvent("GLYPH_UPDATED")
self.eventFrame:RegisterEvent("UNIT_AURA")
 
if( self.initialized ) then return end
self.initialized = true
 
self.resetFrame = CreateFrame("Frame")
self.resetFrame:Hide()
self.resetFrame:SetScript("OnUpdate", function(self) self:Hide() end)
 
-- You can't unhook secure hooks after they are done, so will hook once and the HealComm table will update with the latest functions
-- automagically. If a new function is ever used it'll need a specific variable to indicate those set of hooks.
-- By default most of these are mapped to a more generic function, but I call separate ones so I don't have to rehook
-- if it turns out I need to know something specific
hooksecurefunc("TargetUnit", function(...) HealComm:TargetUnit(...) end)
hooksecurefunc("SpellTargetUnit", function(...) HealComm:SpellTargetUnit(...) end)
hooksecurefunc("AssistUnit", function(...) HealComm:AssistUnit(...) end)
hooksecurefunc("UseAction", function(...) HealComm:UseAction(...) end)
hooksecurefunc("TargetLastFriend", function(...) HealComm:TargetLastFriend(...) end)
hooksecurefunc("TargetLastTarget", function(...) HealComm:TargetLastTarget(...) end)
hooksecurefunc("CastSpellByName", function(...) HealComm:CastSpellByName(...) end)
 
-- Fixes hook error for people who are not on 3.2 yet
if( CastSpellByID ) then
hooksecurefunc("CastSpellByID", function(...) HealComm:CastSpellByID(...) end)
end
end
 
-- General event handler
local function OnEvent(self, event, ...)
HealComm[event](HealComm, ...)
end
 
-- Event handler
HealComm.eventFrame = HealComm.frame or HealComm.eventFrame or CreateFrame("Frame")
HealComm.eventFrame:UnregisterAllEvents()
HealComm.eventFrame:RegisterEvent("UNIT_PET")
HealComm.eventFrame:SetScript("OnEvent", OnEvent)
HealComm.frame = nil
 
-- At PLAYER_LEAVING_WORLD (Actually more like MIRROR_TIMER_STOP but anyway) UnitGUID("player") returns nil, delay registering
-- events and set a playerGUID/playerName combo for all players on PLAYER_LOGIN not just the healers.
function HealComm:PLAYER_LOGIN()
playerGUID = UnitGUID("player")
playerName = UnitName("player")
playerLevel = UnitLevel("player")
 
-- Oddly enough player GUID is not available on file load, so keep the map of player GUID to themselves too
guidToUnit[playerGUID] = "player"
 
if( isHealerClass ) then
self:OnInitialize()
end
 
self.eventFrame:UnregisterEvent("PLAYER_LOGIN")
self.eventFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")
self.eventFrame:RegisterEvent("PARTY_MEMBERS_CHANGED")
self.eventFrame:RegisterEvent("RAID_ROSTER_UPDATE")
 
self:ZONE_CHANGED_NEW_AREA()
self:RAID_ROSTER_UPDATE()
self:PARTY_MEMBERS_CHANGED()
end
 
if( not IsLoggedIn() ) then
HealComm.eventFrame:RegisterEvent("PLAYER_LOGIN")
else
HealComm:PLAYER_LOGIN()
end
Property changes : Added: svn:eol-style + native
ChatThrottleLib/ChatThrottleLib.lua New file
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 an addon somewhere. Get the addon updated or copy in a newer ChatThrottleLib.lua (>=v16) in it!")
-- ATTEMPT to unhook; this'll behave badly if someone else has hooked...
-- ... and if someone has securehooked, they can kiss that goodbye too... >.<
_G.SendChatMessage = _G.ChatThrottleLib.ORIG_SendChatMessage
if _G.ChatThrottleLib.ORIG_SendAddonMessage then
_G.SendAddonMessage = _G.ChatThrottleLib.ORIG_SendAddonMessage
end
end
_G.ChatThrottleLib.ORIG_SendChatMessage = nil
_G.ChatThrottleLib.ORIG_SendAddonMessage = nil
end
 
if not _G.ChatThrottleLib then
_G.ChatThrottleLib = {}
end
 
ChatThrottleLib = _G.ChatThrottleLib -- in case some addon does "local ChatThrottleLib" above us and we're copypasted (AceComm-2, sigh)
local ChatThrottleLib = _G.ChatThrottleLib
 
ChatThrottleLib.version = CTL_VERSION
 
 
 
------------------ TWEAKABLES -----------------
 
ChatThrottleLib.MAX_CPS = 800 -- 2000 seems to be safe if NOTHING ELSE is happening. let's call it 800.
ChatThrottleLib.MSG_OVERHEAD = 40 -- Guesstimate overhead for sending a message; source+dest+chattype+protocolstuff
 
ChatThrottleLib.BURST = 4000 -- WoW's server buffer seems to be about 32KB. 8KB should be safe, but seen disconnects on _some_ servers. Using 4KB now.
 
ChatThrottleLib.MIN_FPS = 20 -- Reduce output CPS to half (and don't burst) if FPS drops below this value
 
 
local setmetatable = setmetatable
local table_remove = table.remove
local tostring = tostring
local GetTime = GetTime
local math_min = math.min
local math_max = math.max
local next = next
local strlen = string.len
local GetFrameRate = GetFrameRate
 
 
 
-----------------------------------------------------------------------
-- 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
]]
 
 
Property changes : Added: svn:eol-style + native