From 3a3719a37a1d1b9811ef501dfbf1b9fd04f8d5ba Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Thu, 12 Feb 2026 21:19:20 -0500 Subject: [PATCH] initial overflowtest --- Conditionals.lua | 196 +++++++++- Core.lua | 68 +++- Extensions/MacroErrorUI.lua | 50 ++- Extensions/OverflowBuffFrame.lua | 620 +++++++++++++++++++++++++++++++ NampowerAPI.lua | 41 +- SuperCleveRoidMacros.toc | 1 + Utility.lua | 19 + 7 files changed, 976 insertions(+), 19 deletions(-) create mode 100644 Extensions/OverflowBuffFrame.lua diff --git a/Conditionals.lua b/Conditionals.lua index 29f37bf..7d72baa 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -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 diff --git a/Core.lua b/Core.lua index 29b6d83..34b4118 100644 --- a/Core.lua +++ b/Core.lua @@ -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") diff --git a/Extensions/MacroErrorUI.lua b/Extensions/MacroErrorUI.lua index b2304db..0d32087 100644 --- a/Extensions/MacroErrorUI.lua +++ b/Extensions/MacroErrorUI.lua @@ -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 diff --git a/Extensions/OverflowBuffFrame.lua b/Extensions/OverflowBuffFrame.lua new file mode 100644 index 0000000..c0d1e50 --- /dev/null +++ b/Extensions/OverflowBuffFrame.lua @@ -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 diff --git a/NampowerAPI.lua b/NampowerAPI.lua index 1a8aca2..e28d2f3 100644 --- a/NampowerAPI.lua +++ b/NampowerAPI.lua @@ -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 diff --git a/SuperCleveRoidMacros.toc b/SuperCleveRoidMacros.toc index 1d66ce0..afadf78 100644 --- a/SuperCleveRoidMacros.toc +++ b/SuperCleveRoidMacros.toc @@ -43,4 +43,5 @@ Extensions\Mouseover\Cursive.lua Extensions\Tooltip\Generic.lua Extensions\CursiveCustomSpells.lua +Extensions\OverflowBuffFrame.lua diff --git a/Utility.lua b/Utility.lua index 7f034e4..ef841ea 100644 --- a/Utility.lua +++ b/Utility.lua @@ -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