473 lines
14 KiB
Lua
473 lines
14 KiB
Lua
-- Modules/BuffEndingAnnouncer/BuffEndingAnnouncer.lua
|
|
-- Announces tracked buffs in SAY shortly before they expire.
|
|
|
|
local ADDON_NAME = "HailMaryGuildTools"
|
|
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
|
if not HMGT then return end
|
|
|
|
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME, true) or {}
|
|
|
|
local BEA = HMGT:NewModule("BuffEndingAnnouncer")
|
|
HMGT.BuffEndingAnnouncer = BEA
|
|
|
|
BEA.runtimeEnabled = false
|
|
BEA.eventFrame = nil
|
|
BEA.ticker = nil
|
|
BEA.lastAnnouncedSecond = {}
|
|
BEA.recentOwnCastAt = {}
|
|
|
|
local function NormalizeThreshold(value, fallback)
|
|
local threshold = tonumber(value)
|
|
if not threshold then
|
|
threshold = tonumber(fallback) or 5
|
|
end
|
|
threshold = math.floor(threshold + 0.5)
|
|
if threshold < 1 then threshold = 1 end
|
|
if threshold > 30 then threshold = 30 end
|
|
return threshold
|
|
end
|
|
|
|
local function GetSpellName(spellId)
|
|
local sid = tonumber(spellId)
|
|
if not sid or sid <= 0 then return nil end
|
|
if C_Spell and type(C_Spell.GetSpellName) == "function" then
|
|
local name = C_Spell.GetSpellName(sid)
|
|
if type(name) == "string" and name ~= "" then
|
|
return name
|
|
end
|
|
end
|
|
if type(GetSpellInfo) == "function" then
|
|
local name = GetSpellInfo(sid)
|
|
if type(name) == "string" and name ~= "" then
|
|
return name
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function IsAuraSourcePlayer(sourceUnit)
|
|
if not sourceUnit then return nil end
|
|
if sourceUnit == "player" then return true end
|
|
if type(UnitIsUnit) == "function" and type(UnitExists) == "function" and UnitExists(sourceUnit) then
|
|
return UnitIsUnit(sourceUnit, "player") and true or false
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function GetPlayerBuffExpiration(spellId)
|
|
local sid = tonumber(spellId)
|
|
if not sid or sid <= 0 then return nil, nil, nil, nil end
|
|
|
|
if C_UnitAuras and type(C_UnitAuras.GetPlayerAuraBySpellID) == "function" then
|
|
local aura = C_UnitAuras.GetPlayerAuraBySpellID(sid)
|
|
if aura then
|
|
local exp = tonumber(aura.expirationTime) or 0
|
|
if exp > 0 then
|
|
local isOwnCaster = IsAuraSourcePlayer(aura.sourceUnit)
|
|
if isOwnCaster == nil and aura.isFromPlayerOrPlayerPet ~= nil then
|
|
isOwnCaster = aura.isFromPlayerOrPlayerPet and true or false
|
|
end
|
|
return exp, tonumber(aura.duration) or 0, aura.name, isOwnCaster
|
|
end
|
|
end
|
|
end
|
|
|
|
if AuraUtil and type(AuraUtil.FindAuraBySpellID) == "function" then
|
|
local name, _, _, _, duration, expirationTime, sourceUnit, _, _, _, _, _, castByPlayer =
|
|
AuraUtil.FindAuraBySpellID(sid, "player", "HELPFUL")
|
|
local exp = tonumber(expirationTime) or 0
|
|
if name and exp > 0 then
|
|
local isOwnCaster = IsAuraSourcePlayer(sourceUnit)
|
|
if isOwnCaster == nil and castByPlayer ~= nil then
|
|
isOwnCaster = castByPlayer and true or false
|
|
end
|
|
return exp, tonumber(duration) or 0, name, isOwnCaster
|
|
end
|
|
end
|
|
|
|
return nil, nil, nil, nil
|
|
end
|
|
|
|
local function GetPlayerChannelExpiration(spellId)
|
|
local sid = tonumber(spellId)
|
|
if not sid or sid <= 0 then return nil, nil, nil, nil end
|
|
|
|
if type(UnitChannelInfo) ~= "function" then
|
|
return nil, nil, nil, nil
|
|
end
|
|
|
|
local name, _, _, startTimeMS, endTimeMS, _, _, channelSpellId = UnitChannelInfo("player")
|
|
local activeSpellId = tonumber(channelSpellId)
|
|
if activeSpellId ~= sid then
|
|
return nil, nil, nil, nil
|
|
end
|
|
|
|
local startTime = tonumber(startTimeMS) or 0
|
|
local endTime = tonumber(endTimeMS) or 0
|
|
if endTime <= 0 or endTime <= startTime then
|
|
return nil, nil, nil, nil
|
|
end
|
|
|
|
return endTime / 1000, (endTime - startTime) / 1000, name, true
|
|
end
|
|
|
|
local function GetTrackedSpellExpiration(spellId)
|
|
local expirationTime, duration, name, isOwnCaster = GetPlayerBuffExpiration(spellId)
|
|
if expirationTime and expirationTime > 0 then
|
|
return expirationTime, duration, name, isOwnCaster, "aura"
|
|
end
|
|
|
|
expirationTime, duration, name, isOwnCaster = GetPlayerChannelExpiration(spellId)
|
|
if expirationTime and expirationTime > 0 then
|
|
return expirationTime, duration, name, isOwnCaster, "channel"
|
|
end
|
|
|
|
return nil, nil, nil, nil, nil
|
|
end
|
|
|
|
function BEA:GetSettings()
|
|
local p = HMGT.db and HMGT.db.profile
|
|
if not p then return nil end
|
|
p.buffEndingAnnouncer = p.buffEndingAnnouncer or {}
|
|
p.buffEndingAnnouncer.announceAtSec = NormalizeThreshold(p.buffEndingAnnouncer.announceAtSec, 5)
|
|
p.buffEndingAnnouncer.trackedBuffs = p.buffEndingAnnouncer.trackedBuffs or {}
|
|
return p.buffEndingAnnouncer
|
|
end
|
|
|
|
function BEA:GetDefaultThreshold()
|
|
local s = self:GetSettings()
|
|
return NormalizeThreshold(s and s.announceAtSec, 5)
|
|
end
|
|
|
|
function BEA:GetTrackedBuffThreshold(spellId)
|
|
local sid = tonumber(spellId)
|
|
if not sid or sid <= 0 then return nil end
|
|
|
|
local settings = self:GetSettings()
|
|
local tracked = settings and settings.trackedBuffs
|
|
local value = tracked and tracked[sid]
|
|
if value == nil then
|
|
return nil
|
|
end
|
|
|
|
if type(value) == "table" then
|
|
value = value.threshold
|
|
end
|
|
if value == true then
|
|
value = self:GetDefaultThreshold()
|
|
end
|
|
|
|
return NormalizeThreshold(value, self:GetDefaultThreshold())
|
|
end
|
|
|
|
function BEA:SetTrackedBuffThreshold(spellId, threshold)
|
|
local sid = tonumber(spellId)
|
|
if not sid or sid <= 0 then
|
|
return false
|
|
end
|
|
|
|
local s = self:GetSettings()
|
|
s.trackedBuffs[sid] = NormalizeThreshold(threshold, self:GetDefaultThreshold())
|
|
self.lastAnnouncedSecond[sid] = nil
|
|
self:Refresh()
|
|
return true
|
|
end
|
|
|
|
function BEA:GetTrackedBuffEntries()
|
|
local entries = {}
|
|
for _, sid in ipairs(self:GetTrackedBuffSpellIds()) do
|
|
entries[#entries + 1] = {
|
|
spellId = sid,
|
|
name = GetSpellName(sid) or ("Spell " .. tostring(sid)),
|
|
threshold = self:GetTrackedBuffThreshold(sid) or self:GetDefaultThreshold(),
|
|
}
|
|
end
|
|
return entries
|
|
end
|
|
|
|
function BEA:GetTrackedBuffSpellIds()
|
|
local ids = {}
|
|
local settings = self:GetSettings()
|
|
local tracked = (settings and settings.trackedBuffs) or {}
|
|
for sid, value in pairs(tracked) do
|
|
local id = tonumber(sid)
|
|
if id and id > 0 and value ~= nil and value ~= false then
|
|
ids[#ids + 1] = id
|
|
end
|
|
end
|
|
table.sort(ids, function(a, b)
|
|
local nameA = tostring(GetSpellName(a) or a):lower()
|
|
local nameB = tostring(GetSpellName(b) or b):lower()
|
|
if nameA == nameB then
|
|
return a < b
|
|
end
|
|
return nameA < nameB
|
|
end)
|
|
return ids
|
|
end
|
|
|
|
function BEA:ResetAnnouncements()
|
|
for sid in pairs(self.lastAnnouncedSecond) do
|
|
self.lastAnnouncedSecond[sid] = nil
|
|
end
|
|
end
|
|
|
|
function BEA:ResetRecentOwnCasts()
|
|
for sid in pairs(self.recentOwnCastAt) do
|
|
self.recentOwnCastAt[sid] = nil
|
|
end
|
|
end
|
|
|
|
function BEA:StopTicker()
|
|
if self.ticker then
|
|
self.ticker:Cancel()
|
|
self.ticker = nil
|
|
end
|
|
end
|
|
|
|
function BEA:HasTrackedBuffsConfigured()
|
|
return #self:GetTrackedBuffSpellIds() > 0
|
|
end
|
|
|
|
function BEA:UpdateRuntimeEventRegistrations()
|
|
if not self.eventFrame then
|
|
return
|
|
end
|
|
|
|
self.eventFrame:UnregisterEvent("UNIT_AURA")
|
|
self.eventFrame:UnregisterEvent("UNIT_SPELLCAST_SUCCEEDED")
|
|
self.eventFrame:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_START")
|
|
self.eventFrame:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")
|
|
self.eventFrame:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")
|
|
|
|
if not self.runtimeEnabled or not self:HasTrackedBuffsConfigured() then
|
|
return
|
|
end
|
|
|
|
self.eventFrame:RegisterUnitEvent("UNIT_AURA", "player")
|
|
self.eventFrame:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player")
|
|
self.eventFrame:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_START", "player")
|
|
self.eventFrame:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_UPDATE", "player")
|
|
self.eventFrame:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_STOP", "player")
|
|
end
|
|
|
|
function BEA:EnsureTicker()
|
|
if self.ticker then return end
|
|
self.ticker = C_Timer.NewTicker(0.1, function()
|
|
self:OnTicker()
|
|
end)
|
|
end
|
|
|
|
function BEA:IsProfileEnabled()
|
|
local s = self:GetSettings()
|
|
return s and s.enabled == true
|
|
end
|
|
|
|
function BEA:BuildAnnouncement(spellName, secondsLeft)
|
|
local template = L["BEA_MSG_TEMPLATE"] or "%s ending in %d"
|
|
return string.format(template, tostring(spellName or "?"), tonumber(secondsLeft) or 0)
|
|
end
|
|
|
|
function BEA:IsOwnBuffCaster(spellId, isOwnCaster, duration, now)
|
|
if isOwnCaster == true then
|
|
return true
|
|
end
|
|
if isOwnCaster == false then
|
|
return false
|
|
end
|
|
|
|
-- Fallback when aura source information is missing:
|
|
-- only trust a very recent own cast of the tracked spell.
|
|
local castAt = self.recentOwnCastAt[tonumber(spellId)]
|
|
if not castAt then
|
|
return false
|
|
end
|
|
|
|
local maxAge = tonumber(duration) or 0
|
|
if maxAge < 3 then maxAge = 3 end
|
|
if maxAge > 12 then maxAge = 12 end
|
|
return (tonumber(now) or GetTime()) - castAt <= maxAge
|
|
end
|
|
|
|
function BEA:EvaluateTrackedBuffs()
|
|
if not self:IsProfileEnabled() then
|
|
self:ResetAnnouncements()
|
|
return false
|
|
end
|
|
|
|
local ids = self:GetTrackedBuffSpellIds()
|
|
if #ids == 0 then
|
|
self:ResetAnnouncements()
|
|
return false
|
|
end
|
|
|
|
local now = GetTime()
|
|
local hasAnyTrackedBuff = false
|
|
|
|
for _, sid in ipairs(ids) do
|
|
local threshold = self:GetTrackedBuffThreshold(sid)
|
|
local expirationTime, duration, auraName, isOwnCaster, stateKind = GetTrackedSpellExpiration(sid)
|
|
if expirationTime and expirationTime > now then
|
|
local isOwnSource = (stateKind == "channel") or self:IsOwnBuffCaster(sid, isOwnCaster, duration, now)
|
|
if isOwnSource then
|
|
hasAnyTrackedBuff = true
|
|
local remaining = expirationTime - now
|
|
if threshold and remaining > 0 and remaining <= threshold then
|
|
local second = math.ceil(remaining - 0.0001)
|
|
if second < 1 then second = 1 end
|
|
if self.lastAnnouncedSecond[sid] ~= second then
|
|
local msg = self:BuildAnnouncement(auraName or GetSpellName(sid) or ("Spell " .. tostring(sid)), second)
|
|
SendChatMessage(msg, "SAY")
|
|
self.lastAnnouncedSecond[sid] = second
|
|
end
|
|
else
|
|
self.lastAnnouncedSecond[sid] = nil
|
|
end
|
|
else
|
|
self.lastAnnouncedSecond[sid] = nil
|
|
end
|
|
else
|
|
self.lastAnnouncedSecond[sid] = nil
|
|
end
|
|
|
|
local castAt = self.recentOwnCastAt[sid]
|
|
if castAt and (now - castAt) > 30 then
|
|
self.recentOwnCastAt[sid] = nil
|
|
end
|
|
end
|
|
|
|
return hasAnyTrackedBuff
|
|
end
|
|
|
|
function BEA:Refresh()
|
|
if not self.runtimeEnabled then return end
|
|
self:UpdateRuntimeEventRegistrations()
|
|
local active = self:EvaluateTrackedBuffs()
|
|
if active then
|
|
self:EnsureTicker()
|
|
else
|
|
self:StopTicker()
|
|
end
|
|
end
|
|
|
|
function BEA:OnTicker()
|
|
if not self.runtimeEnabled then
|
|
self:StopTicker()
|
|
return
|
|
end
|
|
local active = self:EvaluateTrackedBuffs()
|
|
if not active then
|
|
self:StopTicker()
|
|
end
|
|
end
|
|
|
|
function BEA:OnEvent(event, ...)
|
|
if event == "UNIT_AURA" then
|
|
local unit = ...
|
|
if unit ~= "player" then
|
|
return
|
|
end
|
|
elseif event == "UNIT_SPELLCAST_SUCCEEDED" then
|
|
local unit, _, spellId = ...
|
|
if unit == "player" then
|
|
local sid = tonumber(spellId)
|
|
if sid and sid > 0 then
|
|
local s = self:GetSettings()
|
|
if s and s.trackedBuffs and s.trackedBuffs[sid] then
|
|
self.recentOwnCastAt[sid] = GetTime()
|
|
end
|
|
end
|
|
end
|
|
elseif event == "UNIT_SPELLCAST_CHANNEL_START" or event == "UNIT_SPELLCAST_CHANNEL_UPDATE" or event == "UNIT_SPELLCAST_CHANNEL_STOP" then
|
|
local unit = ...
|
|
if unit ~= "player" then
|
|
return
|
|
end
|
|
elseif event == "PLAYER_ENTERING_WORLD" or event == "PLAYER_DEAD" then
|
|
self:ResetAnnouncements()
|
|
self:ResetRecentOwnCasts()
|
|
end
|
|
self:Refresh()
|
|
end
|
|
|
|
function BEA:StartRuntime()
|
|
if not self:IsProfileEnabled() then return end
|
|
if self.runtimeEnabled then
|
|
self:Refresh()
|
|
return
|
|
end
|
|
|
|
self.runtimeEnabled = true
|
|
if not self.eventFrame then
|
|
self.eventFrame = CreateFrame("Frame")
|
|
self.eventFrame:SetScript("OnEvent", function(_, event, ...)
|
|
self:OnEvent(event, ...)
|
|
end)
|
|
end
|
|
self.eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
self.eventFrame:RegisterEvent("PLAYER_DEAD")
|
|
self:Refresh()
|
|
end
|
|
|
|
function BEA:StopRuntime()
|
|
if self.eventFrame then
|
|
self.eventFrame:UnregisterEvent("UNIT_AURA")
|
|
self.eventFrame:UnregisterEvent("UNIT_SPELLCAST_SUCCEEDED")
|
|
self.eventFrame:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_START")
|
|
self.eventFrame:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE")
|
|
self.eventFrame:UnregisterEvent("UNIT_SPELLCAST_CHANNEL_STOP")
|
|
self.eventFrame:UnregisterEvent("PLAYER_ENTERING_WORLD")
|
|
self.eventFrame:UnregisterEvent("PLAYER_DEAD")
|
|
end
|
|
self.runtimeEnabled = false
|
|
self:StopTicker()
|
|
self:ResetAnnouncements()
|
|
self:ResetRecentOwnCasts()
|
|
end
|
|
|
|
function BEA:OnInitialize()
|
|
HMGT.BuffEndingAnnouncer = self
|
|
end
|
|
|
|
function BEA:OnEnable()
|
|
self:StartRuntime()
|
|
end
|
|
|
|
function BEA:OnDisable()
|
|
self:StopRuntime()
|
|
end
|
|
|
|
function BEA:AddTrackedBuff(spellId, threshold)
|
|
local sid = tonumber(spellId)
|
|
if not sid or sid <= 0 then
|
|
return false, "invalid"
|
|
end
|
|
local name = GetSpellName(sid)
|
|
if not name then
|
|
return false, "invalid"
|
|
end
|
|
|
|
local s = self:GetSettings()
|
|
s.trackedBuffs[sid] = NormalizeThreshold(threshold, self:GetDefaultThreshold())
|
|
self.lastAnnouncedSecond[sid] = nil
|
|
self:Refresh()
|
|
return true, name
|
|
end
|
|
|
|
function BEA:RemoveTrackedBuff(spellId)
|
|
local sid = tonumber(spellId)
|
|
if not sid or sid <= 0 then
|
|
return false, "invalid"
|
|
end
|
|
|
|
local s = self:GetSettings()
|
|
if not s.trackedBuffs[sid] then
|
|
return false, "missing"
|
|
end
|
|
|
|
s.trackedBuffs[sid] = nil
|
|
self.lastAnnouncedSecond[sid] = nil
|
|
self:Refresh()
|
|
return true, GetSpellName(sid) or tostring(sid)
|
|
end
|