diff --git a/api/api.lua b/api/api.lua index 3c740f17..a6f9cf2f 100644 --- a/api/api.lua +++ b/api/api.lua @@ -204,6 +204,26 @@ function pfUI.api.UnitHasBuff(unit, name) return C_UnitAuras.GetAuraDataBySpellName(unit, name, "HELPFUL") ~= nil or nil end +-- [ ScanAuraSlots ] +-- Fills `buf` with the slot ids of the auras on `unit` that match `filter` and +-- returns how many (buf[1..n]; entries past n are cleared), so a module can +-- keep one buffer and refill it every refresh. Read each aura with +-- C_UnitAuras.UnitAuraBySlot(unit, buf[i]) (positional, no table) or +-- GetAuraDataBySlot. One enumeration walks the aura array once, where a +-- by-index loop (UnitAura(unit, i)) re-walks it from the start for every i. +-- Uses GetAuraSlots' fill-a-table form (table as the 5th argument) instead of +-- its vararg return: Lua 5.0 builds an `arg` table for every vararg call, so +-- collecting the returns in a Lua helper would allocate once per scan. +-- unit [string] unit token +-- filter [string] aura filter ("HELPFUL", "HARMFUL|PLAYER", ...) +-- buf [table] reusable buffer, filled in place +-- max [number] optional cap on the slot count (nil = all) +-- return: [number] count of slot ids written to buf +function pfUI.api.ScanAuraSlots(unit, filter, buf, max) + local _, n = C_UnitAuras.GetAuraSlots(unit, filter, max, nil, buf) + return n +end + -- [ IsPlayerGuid ] -- Returns whether a GUID or unit token refers to the local player. -- guid [string] A unit GUID (or unitID) to test. diff --git a/api/unitframes.lua b/api/unitframes.lua index b74e34ae..b07c5edf 100644 --- a/api/unitframes.lua +++ b/api/unitframes.lua @@ -4,6 +4,12 @@ setfenv(1, pfUI:GetEnvironment()) pfUI.uf = CreateFrame("Frame", nil, UIParent) pfUI.uf.frames = {} +-- Reusable buffer for C_UnitAuras.GetAuraSlots slot ids (filled by +-- ScanAuraSlots, api.lua). Each aura scan in RefreshUnit fills it once, then +-- reads every aura by slot id: one array walk per scan instead of one per index. +-- Scans run back to back and consume the buffer before the next refill. +local auraSlots = {} + -- ============================================================================ -- GUID-based Roster Tracking for Smart Updates -- Only updates frames where the unit actually changed, not ALL 40 frames @@ -1635,11 +1641,13 @@ function pfUI.uf:RefreshUnit(unit, component) -- buffs if unit.buffs and ( component == "all" or component == "aura" ) then + -- one GetAuraSlots enumeration per refresh, then a positional read per + -- slot id: allocates nothing and never re-walks the aura array per icon + ScanAuraSlots(unitstr, "HELPFUL", auraSlots, unit.config.bufflimit) for i=1, unit.config.bufflimit do if not unit.buffs[i] then break end - -- positional UnitBuff allocates nothing (vs a table per icon per refresh) - local name, icon, count, _, duration, expirationTime, _, _, _, spellId = C_UnitAuras.UnitBuff(unitstr, i) + local name, icon, count, _, duration, expirationTime, _, _, _, spellId = C_UnitAuras.UnitAuraBySlot(unitstr, auraSlots[i]) if name then unit.buffs[i].texture:SetTexture(icon) @@ -1723,6 +1731,13 @@ function pfUI.uf:RefreshUnit(unit, component) reposition = true end + -- 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). One GetAuraSlots enumeration per refresh; the i-th slot + -- is the i-th aura of the filtered list, so `i` stays the tooltip index. + local filter = (unit.label ~= "player" and selfdebuff == "1") and "HARMFUL|PLAYER" or "HARMFUL" + ScanAuraSlots(unitstr, filter, auraSlots, unit.config.debufflimit) + for i=1, unit.config.debufflimit do if not unit.debuffs[i] then break end @@ -1739,12 +1754,8 @@ function pfUI.uf:RefreshUnit(unit, component) invert_h * ((row+buffrow)*(multiply*default_border + unit.config.debuffsize + 1) + (multiply*default_border + 1))) end - -- 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). - -- 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) + -- positional read by slot id allocates nothing + local name, icon, count, dispelType, duration, expirationTime = C_UnitAuras.UnitAuraBySlot(unitstr, auraSlots[i]) if name then texture, stacks, dtype = icon, count, dispelType else @@ -1834,8 +1845,9 @@ function pfUI.uf:RefreshUnit(unit, component) 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) + local n = ScanAuraSlots(unitstr, "HARMFUL", auraSlots) + for i=1,n do + local name, _, _, dispelType = C_UnitAuras.UnitAuraBySlot(unitstr, auraSlots[i]) if name and dispelType and dispelType ~= "" then present[dispelType] = true end end @@ -1925,9 +1937,9 @@ function pfUI.uf:RefreshUnit(unit, component) local pos = 1 if table.getn(unit.indicators) > 0 then - local i = 1 - while true do - local name, icon, count, _, _, expirationTime = C_UnitAuras.UnitBuff(unitstr, i) + local n = ScanAuraSlots(unitstr, "HELPFUL", auraSlots) + for i=1,n do + local name, icon, count, _, _, expirationTime = C_UnitAuras.UnitAuraBySlot(unitstr, auraSlots[i]) if not name then break end local texLower = string.lower(icon) local timeleft = expirationTime > 0 and (expirationTime - GetTime()) or nil @@ -1948,14 +1960,13 @@ function pfUI.uf:RefreshUnit(unit, component) break end end - i = i + 1 end end if table.getn(unit.indicator_custom) > 0 then - local ai = 1 - while true do - local name, icon, count, _, _, expirationTime = C_UnitAuras.UnitBuff(unitstr, ai) + local n = ScanAuraSlots(unitstr, "HELPFUL", auraSlots) + for i=1,n do + local name, icon, count, _, _, expirationTime = C_UnitAuras.UnitAuraBySlot(unitstr, auraSlots[i]) if not name then break end local timeleft = expirationTime > 0 and (expirationTime - GetTime()) or nil local lowerName = string.lower(name) @@ -1966,12 +1977,12 @@ function pfUI.uf:RefreshUnit(unit, component) break end end - ai = ai + 1 end - local debuffFilter = unit.config.selfdebuff == "1" and "PLAYER" or nil - for i=1,16 do -- scan for custom debuffs - local name, icon, count, _, _, expirationTime = C_UnitAuras.UnitDebuff(unitstr, i, debuffFilter) + local debuffFilter = unit.config.selfdebuff == "1" and "HARMFUL|PLAYER" or "HARMFUL" + n = ScanAuraSlots(unitstr, debuffFilter, auraSlots) + for i=1,n do -- scan for custom debuffs + local name, icon, count, _, _, expirationTime = C_UnitAuras.UnitAuraBySlot(unitstr, auraSlots[i]) if name then local timeleft = expirationTime > 0 and (expirationTime - GetTime()) or nil for _, filter in pairs(unit.indicator_custom) do diff --git a/modules/buff.lua b/modules/buff.lua index b7b18774..beff5dc3 100644 --- a/modules/buff.lua +++ b/modules/buff.lua @@ -7,6 +7,15 @@ pfUI:RegisterModule("buff", function () local br, bg, bb, ba = GetStringColor(pfUI_config.appearance.border.color) + -- Player aura slot ids, enumerated once per refresh pass (ScanPlayerAuraSlots) + -- and read per button by its aura index: one GetAuraSlots walk per range + -- instead of a by-index walk per button. + local helpfulSlots, harmfulSlots = {}, {} + local function ScanPlayerAuraSlots() + ScanAuraSlots("player", "HELPFUL", helpfulSlots, 32) + ScanAuraSlots("player", "HARMFUL", harmfulSlots, 16) + end + local function RefreshBuffButton(buff) if buff.btype == "HELPFUL" then if C.buffs.separateweapons == "1" then @@ -23,7 +32,8 @@ pfUI:RegisterModule("buff", function () CreateBackdropShadow(buff) end - local name, icon, count, dispelType, _, expirationTime, _, _, _, spellId = C_UnitAuras.UnitAura("player", buff.id, buff.btype) + local slots = buff.btype == "HELPFUL" and helpfulSlots or harmfulSlots + local name, icon, count, dispelType, _, expirationTime, _, _, _, spellId = C_UnitAuras.UnitAuraBySlot("player", slots[buff.id]) --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 @@ -166,6 +176,8 @@ pfUI:RegisterModule("buff", function () pfUI.buff.wepbuffs.count = 0 end + ScanPlayerAuraSlots() + for i=1,32 do RefreshBuffButton(pfUI.buff.buffs.buttons[i]) end @@ -246,6 +258,9 @@ pfUI:RegisterModule("buff", function () end end) + -- CreateBuffButton refreshes each new button from the slot buffers + ScanPlayerAuraSlots() + -- Weapon Buffs pfUI.buff.wepbuffs = CreateFrame("Frame", "pfWepBuffFrame", UIParent) pfUI.buff.wepbuffs.count = 0 diff --git a/modules/buffwatch.lua b/modules/buffwatch.lua index f21a09d4..6dc7a1f0 100644 --- a/modules/buffwatch.lua +++ b/modules/buffwatch.lua @@ -73,9 +73,12 @@ pfUI:RegisterModule("buffwatch", function () return anchor end - local function GetBuffData(unit, id, type, selfdebuff) - local filter = (selfdebuff and type == "HARMFUL") and "HARMFUL|PLAYER" or type - local name, icon, count, dispelType, _, expirationTime = C_UnitAuras.UnitAura(unit, id, filter) + -- reusable GetAuraSlots buffer, filled once per RefreshBuffBarFrame + local auraSlots = {} + + -- Reads one aura by the slot id GetAuraSlots returned (nil slot -> nil). + local function GetBuffData(unit, slot) + local name, icon, count, dispelType, _, expirationTime = C_UnitAuras.UnitAuraBySlot(unit, slot) if not name then return end local remaining = expirationTime > 0 and (expirationTime - GetTime()) or 0 return remaining, icon, name, count, dispelType @@ -234,9 +237,15 @@ pfUI:RegisterModule("buffwatch", function () local function RefreshBuffBarFrame(frame) -- reinitialize all active buffs local selfdebuff = frame.config.selfdebuff == "1" + local filter = (selfdebuff and frame.type == "HARMFUL") and "HARMFUL|PLAYER" or frame.type + + -- one GetAuraSlots enumeration per refresh instead of a by-index walk per + -- bar; the i-th slot is the i-th aura of the filtered list, so `i` stays + -- the index the tooltip / cancel handlers pass to the by-index API + ScanAuraSlots(frame.unit, filter, auraSlots, 32) for i=1,32 do - local timeleft, texture, name, stacks, dtype = GetBuffData(frame.unit, i, frame.type, selfdebuff) + local timeleft, texture, name, stacks, dtype = GetBuffData(frame.unit, auraSlots[i]) timeleft = timeleft or 0 if texture and name and name ~= "" and BuffIsVisible(frame.config, name) then diff --git a/modules/nameplates.lua b/modules/nameplates.lua index b280770a..e27b892f 100644 --- a/modules/nameplates.lua +++ b/modules/nameplates.lua @@ -116,6 +116,7 @@ pfUI:RegisterModule("nameplates", function () -- Reusable per-plate debuff display buffer (avoid GC churn from per-call table creation) local debuffDisplayBuf = {} -- [i] = { effect, texture, stacks, dtype, duration, timeleft } for i = 1, 16 do debuffDisplayBuf[i] = {} end + local auraSlots = {} -- reusable GetAuraSlots buffer for the per-plate aura scan local threatMemory = {} -- guid -> true if mob had player targeted -- local debuffSeen = {} -- reusable table for debuff tracking (avoid GC churn) @@ -1312,11 +1313,12 @@ nameplates:RegisterEvent("PLAYER_GUILD_UPDATE") if unitstr then local filter = cfg.owndebuffs and "HARMFUL|PLAYER" or "HARMFUL" local now = GetTime() - -- 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) + -- one GetAuraSlots enumeration, then positional by-slot reads straight + -- into the reusable buffer: the per-plate scan allocates nothing (no + -- per-aura table, no result array) and walks the aura array once + local n = ScanAuraSlots(unitstr, filter, auraSlots, 16) + for i = 1, n do + local aname, icon, count, dispelType, duration, expirationTime = C_UnitAuras.UnitAuraBySlot(unitstr, auraSlots[i]) if not aname then break end debuffCount = debuffCount + 1 local b = debuffDisplayBuf[debuffCount] @@ -1326,7 +1328,6 @@ nameplates:RegisterEvent("PLAYER_GUILD_UPDATE") 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 diff --git a/modules/tooltip.lua b/modules/tooltip.lua index 03bdabbb..243614c6 100644 --- a/modules/tooltip.lua +++ b/modules/tooltip.lua @@ -189,6 +189,7 @@ pfUI:RegisterModule("tooltip", function () -- Icons come from a ClassicAPI object pool: ReleaseAll hides the whole -- set each refresh, then we Acquire and re-anchor left-to-right. local BUFF_SIZE, BUFF_SPACING, BUFF_MAX = 20, 2, 32 + local auraSlots = {} -- reusable GetAuraSlots buffer for UpdateBuffs pfUI.tooltip.buffs = CreateFrame("Frame", "pfTooltipBuffs", GameTooltip) pfUI.tooltip.buffs:SetPoint("BOTTOMLEFT", GameTooltipStatusBar, "TOPLEFT", 0, default_border + 2) pfUI.tooltip.buffs:SetHeight(BUFF_SIZE) @@ -239,17 +240,20 @@ pfUI:RegisterModule("tooltip", function () end local prev, count = nil, 0 - for i = 1, BUFF_MAX do - local aura = C_UnitAuras.GetBuffDataByIndex(unit, i) - if not aura then break end + -- one GetAuraSlots enumeration + positional by-slot reads: no per-aura + -- table and no per-index re-walk of the aura array + local n = ScanAuraSlots(unit, "HELPFUL", auraSlots, BUFF_MAX) + for i = 1, n do + local name, texture, applications, _, _, expirationTime = C_UnitAuras.UnitAuraBySlot(unit, auraSlots[i]) + if not name then break end count = count + 1 local icon = pfUI.tooltip.buffpool:Acquire() - icon.texture:SetTexture(aura.icon) - icon.stacks:SetText(aura.applications and aura.applications > 1 and aura.applications or "") + icon.texture:SetTexture(texture) + icon.stacks:SetText(applications and applications > 1 and applications or "") - icon.expirationTime = aura.expirationTime - local timeleft = aura.expirationTime and aura.expirationTime > 0 and (aura.expirationTime - GetTime()) or 0 + icon.expirationTime = expirationTime + local timeleft = expirationTime and expirationTime > 0 and (expirationTime - GetTime()) or 0 icon.timer:SetText(timeleft > 0 and GetColoredTimeString(timeleft) or "") if prev then