4 Commits
Author SHA1 Message Date
Brues bea2cedc08 buffwatch: use official dispel-type colours for debuff bars
The debuff-bar auto colour read the vanilla DebuffTypeColor global,
diverging from unitframes/buff which use C_UnitAuras.GetAuraDispelTypeColor
(the BC GlobalColor.dbc palette ClassicAPI ships). Switch buffwatch to the
same accessor so all aura displays share one palette; nil/unknown types
fall back to DEBUFF_TYPE_NONE instead of the old ad-hoc (1, .2, .2).
2026-08-23 13:25:06 -05:00
Brues 23c783622c buff/buffwatch: positional aura reads in hot refresh loops
buff.lua RefreshBuffButton runs 50x per PLAYER_AURAS_CHANGED and
buffwatch.lua GetBuffData runs 32x per RefreshBuffBarFrame; both now use
the zero-allocation positional C_UnitAuras.UnitAura instead of the
table-allocating GetAuraDataByIndex.

buffwatch also threads the aura's dispelType out of GetBuffData onto the
frame.buffs row, so the debuff-bar colour reads it from the row instead
of re-fetching the aura with GetDebuffDataByIndex on every name change.

Cold single-lookups (hover/click tooltips) keep the readable table API.
2026-08-23 13:05:42 -05:00
Brues 3cb52618a9 unitframes: scan debuff slots once for dispel indicators
The dispellable-indicator block re-scanned all 16 harmful slots for
every dispellable type (O(types x 16) UnitDebuff calls per refresh).
Scan the 16 slots once into a reusable set, then have the per-type loop
check the set. Zero-allocation (the set is cleared and reused within the
single RefreshUnit call).
2026-08-23 13:05:29 -05:00
Brues 461ad22a57 Use positional C_UnitAuras accessors in hot aura scans
Swap the per-frame aura-refresh loops from table-allocating
GetAuraDataByIndex/GetBuffDataByIndex/GetDebuffDataByIndex/GetUnitAuras
to the zero-allocation positional C_UnitAuras.UnitAura/UnitBuff/UnitDebuff.

unitframes: buff/debuff icon loops, the dispellable-indicator 16-slot
scan, the two GetUnitAuras indicator passes, and the custom-debuff scan.
nameplates: the per-plate debuff scan now writes positional results
straight into the reusable display buffer, dropping the per-aura tables
and the result array.

