聊天重做前缓存
This commit is contained in:
594
AuraTracker.lua
Normal file
594
AuraTracker.lua
Normal file
@@ -0,0 +1,594 @@
|
||||
SFrames.AuraTracker = SFrames.AuraTracker or {}
|
||||
|
||||
local AT = SFrames.AuraTracker
|
||||
|
||||
AT.units = AT.units or {}
|
||||
AT.unitRefs = AT.unitRefs or {}
|
||||
AT.durationCache = AT.durationCache or { buff = {}, debuff = {} }
|
||||
AT.initialized = AT.initialized or false
|
||||
|
||||
local SOURCE_PRIORITY = {
|
||||
player_native = 6,
|
||||
superwow = 5,
|
||||
nanamiplates = 4,
|
||||
shagutweaks = 3,
|
||||
combat_log = 2,
|
||||
estimated = 1,
|
||||
}
|
||||
|
||||
local function GetNow()
|
||||
return GetTime and GetTime() or 0
|
||||
end
|
||||
|
||||
local function IsBuffType(auraType)
|
||||
return auraType == "buff"
|
||||
end
|
||||
|
||||
local function ClampPositive(value)
|
||||
value = tonumber(value) or 0
|
||||
if value > 0 then
|
||||
return value
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function SafeUnitGUID(unit)
|
||||
if unit and UnitGUID then
|
||||
local ok, guid = pcall(UnitGUID, unit)
|
||||
if ok and guid and guid ~= "" then
|
||||
return guid
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function SafeUnitName(unit)
|
||||
if unit and UnitName then
|
||||
local ok, name = pcall(UnitName, unit)
|
||||
if ok and name and name ~= "" then
|
||||
return name
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function GetUnitKey(unit)
|
||||
local guid = SafeUnitGUID(unit)
|
||||
if guid then
|
||||
return guid
|
||||
end
|
||||
local name = SafeUnitName(unit)
|
||||
if name then
|
||||
return "name:" .. name
|
||||
end
|
||||
return unit and ("unit:" .. unit) or nil
|
||||
end
|
||||
|
||||
local function DurationCacheKey(auraType, spellId, name, texture)
|
||||
if spellId and spellId > 0 then
|
||||
return auraType .. ":id:" .. tostring(spellId)
|
||||
end
|
||||
if name and name ~= "" then
|
||||
return auraType .. ":name:" .. string.lower(name)
|
||||
end
|
||||
return auraType .. ":tex:" .. tostring(texture or "")
|
||||
end
|
||||
|
||||
local function BuildStateKeys(auraType, spellId, name, texture, casterKey)
|
||||
local keys = {}
|
||||
if spellId and spellId > 0 then
|
||||
tinsert(keys, auraType .. ":id:" .. tostring(spellId))
|
||||
if casterKey and casterKey ~= "" then
|
||||
tinsert(keys, auraType .. ":id:" .. tostring(spellId) .. ":caster:" .. casterKey)
|
||||
end
|
||||
end
|
||||
if name and name ~= "" then
|
||||
local lowerName = string.lower(name)
|
||||
tinsert(keys, auraType .. ":name:" .. lowerName)
|
||||
if casterKey and casterKey ~= "" then
|
||||
tinsert(keys, auraType .. ":name:" .. lowerName .. ":caster:" .. casterKey)
|
||||
end
|
||||
if texture and texture ~= "" then
|
||||
tinsert(keys, auraType .. ":name:" .. lowerName .. ":tex:" .. texture)
|
||||
end
|
||||
elseif texture and texture ~= "" then
|
||||
tinsert(keys, auraType .. ":tex:" .. texture)
|
||||
end
|
||||
return keys
|
||||
end
|
||||
|
||||
local function TooltipLine1()
|
||||
return getglobal("SFramesScanTooltipTextLeft1")
|
||||
end
|
||||
|
||||
local function CLMatch(msg, pattern)
|
||||
if not msg or not pattern or pattern == "" then return nil end
|
||||
local pat = string.gsub(pattern, "%%%d?%$?s", "(.+)")
|
||||
pat = string.gsub(pat, "%%%d?%$?d", "(%%d+)")
|
||||
for a, b, c in string.gfind(msg, pat) do
|
||||
return a, b, c
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function AT:GetUnitState(unitOrGUID)
|
||||
local key = unitOrGUID
|
||||
if not key then return nil end
|
||||
if string.find(key, "^target") or string.find(key, "^player") or string.find(key, "^party") or string.find(key, "^raid") or string.find(key, "^pet") then
|
||||
key = GetUnitKey(key)
|
||||
end
|
||||
if not key then return nil end
|
||||
if not self.units[key] then
|
||||
self.units[key] = {
|
||||
guid = key,
|
||||
buffs = {},
|
||||
debuffs = {},
|
||||
maps = { buff = {}, debuff = {} },
|
||||
snapshotCount = 0,
|
||||
lastSeen = 0,
|
||||
name = nil,
|
||||
level = 0,
|
||||
}
|
||||
end
|
||||
return self.units[key]
|
||||
end
|
||||
|
||||
function AT:ClearUnit(unitOrGUID)
|
||||
local key = unitOrGUID
|
||||
if not key then return end
|
||||
if not self.units[key] then
|
||||
key = GetUnitKey(unitOrGUID)
|
||||
end
|
||||
if not key then return end
|
||||
self.units[key] = nil
|
||||
|
||||
for unit, guid in pairs(self.unitRefs) do
|
||||
if guid == key then
|
||||
self.unitRefs[unit] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function AT:ClearCurrentTarget()
|
||||
local oldGUID = self.unitRefs["target"]
|
||||
self.unitRefs["target"] = nil
|
||||
if oldGUID then
|
||||
local state = self.units[oldGUID]
|
||||
if state then
|
||||
state.maps.buff = {}
|
||||
state.maps.debuff = {}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function AT:RememberDuration(auraType, spellId, name, texture, duration)
|
||||
duration = ClampPositive(duration)
|
||||
if not duration then return end
|
||||
self.durationCache[auraType][DurationCacheKey(auraType, spellId, name, texture)] = duration
|
||||
end
|
||||
|
||||
function AT:GetRememberedDuration(auraType, spellId, name, texture)
|
||||
return self.durationCache[auraType][DurationCacheKey(auraType, spellId, name, texture)]
|
||||
end
|
||||
|
||||
function AT:ReadAuraName(unit, index, isBuff, auraID)
|
||||
if auraID and auraID > 0 and SpellInfo then
|
||||
local ok, spellName = pcall(SpellInfo, auraID)
|
||||
if ok and spellName and spellName ~= "" then
|
||||
return spellName
|
||||
end
|
||||
end
|
||||
|
||||
-- Tooltip scan is expensive and can crash on invalid unit/index state.
|
||||
-- Only attempt if unit still exists and the aura slot is still valid.
|
||||
if not SFrames.Tooltip then return nil end
|
||||
if not unit or not UnitExists or not UnitExists(unit) then return nil end
|
||||
|
||||
local ok, text = pcall(function()
|
||||
SFrames.Tooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||||
SFrames.Tooltip:ClearLines()
|
||||
if isBuff then
|
||||
SFrames.Tooltip:SetUnitBuff(unit, index)
|
||||
else
|
||||
SFrames.Tooltip:SetUnitDebuff(unit, index)
|
||||
end
|
||||
local line = TooltipLine1()
|
||||
local t = line and line.GetText and line:GetText() or nil
|
||||
SFrames.Tooltip:Hide()
|
||||
return t
|
||||
end)
|
||||
|
||||
if ok and text and text ~= "" then
|
||||
return text
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function AT:GetPlayerAuraTime(unit, index, isBuff, texture)
|
||||
if not UnitIsUnit or not UnitIsUnit(unit, "player") then
|
||||
return nil, nil, nil
|
||||
end
|
||||
if not GetPlayerBuff or not GetPlayerBuffTexture or not GetPlayerBuffTimeLeft then
|
||||
return nil, nil, nil
|
||||
end
|
||||
|
||||
local filter = isBuff and "HELPFUL" or "HARMFUL"
|
||||
for i = 0, 31 do
|
||||
local buffIndex = GetPlayerBuff(i, filter)
|
||||
if buffIndex and buffIndex >= 0 and GetPlayerBuffTexture(buffIndex) == texture then
|
||||
local timeLeft = ClampPositive(GetPlayerBuffTimeLeft(buffIndex))
|
||||
if timeLeft then
|
||||
return timeLeft, nil, "player_native"
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil, nil, nil
|
||||
end
|
||||
|
||||
function AT:GetExternalTime(unit, auraType, index, state)
|
||||
local isBuff = IsBuffType(auraType)
|
||||
local timeLeft, duration, source
|
||||
|
||||
if state and state.texture then
|
||||
timeLeft, duration, source = self:GetPlayerAuraTime(unit, index, isBuff, state.texture)
|
||||
if timeLeft then
|
||||
return timeLeft, duration, source
|
||||
end
|
||||
end
|
||||
|
||||
if not isBuff and NanamiPlates_SpellDB and NanamiPlates_SpellDB.UnitDebuff then
|
||||
local effect, _, _, _, _, npDuration, npTimeLeft = NanamiPlates_SpellDB:UnitDebuff(unit, index)
|
||||
npTimeLeft = ClampPositive(npTimeLeft)
|
||||
npDuration = ClampPositive(npDuration)
|
||||
if effect and effect ~= "" and (not state.name or state.name == "") then
|
||||
state.name = effect
|
||||
end
|
||||
if npTimeLeft then
|
||||
return npTimeLeft, npDuration, "nanamiplates"
|
||||
end
|
||||
end
|
||||
|
||||
if not isBuff and ShaguTweaks and ShaguTweaks.libdebuff and ShaguTweaks.libdebuff.UnitDebuff then
|
||||
local _, _, _, _, _, shaguDuration, shaguTimeLeft = ShaguTweaks.libdebuff:UnitDebuff(unit, index)
|
||||
shaguTimeLeft = ClampPositive(shaguTimeLeft)
|
||||
shaguDuration = ClampPositive(shaguDuration)
|
||||
if shaguTimeLeft then
|
||||
return shaguTimeLeft, shaguDuration, "shagutweaks"
|
||||
end
|
||||
end
|
||||
|
||||
return nil, nil, nil
|
||||
end
|
||||
|
||||
function AT:ApplyTiming(state, timeLeft, duration, source, now, allowWeaker)
|
||||
timeLeft = ClampPositive(timeLeft)
|
||||
if not timeLeft then return false end
|
||||
|
||||
duration = ClampPositive(duration) or state.duration or timeLeft
|
||||
local newPriority = SOURCE_PRIORITY[source] or 0
|
||||
local oldPriority = SOURCE_PRIORITY[state.source] or 0
|
||||
|
||||
if not allowWeaker and state.expirationTime and state.expirationTime > now and oldPriority > newPriority then
|
||||
return false
|
||||
end
|
||||
|
||||
state.duration = duration
|
||||
state.expirationTime = now + timeLeft
|
||||
state.appliedAt = state.expirationTime - duration
|
||||
state.source = source
|
||||
state.isEstimated = (source == "estimated") and 1 or nil
|
||||
|
||||
self:RememberDuration(state.auraType, state.spellId, state.name, state.texture, duration)
|
||||
return true
|
||||
end
|
||||
|
||||
function AT:CaptureAura(unit, auraType, index)
|
||||
local isBuff = IsBuffType(auraType)
|
||||
local texture, count, dispelType, auraID
|
||||
local hasSuperWoW = SFrames.superwow_active and SpellInfo
|
||||
|
||||
if isBuff then
|
||||
texture, auraID = UnitBuff(unit, index)
|
||||
else
|
||||
texture, count, dispelType, auraID = UnitDebuff(unit, index)
|
||||
end
|
||||
|
||||
if not texture then
|
||||
return nil
|
||||
end
|
||||
|
||||
local spellId = (hasSuperWoW and type(auraID) == "number" and auraID > 0) and auraID or nil
|
||||
-- Only call ReadAuraName when we have a spellId (fast path) or for the first 16 slots
|
||||
-- to avoid spamming tooltip API on every aura slot every UNIT_AURA event.
|
||||
local name
|
||||
if spellId or index <= 16 then
|
||||
name = self:ReadAuraName(unit, index, isBuff, spellId)
|
||||
end
|
||||
|
||||
return {
|
||||
auraType = auraType,
|
||||
index = index,
|
||||
spellId = spellId,
|
||||
name = name,
|
||||
texture = texture,
|
||||
stacks = tonumber(count) or 0,
|
||||
dispelType = dispelType,
|
||||
casterKey = nil,
|
||||
}
|
||||
end
|
||||
|
||||
function AT:FindPreviousState(unitState, auraType, index, info)
|
||||
local previous = unitState.maps[auraType][index]
|
||||
if previous then
|
||||
local keys = BuildStateKeys(auraType, info.spellId, info.name, info.texture, info.casterKey)
|
||||
local prevKeys = BuildStateKeys(auraType, previous.spellId, previous.name, previous.texture, previous.casterKey)
|
||||
|
||||
for _, key in ipairs(keys) do
|
||||
for _, prevKey in ipairs(prevKeys) do
|
||||
if key == prevKey then
|
||||
return previous
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local states = IsBuffType(auraType) and unitState.buffs or unitState.debuffs
|
||||
local keys = BuildStateKeys(auraType, info.spellId, info.name, info.texture, info.casterKey)
|
||||
for _, key in ipairs(keys) do
|
||||
local state = states[key]
|
||||
if state then
|
||||
return state
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function AT:UpdateSnapshotState(unitState, unit, auraType, index, info, now)
|
||||
local state = self:FindPreviousState(unitState, auraType, index, info)
|
||||
if not state then
|
||||
state = {
|
||||
guid = unitState.guid,
|
||||
auraType = auraType,
|
||||
firstSeenAt = now,
|
||||
}
|
||||
end
|
||||
|
||||
state.guid = unitState.guid
|
||||
state.auraType = auraType
|
||||
state.spellId = info.spellId
|
||||
state.name = info.name or state.name
|
||||
state.texture = info.texture
|
||||
state.stacks = info.stacks
|
||||
state.dispelType = info.dispelType
|
||||
state.casterKey = info.casterKey
|
||||
state.lastSeen = now
|
||||
state.index = index
|
||||
|
||||
local timeLeft, duration, source = self:GetExternalTime(unit, auraType, index, state)
|
||||
if timeLeft then
|
||||
self:ApplyTiming(state, timeLeft, duration, source, now)
|
||||
elseif state.expirationTime and state.expirationTime <= now then
|
||||
state.expirationTime = nil
|
||||
state.duration = nil
|
||||
state.appliedAt = nil
|
||||
state.source = nil
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
function AT:RebuildStateMaps(unitState, auraType, slots, now)
|
||||
local active = {}
|
||||
local newMap = {}
|
||||
|
||||
for index, state in pairs(slots) do
|
||||
if state then
|
||||
for _, key in ipairs(BuildStateKeys(auraType, state.spellId, state.name, state.texture, state.casterKey)) do
|
||||
active[key] = state
|
||||
end
|
||||
newMap[index] = state
|
||||
end
|
||||
end
|
||||
|
||||
if IsBuffType(auraType) then
|
||||
unitState.buffs = active
|
||||
else
|
||||
unitState.debuffs = active
|
||||
end
|
||||
unitState.maps[auraType] = newMap
|
||||
unitState.lastSeen = now
|
||||
end
|
||||
|
||||
function AT:ApplyCombatHint(auraType, auraName)
|
||||
if not auraName or auraName == "" then return end
|
||||
if auraType ~= "debuff" then return end
|
||||
local guid = self.unitRefs["target"]
|
||||
if not guid then return end
|
||||
|
||||
local unitState = self.units[guid]
|
||||
if not unitState then return end
|
||||
|
||||
local active = IsBuffType(auraType) and unitState.buffs or unitState.debuffs
|
||||
local state = active[auraType .. ":name:" .. string.lower(auraName)]
|
||||
if not state then return end
|
||||
|
||||
local remembered = self:GetRememberedDuration(auraType, state.spellId, state.name, state.texture)
|
||||
if remembered then
|
||||
self:ApplyTiming(state, remembered, remembered, "combat_log", GetNow(), true)
|
||||
end
|
||||
end
|
||||
|
||||
function AT:ClearCombatHint(auraName)
|
||||
if not auraName or auraName == "" then return end
|
||||
local guid = self.unitRefs["target"]
|
||||
if not guid then return end
|
||||
|
||||
local unitState = self.units[guid]
|
||||
if not unitState then return end
|
||||
|
||||
local lowerName = string.lower(auraName)
|
||||
for _, auraType in ipairs({ "buff", "debuff" }) do
|
||||
local active = IsBuffType(auraType) and unitState.buffs or unitState.debuffs
|
||||
local state = active[auraType .. ":name:" .. lowerName]
|
||||
if state then
|
||||
state.expirationTime = nil
|
||||
state.duration = nil
|
||||
state.appliedAt = nil
|
||||
state.source = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function AT:HandleCombatMessage(msg)
|
||||
if not msg or msg == "" or not UnitExists or not UnitExists("target") then return end
|
||||
|
||||
local targetName = SafeUnitName("target")
|
||||
if not targetName then return end
|
||||
|
||||
local targetUnit, auraName = CLMatch(msg, AURAADDEDOTHERHARMFUL or "%s is afflicted by %s.")
|
||||
if targetUnit == targetName and auraName then
|
||||
self:ApplyCombatHint("debuff", auraName)
|
||||
return
|
||||
end
|
||||
|
||||
auraName, targetUnit = CLMatch(msg, AURAREMOVEDOTHER or "%s fades from %s.")
|
||||
if targetUnit == targetName and auraName then
|
||||
self:ClearCombatHint(auraName)
|
||||
end
|
||||
end
|
||||
|
||||
function AT:HandleAuraSnapshot(unit)
|
||||
if not unit then return end
|
||||
if not UnitExists or not UnitExists(unit) then
|
||||
if unit == "target" then
|
||||
self:ClearCurrentTarget()
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local guid = GetUnitKey(unit)
|
||||
if not guid then return end
|
||||
|
||||
local now = GetNow()
|
||||
local unitState = self:GetUnitState(guid)
|
||||
unitState.guid = guid
|
||||
unitState.name = SafeUnitName(unit)
|
||||
unitState.level = (UnitLevel and UnitLevel(unit)) or 0
|
||||
unitState.lastSeen = now
|
||||
|
||||
self.unitRefs[unit] = guid
|
||||
|
||||
local buffSlots = {}
|
||||
local debuffSlots = {}
|
||||
|
||||
for i = 1, 32 do
|
||||
local info = self:CaptureAura(unit, "buff", i)
|
||||
if info then
|
||||
buffSlots[i] = self:UpdateSnapshotState(unitState, unit, "buff", i, info, now)
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, 32 do
|
||||
local info = self:CaptureAura(unit, "debuff", i)
|
||||
if info then
|
||||
debuffSlots[i] = self:UpdateSnapshotState(unitState, unit, "debuff", i, info, now)
|
||||
end
|
||||
end
|
||||
|
||||
self:RebuildStateMaps(unitState, "buff", buffSlots, now)
|
||||
self:RebuildStateMaps(unitState, "debuff", debuffSlots, now)
|
||||
|
||||
unitState.snapshotCount = unitState.snapshotCount + 1
|
||||
end
|
||||
|
||||
function AT:GetAuraState(unit, auraType, index)
|
||||
local unitState = self:GetUnitState(unit)
|
||||
if not unitState then return nil end
|
||||
local map = unitState.maps[auraType]
|
||||
return map and map[index] or nil
|
||||
end
|
||||
|
||||
function AT:GetAuraTimeLeft(unit, auraType, index)
|
||||
local state = self:GetAuraState(unit, auraType, index)
|
||||
if not state or not state.expirationTime then
|
||||
return nil
|
||||
end
|
||||
|
||||
local remaining = state.expirationTime - GetNow()
|
||||
if remaining > 0 then
|
||||
return remaining
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function AT:PurgeStaleUnits()
|
||||
local now = GetNow()
|
||||
local activeTargetGUID = self.unitRefs["target"]
|
||||
for guid, state in pairs(self.units) do
|
||||
if guid ~= activeTargetGUID and state.lastSeen and (now - state.lastSeen) > 120 then
|
||||
self.units[guid] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function AT:OnEvent()
|
||||
if event == "PLAYER_TARGET_CHANGED" then
|
||||
if UnitExists and UnitExists("target") then
|
||||
self:HandleAuraSnapshot("target")
|
||||
else
|
||||
self:ClearCurrentTarget()
|
||||
end
|
||||
self:PurgeStaleUnits()
|
||||
return
|
||||
end
|
||||
|
||||
if event == "UNIT_AURA" and arg1 == "target" then
|
||||
self:HandleAuraSnapshot("target")
|
||||
return
|
||||
end
|
||||
|
||||
if event == "PLAYER_ENTERING_WORLD" then
|
||||
self:ClearCurrentTarget()
|
||||
self:PurgeStaleUnits()
|
||||
return
|
||||
end
|
||||
|
||||
if arg1 and string.find(event, "CHAT_MSG_SPELL") then
|
||||
self:HandleCombatMessage(arg1)
|
||||
end
|
||||
end
|
||||
|
||||
function AT:Initialize()
|
||||
if self.initialized then return end
|
||||
self.initialized = true
|
||||
|
||||
local frame = CreateFrame("Frame", "SFramesAuraTracker", UIParent)
|
||||
self.frame = frame
|
||||
|
||||
frame:RegisterEvent("PLAYER_TARGET_CHANGED")
|
||||
frame:RegisterEvent("UNIT_AURA")
|
||||
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
local chatEvents = {
|
||||
"CHAT_MSG_SPELL_SELF_DAMAGE",
|
||||
"CHAT_MSG_SPELL_SELF_BUFF",
|
||||
"CHAT_MSG_SPELL_PARTY_DAMAGE",
|
||||
"CHAT_MSG_SPELL_PARTY_BUFF",
|
||||
"CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE",
|
||||
"CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF",
|
||||
"CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE",
|
||||
"CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF",
|
||||
"CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE",
|
||||
"CHAT_MSG_SPELL_CREATURE_VS_PARTY_DAMAGE",
|
||||
"CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE",
|
||||
"CHAT_MSG_SPELL_CREATURE_VS_CREATURE_BUFF",
|
||||
"CHAT_MSG_SPELL_CREATURE_VS_PARTY_BUFF",
|
||||
"CHAT_MSG_SPELL_CREATURE_VS_SELF_BUFF",
|
||||
}
|
||||
for _, ev in ipairs(chatEvents) do
|
||||
frame:RegisterEvent(ev)
|
||||
end
|
||||
frame:SetScript("OnEvent", function()
|
||||
AT:OnEvent()
|
||||
end)
|
||||
end
|
||||
Reference in New Issue
Block a user