Replace Nampower combat events with ClassicAPI events

Read casts from UNIT_SPELLCAST_* and heal amounts from
COMBAT_LOG_EVENT_UNFILTERED in libpredict, swingtimer and the prowl
page switch. Drop libdebuff's AURA_CAST/DEBUFF_* bookkeeping and hook
tables, which nothing read: its aura readers already use C_UnitAuras.
Drop the unitframe BUFF_ADDED_OTHER start-time tracker, since
C_UnitAuras stamps an expiration for auras seen being applied.
This commit is contained in:
Brues
2026-10-23 21:27:28 -05:00
parent b73ed88346
commit 3859678133
5 changed files with 203 additions and 1895 deletions
-35
View File
@@ -897,32 +897,6 @@ function pfUI.uf.OnShow()
pfUI.uf:RefreshUnit(this, "base")
end
-- GUID-keyed aura start-time tracker, fed by Nampower's BUFF_ADDED_OTHER /
-- BUFF_REMOVED_OTHER. Non-player units have aura.expirationTime == 0 (server
-- doesn't broadcast time), so we reconstruct the swirl from "when we saw it
-- applied" + aura.duration (Spell.dbc base). Only auras we actually witnessed
-- being cast get tracked — pre-existing auras (already on a unit when you
-- target them) intentionally show no timer rather than a fabricated one.
pfUI.uf.aura_starts = {} -- {[guid] = {[spellId] = startTime}}
local auraTracker = CreateFrame("Frame", "pfUF_AuraTracker", UIParent)
auraTracker:RegisterEvent("BUFF_ADDED_OTHER")
auraTracker:RegisterEvent("BUFF_REMOVED_OTHER")
auraTracker:SetScript("OnEvent", function()
-- Nampower arg layout: arg1=guid, arg3=spellId. State code 0=added, 1=removed,
-- 2=modified; we treat any added/modified as "reset start = now" and let the
-- REMOVED event clear the entry.
if not arg1 or not arg3 or arg3 == 0 then return end
if event == "BUFF_REMOVED_OTHER" then
if pfUI.uf.aura_starts[arg1] then
pfUI.uf.aura_starts[arg1][arg3] = nil
end
else
pfUI.uf.aura_starts[arg1] = pfUI.uf.aura_starts[arg1] or {}
pfUI.uf.aura_starts[arg1][arg3] = GetTime()
end
end)
function pfUI.uf.OnEvent()
-- Handle shutdown to prevent crash 132
if event == "PLAYER_LOGOUT" then
@@ -1752,15 +1726,6 @@ function pfUI.uf:RefreshUnit(unit, component)
else
CooldownFrame_SetTimer(unit.buffs[i].cd, 0, 0, 0)
end
elseif duration > 0 then
local guid = UnitGUID(unitstr)
local guidStarts = guid and pfUI.uf.aura_starts[guid]
local start = guidStarts and guidStarts[spellId]
if start then
CooldownFrame_SetTimer(unit.buffs[i].cd, start, duration, 1)
else
CooldownFrame_SetTimer(unit.buffs[i].cd, 0, 0, 0)
end
else
CooldownFrame_SetTimer(unit.buffs[i].cd, 0, 0, 0)
end
+8 -1616
View File
File diff suppressed because it is too large Load Diff
+159 -206
View File
@@ -13,34 +13,23 @@ setfenv(1, pfUI:GetEnvironment())
-- including resurrections. It has an option to disable the sending of those
-- messages in case HealComm is already active.
--
-- HOT TRACKING INTEGRATION (NEW):
-- With Nampower enabled, HoT tracking now primarily uses libdebuff's AURA_CAST
-- event system for accurate server-side buff/debuff tracking with full rank
-- protection. GetHotDuration() first checks libdebuff, then falls back to the
-- legacy prediction system for backwards compatibility with non-Nampower clients.
-- This provides:
-- - Accurate duration from server (no prediction needed)
-- - Automatic rank protection (lower ranks won't overwrite higher ranks)
-- - Support for multiple casters of same HoT on one target
-- - Zero event overhead (libdebuff already tracks all auras)
-- Casts are read from ClassicAPI's UNIT_SPELLCAST_* events and heal amounts
-- from COMBAT_LOG_EVENT_UNFILTERED. GetHotDuration() reads live HoT timers
-- from C_UnitAuras (via libdebuff) and falls back to the HealComm-fed table.
-- return instantly when another libpredict is already active
if pfUI.api.libpredict then return end
-- Check if libdebuff integration is available
local libdebuff_available = (pfUI.api.libdebuff and pfUI.api.libdebuff.GetBestAuraCast) and true or false
-- Check if Nampower is available for SPELL_FAILED events
local senttarget
local heals, ress, events, hots = {}, {}, {}, {}
local spell_queue = { "DUMMY", "DUMMYRank 9", "TARGET" }
local player = UnitName("player")
local cache, gear_string = {}, ""
local foreignCache = {} -- [casterName][spellKey] = amount, in-memory cache for other healers
local rejuvDuration, renewDuration = 12, 15 --default durations
local ressGuidToName = {} -- [casterGuid] = casterName, for SPELL_FAILED_OTHER cleanup
local healGuidToName = {} -- [casterGuid] = casterName, for SPELL_FAILED_OTHER cleanup
local ressGuidToName = {} -- [casterGuid] = casterName, for interrupted-cast cleanup
local healGuidToName = {} -- [casterGuid] = casterName, for interrupted-cast cleanup
local sentTargets = {} -- [castGUID] = target name of an own cast, from UNIT_SPELLCAST_SENT
local castTargets = {} -- [castGUID] = target name of a foreign cast, from UNIT_SPELLCAST_START
local ress_timers = {} -- [target][sender] = expiry_timestamp (60s rez window)
local RESS_TIMEOUT = 60 -- Vanilla: rez offer expires after 60s
@@ -124,43 +113,25 @@ libpredict:SetScript("OnEvent", function()
if origOnEvent then origOnEvent() end
end)
-- SPELL_START_SELF: own cast started (heals + rez)
pfUI.libdebuff_spell_start_self_hooks = pfUI.libdebuff_spell_start_self_hooks or {}
pfUI.libdebuff_spell_start_self_hooks["libpredict"] = function(spellId, casterGuid, targetGuid, castTime)
-- Own cast-time spell started (heals + rez)
local function OnOwnCastStart(spellId, castGUID)
local spellName = C_Spell.GetSpellName(spellId)
if not spellName then return end
local pendingTarget = nil
local pendingTargetGuid = nil
local pending = pfUI.libpredict_pending_cast
if pending and pending.spellName == spellName and pending.targetGuid
and pending.time and (GetTime() - pending.time) < 1 then
-- If SPELL_CAST_EVENT target differs from SPELL_START_SELF target,
-- the client redirected the cast (e.g. offline target → selfcast).
-- Discard pending and trust the actual targetGuid from SPELL_START_SELF.
if pending.targetGuid == targetGuid then
local name = UnitName(pending.targetGuid)
if name and name ~= UNKNOWNOBJECT and name ~= UKNOWNBEING then
pendingTarget = name
pendingTargetGuid = pending.targetGuid
end
end
pending.spellId = nil
pending.spellName = nil
pending.targetGuid = nil
pending.time = nil
end
local target = pendingTarget or resolveNameFromGuid(targetGuid) or senttarget or spell_queue[3]
-- resolved GUID: prefer pendingTargetGuid (mouseover/click-to-cast), fall back to targetGuid
local resolvedTargetGuid = pendingTargetGuid or targetGuid
-- SENT names the unit the cast went out at; self and no-target casts
-- carry none, so fall back to what the cast hooks recorded
local target = sentTargets[castGUID] or spell_queue[3]
local _, _, _, startMs, endMs = C_Spell.CastingInfo()
local castTime = startMs and endMs - startMs or 0
libpredict.sender.castGUID = castGUID
libpredict.sender.delayMs = 0
libpredict.sender.current_cast = spellName
libpredict.sender.current_cast_target = target
if L["resurrections"][spellName] then
if target then
libpredict:Ress(player, target, casterGuid)
libpredict:Ress(player, target, UnitGUID("player"))
libpredict.sender:SendHealCommMsg("Resurrection/" .. target .. "/start/")
libpredict.sender:SendResCommMsg("RES " .. target)
libpredict.sender.resurrecting = true
@@ -235,27 +206,17 @@ pfUI.libdebuff_spell_start_self_hooks["libpredict"] = function(spellId, casterGu
return -- skip the generic Heal call below
end
-- If the resolved heal target is itself hostile (e.g. self-cast heal while targeting an enemy),
-- store the heal under the player so the player frame shows the prediction.
-- Use resolvedTargetGuid (prefers pendingTargetGuid from mouseover/click-to-cast) so we
-- don't wrongly fall back to player when healing a friend via mouseover with a foe in target.
local healTarget = target
local unitTargetGuid = UnitGUID and UnitGUID("target")
local healTargetIsHostile = resolvedTargetGuid and unitTargetGuid
and resolvedTargetGuid == unitTargetGuid
and UnitExists("target")
and (UnitCanAttack("player", "target") or not UnitIsFriend("player", "target"))
if healTargetIsHostile then
healTarget = player
end
libpredict:Heal(player, healTarget, amount, casttime)
libpredict.sender:SendHealCommMsg("Heal/" .. (healTarget or "") .. "/" .. amount .. "/" .. casttime .. "/")
libpredict:Heal(player, target, amount, casttime)
libpredict.sender:SendHealCommMsg("Heal/" .. (target or "") .. "/" .. amount .. "/" .. casttime .. "/")
libpredict.sender.healing = true
end
end
-- SPELL_GO_SELF: own cast landed (HealStop + Regrowth timer)
pfUI.libdebuff_spell_go_hooks["libpredict_sender"] = function(spellId)
-- Own cast landed: HealStop, Regrowth timer, instant HoTs, own rez timer
local function OnOwnCastSucceeded(spellId, castGUID)
local target = sentTargets[castGUID] or spell_queue[3]
sentTargets[castGUID] = nil
libpredict:HealStop(player)
local spellName = C_Spell.GetSpellName(spellId)
if spellName == REGROWTH then
@@ -267,32 +228,57 @@ pfUI.libdebuff_spell_go_hooks["libpredict_sender"] = function(spellId)
end
libpredict.sender.regrowth_timer = now + 0.1
end
libpredict.sender.castGUID = nil
libpredict.sender.current_cast = nil
libpredict.sender.current_cast_target = nil
if not target then return end
-- Instant HoTs — classify by name (rank-independent) instead of a
-- hardcoded per-rank ID table.
local hotType
if spellName == REJUVENATION then hotType = "Reju"
elseif spellName == RENEW then hotType = "Renew"
end
if hotType then
local duration = hotType == "Reju" and rejuvDuration or renewDuration
local rank = 0
local rankSub = C_Spell.GetSpellSubtext(spellId)
if rankSub and rankSub ~= "" then
rank = tonumber((string.gsub(rankSub, "Rank ", ""))) or 0
end
libpredict:Hot(player, target, hotType, duration, nil, "UNIT_SPELLCAST_SUCCEEDED", rank)
libpredict.sender:SendHealCommMsg(hotType .. "/" .. target .. "/" .. duration .. "/" .. rank .. "/")
end
if isRezSpell(spellId) then
libpredict:RessSetTimer(player, target)
end
end
-- SPELL_START_OTHER: foreign cast started (heals + rez)
-- Signature: fn(spellId, casterGuid, targetGuid, castTime)
pfUI.libdebuff_spell_start_other_hooks = pfUI.libdebuff_spell_start_other_hooks or {}
pfUI.libdebuff_spell_start_other_hooks["libpredict"] = function(spellId, casterGuid, targetGuid, castTime)
-- Foreign cast-time spell started (heals + rez)
local function OnOtherCastStart(unit, spellId, castGUID)
local spellName = C_Spell.GetSpellName(spellId)
if not spellName then return end
local casterGuid = UnitGUID(unit)
local casterName = resolveNameFromGuid(casterGuid)
if not casterName then return end
local targetName = UnitSpellTargetName(unit)
if not targetName then return end
castTargets[castGUID] = targetName
-- Resurrection cast
if L["resurrections"][spellName] then
local targetName = resolveNameFromGuid(targetGuid)
if targetName then
libpredict:Ress(casterName, targetName, casterGuid)
end
libpredict:Ress(casterName, targetName, casterGuid)
return
end
-- Heal cast: look up cached amount from a previous cast by this healer
local targetName = resolveNameFromGuid(targetGuid)
if not targetName then return end
local _, _, _, startMs, endMs = C_Spell.UnitCastingInfo(unit)
if not startMs then return end
local castTime = endMs - startMs
local rankStr = C_Spell.GetSpellSubtext(spellId) or ""
local spellKey = spellName .. (rankStr or "")
@@ -338,77 +324,26 @@ pfUI.libdebuff_spell_start_other_hooks["libpredict"] = function(spellId, casterG
libpredict:Heal(casterName, targetName, amount, castTime, casterGuid)
end
-- SPELL_GO_SELF: own cast landed - HoTs + own rez timer
-- Signature: fn(spellId, arg1, arg2, arg3, arg4, arg5, arg6, arg7)
pfUI.libdebuff_spell_go_hooks = pfUI.libdebuff_spell_go_hooks or {}
pfUI.libdebuff_spell_go_hooks["libpredict"] = function(spellId, a1, a2, a3, a4, a5, a6, a7)
-- Instant HoTs — classify by name (rank-independent) instead of a
-- hardcoded per-rank ID table.
local spellName = C_Spell.GetSpellName(spellId)
local hotType
if spellName == REJUVENATION then hotType = "Reju"
elseif spellName == RENEW then hotType = "Renew"
end
if hotType then
local targetGuid = a4
local targetName = resolveNameFromGuid(targetGuid)
if targetName then
local duration
if hotType == "Reju" then duration = rejuvDuration or 12
elseif hotType == "Renew" then duration = renewDuration or 15
end
local rank = 0
local rankSub = C_Spell.GetSpellSubtext(spellId)
if rankSub and rankSub ~= "" then
rank = tonumber((string.gsub(rankSub, "Rank ", ""))) or 0
end
local playerName = UnitName("player")
libpredict:Hot(playerName, targetName, hotType, duration, nil, "SPELL_GO_SELF", rank)
local rankStr = tostring(rank)
if libpredict.sender and libpredict.sender.SendHealCommMsg then
libpredict.sender:SendHealCommMsg(hotType .. "/" .. targetName .. "/" .. duration .. "/" .. rankStr .. "/")
elseif IsInRaid() then
SendAddonMessage("HealComm", hotType .. "/" .. targetName .. "/" .. duration .. "/" .. rankStr .. "/", "RAID")
elseif IsInGroup() then
SendAddonMessage("HealComm", hotType .. "/" .. targetName .. "/" .. duration .. "/" .. rankStr .. "/", "PARTY")
end
end
end
-- Own rez landed: set timer
if isRezSpell(spellId) then
local targetGuid = a4
local targetName = resolveNameFromGuid(targetGuid)
local playerName = UnitName("player")
if playerName and targetName then
libpredict:RessSetTimer(playerName, targetName)
end
end
end
-- SPELL_GO_OTHER: foreign cast landed - rez timer + HealStop
-- Signature: fn(spellId, casterGuid, targetGuid)
pfUI.libdebuff_spell_go_other_hooks = pfUI.libdebuff_spell_go_other_hooks or {}
pfUI.libdebuff_spell_go_other_hooks["libpredict"] = function(spellId, casterGuid, targetGuid)
-- Foreign cast landed: rez timer, or the predicted heal is fulfilled
local function OnOtherCastSucceeded(unit, spellId, castGUID)
local casterGuid = UnitGUID(unit)
local casterName = resolveNameFromGuid(casterGuid)
if not casterName then return end
if isRezSpell(spellId) then
local targetName = resolveNameFromGuid(targetGuid)
local targetName = castTargets[castGUID]
if targetName then
libpredict:RessSetTimer(casterName, targetName)
end
return
end
-- Heal landed → prediction fulfilled, clean up
libpredict:HealStop(casterName)
end
-- SPELL_FAILED_OTHER: remove cancelled resses/heals
-- Signature: fn(casterGuid, spellId)
pfUI.libdebuff_spell_failed_other_hooks = pfUI.libdebuff_spell_failed_other_hooks or {}
pfUI.libdebuff_spell_failed_other_hooks["libpredict"] = function(casterGuid, spellId)
-- Foreign cast interrupted: remove cancelled resses/heals
local function OnOtherCastInterrupted(unit)
local casterGuid = UnitGUID(unit)
if not casterGuid then return end
local ressName = ressGuidToName[casterGuid]
if ressName then
@@ -567,7 +502,7 @@ function libpredict:ParseChatMessage(sender, msg, comm)
libpredict:Heal(sender, target, heal, time)
end
elseif msgtype == "Ress" then
-- HealComm: only for other players (own resses tracked via SPELL_START_SELF hook)
-- HealComm: only for other players (own resses tracked via UNIT_SPELLCAST_START)
local playerName = UnitName("player")
if sender ~= playerName then
local senderGuid
@@ -912,7 +847,7 @@ hooksecurefunc("CastSpell", function(id, bookType)
rankNum = tonumber((string.gsub(rank, "Rank ", ""))) or nil
end
-- Instant HoTs: libdebuff/Nampower via GetHotDuration, hook method as fallback
-- Instant HoTs: UNIT_SPELLCAST_SUCCEEDED and C_UnitAuras, hook method as fallback
if effect == REJUVENATION then
local target = spell_queue[3]
@@ -976,7 +911,7 @@ hooksecurefunc("CastSpellByName", function(effect, target)
spell_queue[3] = target or default
end
-- Instant HoTs: libdebuff/Nampower via GetHotDuration, hook method as fallback
-- Instant HoTs: UNIT_SPELLCAST_SUCCEEDED and C_UnitAuras, hook method as fallback
if effect == REJUVENATION then
local hotTarget = target or default
@@ -1037,7 +972,7 @@ hooksecurefunc("UseAction", function(slot, target, selfcast)
rankNum = tonumber((string.gsub(rank, "Rank ", ""))) or nil
end
-- Instant HoTs: libdebuff/Nampower via GetHotDuration, hook method as fallback
-- Instant HoTs: UNIT_SPELLCAST_SUCCEEDED and C_UnitAuras, hook method as fallback
if effect == REJUVENATION then
local hotTarget = spell_queue[3]
@@ -1127,15 +1062,17 @@ libpredict.sender:SetScript("OnUpdate", function()
end
end)
-- Nampower events
libpredict.sender:RegisterEvent("SPELL_FAILED_SELF")
libpredict.sender:RegisterEvent("SPELL_DELAYED_SELF")
libpredict.sender:RegisterEvent("SPELL_HEAL_BY_SELF")
libpredict.sender:RegisterEvent("SPELL_HEAL_BY_OTHER") -- populates foreignCache for other healers
-- force cache updates
libpredict.sender:RegisterUnitEvent("UNIT_INVENTORY_CHANGED", "player")
libpredict.sender:RegisterEvent("SKILL_LINES_CHANGED")
-- Own casts arrive as "player"; every other caster fires once per unit token
-- it currently holds (raidN, target, nameplateN, ...), so other-unit handlers
-- drop the repeats by castGUID.
libpredict.sender:RegisterEvent("UNIT_SPELLCAST_SENT")
libpredict.sender:RegisterEvent("UNIT_SPELLCAST_START")
libpredict.sender:RegisterEvent("UNIT_SPELLCAST_DELAYED")
libpredict.sender:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
libpredict.sender:RegisterEvent("UNIT_SPELLCAST_FAILED")
libpredict.sender:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
libpredict.sender:RegisterEvent("UNIT_SPELLCAST_STOP")
libpredict.sender:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
-- Shared cleanup helper for failed/interrupted casts
local function onCastFailed()
@@ -1145,7 +1082,7 @@ local function onCastFailed()
s:SendHealCommMsg("Healstop")
s.healing = nil
elseif s.resurrecting then
local target = s.current_cast_target or senttarget or spell_queue[3]
local target = s.current_cast_target or spell_queue[3]
libpredict:RessStop(UnitName("player"))
s:SendHealCommMsg("Resurrection/stop/")
s:SendResCommMsg("RESNO " .. (target or ""))
@@ -1157,81 +1094,103 @@ local function onCastFailed()
s.regrowth_target_next = nil
s.regrowth_start_next = nil
end
s.castGUID = nil
s.current_cast = nil
s.current_cast_target = nil
end
libpredict.sender:SetScript("OnEvent", function()
-- ============================================================
-- NAMPOWER PATH
-- ============================================================
if event == "SPELL_HEAL_BY_SELF" then
-- arg1=targetGuid, arg2=casterGuid, arg3=spellId, arg4=amount, arg5=critical, arg6=periodic
local spellId = arg3
local amount = arg4
local isCrit = arg5 == 1
local isPeriodic = arg6 == 1
local spellName = C_Spell.GetSpellName(spellId)
if spellName and spell_queue[1] == spellName then
-- Real heal amounts: own heals refresh the spell cache, other group healers'
-- heals fill foreignCache (one event per hit target for AoE heals like PoH).
local function OnHeal()
-- arg4=sourceGUID, arg12=spellId, arg13=spellName, arg15=amount, arg18=critical
local spellId, spellName, amount, isCrit = arg12, arg13, arg15, arg18 == 1
if not spellName or not amount or amount <= 0 then return end
if IsPlayerGuid(arg4) then
if spell_queue[1] == spellName then
UpdateCache(spell_queue[2], amount, isCrit)
end
return
end
elseif event == "SPELL_HEAL_BY_OTHER" then
-- Fires once per hit target for AoE heals (e.g. PoH).
-- Use this to build a per-caster cache of real heal amounts.
-- arg1=targetGuid, arg2=casterGuid, arg3=spellId, arg4=amount, arg5=critical, arg6=periodic
local casterGuid = arg2
local spellId = arg3
local amount = arg4
local isCrit = arg5 == 1
local isPeriodic = arg6 == 1
local casterName = resolveNameFromGuid(arg4)
if not casterName then return end
if isPeriodic or not spellId or not amount or amount <= 0 then return end
local spellKey = spellName .. (C_Spell.GetSpellSubtext(spellId) or "")
local casterName = resolveNameFromGuid(casterGuid)
if not casterName or casterName == player then return end -- own heals handled by SPELL_HEAL_BY_SELF
foreignCache[casterName] = foreignCache[casterName] or {}
local existing = foreignCache[casterName][spellKey]
-- Store highest non-crit value for best prediction accuracy
if isCrit then
-- Estimate base from crit (vanilla crit = 150%)
local base = math.floor(amount * 2 / 3)
if not existing or base > existing then
foreignCache[casterName][spellKey] = base
end
else
if not existing or amount > existing then
foreignCache[casterName][spellKey] = amount
end
end
end
local spellName = C_Spell.GetSpellName(spellId)
if not spellName then return end
local rankStr = C_Spell.GetSpellSubtext(spellId)
local spellKey = spellName .. (rankStr or "")
local lastOtherCast = {} -- [event] = castGUID of the last other-unit event handled
foreignCache[casterName] = foreignCache[casterName] or {}
local existing = foreignCache[casterName][spellKey]
-- Store highest non-crit value for best prediction accuracy
if isCrit then
-- Estimate base from crit (vanilla crit = 150%)
local base = math.floor(amount * 2 / 3)
if not existing or base > existing then
foreignCache[casterName][spellKey] = base
end
else
if not existing or amount > existing then
foreignCache[casterName][spellKey] = amount
libpredict.sender:SetScript("OnEvent", function()
if event == "COMBAT_LOG_EVENT_UNFILTERED" then
if arg2 == "SPELL_HEAL" then OnHeal() end
return
end
-- UNIT_SPELLCAST_SENT: arg1=unit, arg2=target token, arg3=castGUID, arg4=spellID
-- every other UNIT_SPELLCAST_*: arg1=unit, arg2=castGUID, arg3=spellID
local unit = arg1
if unit == "player" then
if event == "UNIT_SPELLCAST_SENT" then
sentTargets[arg3] = arg2 ~= "" and UnitName(arg2) or nil
elseif event == "UNIT_SPELLCAST_START" then
OnOwnCastStart(arg3, arg2)
elseif event == "UNIT_SPELLCAST_SUCCEEDED" then
OnOwnCastSucceeded(arg3, arg2)
elseif event == "UNIT_SPELLCAST_FAILED" or event == "UNIT_SPELLCAST_INTERRUPTED" then
sentTargets[arg2] = nil
-- a refused cast (cooldown, range) fails under its own castGUID and
-- must not cancel the heal that is still casting
if arg2 == this.castGUID then onCastFailed() end
elseif event == "UNIT_SPELLCAST_DELAYED" then
-- delayTimeMs is the pushback so far; HealComm wants each step
local _, _, _, _, _, _, _, _, _, _, delayMs = C_Spell.CastingInfo()
if this.healing and delayMs and delayMs > this.delayMs then
local step = delayMs - this.delayMs
this.delayMs = delayMs
libpredict:HealDelay(player, step)
this:SendHealCommMsg("Healdelay/" .. step .. "/")
end
end
return
end
elseif event == "SPELL_FAILED_SELF" then
onCastFailed()
elseif event == "SPELL_DELAYED_SELF" then
-- arg1=casterGuid, arg2=delayMs
if libpredict.sender.healing then
libpredict:HealDelay(player, arg2)
libpredict.sender:SendHealCommMsg("Healdelay/" .. arg2 .. "/")
end
if UnitIsUnit(unit, "player") or lastOtherCast[event] == arg2 then return end
lastOtherCast[event] = arg2
if event == "UNIT_SPELLCAST_START" then
OnOtherCastStart(unit, arg3, arg2)
elseif event == "UNIT_SPELLCAST_SUCCEEDED" then
OnOtherCastSucceeded(unit, arg3, arg2)
elseif event == "UNIT_SPELLCAST_INTERRUPTED" then
OnOtherCastInterrupted(unit)
elseif event == "UNIT_SPELLCAST_STOP" then
castTargets[arg2] = nil
end
end)
function libpredict:GetHotDuration(unit, spell)
if unit == UNKNOWNOBJECT or unit == UKNOWNBEING then return end
-- NEW: Try libdebuff first (Nampower AURA_CAST events)
if pfUI.api.libdebuff and pfUI.api.libdebuff.GetBestAuraCast then
-- Live aura timer first (C_UnitAuras via libdebuff)
if pfUI.api.libdebuff then
local guid = UnitGUID(unit)
if guid then
-- Get the best (highest rank) aura cast for this spell
local spellName = spell
-- Map short spell codes to full names
@@ -1246,7 +1205,6 @@ function libpredict:GetHotDuration(unit, spell)
local start, duration, timeleft, rank, casterGuid = pfUI.api.libdebuff:GetBestAuraCast(guid, spellName)
if start and duration and timeleft then
-- SUCCESS: libdebuff has accurate server-side data!
if libpredict.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[GetHotDuration]|r %s on %s via libdebuff: dur=%.1fs timeleft=%.1fs rank=%d",
spell, unit, duration, timeleft, rank or 0))
@@ -1256,7 +1214,7 @@ function libpredict:GetHotDuration(unit, spell)
end
end
-- FALLBACK: Use old prediction system (for non-Nampower clients or no AURA_CAST data)
-- FALLBACK: the HealComm/cast-fed table, for HoTs C_UnitAuras has no timer for
local start, duration, timeleft
local now = pfUI.uf.now or GetTime()
@@ -1290,16 +1248,11 @@ _G.SlashCmdList.HOTDEBUG = function()
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[HoT Tracking Debug]|r")
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff========================================|r")
-- Check libdebuff availability
local libdebuff_now = (pfUI.api.libdebuff and pfUI.api.libdebuff.GetBestAuraCast) and true or false
if libdebuff_now then
if pfUI.api.libdebuff then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[PRIMARY]|r libdebuff integration: ACTIVE")
DEFAULT_CHAT_FRAME:AddMessage(" Using AURA_CAST events for server-side tracking")
DEFAULT_CHAT_FRAME:AddMessage(" Rank protection: ENABLED")
DEFAULT_CHAT_FRAME:AddMessage(" Using C_UnitAuras timers")
else
DEFAULT_CHAT_FRAME:AddMessage("|cffff9900[PRIMARY]|r libdebuff integration: NOT AVAILABLE")
DEFAULT_CHAT_FRAME:AddMessage(" Reason: Nampower not enabled or libdebuff outdated")
end
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[FALLBACK]|r Legacy prediction system: ACTIVE")
+16 -14
View File
@@ -897,6 +897,10 @@ pfUI:RegisterModule("actionbar", function ()
pageswitch:RegisterEvent("UPDATE_SHAPESHIFT_FORM")
pageswitch:RegisterEvent("PLAYER_AURAS_CHANGED")
pageswitch:RegisterEvent("PLAYER_LOGOUT")
local PROWL_IDS = { [5215] = true, [6783] = true, [9913] = true }
if class == "DRUID" then
pageswitch:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player")
end
pageswitch:SetScript("OnEvent", function()
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
@@ -919,6 +923,18 @@ pfUI:RegisterModule("actionbar", function ()
return
end
-- Eager prowl detection on the cast itself — zero-latency vs.
-- PLAYER_AURAS_CHANGED, which can land a frame or two behind it.
-- arg3 = spellID
if event == "UNIT_SPELLCAST_SUCCEEDED" then
if PROWL_IDS[arg3] then
inCatForm = true
prowlActive = true
formpaging = true
end
return
end
if event == "UPDATE_SHAPESHIFT_FORM" then
-- Authoritative form-change signal — recompute and short-circuit.
inCatForm = HasCatForm()
@@ -942,20 +958,6 @@ pfUI:RegisterModule("actionbar", function ()
end
end)
-- Eager prowl detection via Nampower SPELL_GO_SELF — zero-latency vs.
-- PLAYER_AURAS_CHANGED, which can land a frame or two behind the cast.
-- Form changes are no longer hooked here; UPDATE_SHAPESHIFT_FORM covers
-- those without needing a per-spellID watchlist.
local PROWL_IDS = { [5215] = true, [6783] = true, [9913] = true }
pfUI.libdebuff_spell_go_hooks = pfUI.libdebuff_spell_go_hooks or {}
pfUI.libdebuff_spell_go_hooks["actionbar_prowl"] = function(spellId)
if class ~= "DRUID" then return end
if PROWL_IDS[spellId] then
inCatForm = true
prowlActive = true
formpaging = true
end
end
pageswitch:SetScript("OnUpdate", function()
-- switch actionbar page depending on meta key that is pressed
if C.bars.pagemastershift == "1" and IsShiftKeyDown() then
+20 -24
View File
@@ -635,25 +635,6 @@ pfUI:RegisterModule("swingtimer", function ()
end
end)
-- SPELL_GO hook via libdebuff: an on-next-swing ability (HS / Cleave /
-- Maul) resolving consumes the queue, so drop the queued color.
pfUI.libdebuff_spell_go_hooks = pfUI.libdebuff_spell_go_hooks or {}
pfUI.libdebuff_spell_go_hooks["swingtimer"] = function(spellId)
if IsOnSwingSpell(spellId) then
S.hsQueued = false; S.cleaveQueued = false; S.maulQueued = false
end
end
-- SPELL_CAST_EVENT hook: HS/Cleave/Maul queue tracking
pfUI.libdebuff_spell_cast_hooks = pfUI.libdebuff_spell_cast_hooks or {}
pfUI.libdebuff_spell_cast_hooks["swingtimer"] = function(success, spellId)
local kind = ClassifyOnSwingSpell(spellId)
if not kind then return end
S.useSpellQueueEvent = true
SetQueuedKind(kind)
end
local events = CreateFrame("Frame")
events:RegisterEvent("PLAYER_SWING")
events:RegisterEvent("PLAYER_ENTERING_WORLD")
@@ -661,7 +642,9 @@ pfUI:RegisterModule("swingtimer", function ()
events:RegisterEvent("PLAYER_REGEN_DISABLED")
events:RegisterEvent("PLAYER_REGEN_ENABLED")
events:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
events:RegisterUnitEvent("UNIT_DIED", UnitGUID("player"))
events:RegisterEvent("PLAYER_DEAD")
events:RegisterUnitEvent("UNIT_SPELLCAST_SENT", "player")
events:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player")
events:RegisterEvent("SPELL_QUEUE_EVENT")
events:RegisterEvent("START_AUTOATTACK")
events:RegisterEvent("STOP_AUTOATTACK")
@@ -679,6 +662,21 @@ pfUI:RegisterModule("swingtimer", function ()
StartRanged(arg1)
end
elseif event == "UNIT_SPELLCAST_SENT" then
-- arg4 = spellID: an on-next-swing ability (HS / Cleave / Maul) queued
local kind = ClassifyOnSwingSpell(arg4)
if kind then
S.useSpellQueueEvent = true
SetQueuedKind(kind)
end
elseif event == "UNIT_SPELLCAST_SUCCEEDED" then
-- arg3 = spellID: the on-next-swing ability resolved, which consumes
-- the queue, so drop the queued color
if IsOnSwingSpell(arg3) then
S.hsQueued = false; S.cleaveQueued = false; S.maulQueued = false
end
elseif event == "SPELL_QUEUE_EVENT" then
local eventCode = arg1 or -1
local spellId = arg2 or 0
@@ -728,10 +726,8 @@ pfUI:RegisterModule("swingtimer", function ()
S.cleaveQueued = false
S.maulQueued = false
elseif event == "UNIT_DIED" then
if IsPlayerGuid(arg1) then
ResetAll()
end
elseif event == "PLAYER_DEAD" then
ResetAll()
end
end)