initial overflowtest

This commit is contained in:
Jrc13245
2026-02-12 21:19:20 -05:00
parent f2bce42acb
commit 3a3719a37a
7 changed files with 976 additions and 19 deletions
+195 -1
View File
@@ -849,6 +849,14 @@ CleveRoids.AuraCapStatus = {
lastCleanupTime = 0,
}
-- Overflow buff tracking: buffs applied while buff-capped (v2.34+)
-- The server maintains up to 48 buff slots (32 visible + 16 invisible overflow).
-- Buffs in slots 33-48 are mechanically active but have no client aura slot,
-- so GetPlayerBuff/GetPlayerAuraDuration can't see them.
-- Populated from AURA_CAST_ON_SELF when auraCapStatus indicates buff bar full.
-- Format: [spellId] = { timestamp = GetTime(), durationSec = durationMs/1000 }
CleveRoids.OverflowBuffs = {}
-- All-caster aura duration tracking (from AURA_CAST events)
-- Tracks buff/debuff durations from ANY caster, not just player
-- Structure: [targetGuid][spellId] = { start, duration, casterGuid }
@@ -994,10 +1002,68 @@ local function OnAuraCastSelf(spellId, casterGuid, targetGuid, effect, effectAur
local now = GetTime()
CleveRoids.AuraCapStatus.playerLastUpdate = now
local buffCapped = HasHitFlag(auraCapStatus, AURA_CAP_BUFF_FULL)
-- Check buff bar full (bit 1)
CleveRoids.AuraCapStatus.playerBuffCapped = HasHitFlag(auraCapStatus, AURA_CAP_BUFF_FULL)
CleveRoids.AuraCapStatus.playerBuffCapped = buffCapped
-- Check debuff bar full (bit 2)
CleveRoids.AuraCapStatus.playerDebuffCapped = HasHitFlag(auraCapStatus, AURA_CAP_DEBUFF_FULL)
-- Track overflow buffs: if buff bar is full, this aura may land in server slots 33-48
-- (no client aura slot). Store spellId + duration so /cancelaura and [mybuff] can find it.
-- But first verify it's actually a buff — debuffs applied while buff-capped go into
-- debuff slots (32-47), not buff overflow. Player debuffs never overflow into buff slots.
if buffCapped and spellId and spellId > 0 and effectAuraName and effectAuraName > 0 then
-- Check debuff slots (32-47) — if the spellId is there, it's a debuff, not overflow
local isDebuff = false
if _G.GetPlayerAuraDuration then
for slot = 32, 47 do
local sid = _G.GetPlayerAuraDuration(slot)
if sid and sid == spellId then
isDebuff = true
break
end
end
end
if not isDebuff then
local entry = CleveRoids.OverflowBuffs[spellId]
if not entry then
entry = {}
CleveRoids.OverflowBuffs[spellId] = entry
end
entry.timestamp = now
entry.durationSec = durationMs and (durationMs / 1000) or 0
end
elseif not buffCapped and next(CleveRoids.OverflowBuffs) then
-- No longer buff-capped: some overflow buffs may have gotten real slots.
-- Only remove entries that now appear in a visible aura slot.
-- The server does NOT auto-migrate overflow buffs into freed slots,
-- so we can't blindly clear everything.
if _G.GetPlayerAuraDuration then
local visibleSpells = {}
for slot = 0, 31 do
local sid = _G.GetPlayerAuraDuration(slot)
if sid and sid > 0 then
visibleSpells[sid] = true
end
end
for k in pairs(CleveRoids.OverflowBuffs) do
if visibleSpells[k] then
CleveRoids.OverflowBuffs[k] = nil
end
end
end
-- Also prune expired entries
for k, entry in pairs(CleveRoids.OverflowBuffs) do
if entry.durationSec and entry.durationSec > 0 then
local elapsed = now - (entry.timestamp or 0)
if elapsed > entry.durationSec then
CleveRoids.OverflowBuffs[k] = nil
end
end
end
end
end
local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAuraName,
@@ -1663,6 +1729,58 @@ end
function CleveRoids.CancelAura(auraName)
local ix = 0
auraName = string.lower(string.gsub(auraName, "_"," "))
-- v2.34+ path: cancel by spell ID (works for buff-capped overflow auras too)
local API = CleveRoids.NampowerAPI
if API and API.features.hasCancelPlayerAuraSpellId and CleveRoids.hasSuperwow then
-- First scan visible buffs via GetPlayerBuff (fast, covers normal case)
while true do
local aura_ix = GetPlayerBuff(ix, "HELPFUL")
ix = ix + 1
if aura_ix == -1 then break end
local bid = GetPlayerBuffID(aura_ix)
bid = (bid < -1) and (bid + 65536) or bid
if string.lower(SpellInfo(bid)) == auraName then
_G.CancelPlayerAuraSpellId(bid, 1)
return true
end
end
-- Not found in visible buffs - scan all 32 raw aura slots
-- GetPlayerAuraDuration reads unit data fields directly (same 32 slots but bypasses UI filtering)
if API.features.hasGetPlayerAuraDuration and _G.GetPlayerAuraDuration then
for slot = 0, 31 do
local spellId = _G.GetPlayerAuraDuration(slot)
if spellId and spellId > 0 then
local name = SpellInfo(spellId)
if name and string.lower(name) == auraName then
_G.CancelPlayerAuraSpellId(spellId, 1)
return true
end
end
end
end
-- Final fallback: check overflow buff tracking (buffs applied while buff-capped
-- that have NO client aura slot - tracked via AURA_CAST_ON_SELF events)
for spellId, entry in pairs(CleveRoids.OverflowBuffs) do
-- Skip expired overflow entries
local elapsed = GetTime() - (entry.timestamp or 0)
if entry.durationSec and entry.durationSec > 0 and elapsed > entry.durationSec then
CleveRoids.OverflowBuffs[spellId] = nil
else
local name = SpellInfo(spellId)
if name and string.lower(name) == auraName then
_G.CancelPlayerAuraSpellId(spellId, 1)
CleveRoids.OverflowBuffs[spellId] = nil
return true
end
end
end
return false
end
-- Legacy path for older Nampower versions
while true do
local aura_ix = GetPlayerBuff(ix,"HELPFUL")
ix = ix + 1
@@ -3281,6 +3399,82 @@ function CleveRoids.ValidateAura(unit, args, isbuff)
end
end
-- Overflow buff fallback: player buffs in server slots 33-48 (no client slot)
-- Check AURA_CAST_ON_SELF tracked overflow data for presence + duration
if not found and isPlayer and isbuff and next(CleveRoids.OverflowBuffs) then
local now = GetTime()
for spellId, entry in pairs(CleveRoids.OverflowBuffs) do
-- Prune expired entries
local elapsed = now - (entry.timestamp or 0)
if entry.durationSec and entry.durationSec > 0 and elapsed > entry.durationSec then
CleveRoids.OverflowBuffs[spellId] = nil
else
local match = false
if searchID then
match = (spellId == searchID)
elseif searchName then
local lowerName = GetLowercaseSpellName(spellId)
match = (lowerName and lowerName == searchName)
end
if match then
found = true
stacks = 0
-- Compute remaining time from apply timestamp + duration
if entry.durationSec and entry.durationSec > 0 then
remaining = entry.durationSec - elapsed
if remaining < 0 then remaining = 0 end
else
remaining = -1 -- Unknown duration (permanent buff)
end
break
end
end
end
end
-- Non-player overflow fallback: buff exists in server slots 33-48 (no client slot)
-- AllCasterAuraTracking already has data from AURA_CAST_ON_OTHER for all aura applications.
-- If the normal scan didn't find the buff, check there for presence + duration.
-- Guard: verify the spell isn't a visible debuff on the target (AllCasterAuraTracking
-- stores both buffs and debuffs, so without this check [buff:DebuffName] could false-positive).
if not found and not isPlayer and isbuff and (searchID or searchName) then
local _, targetGuid = UnitExists(unit)
if targetGuid then
-- Check if the spell is in a visible debuff slot — if so, it's a debuff, not a buff
local isDebuff = false
local di = 1
while true do
local dtex, _, _, dspellId = UnitDebuff(unit, di)
if not dtex then break end
if dspellId then
if searchID then
if dspellId == searchID then
isDebuff = true
break
end
elseif searchName then
local lowerName = GetLowercaseSpellName(dspellId)
if lowerName and lowerName == searchName then
isDebuff = true
break
end
end
end
di = di + 1
end
if not isDebuff then
local trackRemaining = CleveRoids.FindAllCasterAuraByName(targetGuid,
searchID and tostring(searchID) or args.name)
if trackRemaining then
found = true
stacks = 0
remaining = trackRemaining
end
end
end
end
local ops = CleveRoids.operators
local cmp = CleveRoids.comparators
+54 -14
View File
@@ -3181,16 +3181,15 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand)
end
end
-- Item not found via v2.18 lookup
-- Item not found via v2.18 lookup - fall through to legacy path
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cffff8800[EquipLog] Item '" .. tostring(msg) .. "' not found via v2.18 FindPlayerItemSlot|r")
CleveRoids.Print("|cffff8800[EquipLog] Item '" .. tostring(msg) .. "' not found via v2.18 FindPlayerItemSlot, trying legacy fallback|r")
-- Debug: Try direct FindPlayerItemSlot call
if FindPlayerItemSlot then
local bag, slot = FindPlayerItemSlot(msg)
CleveRoids.Print("|cff888888[EquipLog] Direct FindPlayerItemSlot('" .. msg .. "') = bag:" .. tostring(bag) .. " slot:" .. tostring(slot) .. "|r")
end
end
return false
end
-- Fallback for older Nampower versions
@@ -3294,13 +3293,25 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand)
if EquipItemByName then
local ok = pcall(EquipItemByName, msg, invslot)
if ok then
-- Invalidate cache entry if it exists
if CleveRoids.Items then
CleveRoids.Items[msg] = nil
CleveRoids.Items[string_lower(msg)] = nil
-- Verify the item actually landed in the target slot
-- EquipItemByName may silently no-op for same-named items in paired slots
-- (e.g., dual-wielding Scimitar: MH copy found first, "equipped" to same slot)
local newLink = GetInventoryItemLink("player", invslot)
if newLink then
local _, _, newName = string_find(newLink, "|h%[(.-)%]|h")
if newName and string_lower(newName) == string_lower(msg) then
if CleveRoids.Items then
CleveRoids.Items[msg] = nil
CleveRoids.Items[string_lower(msg)] = nil
end
InvalidateDisplacedItem()
return true
end
end
-- Verification failed - EquipItemByName didn't place item in target slot
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cffff8800[EquipLog] EquipItemByName('" .. msg .. "', " .. invslot .. ") succeeded but item not in target slot, trying fallback|r")
end
InvalidateDisplacedItem()
return true
end
end
@@ -3336,12 +3347,23 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand)
if item.name and EquipItemByName and item.name ~= msg then
local ok = pcall(EquipItemByName, item.name, invslot)
if ok then
if CleveRoids.Items then
CleveRoids.Items[item.name] = nil
CleveRoids.Items[string_lower(item.name)] = nil
-- Verify the item actually landed in the target slot (same guard as above)
local newLink = GetInventoryItemLink("player", invslot)
if newLink then
local _, _, newName = string_find(newLink, "|h%[(.-)%]|h")
if newName and string_lower(newName) == string_lower(item.name) then
if CleveRoids.Items then
CleveRoids.Items[item.name] = nil
CleveRoids.Items[string_lower(item.name)] = nil
end
InvalidateDisplacedItem()
return true
end
end
-- Verification failed - fall through to manual cursor-based equip
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cffff8800[EquipLog] EquipItemByName('" .. item.name .. "', " .. invslot .. ") succeeded but item not in target slot, trying manual fallback|r")
end
InvalidateDisplacedItem()
return true
end
end
@@ -5824,6 +5846,8 @@ SlashCmdList["CLEVEROID"] = function(msg)
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid tankdebug - Debug tank targeting conditionals')
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00All-Caster Aura Tracking:|r")
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid auradebug - Debug buff/debuff time tracking from all casters')
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Overflow Buff Frame:|r")
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid overflowtest - Toggle overflow buff frame test mode')
return
end
@@ -6559,6 +6583,22 @@ SlashCmdList["CLEVEROID"] = function(msg)
return
end
-- overflowtest (toggle overflow buff frame test mode)
if cmd == "overflowtest" then
if CleveRoids.ToggleOverflowTest then
CleveRoids.ToggleOverflowTest()
else
CleveRoids.Print("|cffff0000OverflowBuffFrame extension not loaded|r")
end
return
end
-- Delegate to extension console handlers (chain pattern)
if CleveRoids.HandleConsoleCommand then
CleveRoids.HandleConsoleCommand(msg)
return
end
-- Unknown command fallback
CleveRoids.Print("Usage:")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid - Show current settings")
+47 -3
View File
@@ -29,6 +29,7 @@ local pendingValidation = false
local lastSelectedMacro = nil
local updateFrame = nil
local hooked = false
local measureFs = nil -- hidden FontString for measuring text width (wrap detection)
-- Cache for current errors (avoids re-validation on every frame)
local currentErrors = nil
@@ -186,18 +187,61 @@ local function UpdateLineHighlights(errors)
local editWidth = MacroFrameText:GetWidth()
if editWidth < 10 then editWidth = 260 end -- fallback
-- Create measurement FontString lazily (same font as EditBox, hidden)
if not measureFs then
measureFs = (MacroFrame or UIParent):CreateFontString(nil, "OVERLAY")
measureFs:Hide()
end
local fontPath, fontSize, fontFlags = MacroFrameText:GetFont()
if fontPath then
measureFs:SetFont(fontPath, fontSize, fontFlags)
end
-- Split macro text into logical lines for wrap calculation
local text = MacroFrameText:GetText() or ""
local logicalLines = {}
local lineStart = 1
while true do
local nlPos = string.find(text, "\n", lineStart, true)
if nlPos then
table.insert(logicalLines, string.sub(text, lineStart, nlPos - 1))
lineStart = nlPos + 1
else
table.insert(logicalLines, string.sub(text, lineStart))
break
end
end
-- Calculate visual line count for each logical line
local visualCounts = {}
for i, line in ipairs(logicalLines) do
if line == "" or editWidth <= 0 then
visualCounts[i] = 1
else
measureFs:SetText(line)
local textWidth = measureFs:GetStringWidth()
visualCounts[i] = math.max(1, math.ceil(textWidth / editWidth))
end
end
local highlightIdx = 1
for lineNum, _ in pairs(errorLines) do
if highlightIdx > HIGHLIGHT_POOL_SIZE then break end
local tex = lineHighlights[highlightIdx]
-- Position: top text inset + (lineNum-1) lines down
local yOffset = -topInset - ((lineNum - 1) * lineHeight)
-- Y offset: sum visual lines of all preceding logical lines
local yLines = 0
for i = 1, lineNum - 1 do
yLines = yLines + (visualCounts[i] or 1)
end
local yOffset = -topInset - (yLines * lineHeight)
local highlightHeight = (visualCounts[lineNum] or 1) * lineHeight
tex:ClearAllPoints()
tex:SetPoint("TOPLEFT", MacroFrameText, "TOPLEFT", -2, yOffset)
tex:SetWidth(editWidth + 4)
tex:SetHeight(lineHeight)
tex:SetHeight(highlightHeight)
tex:Show()
highlightIdx = highlightIdx + 1
+620
View File
@@ -0,0 +1,620 @@
--[[
OverflowBuffFrame Extension
Two frames displaying overflow buffs (slots 33-48) for the player
and the current target (if a group/party member).
Each frame shows 2 rows of 8 icons = 16 slots.
Author: Mewtiny
License: MIT License
]]
local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
-- ============================================================================
-- Constants
-- ============================================================================
local ICON_SIZE = 24
local ICON_SPACING = 2
local ICONS_PER_ROW = 8
local NUM_ROWS = 2
local ICONS_PER_FRAME = ICONS_PER_ROW * NUM_ROWS -- 16
local ROW_SPACING = 16 -- vertical gap between rows (icon + duration text)
local DURATION_FONT_SIZE = 10
local DURATION_OFFSET_Y = -3
local UPDATE_INTERVAL = 0.2
local LABEL_HEIGHT = 12
-- ============================================================================
-- State
-- ============================================================================
local playerFrame = nil
local targetFrame = nil
local playerIcons = {} -- [1..16]
local targetIcons = {} -- [1..16]
local playerLabelFs = nil
local targetLabelFs = nil
local updateFrame = nil
local updateElapsed = 0
local testMode = false
local lastPlayerCount = 0
local lastTargetCount = 0
-- ============================================================================
-- Duration Formatting (matches pfUI api.lua:1324 style)
-- ============================================================================
local function FormatDuration(seconds)
if not seconds or seconds <= 0 then return "" end
if seconds >= 100 then
return math.floor(seconds / 60 + 0.5) .. "m"
elseif seconds > 5 then
return math.floor(seconds + 0.5) .. "s"
else
return string.format("%.1f", seconds)
end
end
-- Forward declarations (needed by OnClick closures in CreateIconButton)
local RebuildFrame
-- ============================================================================
-- Icon Button Creation
-- ============================================================================
local function CreateIconButton(parent, index, iconTable)
local btn = CreateFrame("Button", nil, parent)
btn:SetWidth(ICON_SIZE)
btn:SetHeight(ICON_SIZE)
btn:RegisterForClicks("RightButtonUp")
-- Dark 1px border backdrop (pfUI style)
btn:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8x8",
edgeFile = "Interface\\Buttons\\WHITE8x8",
tile = false, tileSize = 0, edgeSize = 1,
insets = { left = 0, right = 0, top = 0, bottom = 0 },
})
btn:SetBackdropColor(0, 0, 0, 0.6)
btn:SetBackdropBorderColor(0.1, 0.1, 0.1, 0.9)
-- Icon texture (inset by 1px for border)
local icon = btn:CreateTexture(nil, "ARTWORK")
icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 1, -1)
icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -1, 1)
icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
icon:Hide()
-- Duration text below icon
local dur = btn:CreateFontString(nil, "OVERLAY")
dur:SetFont("Fonts\\FRIZQT__.TTF", DURATION_FONT_SIZE, "OUTLINE")
dur:SetPoint("TOP", btn, "BOTTOM", 0, DURATION_OFFSET_Y)
dur:SetJustifyH("CENTER")
dur:SetTextColor(1, 1, 1, 1)
dur:SetText("")
-- Right-click to cancel player overflow buffs
btn:SetScript("OnClick", function()
local data = iconTable[index]
if not data or not data.spellId then return end
if data.source ~= "player" then return end
-- CancelPlayerAuraSpellId(spellId, ignoreMissing)
-- ignoreMissing=1 is required for overflow buffs in invisible slots 33-48
if CancelPlayerAuraSpellId then
CancelPlayerAuraSpellId(data.spellId, 1)
end
-- Remove from overflow tracking
if CleveRoids.OverflowBuffs then
CleveRoids.OverflowBuffs[data.spellId] = nil
end
-- Hide tooltip and rebuild
GameTooltip:Hide()
RebuildFrame()
end)
-- Tooltip
btn:SetScript("OnEnter", function()
local data = iconTable[index]
if not data or not data.spellId then return end
GameTooltip:SetOwner(btn, "ANCHOR_BOTTOMLEFT")
local spellName = SpellInfo and SpellInfo(data.spellId) or ("Spell " .. data.spellId)
if spellName then
local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "")
GameTooltip:AddLine(baseName, 1, 1, 1)
end
if data.remaining and data.remaining > 0 then
GameTooltip:AddLine(FormatDuration(data.remaining) .. " remaining", 0.7, 0.7, 0.7)
end
if data.source == "player" then
GameTooltip:AddLine("Right-click to cancel", 0.8, 0.8, 0.5)
else
GameTooltip:AddLine("Overflow buff (target)", 0.5, 1.0, 0.8)
end
GameTooltip:Show()
end)
btn:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
btn:Hide()
return {
button = btn,
icon = icon,
duration = dur,
spellId = nil,
remaining = nil,
source = nil,
}
end
-- ============================================================================
-- Frame Creation
-- ============================================================================
-- Compute frame dimensions
local FRAME_WIDTH = ICONS_PER_ROW * (ICON_SIZE + ICON_SPACING) - ICON_SPACING
local FRAME_HEIGHT = LABEL_HEIGHT + NUM_ROWS * (ICON_SIZE + ROW_SPACING)
local function CreateBuffFrame(name, savedPosKey, defaultY, labelColor, iconTable)
local frame = CreateFrame("Frame", name, UIParent)
frame:SetWidth(FRAME_WIDTH)
frame:SetHeight(FRAME_HEIGHT)
frame:SetPoint("TOP", UIParent, "TOP", 0, defaultY)
frame:SetFrameStrata("MEDIUM")
frame:SetMovable(true)
frame:EnableMouse(false)
frame:Hide()
-- Section label
local label = frame:CreateFontString(nil, "OVERLAY")
label:SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE")
label:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
label:SetTextColor(labelColor[1], labelColor[2], labelColor[3], 0.8)
label:SetText("")
-- Create 16 icon slots in 2 rows of 8
for i = 1, ICONS_PER_FRAME do
iconTable[i] = CreateIconButton(frame, i, iconTable)
local row = math.floor((i - 1) / ICONS_PER_ROW)
local col = math.mod(i - 1, ICONS_PER_ROW)
local xOff = col * (ICON_SIZE + ICON_SPACING)
local yOff = -(LABEL_HEIGHT + row * (ICON_SIZE + ROW_SPACING))
iconTable[i].button:SetPoint("TOPLEFT", frame, "TOPLEFT", xOff, yOff)
end
-- Shift+drag to move
frame:SetScript("OnMouseDown", function()
if IsShiftKeyDown() then
frame:StartMoving()
end
end)
frame:SetScript("OnMouseUp", function()
frame:StopMovingOrSizing()
local _, _, _, x, y = frame:GetPoint(1)
CleveRoidMacros = CleveRoidMacros or {}
CleveRoidMacros[savedPosKey] = { x = x, y = y }
end)
-- Restore saved position
CleveRoidMacros = CleveRoidMacros or {}
if CleveRoidMacros[savedPosKey] then
frame:ClearAllPoints()
frame:SetPoint("TOP", UIParent, "TOP",
CleveRoidMacros[savedPosKey].x or 0,
CleveRoidMacros[savedPosKey].y or defaultY)
end
return frame, label
end
-- ============================================================================
-- Data Collection
-- ============================================================================
local function GetPlayerOverflowBuffs()
local results = {}
local overflowBuffs = CleveRoids.OverflowBuffs
if not overflowBuffs then return results end
local now = GetTime()
for spellId, entry in pairs(overflowBuffs) do
if entry.timestamp and entry.durationSec then
local remaining = entry.durationSec - (now - entry.timestamp)
if remaining > 0 then
table.insert(results, {
spellId = spellId,
remaining = remaining,
source = "player",
})
end
end
end
table.sort(results, function(a, b) return a.remaining > b.remaining end)
return results
end
local function GetTargetOverflowBuffs()
local results = {}
if not UnitExists("target") then return results end
if not testMode and not (UnitInParty("target") or UnitInRaid("target")) then return results end
local _, targetGuid = UnitExists("target")
if not targetGuid then return results end
local trackingData = CleveRoids.AllCasterAuraTracking
if not trackingData or not trackingData[targetGuid] then return results end
-- Build set of visible buff textures on target
local visibleBuffs = {}
for i = 1, 32 do
local texture = UnitBuff("target", i)
if not texture then break end
visibleBuffs[texture] = true
end
local now = GetTime()
local lib = CleveRoids.libdebuff
for spellId, auraData in pairs(trackingData[targetGuid]) do
if auraData.start and auraData.duration then
local remaining = auraData.duration + auraData.start - now
if remaining > 0 then
local isVisible = false
if not auraData._testEntry then
local spellIcon = lib and lib:GetCachedIcon(spellId)
isVisible = spellIcon and visibleBuffs[spellIcon]
end
if not isVisible then
table.insert(results, {
spellId = spellId,
remaining = remaining,
source = "target",
})
end
end
end
end
table.sort(results, function(a, b) return a.remaining > b.remaining end)
return results
end
-- ============================================================================
-- Layout
-- ============================================================================
local function PopulateFrame(frame, label, labelText, iconTable, buffs, countRef)
local count = table.getn(buffs)
if count > ICONS_PER_FRAME then count = ICONS_PER_FRAME end
local lib = CleveRoids.libdebuff
for i = 1, count do
local data = iconTable[i]
local buff = buffs[i]
local texture = lib and lib:GetCachedIcon(buff.spellId)
if texture then
data.icon:SetTexture(texture)
else
data.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
end
data.icon:Show()
data.spellId = buff.spellId
data.remaining = buff.remaining
data.source = buff.source
data.duration:SetText(FormatDuration(buff.remaining))
data.button:EnableMouse(true)
data.button:Show()
end
-- Hide unused
for i = count + 1, ICONS_PER_FRAME do
local data = iconTable[i]
data.button:Hide()
data.icon:Hide()
data.duration:SetText("")
data.spellId = nil
data.remaining = nil
data.source = nil
end
if count > 0 then
label:SetText(labelText)
frame:EnableMouse(true)
frame:Show()
else
label:SetText("")
frame:EnableMouse(false)
frame:Hide()
end
return count
end
RebuildFrame = function()
if not playerFrame then return end
local pBuffs = GetPlayerOverflowBuffs()
lastPlayerCount = PopulateFrame(playerFrame, playerLabelFs, "Overflow (You)", playerIcons, pBuffs)
local tBuffs = GetTargetOverflowBuffs()
local targetName = UnitExists("target") and UnitName("target") or "Target"
lastTargetCount = PopulateFrame(targetFrame, targetLabelFs, "Overflow (" .. targetName .. ")", targetIcons, tBuffs)
end
-- Refresh only duration text (no full rebuild)
local function RefreshDurations()
local anyExpired = false
local now = GetTime()
-- Player durations
if playerFrame and playerFrame:IsVisible() then
for i = 1, lastPlayerCount do
local data = playerIcons[i]
if data.spellId then
local entry = CleveRoids.OverflowBuffs and CleveRoids.OverflowBuffs[data.spellId]
if entry and entry.timestamp and entry.durationSec then
local remaining = entry.durationSec - (now - entry.timestamp)
if remaining > 0 then
data.remaining = remaining
data.duration:SetText(FormatDuration(remaining))
else
anyExpired = true
end
else
anyExpired = true
end
end
end
end
-- Target durations
if targetFrame and targetFrame:IsVisible() then
local targetGuid = nil
if UnitExists("target") then
local _, guid = UnitExists("target")
targetGuid = guid
end
for i = 1, lastTargetCount do
local data = targetIcons[i]
if data.spellId and targetGuid then
local remaining = CleveRoids.GetAllCasterAuraTimeRemaining(targetGuid, data.spellId) or 0
if remaining > 0 then
data.remaining = remaining
data.duration:SetText(FormatDuration(remaining))
else
anyExpired = true
end
else
anyExpired = true
end
end
end
if anyExpired then
RebuildFrame()
end
end
-- ============================================================================
-- Test Mode
-- ============================================================================
local TEST_PLAYER_SPELL_IDS = {
1126, -- Mark of the Wild
1243, -- Power Word: Fortitude
1461, -- Arcane Intellect
14752, -- Divine Spirit
976, -- Shadow Protection
6307, -- Blood Pact
20217, -- Blessing of Kings
19740, -- Blessing of Might
21849, -- Gift of the Wild
25898, -- Greater Blessing of Kings
25899, -- Greater Blessing of Sanctuary
25890, -- Greater Blessing of Light
10938, -- Greater Power Word: Fortitude
10157, -- Greater Arcane Intellect
20911, -- Blessing of Sanctuary
25782, -- Greater Blessing of Might
}
local TEST_TARGET_SPELL_IDS = {
467, -- Thorns
10060, -- Power Infusion
17007, -- Leader of the Pack
24932, -- Leader of the Pack aura
19506, -- Trueshot Aura
8936, -- Regrowth
774, -- Rejuvenation
139, -- Renew
1022, -- Blessing of Protection
6346, -- Fear Ward
1044, -- Blessing of Freedom
10958, -- Shadow Protection (rank)
27681, -- Prayer of Spirit
21562, -- Prayer of Fortitude
20914, -- Blessing of Sanctuary (rank)
25916, -- Greater Blessing of Wisdom
}
local testTargetGuid = nil
local testStartTime = 0 -- when test mode was enabled (for duration calc)
-- Remove test entries from a specific GUID in AllCasterAuraTracking
local function ClearTestTargetData(guid)
if not guid or not CleveRoids.AllCasterAuraTracking then return end
local targetData = CleveRoids.AllCasterAuraTracking[guid]
if not targetData then return end
for spellId, entry in pairs(targetData) do
if entry._testEntry then
targetData[spellId] = nil
end
end
if not next(targetData) then
CleveRoids.AllCasterAuraTracking[guid] = nil
end
end
-- Inject test entries for the current target
local function InjectTestTargetData()
if not UnitExists("target") then return end
local _, targetGuid = UnitExists("target")
if not targetGuid then return end
-- Clean up old test target if it changed
if testTargetGuid and testTargetGuid ~= targetGuid then
ClearTestTargetData(testTargetGuid)
end
testTargetGuid = targetGuid
CleveRoids.AllCasterAuraTracking = CleveRoids.AllCasterAuraTracking or {}
if not CleveRoids.AllCasterAuraTracking[targetGuid] then
CleveRoids.AllCasterAuraTracking[targetGuid] = {}
end
local targetDurations = { 8, 15, 25, 40, 55, 90, 150, 240, 400, 500, 700, 1000, 1300, 1600, 2000, 3000 }
for i = 1, table.getn(TEST_TARGET_SPELL_IDS) do
local spellId = TEST_TARGET_SPELL_IDS[i]
local dur = targetDurations[i] or 60
CleveRoids.AllCasterAuraTracking[targetGuid][spellId] = {
start = testStartTime,
duration = dur,
casterGuid = "test",
_testEntry = true,
}
end
end
local function EnableTestMode()
testMode = true
testStartTime = GetTime()
CleveRoids.OverflowBuffs = CleveRoids.OverflowBuffs or {}
local playerDurations = { 5, 10, 30, 45, 60, 120, 180, 300, 450, 600, 900, 1200, 1500, 1800, 2400, 3600 }
for i = 1, table.getn(TEST_PLAYER_SPELL_IDS) do
local spellId = TEST_PLAYER_SPELL_IDS[i]
local dur = playerDurations[i] or 60
CleveRoids.OverflowBuffs[spellId] = {
timestamp = testStartTime,
durationSec = dur,
_testEntry = true,
}
end
-- Inject target test data if we have a target
InjectTestTargetData()
CleveRoids.Print("|cff00ff00Overflow buff frame test mode enabled|r")
CleveRoids.Print(" Shift+drag to reposition the frames")
RebuildFrame()
end
local function DisableTestMode()
testMode = false
if CleveRoids.OverflowBuffs then
for spellId, entry in pairs(CleveRoids.OverflowBuffs) do
if entry._testEntry then
CleveRoids.OverflowBuffs[spellId] = nil
end
end
end
ClearTestTargetData(testTargetGuid)
testTargetGuid = nil
CleveRoids.Print("|cffff9900Overflow buff frame test mode disabled|r")
RebuildFrame()
end
local function ToggleTestMode()
if testMode then
DisableTestMode()
else
EnableTestMode()
end
end
CleveRoids.ToggleOverflowTest = ToggleTestMode
-- ============================================================================
-- OnUpdate Handler
-- ============================================================================
local function OnUpdate()
updateElapsed = updateElapsed + arg1
if updateElapsed < UPDATE_INTERVAL then return end
updateElapsed = 0
RefreshDurations()
end
-- ============================================================================
-- Extension Registration
-- ============================================================================
local ext = CleveRoids.RegisterExtension("OverflowBuffFrame")
ext.OnTargetChanged = function()
-- Re-inject test data for the new target during test mode
if testMode then
InjectTestTargetData()
end
RebuildFrame()
end
ext.OnAuraCast = function()
RebuildFrame()
end
ext.OnGroupChanged = function()
RebuildFrame()
end
ext.OnPlayerDead = function()
if CleveRoids.OverflowBuffs then
if testMode then
for spellId, entry in pairs(CleveRoids.OverflowBuffs) do
if not entry._testEntry then
CleveRoids.OverflowBuffs[spellId] = nil
end
end
else
CleveRoids.OverflowBuffs = {}
end
end
RebuildFrame()
end
ext.OnLoad = function()
playerFrame, playerLabelFs = CreateBuffFrame(
"CleveRoidsOverflowPlayer", "overflowPlayerPos", -100,
{ 0.4, 0.7, 1.0 }, playerIcons)
targetFrame, targetLabelFs = CreateBuffFrame(
"CleveRoidsOverflowTarget", "overflowTargetPos", -170,
{ 0.4, 1.0, 0.7 }, targetIcons)
ext.RegisterEvent("PLAYER_TARGET_CHANGED", "OnTargetChanged")
ext.RegisterEvent("PARTY_MEMBERS_CHANGED", "OnGroupChanged")
ext.RegisterEvent("RAID_ROSTER_UPDATE", "OnGroupChanged")
ext.RegisterEvent("PLAYER_DEAD", "OnPlayerDead")
if CleveRoids.hasNampower then
ext.RegisterEvent("AURA_CAST_ON_SELF", "OnAuraCast")
end
-- Shared OnUpdate frame
updateFrame = CreateFrame("Frame")
updateFrame:SetScript("OnUpdate", OnUpdate)
RebuildFrame()
end
_G["CleveRoids"] = CleveRoids
+40 -1
View File
@@ -85,7 +85,13 @@
- Reads from NampowerSettings addon when available
- Falls back to CVars when addon not present
Current version: v2.33.0
Aura Cancel Functions (v2.34+):
- CancelPlayerAuraSlot(auraSlot) - Cancel buff/debuff by raw 0-based aura slot
- CancelPlayerAuraSpellId(spellId, [ignoreMissing]) - Cancel buff/debuff by spell ID
Pass ignoreMissing=1 to skip aura-slot presence check (for overflow buffs in
server slots 33-48 that have no client aura slot)
Current version: v2.34.0
]]
local _G = _G or getfenv(0)
@@ -242,6 +248,10 @@ API.VERSION_REQUIREMENTS = {
-- v2.33+ - UPDATE_DURATION spellId parameter and GetCastInfo guid fix
["DurationEventSpellId"] = { 2, 33, 0 },
["GetCastInfoGuidFix"] = { 2, 33, 0 },
-- v2.34+ - Aura cancel functions
["CancelPlayerAuraSlot"] = { 2, 34, 0, "CancelPlayerAuraSlot" },
["CancelPlayerAuraSpellId"] = { 2, 34, 0, "CancelPlayerAuraSpellId" },
}
-- Check if a specific feature is available
@@ -382,6 +392,10 @@ local function InitializeFeatures()
f.hasDurationEventSpellId = API.HasFeature("DurationEventSpellId")
f.hasGetCastInfoGuidFix = API.HasFeature("GetCastInfoGuidFix")
-- v2.34+ Aura cancel functions
f.hasCancelPlayerAuraSlot = API.HasFeature("CancelPlayerAuraSlot")
f.hasCancelPlayerAuraSpellId = API.HasFeature("CancelPlayerAuraSpellId")
-- Runtime detection for enhanced spell functions (verify by testing)
if f.hasEnhancedSpellFunctions and GetSpellTexture then
local success, result = pcall(function()
@@ -2982,5 +2996,30 @@ function API.GetSpellPower(mode)
return _G.GetSpellPower(mode)
end
--------------------------------------------------------------------------------
-- AURA CANCEL FUNCTIONS (v2.34+)
--------------------------------------------------------------------------------
-- Cancel a player aura by raw 0-based aura slot (v2.34+)
-- auraSlot: 0-31 for buffs, 32-47 for debuffs
function API.CancelPlayerAuraSlot(auraSlot)
if not API.features.hasCancelPlayerAuraSlot or not _G.CancelPlayerAuraSlot then
return false
end
_G.CancelPlayerAuraSlot(auraSlot)
return true
end
-- Cancel a player aura by spell ID (v2.34+)
-- spellId: the spell ID to cancel
-- ignoreMissing: pass 1 to skip aura-slot presence check (useful for buff-capped hidden auras)
function API.CancelPlayerAuraSpellId(spellId, ignoreMissing)
if not API.features.hasCancelPlayerAuraSpellId or not _G.CancelPlayerAuraSpellId then
return false
end
_G.CancelPlayerAuraSpellId(spellId, ignoreMissing)
return true
end
-- Expose API globally for other addons
_G.CleveRoidsNampowerAPI = API
+1
View File
@@ -43,4 +43,5 @@ Extensions\Mouseover\Cursive.lua
Extensions\Tooltip\Generic.lua
Extensions\CursiveCustomSpells.lua
Extensions\OverflowBuffFrame.lua
+19
View File
@@ -4913,6 +4913,25 @@ ev:SetScript("OnEvent", function()
CleveRoids.castTracking[guid] = nil
end
-- Clean up overflow buff tracking (death removes all buffs)
local _, playerGUID = UnitExists("player")
if playerGUID and guid == playerGUID then
-- Player died: clear all overflow buff entries and reset cap status
if CleveRoids.OverflowBuffs then
for k in pairs(CleveRoids.OverflowBuffs) do
CleveRoids.OverflowBuffs[k] = nil
end
end
if CleveRoids.AuraCapStatus then
CleveRoids.AuraCapStatus.playerBuffCapped = false
CleveRoids.AuraCapStatus.playerDebuffCapped = false
end
end
-- Clean up all-caster aura tracking for the dead unit
if CleveRoids.AllCasterAuraTracking and CleveRoids.AllCasterAuraTracking[guid] then
CleveRoids.AllCasterAuraTracking[guid] = nil
end
-- Clean up GUID to name mapping (after 5 seconds to allow final lookups)
-- Actually, keep it for a bit in case we need it for immunity detection
-- lib.guidToName[guid] = nil