Filters change from HARMFUL|PLAYER/HARMFUL to PLAYER/nil since UnitBuff/
UnitDebuff lock the range and still honor the PLAYER predicate. Cold
single-lookups (hover/click, tooltip, totem icon, libdebuff) keep the
readable table API.
2026-08-23 12:53:56 -05:00
4 changed files with 96 additions and 81 deletions
+66 -53
View File
@@ -1638,39 +1638,40 @@ function pfUI.uf:RefreshUnit(unit, component)
for i=1, unit.config.bufflimit do
if not unit.buffs[i] then break end
local aura = C_UnitAuras.GetBuffDataByIndex(unitstr, i)
-- positional UnitBuff allocates nothing (vs a table per icon per refresh)
local name, icon, count, _, duration, expirationTime, _, _, _, spellId = C_UnitAuras.UnitBuff(unitstr, i)
if aura then
unit.buffs[i].texture:SetTexture(aura.icon)
if name then
unit.buffs[i].texture:SetTexture(icon)
unit.buffs[i]:Show()
if aura.applications > 1 then
unit.buffs[i].stacks:SetText(aura.applications)
if count > 1 then
unit.buffs[i].stacks:SetText(count)
else
unit.buffs[i].stacks:SetText("")
end
if aura.expirationTime > 0 then
if expirationTime > 0 then
-- pfUI's cooldown text falls into a 2^32 wraparound branch when start > GetTime(),
-- which happens for talent-extended buffs where the real duration exceeds aura.duration
-- (the Spell.dbc base). Anchor start to now in that case to keep the remaining math sane.
-- which happens for talent-extended buffs where the real duration exceeds the
-- Spell.dbc base. Anchor start to now in that case to keep the remaining math sane.
local now = GetTime()
local start = aura.expirationTime - aura.duration
local duration = aura.duration
if start > now or duration <= 0 then
start, duration = now, aura.expirationTime - now
local start = expirationTime - duration
local dur = duration
if start > now or dur <= 0 then
start, dur = now, expirationTime - now
end
if duration > 0 then
CooldownFrame_SetTimer(unit.buffs[i].cd, start, duration, 1)
if dur > 0 then
CooldownFrame_SetTimer(unit.buffs[i].cd, start, dur, 1)
else
CooldownFrame_SetTimer(unit.buffs[i].cd, 0, 0, 0)
end
elseif aura.duration > 0 then
elseif duration > 0 then
local guid = UnitGUID(unitstr)
local guidStarts = guid and pfUI.uf.aura_starts[guid]
local start = guidStarts and guidStarts[aura.spellId]
local start = guidStarts and guidStarts[spellId]
if start then
CooldownFrame_SetTimer(unit.buffs[i].cd, start, aura.duration, 1)
CooldownFrame_SetTimer(unit.buffs[i].cd, start, duration, 1)
else
CooldownFrame_SetTimer(unit.buffs[i].cd, 0, 0, 0)
end
@@ -1741,10 +1742,11 @@ function pfUI.uf:RefreshUnit(unit, component)
-- selfdebuff narrows to player-cast harmful auras via the PLAYER filter.
-- Player-frame debuffs aren't gated on it (it'd hide most party-applied
-- effects on you).
local filter = (unit.label ~= "player" and selfdebuff == "1") and "HARMFUL|PLAYER" or "HARMFUL"
local aura = C_UnitAuras.GetAuraDataByIndex(unitstr, i, filter)
if aura then
texture, stacks, dtype = aura.icon, aura.applications, aura.dispelName
-- positional UnitDebuff allocates nothing; PLAYER predicate honored for selfdebuff
local filter = (unit.label ~= "player" and selfdebuff == "1") and "PLAYER" or nil
local name, icon, count, dispelType, duration, expirationTime = C_UnitAuras.UnitDebuff(unitstr, i, filter)
if name then
texture, stacks, dtype = icon, count, dispelType
else
texture, stacks, dtype = nil, 0, nil
end
@@ -1757,18 +1759,18 @@ function pfUI.uf:RefreshUnit(unit, component)
if texture then
unit.debuffs[i]:Show()
if aura and aura.expirationTime > 0 then
if expirationTime > 0 then
-- Cap start to now so talent-extended debuffs (expirationTime past
-- the dbc base duration) don't push start into the future and trip
-- CooldownFrame_SetTimer's 2^32-wraparound branch.
local now = GetTime()
local start = aura.expirationTime - aura.duration
local duration = aura.duration
if start > now or duration <= 0 then
start, duration = now, aura.expirationTime - now
local start = expirationTime - duration
local dur = duration
if start > now or dur <= 0 then
start, dur = now, expirationTime - now
end
if duration > 0 then
CooldownFrame_SetTimer(unit.debuffs[i].cd, start, duration, 1)
if dur > 0 then
CooldownFrame_SetTimer(unit.debuffs[i].cd, start, dur, 1)
else
CooldownFrame_SetTimer(unit.debuffs[i].cd, 0, 0, 0)
end
@@ -1826,6 +1828,17 @@ function pfUI.uf:RefreshUnit(unit, component)
end
end
-- Scan the 16 harmful slots once into a reusable set, instead of
-- re-scanning all 16 for every dispellable type below. Reused across
-- frames (populated and consumed within this single RefreshUnit call).
local present = pfUI.uf.dispelPresent or {}
pfUI.uf.dispelPresent = present
for k in pairs(present) do present[k] = nil end
for i=1,16 do
local name, _, _, dispelType = C_UnitAuras.UnitDebuff(unitstr, i)
if name and dispelType and dispelType ~= "" then present[dispelType] = true end
end
for _, debuff in pairs(unit.dispellable) do
indicator[debuff] = indicator[debuff] or CreateFrame("Frame", nil, indicator)
indicator[debuff]:SetParent(indicator)
@@ -1865,15 +1878,7 @@ function pfUI.uf:RefreshUnit(unit, component)
indicator[debuff].disp = indicator.disp
end
indicator[debuff].visible = nil
for i=1,16 do
local a = C_UnitAuras.GetDebuffDataByIndex(unitstr, i)
local dtype = a and a.dispelName
if dtype == debuff then
indicator[debuff].visible = true
end
end
indicator[debuff].visible = present[debuff]
if indicator[debuff].visible then
indicator[debuff]:Show()
@@ -1920,50 +1925,58 @@ function pfUI.uf:RefreshUnit(unit, component)
local pos = 1
if table.getn(unit.indicators) > 0 then
for _, aura in ipairs(C_UnitAuras.GetUnitAuras(unitstr, "HELPFUL")) do
local texLower = string.lower(aura.icon)
local timeleft = aura.expirationTime > 0 and (aura.expirationTime - GetTime()) or nil
local i = 1
while true do
local name, icon, count, _, _, expirationTime = C_UnitAuras.UnitBuff(unitstr, i)
if not name then break end
local texLower = string.lower(icon)
local timeleft = expirationTime > 0 and (expirationTime - GetTime()) or nil
for _, filter in pairs(unit.indicators) do
if filter == texLower then
local hot = HOT_INDICATORS[texLower]
if hot and string.lower(aura.name) ~= hot.name then
if hot and string.lower(name) ~= hot.name then
break -- texture matches but name disambiguates (e.g. shared icon)
end
if hot then
local start, duration, prediction = libpredict:GetHotDuration(unitstr, hot.predict)
pfUI.uf:AddIcon(unit, pos, aura.icon, timeleft or prediction, aura.applications, tonumber(start), tonumber(duration))
pfUI.uf:AddIcon(unit, pos, icon, timeleft or prediction, count, tonumber(start), tonumber(duration))
else
pfUI.uf:AddIcon(unit, pos, aura.icon, timeleft, aura.applications)
pfUI.uf:AddIcon(unit, pos, icon, timeleft, count)
end
pos = pos + 1
break
end
end
i = i + 1
end
end
if table.getn(unit.indicator_custom) > 0 then
for _, aura in ipairs(C_UnitAuras.GetUnitAuras(unitstr, "HELPFUL")) do
local timeleft = aura.expirationTime > 0 and (aura.expirationTime - GetTime()) or nil
local lowerName = string.lower(aura.name)
local ai = 1
while true do
local name, icon, count, _, _, expirationTime = C_UnitAuras.UnitBuff(unitstr, ai)
if not name then break end
local timeleft = expirationTime > 0 and (expirationTime - GetTime()) or nil
local lowerName = string.lower(name)
for _, filter in pairs(unit.indicator_custom) do
if filter == lowerName then
pfUI.uf:AddIcon(unit, pos, aura.icon, timeleft, aura.applications)
pfUI.uf:AddIcon(unit, pos, icon, timeleft, count)
pos = pos + 1
break
end
end
ai = ai + 1
end
local debuffFilter = unit.config.selfdebuff == "1" and "HARMFUL|PLAYER" or "HARMFUL"
local debuffFilter = unit.config.selfdebuff == "1" and "PLAYER" or nil
for i=1,16 do -- scan for custom debuffs
local aura = C_UnitAuras.GetAuraDataByIndex(unitstr, i, debuffFilter)
if aura then
local timeleft = aura.expirationTime > 0 and (aura.expirationTime - GetTime()) or nil
local name, icon, count, _, _, expirationTime = C_UnitAuras.UnitDebuff(unitstr, i, debuffFilter)
if name then
local timeleft = expirationTime > 0 and (expirationTime - GetTime()) or nil
for _, filter in pairs(unit.indicator_custom) do
if filter == string.lower(aura.name) then
pfUI.uf:AddIcon(unit, pos, aura.icon, timeleft, aura.applications)
if filter == string.lower(name) then
pfUI.uf:AddIcon(unit, pos, icon, timeleft, count)
pos = pos + 1
break
end
+7 -7
View File
@@ -23,7 +23,7 @@ pfUI:RegisterModule("buff", function ()
CreateBackdropShadow(buff)
end
local aura = C_UnitAuras.GetAuraDataByIndex("player", buff.id, buff.btype)
local name, icon, count, dispelType, _, expirationTime, _, _, _, spellId = C_UnitAuras.UnitAura("player", buff.id, buff.btype)
--detect weapon buffs
if buff.btype == "HELPFUL" and ((C.buffs.separateweapons == "0" and buff.gid <= pfUI.buff.wepbuffs.count) or (pfUI.buff.wepbuffs.count > 0 and buff.weapon ~= nil)) then
@@ -55,16 +55,16 @@ pfUI:RegisterModule("buff", function ()
buff.texture:SetTexture(GetInventoryItemTexture("player", 17))
buff.backdrop:SetBackdropBorderColor(GetItemQualityColor(GetInventoryItemQuality("player", 17) or 1))
end
elseif aura and (( buff.btype == "HARMFUL" and C.buffs.debuffs == "1" ) or ( buff.btype == "HELPFUL" and C.buffs.buffs == "1" )) then
elseif name and (( buff.btype == "HARMFUL" and C.buffs.debuffs == "1" ) or ( buff.btype == "HELPFUL" and C.buffs.buffs == "1" )) then
-- Set Buff Texture and Border
buff.mode = buff.btype
buff.expirationTime = aura.expirationTime
buff.stackCount = aura.applications
buff.spellId = aura.spellId
buff.texture:SetTexture(aura.icon)
buff.expirationTime = expirationTime
buff.stackCount = count
buff.spellId = spellId
buff.texture:SetTexture(icon)
if buff.btype == "HARMFUL" then
local dispelColor = C_UnitAuras.GetAuraDispelTypeColor(aura.dispelName)
local dispelColor = C_UnitAuras.GetAuraDispelTypeColor(dispelType)
buff.backdrop:SetBackdropBorderColor(dispelColor:GetRGBA())
else
buff.backdrop:SetBackdropBorderColor(br,bg,bb,ba)
+10 -11
View File
@@ -75,10 +75,10 @@ pfUI:RegisterModule("buffwatch", function ()
local function GetBuffData(unit, id, type, selfdebuff)
local filter = (selfdebuff and type == "HARMFUL") and "HARMFUL|PLAYER" or type
local aura = C_UnitAuras.GetAuraDataByIndex(unit, id, filter)
if not aura then return end
local remaining = aura.expirationTime > 0 and (aura.expirationTime - GetTime()) or 0
return remaining, aura.icon, aura.name, aura.applications
local name, icon, count, dispelType, _, expirationTime = C_UnitAuras.UnitAura(unit, id, filter)
if not name then return end
local remaining = expirationTime > 0 and (expirationTime - GetTime()) or 0
return remaining, icon, name, count, dispelType
end
local function StatusBarOnClick()
@@ -236,7 +236,7 @@ pfUI:RegisterModule("buffwatch", function ()
local selfdebuff = frame.config.selfdebuff == "1"
for i=1,32 do
local timeleft, texture, name, stacks = GetBuffData(frame.unit, i, frame.type, selfdebuff)
local timeleft, texture, name, stacks, dtype = GetBuffData(frame.unit, i, frame.type, selfdebuff)
timeleft = timeleft or 0
if texture and name and name ~= "" and BuffIsVisible(frame.config, name) then
@@ -245,12 +245,14 @@ pfUI:RegisterModule("buffwatch", function ()
frame.buffs[i][3] = name
frame.buffs[i][4] = texture
frame.buffs[i][5] = stacks
frame.buffs[i][6] = dtype
else
frame.buffs[i][1] = 0
frame.buffs[i][2] = nil
frame.buffs[i][3] = nil
frame.buffs[i][4] = nil
frame.buffs[i][5] = 0
frame.buffs[i][6] = nil
end
end
@@ -294,12 +296,9 @@ pfUI:RegisterModule("buffwatch", function ()
-- calculate dynamic auto color
local r, g, b
if frame.type == "HARMFUL" then
r, g, b = 1, .2, .2
local a = C_UnitAuras.GetDebuffDataByIndex(frame.unit, data[2])
local dtype = a and a.dispelName
if dtype and DebuffTypeColor[dtype] then
r,g,b = DebuffTypeColor[dtype].r,DebuffTypeColor[dtype].g,DebuffTypeColor[dtype].b
end
-- official Blizzard dispel-type colors (matches unitframes/buff);
-- nil/unknown type falls back to the DEBUFF_TYPE_NONE colour
r, g, b = C_UnitAuras.GetAuraDispelTypeColor(data[6] or ""):GetRGBA()
else
r,g,b = str2rgb(data[3])
end
+13 -10
View File
@@ -1313,19 +1313,22 @@ nameplates:RegisterEvent("PLAYER_GUILD_UPDATE")
for i = 1, 16 do debuffDisplayBuf[i].effect = nil end
if unitstr then
local filter = cfg.owndebuffs and "HARMFUL|PLAYER" or "HARMFUL"
local auras = C_UnitAuras.GetUnitAuras(unitstr, filter)
local now = GetTime()
for _, aura in ipairs(auras) do
if debuffCount >= 16 then break end
-- positional UnitAura writes straight into the reusable buffer, so the
-- per-plate scan allocates nothing (no per-aura table, no result array)
local i = 1
while debuffCount < 16 do
local aname, icon, count, dispelType, duration, expirationTime = C_UnitAuras.UnitAura(unitstr, i, filter)
if not aname then break end
debuffCount = debuffCount + 1
local timeleft = (aura.expirationTime and aura.expirationTime > 0) and (aura.expirationTime - now) or nil
local b = debuffDisplayBuf[debuffCount]
b.effect = aura.name
b.texture = aura.icon
b.stacks = aura.applications
b.dtype = aura.dispelName
b.duration = aura.duration
b.timeleft = timeleft
b.effect = aname
b.texture = icon
b.stacks = count
b.dtype = dispelType
b.duration = duration
b.timeleft = (expirationTime and expirationTime > 0) and (expirationTime - now) or nil
i = i + 1
end
end
for i = 1, 16 do