initial commit
This commit is contained in:
472
Modules/BuffEndingAnnouncer/BuffEndingAnnouncer.lua
Normal file
472
Modules/BuffEndingAnnouncer/BuffEndingAnnouncer.lua
Normal file
@@ -0,0 +1,472 @@
|
||||
-- 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
|
||||
Reference in New Issue
Block a user