From 6bf27956fc013da2c1f66cc9084a88a3b9753e18 Mon Sep 17 00:00:00 2001 From: Wurmschwanz Date: Sat, 5 Sep 2026 23:39:09 +0200 Subject: [PATCH] Delete modules directory --- modules/aurarecorder.lua | 1155 ------------ modules/auras.lua | 3474 ------------------------------------ modules/castbar.lua | 386 ---- modules/classcolors.lua | 260 --- modules/combopoints.lua | 299 ---- modules/commands.lua | 592 ------ modules/debug.lua | 43 - modules/fullalpha.lua | 728 -------- modules/guiddebug.lua | 73 - modules/healthpercent.lua | 223 --- modules/immunities.lua | 969 ---------- modules/names.lua | 134 -- modules/scale.lua | 353 ---- modules/shagu_darkmode.lua | 67 - modules/spellprobe.lua | 227 --- modules/tankmode.lua | 181 -- modules/totems.lua | 368 ---- modules/unknown.lua | 498 ------ 18 files changed, 10030 deletions(-) delete mode 100644 modules/aurarecorder.lua delete mode 100644 modules/auras.lua delete mode 100644 modules/castbar.lua delete mode 100644 modules/classcolors.lua delete mode 100644 modules/combopoints.lua delete mode 100644 modules/commands.lua delete mode 100644 modules/debug.lua delete mode 100644 modules/fullalpha.lua delete mode 100644 modules/guiddebug.lua delete mode 100644 modules/healthpercent.lua delete mode 100644 modules/immunities.lua delete mode 100644 modules/names.lua delete mode 100644 modules/scale.lua delete mode 100644 modules/shagu_darkmode.lua delete mode 100644 modules/spellprobe.lua delete mode 100644 modules/tankmode.lua delete mode 100644 modules/totems.lua delete mode 100644 modules/unknown.lua diff --git a/modules/aurarecorder.lua b/modules/aurarecorder.lua deleted file mode 100644 index 4a9a1d9..0000000 --- a/modules/aurarecorder.lua +++ /dev/null @@ -1,1155 +0,0 @@ -BNP = BNP or {} - --- Missing Spell / Aura Recorder -------------------------------------------- --- --- This diagnostic is deliberately dormant until the user presses New --- Recording. While active it records only the selected target, player casts, --- SuperWoW combat events and a throttled snapshot of the selected target's --- negative auras. Closing or stopping the recorder unregisters every event and --- removes its OnUpdate handler. - -local recorder = BNP.auraRecorder or {} -BNP.auraRecorder = recorder - -recorder.lines = recorder.lines or {} -recorder.registered = recorder.registered or {} -recorder.pendingSnapshots = recorder.pendingSnapshots or {} -recorder.active = false -recorder.dirty = false -recorder.limitHit = false -recorder.charCount = recorder.charCount or 0 - -local CAPTURE_CHAR_LIMIT = 48000 -local REPORT_CHAR_LIMIT = 62000 -local CAPTURE_LINE_LIMIT = 700 -local REPORT_LINE_LIMIT = 900 -local AURA_SCAN_INTERVAL = 0.15 -local EDIT_REFRESH_INTERVAL = 0.25 - -local EVENT_NAMES = { - "UNIT_CASTEVENT", - "RAW_COMBATLOG", - "SUPERWOW_COMBAT_LOG_EVENT", - "UNIT_AURA", - "PLAYER_TARGET_CHANGED", - "UNIT_SPELLCAST_SENT", - "UNIT_SPELLCAST_START", - "UNIT_SPELLCAST_STOP", - "UNIT_SPELLCAST_DELAYED", - "UNIT_SPELLCAST_SUCCEEDED", - "UNIT_SPELLCAST_INTERRUPTED", - "UNIT_SPELLCAST_FAILED", - "UNIT_SPELLCAST_FAILED_QUIET", - "UNIT_SPELLCAST_CHANNEL_START", - "UNIT_SPELLCAST_CHANNEL_UPDATE", - "UNIT_SPELLCAST_CHANNEL_STOP", - "UNIT_SPELLCAST_RETICLE_TARGET", - "UNIT_SPELLCAST_RETICLE_CLEAR", - "CHAT_MSG_COMBAT_SELF_HITS", - "CHAT_MSG_SPELL_SELF_DAMAGE", - "CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE", - "CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE", - "CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE", - "CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE", - "CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE", - "CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE", - "CHAT_MSG_SPELL_PERIODIC_PARTY_DAMAGE", - "CHAT_MSG_SPELL_BREAK_AURA", - "CHAT_MSG_SPELL_AURA_GONE_OTHER", - "CHAT_MSG_SPELL_AURA_GONE_SELF", -} - -local SELF_COMBAT_EVENTS = { - CHAT_MSG_COMBAT_SELF_HITS = true, - CHAT_MSG_SPELL_SELF_DAMAGE = true, -} - -local CLASSICAPI_SPELLCAST_EVENTS = { - UNIT_SPELLCAST_SENT = true, - UNIT_SPELLCAST_START = true, - UNIT_SPELLCAST_STOP = true, - UNIT_SPELLCAST_DELAYED = true, - UNIT_SPELLCAST_SUCCEEDED = true, - UNIT_SPELLCAST_INTERRUPTED = true, - UNIT_SPELLCAST_FAILED = true, - UNIT_SPELLCAST_FAILED_QUIET = true, - UNIT_SPELLCAST_CHANNEL_START = true, - UNIT_SPELLCAST_CHANNEL_UPDATE = true, - UNIT_SPELLCAST_CHANNEL_STOP = true, - UNIT_SPELLCAST_RETICLE_TARGET = true, - UNIT_SPELLCAST_RETICLE_CLEAR = true, -} - -local SHADOW_VULNERABILITY_IDS = { - [17794] = true, - [17797] = true, - [17798] = true, - [17799] = true, - [17800] = true, -} - -local SHADOW_BOLT_IDS = { - [686] = true, [695] = true, [705] = true, [1088] = true, - [1106] = true, [7641] = true, [11659] = true, [11660] = true, - [11661] = true, [25307] = true, -} - -local function CleanValue(value, maxLength) - if value == nil then return "nil" end - local text = tostring(value) - text = string.gsub(text, "\r", "\\r") - text = string.gsub(text, "\n", "\\n") - text = string.gsub(text, "\t", "\\t") - maxLength = maxLength or 260 - if string.len(text) > maxLength then - text = string.sub(text, 1, maxLength) .. "..." - end - return text -end - -local function GetUnitGUID(unit) - if not unit then return nil end - local ok, exists, guid = pcall(UnitExists, unit) - if ok and exists and guid then return guid end - if UnitGUID then - local guidOK, classicGUID = pcall(UnitGUID, unit) - if guidOK and classicGUID then return classicGUID end - end - return nil -end - -local function SpellLabel(spellID) - if not spellID then return "nil/?" end - if SpellInfo then - local ok, name, rank = pcall(SpellInfo, spellID) - if ok and name then - local label = tostring(spellID) .. "/" .. CleanValue(name, 100) - if rank and rank ~= "" then label = label .. " " .. CleanValue(rank, 60) end - return label - end - end - if GetSpellInfo then - local ok, name, rank = pcall(GetSpellInfo, spellID) - if ok and name then - local label = tostring(spellID) .. "/" .. CleanValue(name, 100) - if rank and rank ~= "" then label = label .. " " .. CleanValue(rank, 60) end - return label - end - end - return tostring(spellID) .. "/?" -end - -local function RelativeTime() - if not recorder.startedAt then return "+0.000" end - return string.format("+%.3f", GetTime() - recorder.startedAt) -end - -local function ClassicAPIVersionLabel() - if CLASSIC_API_VERSION == nil then return "not detected" end - local numeric = tonumber(CLASSIC_API_VERSION) - if not numeric then return CleanValue(CLASSIC_API_VERSION) end - local major = math.floor(numeric / 10000) - local minor = math.floor((numeric - (major * 10000)) / 100) - local patch = numeric - (major * 10000) - (minor * 100) - return tostring(numeric) .. " (" .. tostring(major) .. "." .. tostring(minor) .. "." .. tostring(patch) .. ")" -end - -local function AppendLine(text, important) - text = CleanValue(text, 1400) - local charLimit = important and REPORT_CHAR_LIMIT or CAPTURE_CHAR_LIMIT - local lineLimit = important and REPORT_LINE_LIMIT or CAPTURE_LINE_LIMIT - local nextChars = (recorder.charCount or 0) + string.len(text) + 1 - - if nextChars > charLimit or table.getn(recorder.lines) >= lineLimit then - if not recorder.limitHit then - local marker = "[CAPTURE LIMIT REACHED - recording stopped before the report became too large]" - table.insert(recorder.lines, marker) - recorder.charCount = (recorder.charCount or 0) + string.len(marker) + 1 - recorder.limitHit = true - recorder.stopRequested = true - recorder.dirty = true - end - return false - end - - table.insert(recorder.lines, text) - recorder.charCount = nextChars - recorder.dirty = true - return true -end - -local function AppendTimed(kind, text) - return AppendLine("[" .. RelativeTime() .. "] " .. tostring(kind) .. " | " .. tostring(text or ""), false) -end - -local function ShallowFields(data) - if type(data) ~= "table" then return CleanValue(data) end - local fields = {} - local key, value - for key, value in pairs(data) do - local valueType = type(value) - if valueType ~= "table" and valueType ~= "function" and valueType ~= "userdata" then - table.insert(fields, CleanValue(key, 80) .. "=" .. CleanValue(value, 220)) - end - end - table.sort(fields) - if table.getn(fields) == 0 then return "(no scalar fields)" end - return table.concat(fields, " | ") -end - -local function IsRelatedText(raw) - if not raw or raw == "" then return false end - raw = tostring(raw) - - if recorder.traceGUID and string.find(raw, tostring(recorder.traceGUID), 1, true) then return true end - if recorder.playerGUID and string.find(raw, tostring(recorder.playerGUID), 1, true) then return true end - if recorder.traceName and recorder.traceName ~= "" and string.find(raw, recorder.traceName, 1, true) then return true end - if recorder.playerName and recorder.playerName ~= "" and string.find(raw, recorder.playerName, 1, true) then return true end - return false -end - -local function CurrentSnapshotUnit() - local targetGUID = GetUnitGUID("target") - if recorder.traceGUID and targetGUID == recorder.traceGUID then return "target" end - if recorder.traceGUID and UnitTokenFromGUID then - local ok, token = pcall(UnitTokenFromGUID, recorder.traceGUID) - if ok and token then return token end - end - if recorder.traceGUID then return recorder.traceGUID end - if UnitName("target") then return "target" end - return nil -end - -local function QueueSnapshot(delay, reason, force) - local pending = recorder.pendingSnapshots - if table.getn(pending) >= 18 then return end - table.insert(pending, { - at = GetTime() + (delay or 0), - reason = reason or "event", - force = force and true or false, - }) -end - -local function HasClassicAuraAPI() - return type(C_UnitAuras) == "table" and type(C_UnitAuras.GetDebuffDataByIndex) == "function" -end - -local function IsShadowVulnerability(spellID, spellName) - local numeric = tonumber(spellID) - if SHADOW_VULNERABILITY_IDS[numeric] then return true end - local name = string.lower(tostring(spellName or "")) - return name == "shadow vulnerability" or name == "schattenverwundbarkeit" -end - -local function IsShadowBolt(spellID, spellName) - local numeric = tonumber(spellID) - if SHADOW_BOLT_IDS[numeric] then return true end - local name = spellName - if (not name or name == "") and numeric and SpellInfo then - local ok, resolved = pcall(SpellInfo, numeric) - if ok then name = resolved end - end - name = string.lower(tostring(name or "")) - return name == "shadow bolt" or name == "schattenblitz" -end - -local function ShouldCaptureClassicAura(aura) - if type(aura) ~= "table" then return true end - local spellID = tonumber(aura.spellId or aura.spellID) - if IsShadowVulnerability(spellID, aura.name) then return true end - - if aura.sourceGUID and recorder.playerGUID then return aura.sourceGUID == recorder.playerGUID end - if aura.sourceUnit then return aura.sourceUnit == "player" end - -- ClassicAPI may omit source data for foreign auras. Unknown ownership is not - -- enough to include a normal aura; Shadow Vulnerability was allowed above. - return false -end - -local function SnapshotClassicAPIAuras(unit, reason, force) - if not HasClassicAuraAPI() then return end - - local signatureParts = {} - local auraLines = {} - local count = 0 - local shadowOnly = force and string.find(tostring(reason or ""), "Shadow", 1, true) - local i - for i = 1, 64 do - local ok, aura = pcall(C_UnitAuras.GetDebuffDataByIndex, unit, i) - if not ok then - if force then - AppendTimed( - "CLASSICAPI_AURA_SNAPSHOT", - "reason=" .. CleanValue(reason) .. " | unit=" .. CleanValue(unit) .. - " | error=" .. CleanValue(aura) - ) - end - return - end - if not aura then break end - - if ShouldCaptureClassicAura(aura) and (not shadowOnly or IsShadowVulnerability(aura.spellId or aura.spellID, aura.name)) then - count = count + 1 - local fields = ShallowFields(aura) - table.insert(signatureParts, fields) - - local remaining = "unknown" - local expiration = tonumber(aura.expirationTime) - if expiration and expiration > 0 then - remaining = string.format("%.3f", expiration - GetTime()) - end - table.insert( - auraLines, - " CLASSIC_AURA " .. tostring(i) .. - " | remaining=" .. remaining .. - " | " .. fields - ) - end - end - - local signature = table.concat(signatureParts, "||") - local snapshotKey = (shadowOnly and "classic-shadow:" or "classic:") .. tostring(recorder.traceGUID or unit) - local previous = recorder.lastAuraSignatures and recorder.lastAuraSignatures[snapshotKey] - local changed = previous ~= signature - - recorder.lastAuraSignatures = recorder.lastAuraSignatures or {} - recorder.lastAuraSignatures[snapshotKey] = signature - if recorder.baselineOnly then return end - if not changed and not force then return end - - AppendTimed( - "CLASSICAPI_AURA_SNAPSHOT", - "reason=" .. CleanValue(reason) .. - " | unit=" .. CleanValue(unit) .. - " | guid=" .. CleanValue(recorder.traceGUID) .. - " | count=" .. tostring(count) .. - " | changed=" .. (changed and "1" or "0") - ) - if count == 0 then - AppendLine(" CLASSIC_AURA none", false) - else - for i = 1, table.getn(auraLines) do AppendLine(auraLines[i], false) end - end -end - -local function SnapshotAuras(reason, force) - local unit = CurrentSnapshotUnit() - if not unit then - if force then AppendTimed("AURA_SNAPSHOT", "reason=" .. CleanValue(reason) .. " | no target unit") end - return - end - - -- ClassicAPI exposes a richer AuraData table including spellId, duration, - -- expirationTime and (where observed) sourceUnit/sourceGUID. Keep this - -- snapshot independent from legacy UnitDebuff so a duration-only refresh is - -- visible even when the vanilla aura slots themselves do not change. - SnapshotClassicAPIAuras(unit, reason, force) - if HasClassicAuraAPI() then return end - - local signatureParts = {} - local auraLines = {} - local count = 0 - local i - for i = 1, 64 do - local ok, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 = pcall(UnitDebuff, unit, i) - if not ok then - if force then - AppendTimed("AURA_SNAPSHOT", "reason=" .. CleanValue(reason) .. " | UnitDebuff error=" .. CleanValue(a1)) - end - return - end - if not a1 then break end - - count = count + 1 - local spellName = "?" - local spellRank = nil - if a4 and SpellInfo then - local spellOK, name, rank = pcall(SpellInfo, a4) - if spellOK and name then spellName = name end - if spellOK then spellRank = rank end - end - - table.insert(signatureParts, - CleanValue(a1) .. ":" .. CleanValue(a2) .. ":" .. CleanValue(a3) .. ":" .. - CleanValue(a4) .. ":" .. CleanValue(a5) .. ":" .. CleanValue(a6) .. ":" .. - CleanValue(a7) .. ":" .. CleanValue(a8) .. ":" .. CleanValue(a9) .. ":" .. CleanValue(a10) - ) - - table.insert(auraLines, - " AURA " .. tostring(i) .. - " | id=" .. CleanValue(a4) .. - " | name=" .. CleanValue(spellName, 120) .. - " | rank=" .. CleanValue(spellRank, 80) .. - " | stacks=" .. CleanValue(a2) .. - " | type=" .. CleanValue(a3) .. - " | texture=" .. CleanValue(a1, 180) .. - " | r5=" .. CleanValue(a5) .. - " | r6=" .. CleanValue(a6) .. - " | r7=" .. CleanValue(a7) .. - " | r8=" .. CleanValue(a8) .. - " | r9=" .. CleanValue(a9) .. - " | r10=" .. CleanValue(a10) - ) - end - - local signature = table.concat(signatureParts, "||") - local snapshotKey = tostring(recorder.traceGUID or unit) - local previous = recorder.lastAuraSignatures and recorder.lastAuraSignatures[snapshotKey] - local changed = previous ~= signature - - recorder.lastAuraSignatures = recorder.lastAuraSignatures or {} - recorder.lastAuraSignatures[snapshotKey] = signature - - if recorder.baselineOnly then return end - - if not changed and not force then return end - - AppendTimed( - "AURA_SNAPSHOT", - "reason=" .. CleanValue(reason) .. - " | unit=" .. CleanValue(unit) .. - " | guid=" .. CleanValue(recorder.traceGUID) .. - " | count=" .. tostring(count) .. - " | changed=" .. (changed and "1" or "0") - ) - - if count == 0 then - AppendLine(" AURA none", false) - else - for i = 1, table.getn(auraLines) do AppendLine(auraLines[i], false) end - end -end - -local function UpdateTarget(reason) - local exists, guid = UnitExists("target") - local name = UnitName("target") - if exists and guid then - local changed = guid ~= recorder.traceGUID - recorder.traceGUID = guid - recorder.traceName = name or "?" - if changed then - AppendTimed( - "TARGET", - "reason=" .. CleanValue(reason) .. " | name=" .. CleanValue(recorder.traceName) .. " | guid=" .. CleanValue(guid) - ) - QueueSnapshot(0.03, "target changed", false) - end - elseif not recorder.traceGUID then - AppendTimed("TARGET", "reason=" .. CleanValue(reason) .. " | no target / no SuperWoW GUID") - else - AppendTimed("TARGET", "reason=" .. CleanValue(reason) .. " | target cleared; keeping guid=" .. CleanValue(recorder.traceGUID)) - end -end - -local function IsRelevantCast(casterGUID, targetGUID) - if recorder.playerGUID and casterGUID == recorder.playerGUID then return true end - return false -end - -local function HandleCastEvent() - local casterGUID = arg1 - local targetGUID = arg2 - local eventType = arg3 - local spellID = arg4 - local duration = arg5 - if not IsRelevantCast(casterGUID, targetGUID) then return end - - AppendTimed( - "UNIT_CASTEVENT", - "type=" .. CleanValue(eventType) .. - " | spell=" .. SpellLabel(spellID) .. - " | caster=" .. CleanValue(casterGUID) .. - " | target=" .. CleanValue(targetGUID) .. - " | castDuration=" .. CleanValue(duration) - ) - - if IsShadowBolt(spellID) then - QueueSnapshot(0.20, "200ms after Shadow Bolt UNIT_CASTEVENT", true) - end -end - -local function HandleRawCombatLog() - local originalEvent = arg1 - local raw = arg2 - if not IsRelatedText(raw) then return end - local lowerRaw = string.lower(tostring(raw or "")) - local ownSignal = false - if recorder.playerGUID and string.find(lowerRaw, string.lower(tostring(recorder.playerGUID)), 1, true) then ownSignal = true end - if recorder.playerName and string.find(lowerRaw, string.lower(tostring(recorder.playerName)), 1, true) then ownSignal = true end - local sharedSignal = string.find(lowerRaw, "shadow vulnerability", 1, true) or - string.find(lowerRaw, "schattenverwundbarkeit", 1, true) or - string.find(lowerRaw, "17794", 1, true) or string.find(lowerRaw, "17797", 1, true) or - string.find(lowerRaw, "17798", 1, true) or string.find(lowerRaw, "17799", 1, true) or - string.find(lowerRaw, "17800", 1, true) - if not ownSignal and not sharedSignal then return end - AppendTimed("RAW_COMBATLOG", "event=" .. CleanValue(originalEvent) .. " | raw=" .. CleanValue(raw, 1100)) - - if sharedSignal then - QueueSnapshot(0.04, "after Shadow Vulnerability RAW_COMBATLOG", true) - QueueSnapshot(0.30, "300ms after Shadow Vulnerability RAW_COMBATLOG", true) - end -end - -local function HandleStructuredCombatLog() - local timestamp = arg1 - local eventType = arg2 - local sourceGUID = arg3 - local sourceName = arg4 - local sourceFlags = arg5 - local destGUID = arg6 - local destName = arg7 - local destFlags = arg8 - local spellID = arg9 - local spellName = arg10 - local spellSchool = arg11 - - local relevant = recorder.playerGUID and sourceGUID == recorder.playerGUID - if recorder.traceGUID and destGUID == recorder.traceGUID and IsShadowVulnerability(spellID, spellName) then relevant = true end - if not relevant then return end - - AppendTimed( - "SUPERWOW_COMBAT_LOG_EVENT", - "timestamp=" .. CleanValue(timestamp) .. - " | event=" .. CleanValue(eventType) .. - " | source=" .. CleanValue(sourceGUID) .. "/" .. CleanValue(sourceName, 100) .. "/" .. CleanValue(sourceFlags) .. - " | dest=" .. CleanValue(destGUID) .. "/" .. CleanValue(destName, 100) .. "/" .. CleanValue(destFlags) .. - " | spell=" .. CleanValue(spellID) .. "/" .. CleanValue(spellName, 120) .. - " | school=" .. CleanValue(spellSchool) .. - " | p12=" .. CleanValue(arg12) .. " | p13=" .. CleanValue(arg13) .. - " | p14=" .. CleanValue(arg14) .. " | p15=" .. CleanValue(arg15) .. - " | p16=" .. CleanValue(arg16) .. " | p17=" .. CleanValue(arg17) .. - " | p18=" .. CleanValue(arg18) .. " | p19=" .. CleanValue(arg19) .. - " | p20=" .. CleanValue(arg20) - ) - - if IsShadowVulnerability(spellID, spellName) then - QueueSnapshot(0.03, "after structured Shadow Vulnerability", true) - QueueSnapshot(0.25, "250ms after structured Shadow Vulnerability", true) - end -end - -local function HandleUnitAura() - local unit = arg1 - if unit ~= "target" then return end - local guid = GetUnitGUID(unit) - AppendTimed("UNIT_AURA", "unit=" .. CleanValue(unit) .. " | guid=" .. CleanValue(guid)) - QueueSnapshot(0.02, "after UNIT_AURA", false) -end - -local function HandleClassicAPISpellcast(eventName) - local unit = arg1 - local targetName = nil - local castGUID, spellID, spellName, rank - - if eventName == "UNIT_SPELLCAST_SENT" then - targetName = arg2 - castGUID = arg3 - spellID = arg4 - spellName = arg5 - rank = arg6 - else - castGUID = arg2 - spellID = arg3 - spellName = arg4 - rank = arg5 - end - - local unitGUID = GetUnitGUID(unit) - local relevant = unit == "player" - if recorder.traceGUID and (unitGUID == recorder.traceGUID or unit == recorder.traceGUID) then relevant = true end - if targetName and recorder.traceName and targetName == recorder.traceName then relevant = true end - if not relevant then return end - - AppendTimed( - "CLASSICAPI_" .. tostring(eventName), - "unit=" .. CleanValue(unit) .. - " | unitGUID=" .. CleanValue(unitGUID) .. - " | castGUID=" .. CleanValue(castGUID) .. - " | spell=" .. CleanValue(spellID) .. "/" .. CleanValue(spellName, 120) .. - " | rank=" .. CleanValue(rank, 80) .. - " | targetName=" .. CleanValue(targetName, 120) - ) - - -- UNIT_CASTEVENT supplies the single post-cast control snapshot. Keeping the - -- modern spellcast events as text avoids three duplicate aura dumps per cast. -end - -local function HandleCombatText(eventName) - local raw = arg1 - if not SELF_COMBAT_EVENTS[eventName] then - if not IsRelatedText(raw) then return end - local lower = string.lower(tostring(raw or "")) - local ownSignal = string.find(lower, "your ", 1, true) - if recorder.playerName and string.find(lower, string.lower(tostring(recorder.playerName)), 1, true) then ownSignal = true end - local sharedSignal = string.find(lower, "shadow vulnerability", 1, true) or - string.find(lower, "schattenverwundbarkeit", 1, true) - if not ownSignal and not sharedSignal then return end - end - AppendTimed( - eventName, - "a1=" .. CleanValue(raw, 1050) .. - " | a2=" .. CleanValue(arg2, 160) .. - " | a3=" .. CleanValue(arg3, 160) .. - " | a4=" .. CleanValue(arg4, 160) - ) -end - -local eventFrame = recorder.eventFrame or CreateFrame("Frame", "BNPAuraRecorderEventFrame") -recorder.eventFrame = eventFrame -eventFrame:SetScript("OnEvent", function() - if not recorder.active then return end - local eventName = event - - if eventName == "UNIT_CASTEVENT" then - HandleCastEvent() - elseif eventName == "RAW_COMBATLOG" then - HandleRawCombatLog() - elseif eventName == "SUPERWOW_COMBAT_LOG_EVENT" then - HandleStructuredCombatLog() - elseif eventName == "UNIT_AURA" then - HandleUnitAura() - elseif eventName == "PLAYER_TARGET_CHANGED" then - UpdateTarget("PLAYER_TARGET_CHANGED") - elseif CLASSICAPI_SPELLCAST_EVENTS[eventName] then - HandleClassicAPISpellcast(eventName) - else - HandleCombatText(eventName) - end -end) - -local function SafeRegister(eventName) - if type(C_EventUtils) == "table" and type(C_EventUtils.IsEventValid) == "function" then - local validOK, valid = pcall(C_EventUtils.IsEventValid, eventName) - if validOK and not valid then return false end - end - local ok = pcall(eventFrame.RegisterEvent, eventFrame, eventName) - if ok then recorder.registered[eventName] = true end - return ok -end - -local function UnregisterRecorderEvents() - pcall(eventFrame.UnregisterAllEvents, eventFrame) - recorder.registered = {} -end - -local function StatusText() - local target = tostring(recorder.traceName or "no target") - local guid = tostring(recorder.traceGUID or "no GUID") - local lineCount = table.getn(recorder.lines) - if recorder.copyNotice then return recorder.copyNotice end - if recorder.active then - return "RECORDING | Target: " .. target .. " | " .. guid .. " | " .. tostring(lineCount) .. " lines" - end - if recorder.limitHit then - return "STOPPED (size limit) | " .. tostring(lineCount) .. " lines | Copy the report" - end - return "STOPPED | Target: " .. target .. " | " .. tostring(lineCount) .. " lines | Copy the report" -end - -local function RefreshRecorderText(force) - if not recorder.frame or not recorder.editBox then return end - if not force and not recorder.dirty then return end - - local report = table.concat(recorder.lines, "\n") - recorder.editBox:SetText(report) - - local visualLines = 0 - local i - for i = 1, table.getn(recorder.lines) do - local length = string.len(recorder.lines[i] or "") - visualLines = visualLines + math.max(1, math.ceil(length / 96)) - end - recorder.editBox:SetHeight(math.max(330, (visualLines * 13) + 24)) - if recorder.scrollFrame and recorder.scrollFrame.UpdateScrollChildRect then - recorder.scrollFrame:UpdateScrollChildRect() - end - - if recorder.status then - recorder.status:SetText(StatusText()) - if recorder.copyNotice then - recorder.status:SetTextColor(0.2, 1, 0.35) - elseif recorder.active then - recorder.status:SetTextColor(0.2, 1, 0.35) - elseif recorder.limitHit then - recorder.status:SetTextColor(1, 0.35, 0.2) - else - recorder.status:SetTextColor(1, 0.82, 0) - end - end - - if recorder.stopButton then - if recorder.active then recorder.stopButton:Enable() else recorder.stopButton:Disable() end - end - recorder.dirty = false -end - -local function DumpScalarTable(label, data, important) - if type(data) ~= "table" then - AppendLine(label .. " | " .. CleanValue(data), important) - return - end - AppendLine(label .. " | " .. ShallowFields(data), important) -end - -local function DumpInternalDiagnostics() - AppendLine("", true) - AppendLine("=== INTERNAL BNP DIAGNOSTICS ===", true) - AppendLine("Target | name=" .. CleanValue(recorder.traceName) .. " | guid=" .. CleanValue(recorder.traceGUID), true) - - local guid = recorder.traceGUID - local cache = guid and BNP.guidAuras and BNP.guidAuras[guid] - AppendLine("--- GUID AURA CACHE ---", true) - if not cache then - AppendLine("CACHE | empty", true) - else - local cacheKey, aura - local cacheCount = 0 - for cacheKey, aura in pairs(cache) do - if type(aura) == "table" then - cacheCount = cacheCount + 1 - AppendLine("CACHE | key=" .. CleanValue(cacheKey) .. " | " .. ShallowFields(aura), true) - end - end - if cacheCount == 0 then AppendLine("CACHE | no aura tables | " .. ShallowFields(cache), true) end - end - - AppendLine("--- SHADOW VULNERABILITY STATE ---", true) - local svState = guid and BNP.svStateByGUID and BNP.svStateByGUID[guid] - if svState then DumpScalarTable("SV_STATE", svState, true) else AppendLine("SV_STATE | none", true) end - - local rendered = false - local plate - for plate in pairs(BNP.plates or {}) do - if plate and plate:IsShown() and plate.GetName and plate:GetName(1) == guid then - local container = plate.BNPAuraContainer - if container and container.icons then - local iconIndex - for iconIndex = 1, table.getn(container.icons) do - local icon = container.icons[iconIndex] - if icon and icon.BNPSVRenderedExpires then - AppendLine( - "SV_RENDER | icon=" .. tostring(iconIndex) .. - " | timerText=" .. CleanValue(icon.timer and icon.timer:GetText()) .. - " | renderedExpires=" .. CleanValue(icon.BNPSVRenderedExpires) .. - " | revision=" .. CleanValue(icon.BNPSVRenderRevision), - true - ) - rendered = true - end - end - end - end - end - if not rendered then AppendLine("SV_RENDER | no visible SV icon", true) end - - AppendLine("--- PENDING AURAS ---", true) - local pending = guid and BNP.pendingAuras and BNP.pendingAuras[guid] - if not pending then - AppendLine("PENDING | none", true) - else - local pendingKey, pendingAura - for pendingKey, pendingAura in pairs(pending) do - AppendLine("PENDING | key=" .. CleanValue(pendingKey) .. " | " .. ShallowFields(pendingAura), true) - end - end - - AppendLine("--- LAST TRACKED AURA TRANSITIONS ---", true) - local trace = BNP.raidTrace or {} - local start = math.max(1, table.getn(trace) - 29) - local i - if table.getn(trace) == 0 then AppendLine("RAID_TRACE | none", true) end - for i = start, table.getn(trace) do - local traceEntry = trace[i] - AppendLine("RAID_TRACE " .. tostring(i) .. " | " .. ShallowFields(traceEntry), true) - end - - AppendLine("--- UNKNOWN SPELL / AURA COLLECTOR ---", true) - local unknown = BNP.unknownBeta - if not unknown or table.getn(unknown.order or {}) == 0 then - AppendLine("UNKNOWN | no indexed entries", true) - else - for i = 1, table.getn(unknown.order) do - local unknownKey = unknown.order[i] - local unknownEntry = unknown.entries and unknown.entries[unknownKey] - if unknownEntry then - AppendLine("UNKNOWN | key=" .. CleanValue(unknownKey) .. " | " .. ShallowFields(unknownEntry), true) - end - end - end - if unknown and unknown.combat then - local effectKey, effect - for effectKey, effect in pairs(unknown.combat) do - AppendLine("UNKNOWN_EFFECT | key=" .. CleanValue(effectKey, 220) .. " | " .. ShallowFields(effect), true) - end - end - - AppendLine("--- DIRECT PLAYER CAST EVENTS ---", true) - local directCount = 0 - local directKey, directEntry - for directKey, directEntry in pairs(BNP.unknownDirectEvents or {}) do - directCount = directCount + 1 - AppendLine("DIRECT | key=" .. CleanValue(directKey) .. " | " .. ShallowFields(directEntry), true) - end - if directCount == 0 then AppendLine("DIRECT | none", true) end - - AppendLine("--- SPELLDB SHADOW AUDIT ---", true) - local spellDB = BNP.SpellDBShadow - if not spellDB then - AppendLine("SPELLDB | unavailable", true) - else - DumpScalarTable("SPELLDB_STATS", spellDB.stats, true) - local recent = spellDB.recent or {} - for i = 1, table.getn(recent) do - AppendLine("SPELLDB_RECENT " .. tostring(i) .. " | " .. ShallowFields(recent[i]), true) - end - end - - AppendLine("--- SHADOW VULNERABILITY PROBE ---", true) - local probe = BNP.svProbe - if not probe then - AppendLine("SV_PROBE | unavailable", true) - else - if probe.last then DumpScalarTable("SV_PROBE_LAST", probe.last, true) else AppendLine("SV_PROBE_LAST | none", true) end - local probeEvents = probe.events or {} - for i = 1, table.getn(probeEvents) do - AppendLine("SV_PROBE_EVENT " .. tostring(i) .. " | " .. ShallowFields(probeEvents[i]), true) - end - local rawEvents = probe.raw or {} - for i = 1, table.getn(rawEvents) do - AppendLine("SV_PROBE_RAW " .. tostring(i) .. " | " .. ShallowFields(rawEvents[i]), true) - end - end -end - -local updateFrame = recorder.updateFrame or CreateFrame("Frame", "BNPAuraRecorderUpdateFrame") -recorder.updateFrame = updateFrame - -local function RecorderOnUpdate() - recorder.scanElapsed = (recorder.scanElapsed or 0) + arg1 - recorder.editElapsed = (recorder.editElapsed or 0) + arg1 - - if recorder.scanElapsed >= AURA_SCAN_INTERVAL then - recorder.scanElapsed = 0 - SnapshotAuras("poll change", false) - - local now = GetTime() - local i = 1 - while i <= table.getn(recorder.pendingSnapshots) do - local pending = recorder.pendingSnapshots[i] - if pending and now >= (pending.at or 0) then - table.remove(recorder.pendingSnapshots, i) - SnapshotAuras(pending.reason, pending.force) - else - i = i + 1 - end - end - end - - if recorder.editElapsed >= EDIT_REFRESH_INTERVAL then - recorder.editElapsed = 0 - RefreshRecorderText(false) - end - - if recorder.stopRequested then - BNP:StopAuraRecorder("capture limit") - end -end - -function BNP:StartAuraRecorder() - -- New Recording is also a clean restart when a capture is already running. - if recorder.active then - recorder.active = false - UnregisterRecorderEvents() - updateFrame:SetScript("OnUpdate", nil) - end - - UnregisterRecorderEvents() - recorder.lines = {} - recorder.charCount = 0 - recorder.limitHit = false - recorder.stopRequested = false - recorder.copyNotice = nil - recorder.pendingSnapshots = {} - recorder.lastAuraSignatures = {} - recorder.startedAt = GetTime() - recorder.stoppedAt = nil - recorder.scanElapsed = 0 - recorder.editElapsed = 0 - recorder.playerGUID = GetUnitGUID("player") - recorder.playerName = UnitName("player") or "?" - recorder.traceGUID = nil - recorder.traceName = nil - recorder.active = true - - local registeredCount = 0 - local i - for i = 1, table.getn(EVENT_NAMES) do - if SafeRegister(EVENT_NAMES[i]) then registeredCount = registeredCount + 1 end - end - local classicSpellcastCount = 0 - local classicEventName - for classicEventName in pairs(CLASSICAPI_SPELLCAST_EVENTS) do - if recorder.registered[classicEventName] then classicSpellcastCount = classicSpellcastCount + 1 end - end - - AppendLine("BNP_MISSING_SPELL_AURA_REPORT v1", true) - AppendLine("Addon | version=" .. CleanValue(BNP.version), true) - AppendLine( - "SuperWoW | string=" .. CleanValue(SUPERWOW_STRING) .. - " | version=" .. CleanValue(SUPERWOW_VERSION) .. - " | CombatLogAdd=" .. (CombatLogAdd and "yes" or "no") .. - " | SpellInfo=" .. (SpellInfo and "yes" or "no"), - true - ) - AppendLine( - "ClassicAPI | version=" .. ClassicAPIVersionLabel() .. - " | C_UnitAuras=" .. (HasClassicAuraAPI() and "yes" or "no") .. - " | UnitGUID=" .. (UnitGUID and "yes" or "no") .. - " | UnitTokenFromGUID=" .. (UnitTokenFromGUID and "yes" or "no") .. - " | CopyToClipboard=" .. (CopyToClipboard and "yes" or "no") .. - " | spellcastEvents=" .. tostring(classicSpellcastCount), - true - ) - if GetBuildInfo then - local version, build, buildDate, interfaceVersion = GetBuildInfo() - AppendLine( - "Client | version=" .. CleanValue(version) .. " | build=" .. CleanValue(build) .. - " | date=" .. CleanValue(buildDate) .. " | interface=" .. CleanValue(interfaceVersion) .. - " | locale=" .. CleanValue(GetLocale and GetLocale()), - true - ) - end - local _, class = UnitClass("player") - AppendLine( - "Player | name=" .. CleanValue(recorder.playerName) .. " | class=" .. CleanValue(class) .. - " | guid=" .. CleanValue(recorder.playerGUID), - true - ) - AppendLine( - "Sources | registered=" .. tostring(registeredCount) .. "/" .. tostring(table.getn(EVENT_NAMES)) .. - " | RAW_COMBATLOG=" .. (recorder.registered.RAW_COMBATLOG and "yes" or "no") .. - " | STRUCTURED=" .. (recorder.registered.SUPERWOW_COMBAT_LOG_EVENT and "yes" or "no") .. - " | CLASSICAPI_AURAS=" .. (HasClassicAuraAPI() and "yes" or "no"), - true - ) - AppendLine("Aura filter | own auras + Shadow Vulnerability from any Warlock", true) - AppendLine("Test | apply aura, refresh while active, then let it expire or remove it", true) - AppendLine("=== LIVE RECORDING ===", true) - - UpdateTarget("recording started") - -- Establish a silent baseline. Existing foreign auras must not appear just - -- because the user pressed Start Recording; only later changes are logged. - recorder.baselineOnly = true - SnapshotAuras("baseline", false) - recorder.baselineOnly = false - - updateFrame:SetScript("OnUpdate", RecorderOnUpdate) - recorder.dirty = true - RefreshRecorderText(true) -end - -function BNP:StopAuraRecorder(reason) - if not recorder.active then - RefreshRecorderText(true) - return - end - - recorder.stopRequested = false - SnapshotAuras("final before stop", true) - recorder.active = false - recorder.stoppedAt = GetTime() - UnregisterRecorderEvents() - updateFrame:SetScript("OnUpdate", nil) - DumpInternalDiagnostics() - AppendLine("=== END REPORT | reason=" .. CleanValue(reason or "user") .. " | elapsed=" .. - string.format("%.3f", recorder.stoppedAt - (recorder.startedAt or recorder.stoppedAt)) .. "s ===", true) - recorder.dirty = true - RefreshRecorderText(true) -end - -function BNP:ClearAuraRecorder() - if recorder.active then self:StopAuraRecorder("cleared") end - recorder.lines = {} - recorder.charCount = 0 - recorder.limitHit = false - recorder.stopRequested = false - recorder.copyNotice = nil - recorder.pendingSnapshots = {} - recorder.lastAuraSignatures = {} - recorder.traceGUID = nil - recorder.traceName = nil - recorder.dirty = true - RefreshRecorderText(true) -end - -function BNP:SelectAllAuraRecorderText() - if not recorder.editBox then return end - RefreshRecorderText(true) - recorder.editBox:SetFocus() - recorder.editBox:HighlightText() -end - -function BNP:CopyAuraRecorderText() - RefreshRecorderText(true) - local report = table.concat(recorder.lines, "\n") - - if CopyToClipboard then - local ok = pcall(CopyToClipboard, report) - if ok then - recorder.copyNotice = "COPIED TO CLIPBOARD | Paste the report with Ctrl+V" - recorder.dirty = true - RefreshRecorderText(true) - return true - end - end - - -- Older ClassicAPI builds and plain 1.12 clients have no direct clipboard - -- function. Keep the normal edit-box path as a complete fallback. - self:SelectAllAuraRecorderText() - recorder.copyNotice = "TEXT SELECTED | Press Ctrl+C, then paste the report" - recorder.dirty = true - RefreshRecorderText(true) - recorder.editBox:SetFocus() - recorder.editBox:HighlightText() - return false -end - -function BNP:CreateAuraRecorderWindow() - if recorder.frame then return recorder.frame end - - local frame = CreateFrame("Frame", "BNPAuraRecorderFrame", UIParent) - frame:SetWidth(760) - frame:SetHeight(560) - frame:SetPoint("CENTER", UIParent, "CENTER", 0, 20) - frame:SetFrameStrata("DIALOG") - frame:SetBackdrop({ - bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", - edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", - tile = true, tileSize = 32, edgeSize = 32, - insets = { left = 11, right = 12, top = 12, bottom = 11 } - }) - frame:SetMovable(true) - frame:EnableMouse(true) - frame:RegisterForDrag("LeftButton") - frame:SetScript("OnDragStart", function() this:StartMoving() end) - frame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end) - frame:SetScript("OnHide", function() - if recorder.active then BNP:StopAuraRecorder("window closed") end - end) - frame:Hide() - recorder.frame = frame - - local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") - title:SetPoint("TOP", frame, "TOP", 0, -18) - title:SetText("Missing Spell / Aura Recorder") - - local instructions = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") - instructions:SetPoint("TOPLEFT", frame, "TOPLEFT", 26, -46) - instructions:SetWidth(706) - instructions:SetJustifyH("LEFT") - if CopyToClipboard then - instructions:SetText("1. Select the affected unit. 2. Start Recording. 3. Apply the aura, refresh it while active, then let it expire or remove it. 4. Stop. 5. Copy Report and paste it.") - else - instructions:SetText("1. Select the affected unit. 2. Start Recording. 3. Apply the aura, refresh it while active, then let it expire or remove it. 4. Stop. 5. Select All, press Ctrl+C and paste it.") - end - - local status = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") - status:SetPoint("TOPLEFT", frame, "TOPLEFT", 26, -86) - status:SetWidth(706) - status:SetJustifyH("LEFT") - recorder.status = status - - local startButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") - startButton:SetPoint("TOPLEFT", frame, "TOPLEFT", 26, -112) - startButton:SetWidth(126) - startButton:SetHeight(24) - startButton:SetText("Start Recording") - startButton:SetScript("OnClick", function() BNP:StartAuraRecorder() end) - recorder.startButton = startButton - - local stopButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") - stopButton:SetPoint("LEFT", startButton, "RIGHT", 8, 0) - stopButton:SetWidth(82) - stopButton:SetHeight(24) - stopButton:SetText("Stop") - stopButton:SetScript("OnClick", function() BNP:StopAuraRecorder("user") end) - recorder.stopButton = stopButton - - local clearButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") - clearButton:SetPoint("LEFT", stopButton, "RIGHT", 8, 0) - clearButton:SetWidth(82) - clearButton:SetHeight(24) - clearButton:SetText("Clear") - clearButton:SetScript("OnClick", function() BNP:ClearAuraRecorder() end) - - local selectButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate") - selectButton:SetPoint("LEFT", clearButton, "RIGHT", 8, 0) - selectButton:SetWidth(108) - selectButton:SetHeight(24) - selectButton:SetText(CopyToClipboard and "Copy Report" or "Select All") - selectButton:SetScript("OnClick", function() BNP:CopyAuraRecorderText() end) - - local copyHint = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") - copyHint:SetPoint("LEFT", selectButton, "RIGHT", 10, 0) - copyHint:SetText(CopyToClipboard and "then paste" or "then Ctrl+C") - - local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton") - close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -5, -5) - - local textBG = CreateFrame("Frame", nil, frame) - textBG:SetPoint("TOPLEFT", frame, "TOPLEFT", 24, -146) - textBG:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -24, 48) - textBG:SetBackdrop({ - bgFile = "Interface\\Buttons\\WHITE8X8", - edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", - tile = true, tileSize = 16, edgeSize = 12, - insets = { left = 3, right = 3, top = 3, bottom = 3 } - }) - textBG:SetBackdropColor(0.015, 0.015, 0.015, 0.95) - textBG:SetBackdropBorderColor(0.35, 0.35, 0.35, 1) - - local scroll = CreateFrame("ScrollFrame", "BNPAuraRecorderScrollFrame", textBG, "UIPanelScrollFrameTemplate") - scroll:SetPoint("TOPLEFT", textBG, "TOPLEFT", 8, -8) - scroll:SetPoint("BOTTOMRIGHT", textBG, "BOTTOMRIGHT", -28, 8) - recorder.scrollFrame = scroll - - local edit = CreateFrame("EditBox", "BNPAuraRecorderEditBox", scroll) - edit:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0) - edit:SetWidth(676) - edit:SetHeight(330) - edit:SetMultiLine(true) - edit:SetAutoFocus(false) - edit:SetFontObject(ChatFontNormal) - edit:SetTextColor(0.92, 0.92, 0.92) - edit:SetMaxLetters(65000) - edit:EnableMouse(true) - edit:SetScript("OnEscapePressed", function() this:ClearFocus() end) - edit:SetScript("OnTextChanged", function() - if recorder.scrollFrame and recorder.scrollFrame.UpdateScrollChildRect then - recorder.scrollFrame:UpdateScrollChildRect() - end - end) - scroll:SetScrollChild(edit) - recorder.editBox = edit - - local footer = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") - footer:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 26, 24) - footer:SetWidth(706) - footer:SetJustifyH("LEFT") - footer:SetText("The recorder changes no tracking settings. It is completely inactive after Stop or Close.") - - RefreshRecorderText(true) - return frame -end - -function BNP:OpenAuraRecorder() - local function OpenFullRecorder() - local frame = BNP:CreateAuraRecorderWindow() - frame:Show() - RefreshRecorderText(true) - end - - local ok = pcall(OpenFullRecorder) - if not ok and self.OpenEmergencyAuraRecorder then - self:OpenEmergencyAuraRecorder() - end -end diff --git a/modules/auras.lua b/modules/auras.lua deleted file mode 100644 index d09a331..0000000 --- a/modules/auras.lua +++ /dev/null @@ -1,3474 +0,0 @@ -if not BNP.libnameplate then return end -if not CombatLogAdd or not SpellInfo then return end - -local _, playerClass = UnitClass("player") -local UI = BNP.UI or {} -local ICON_SIZE = UI.ICON_SIZE or 18 -local function GetIconSize() - return (BNP.GetIconSize and BNP:GetIconSize()) or ICON_SIZE -end -local function GetCCIconSize() - return (BNP.GetCCIconSize and BNP:GetCCIconSize()) or GetIconSize() -end -local ICON_OFFSET_Y = UI.ICON_OFFSET_Y or 15 -local COMBO_LAYOUT_CLASS = (playerClass == "ROGUE" or playerClass == "DRUID") -local COMBO_LAYOUT_OFFSET = 16 -local ROW_SPACING = 2 -local NAME_HIDDEN_COMPACT_SHIFT = 10 - -local function GetBaseAuraOffsetY() - if COMBO_LAYOUT_CLASS and BNP.AreComboPointsEnabled and BNP:AreComboPointsEnabled() then - return ICON_OFFSET_Y + COMBO_LAYOUT_OFFSET - end - return ICON_OFFSET_Y -end - -local function GetDebuffPosition() - if BNP.GetDebuffPosition then return BNP:GetDebuffPosition() end - return "top" -end - -local function GetCCPosition() - if BNP.GetCCPosition then return BNP:GetCCPosition() end - return GetDebuffPosition() -end - -local function IsTopDebuffPosition(position) - return position == "top" or position == "top_left" or position == "top_right" -end - -local function IsBottomDebuffPosition(position) - return position == "bottom_mid" or position == "bottom_left" or position == "bottom_right" -end - -local function SharesTopRowFamily() - -- Top Left/Right are horizontal alignments of the same top aura row. - -- CCs only have a generic Top position, so when the user disables the - -- separate CC row they must share the debuff container instead of creating - -- a second centered container that can overlap larger CC icons. - return IsTopDebuffPosition(GetDebuffPosition()) and GetCCPosition() == "top" -end - -local function UseSeparateCCRow() - -- The separate-row option applies to every top alignment, not only Top Mid. - if BNP.AreDebuffsEnabled and not BNP:AreDebuffsEnabled() then return false end - if BNP.AreCrowdControlEnabled and not BNP:AreCrowdControlEnabled() then return false end - return SharesTopRowFamily() - and BNP.IsSeparateCCRowEnabled and BNP:IsSeparateCCRowEnabled() -end - -local function UseDedicatedCCContainer() - -- Top Mid/Left/Right are the same logical row when Separate CC Row is off. - -- LayoutActiveIcons() already accounts for mixed debuff/CC icon sizes, so - -- keeping them in one container prevents overlap (e.g. 18px debuffs + 24px CCs). - if SharesTopRowFamily() then - return UseSeparateCCRow() - end - - -- Other genuinely different positions use the independent CC container. - return GetCCPosition() ~= GetDebuffPosition() or UseSeparateCCRow() -end - -local function GetAuraOffsetY() - return GetBaseAuraOffsetY() -end - -local function GetCCOffsetY() - local y = GetBaseAuraOffsetY() - if UseSeparateCCRow() and BNP.AreCrowdControlEnabled and BNP:AreCrowdControlEnabled() then - y = y + GetIconSize() + ROW_SPACING - end - return y -end - -local ICON_SPACING = UI.ICON_SPACING or 2 -local SIDE_SPACING = 4 -local BOTTOM_SPACING = 4 -local UPDATE_INTERVAL = 0.05 - -local function GetSideCCOffsetY() - if UseSeparateCCRow() and BNP.AreCrowdControlEnabled and BNP:AreCrowdControlEnabled() then - return GetIconSize() + ROW_SPACING - end - return 0 -end - -local function GetLevelRegionWidth(region) - if not region then return 0 end - if region.IsShown and not region:IsShown() then return 0 end - if region.GetStringWidth then - local width = tonumber(region:GetStringWidth()) or 0 - if width > 0 then return width end - end - if region.GetWidth then - return math.max(0, tonumber(region:GetWidth()) or 0) - end - return 0 -end - -local function GetRightAuraSpacing(plate) - local extra = 0 - if plate then - extra = extra + GetLevelRegionWidth(plate.level) - extra = extra + GetLevelRegionWidth(plate.levelicon) - end - - if extra > 0 then - extra = extra + 6 - end - - return SIDE_SPACING + extra -end - -local function AnchorAuraContainer(plate, container, isCC) - if not plate or not container then return end - container:ClearAllPoints() - - local position = isCC and GetCCPosition() or GetDebuffPosition() - -- When BNP hides the Blizzard name, reclaim that empty vertical space. - -- Only Top rows move; Left/Right are already centered directly on the bar. - local nameHidden = (BNP.IsPlateNameHidden and BNP:IsPlateNameHidden(plate)) or plate.BNPNameHidden - local compactShift = (nameHidden and (isCC and position == "top" or (not isCC and IsTopDebuffPosition(position)))) and -NAME_HIDDEN_COMPACT_SHIFT or 0 - local anchor = plate.healthbar or plate - - if position == "left" then - container:SetPoint("RIGHT", anchor, "LEFT", -SIDE_SPACING, isCC and GetSideCCOffsetY() or 0) - elseif position == "right" then - container:SetPoint("LEFT", anchor, "RIGHT", GetRightAuraSpacing(plate), isCC and GetSideCCOffsetY() or 0) - elseif not isCC and IsBottomDebuffPosition(position) then - if position == "bottom_left" then - container:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -BOTTOM_SPACING) - elseif position == "bottom_right" then - container:SetPoint("TOPRIGHT", anchor, "BOTTOMRIGHT", 0, -BOTTOM_SPACING) - else - container:SetPoint("TOP", anchor, "BOTTOM", 0, -BOTTOM_SPACING) - end - elseif not isCC and position == "top_left" then - if plate.healthbar then - container:SetPoint("BOTTOMLEFT", plate.healthbar, "TOPLEFT", 0, GetAuraOffsetY() + compactShift) - else - container:SetPoint("BOTTOMLEFT", plate, "BOTTOMLEFT", 0, 24 + compactShift) - end - elseif not isCC and position == "top_right" then - if plate.healthbar then - container:SetPoint("BOTTOMRIGHT", plate.healthbar, "TOPRIGHT", 0, GetAuraOffsetY() + compactShift) - else - container:SetPoint("BOTTOMRIGHT", plate, "BOTTOMRIGHT", 0, 24 + compactShift) - end - else - if plate.healthbar then - container:SetPoint("BOTTOM", plate.healthbar, "TOP", 0, (isCC and GetCCOffsetY() or GetAuraOffsetY()) + compactShift) - else - container:SetPoint("BOTTOM", plate, "BOTTOM", 0, 24 + (isCC and (GetCCOffsetY() - GetAuraOffsetY()) or 0) + compactShift) - end - end -end - -function BNP:RefreshAuraLayoutForPlate(plate) - if not plate then return end - if plate.BNPAuraContainer then AnchorAuraContainer(plate, plate.BNPAuraContainer, false) end - if plate.BNPCCContainer then AnchorAuraContainer(plate, plate.BNPCCContainer, true) end - plate.BNPAuraLastNameHidden = (BNP.IsPlateNameHidden and BNP:IsPlateNameHidden(plate)) or (plate.BNPNameHidden and true or false) -end - --- Aura rows are anchored to the projected nameplate but parented beside it. --- That keeps their opacity independent from transient Vanilla nameplate alpha --- changes while preserving the native world/nameplate layer. - --- SuperWoW GUID cache. Auras are learned while a unit is the current target --- and remain attached to that unit's unique GUID after the target changes. -BNP.guidAuras = BNP.guidAuras or {} -BNP.guidNames = BNP.guidNames or {} -BNP.guidLiveCCs = BNP.guidLiveCCs or {} --- Shadow Vulnerability is a shared proc aura whose refresh can be invisible to --- vanilla UnitDebuff (same spell ID, same stack count, no duration data). Keep --- its countdown in a dedicated GUID state so generic shared-aura scans can --- never accidentally replace or freeze the timer. -BNP.svExpiryByGUID = BNP.svExpiryByGUID or {} --- Fully isolated Shadow Vulnerability state. Generic shared-aura code may --- mirror this state for rendering, but it must never own or rewrite the timer. -BNP.svStateByGUID = BNP.svStateByGUID or {} --- Monotonic render revision per GUID. A confirmed refresh increments this so --- the normal nameplate renderer can invalidate any cached timer text safely. -BNP.svRenderRevisionByGUID = BNP.svRenderRevisionByGUID or {} - -BNP.pvpAuraProtection = BNP.pvpAuraProtection or {} -local PVP_AURA_PROTECTION_TIME = 6.0 - -local function ProtectAuraCache(guid, reason) - if not guid then return end - BNP.pvpAuraProtection[guid] = { - untilTime = GetTime() + PVP_AURA_PROTECTION_TIME, - reason = reason, - } -end - -local function AuraCacheProtected(guid, now) - local state = guid and BNP.pvpAuraProtection[guid] - if not state then return false end - if now <= (state.untilTime or 0) then return true end - BNP.pvpAuraProtection[guid] = nil - return false -end - -local CLASS_AURAS = { - WARLOCK = BNP.WarlockAuras, - PRIEST = BNP.PriestAuras, - WARRIOR = BNP.WarriorAuras, - ROGUE = BNP.RogueAuras, - HUNTER = BNP.HunterAuras, - MAGE = BNP.MageAuras, - DRUID = BNP.DruidAuras, - SHAMAN = BNP.ShamanAuras, - PALADIN = BNP.PaladinAuras, -} -local AURA_DEFS = CLASS_AURAS[playerClass] or {} -local HAS_SHARED_AURAS = false -do - local _, def - for _, def in ipairs(AURA_DEFS) do - if def.shared then - HAS_SHARED_AURAS = true - break - end - end -end -local MAX_VISIBLE_ICONS = 8 - -local function PositionAuraIcon(icon, container, index, visibleCount, iconSize, isCC) - if not icon or not container then return end - local position = isCC and GetCCPosition() or GetDebuffPosition() - local step = iconSize + ICON_SPACING - - icon:ClearAllPoints() - - if position == "left" or position == "bottom_right" or position == "top_right" then - icon:SetPoint("RIGHT", container, "RIGHT", -((index - 1) * step), 0) - elseif position == "right" or position == "bottom_left" or position == "top_left" then - icon:SetPoint("LEFT", container, "LEFT", (index - 1) * step, 0) - else - local maxWidth = iconSize * MAX_VISIBLE_ICONS + ICON_SPACING * (MAX_VISIBLE_ICONS - 1) - local rowWidth = 0 - if visibleCount and visibleCount > 0 then - rowWidth = iconSize * visibleCount + ICON_SPACING * (visibleCount - 1) - end - local startX = (maxWidth - rowWidth) / 2 - icon:SetPoint("LEFT", container, "LEFT", startX + (index - 1) * step, 0) - end -end - -local function GetAuraFrameLevel(plate) - local plateLevel = 0 - if plate and plate.GetFrameLevel then - plateLevel = tonumber(plate:GetFrameLevel()) or plateLevel - end - return math.max(0, plateLevel + 1) -end - -local function ApplyAuraFrameLevel(plate, container) - if not container then return end - - -- Aura containers are siblings of the projected plate (not children) so they - -- do not inherit transient plate alpha. Mirror only the plate's native strata - -- and relative level; never modify the projected nameplate's own strata. - if plate and plate.GetFrameStrata and container.GetFrameStrata and container.SetFrameStrata then - local strata = plate:GetFrameStrata() - if strata and container:GetFrameStrata() ~= strata then - container:SetFrameStrata(strata) - end - end - - local level = GetAuraFrameLevel(plate) - if container.GetFrameLevel and container.SetFrameLevel and - container:GetFrameLevel() ~= level then - container:SetFrameLevel(level) - end - - local i - for i = 1, table.getn(container.icons or {}) do - local icon = container.icons[i] - if icon and icon.GetFrameLevel and icon.SetFrameLevel and - icon:GetFrameLevel() ~= level then - icon:SetFrameLevel(level) - end - end -end - --- Target auras stay opaque and non-target auras remain consistently faded. --- This is identity-based and never changes with camera or screen position. -local TARGET_PRIORITY_AURA_ALPHA = 0.35 -local TARGET_PRIORITY_PLATE_HOLD = 0.25 -local targetPriorityPlate = nil -local targetPriorityGUID = nil -local targetPriorityPlateSeenAt = -100 - -local function RefreshTargetPriorityIdentity() - local exists, guid = UnitExists("target") - if not exists or not guid then - targetPriorityPlate = nil - targetPriorityGUID = nil - targetPriorityPlateSeenAt = -100 - return - end - - local now = GetTime() - - -- A real target switch invalidates the previous frame immediately. For the - -- same GUID, however, keep the last exact ClassicAPI target plate through a - -- very short nil result. Dense raid stacks can make projected plate lookup - -- flicker for a frame; dropping the cached target here made its DoTs/Banish - -- alternate between opaque and the 0.35 non-target alpha. - if targetPriorityGUID ~= guid then - targetPriorityPlate = nil - targetPriorityPlateSeenAt = -100 - end - targetPriorityGUID = guid - - local resolved = nil - if C_NamePlate and - type(C_NamePlate.GetNamePlateForUnit) == "function" then - local ok, plate = pcall(C_NamePlate.GetNamePlateForUnit, "target") - if ok and plate then resolved = plate end - end - - if resolved then - targetPriorityPlate = resolved - targetPriorityPlateSeenAt = now - elseif targetPriorityPlate and - now - targetPriorityPlateSeenAt > TARGET_PRIORITY_PLATE_HOLD then - targetPriorityPlate = nil - end -end - -local function SetTargetPriorityAuraAlpha(container, alpha) - if not container then return end - alpha = alpha or 1 - if container.SetAlpha and - (not container.GetAlpha or container:GetAlpha() ~= alpha) then - container:SetAlpha(alpha) - end -end - -local function ApplyTargetAuraPriority(plate, container) - if not container then return end - - -- Flicker fix: aura opacity must not depend on transient target/nameplate - -- resolution. In dense raid stacks ClassicAPI/SuperWoW can briefly resolve - -- a projected plate differently, which previously made the whole debuff/CC - -- row alternate between alpha 1.0 and 0.35. This alpha is separate from the - -- user-facing Non-Target Alpha slider, so setting that slider to 100% did - -- not disable the flicker. Keep aura containers fully opaque and let target - -- priority be handled by nameplate layering only. - SetTargetPriorityAuraAlpha(container, 1) -end - -function BNP:RefreshAuraPriorityAlpha() - RefreshTargetPriorityIdentity() - - local plate - for plate in pairs(self.plates or {}) do - ApplyAuraFrameLevel(plate, plate.BNPAuraContainer) - ApplyAuraFrameLevel(plate, plate.BNPCCContainer) - ApplyTargetAuraPriority(plate, plate.BNPAuraContainer) - ApplyTargetAuraPriority(plate, plate.BNPCCContainer) - end -end - -local function CleanText(text) - if not text then return nil end - text = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "") - text = string.gsub(text, "|r", "") - return text -end - -local function NameMatches(def, name) - if not name then return false end - local lower = string.lower(name) - for _, known in ipairs(def.names) do - if lower == string.lower(known) then return true end - end - return false -end - -local function AuraMatches(def, effect, texture) - if NameMatches(def, effect) then return true end - if texture and def.textureMatch then - return string.find(string.lower(texture), def.textureMatch) and true or false - end - return false -end - -local function ScanSpellbook() - if table.getn(AURA_DEFS) == 0 then return end - - local i = 1 - while true do - local name = GetSpellName(i, BOOKTYPE_SPELL) - if not name then break end - - local texture = GetSpellTexture(i, BOOKTYPE_SPELL) - local lowerTexture = texture and string.lower(texture) or "" - - for _, def in ipairs(AURA_DEFS) do - if NameMatches(def, name) or (def.textureMatch and string.find(lowerTexture, def.textureMatch)) then - def.localizedName = name - def.texture = texture or def.texture - end - end - - i = i + 1 - end - if BNP.RefreshSpellbookDurations then BNP:RefreshSpellbookDurations() end -end - -local function CreateAuraIcon(parent, index) - local icon = CreateFrame("Frame", nil, parent) - local size = GetIconSize() - icon:SetWidth(size) - icon:SetHeight(size) - icon:SetFrameLevel(parent.GetFrameLevel and parent:GetFrameLevel() or 0) - - local texture = icon:CreateTexture(nil, "ARTWORK") - texture:SetAllPoints(icon) - icon.texture = texture - - local timer = icon:CreateFontString(nil, "OVERLAY") - timer:SetFont(UI.TIMER_FONT or "Fonts\\FRIZQT__.TTF", UI.TIMER_SIZE or 8, "OUTLINE") - timer:SetPoint("CENTER", icon, "CENTER", UI.TIMER_OFFSET_X or 0, UI.TIMER_OFFSET_Y or 0) - timer:SetTextColor(1, 1, 1) - icon.timer = timer - - local stack = icon:CreateFontString(nil, "OVERLAY") - stack:SetFont(UI.TIMER_FONT or "Fonts\\FRIZQT__.TTF", 8, "OUTLINE") - stack:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", -1, 1) - stack:SetTextColor(1, 1, 1) - stack:SetText("") - icon.stack = stack - - icon.index = index - icon:Hide() - - return icon -end - -local function HidePlateAuraFrames(plate) - if not plate then return end - - local containers = { plate.BNPAuraContainer, plate.BNPCCContainer } - local _, container, i - for _, container in ipairs(containers) do - if container then - for i = 1, table.getn(container.icons or {}) do - container.icons[i]:Hide() - end - container:Hide() - end - end -end - -local function InstallAuraHideGuard(plate) - if not plate or plate.BNPAuraHideGuard then return end - - local oldOnHide = plate:GetScript("OnHide") - plate:SetScript("OnHide", function() - if oldOnHide then oldOnHide() end - local current = this or plate - HidePlateAuraFrames(current) - -- If Blizzard reuses this frame for another unit, do not hold the old GUID - -- through the next OnShow. The next visible identity must be reconfirmed. - current.BNPAuraForceFreshIdentity = true - current.BNPAuraGUIDCandidate = nil - current.BNPAuraGUIDCandidateCount = 0 - end) - plate.BNPAuraHideGuard = true -end - -local function GetAuraVisualParent(plate) - -- Aura rows must not inherit the projected nameplate's alpha. In large raids - -- the Vanilla client can transiently adjust/recycle a non-target plate's alpha - -- while resolving dense stacks. A child frame inherits that alpha even when - -- its own alpha is forced to 1, which made debuffs pulse grey/colored. - -- Use the nameplate's native parent (normally WorldFrame) as a sibling parent - -- and keep anchoring to the real healthbar so position still follows the plate. - if plate and plate.GetParent then - local parent = plate:GetParent() - if parent then return parent end - end - return WorldFrame or UIParent -end - -local function CreateAuraContainer(plate) - if not plate then return end - BNP:RegisterPlate(plate) - if plate.BNPAuraContainer then return end - - local size = GetIconSize() - local width = size * MAX_VISIBLE_ICONS + ICON_SPACING * (MAX_VISIBLE_ICONS - 1) - local container = CreateFrame("Frame", nil, GetAuraVisualParent(plate)) - container:SetWidth(width) - container:SetHeight(size) - container:SetFrameLevel(GetAuraFrameLevel(plate)) - - AnchorAuraContainer(plate, container, false) - - container.icons = {} - for i = 1, MAX_VISIBLE_ICONS do - local icon = CreateAuraIcon(container, i) - PositionAuraIcon(icon, container, i, 0, size) - container.icons[i] = icon - end - - container:Hide() - plate.BNPAuraContainer = container - ApplyAuraFrameLevel(plate, container) - InstallAuraHideGuard(plate) -end - -local function CreateCCContainer(plate) - if not plate then return end - BNP:RegisterPlate(plate) - if plate.BNPCCContainer then return end - - local size = GetCCIconSize() - local width = size * MAX_VISIBLE_ICONS + ICON_SPACING * (MAX_VISIBLE_ICONS - 1) - local container = CreateFrame("Frame", nil, GetAuraVisualParent(plate)) - container:SetWidth(width) - container:SetHeight(size) - container:SetFrameLevel(GetAuraFrameLevel(plate)) - - AnchorAuraContainer(plate, container, true) - - container.icons = {} - for i = 1, MAX_VISIBLE_ICONS do - local icon = CreateAuraIcon(container, i) - PositionAuraIcon(icon, container, i, 0, size, true) - container.icons[i] = icon - end - - container:Hide() - plate.BNPCCContainer = container - ApplyAuraFrameLevel(plate, container) -end - -local function GetPlateGUID(plate) - if not plate or not plate.GetName then return nil end - - -- SuperWoW stores the unit GUID/token directly on the nameplate name slot. - -- Use that exact token, just like Tank Mode and ShaguTweaks do. This avoids - -- unreliable UnitExists() resolution and, importantly, avoids same-name - -- fallback collisions between two mobs with identical names. - local token = plate:GetName(1) - if token and token ~= "" then return token end - return nil -end - -local function GetTargetGUID() - local exists, guid = UnitExists("target") - if exists and guid then return guid end - return nil -end - -local function ClearGUIDAuraState(guid) - if not guid then return end - BNP.guidAuras[guid] = nil - if BNP.svStateByGUID then BNP.svStateByGUID[guid] = nil end - if BNP.svExpiryByGUID then BNP.svExpiryByGUID[guid] = nil end - if BNP.svRenderRevisionByGUID then BNP.svRenderRevisionByGUID[guid] = nil end - if BNP.guidLiveCCs then BNP.guidLiveCCs[guid] = nil end - if BNP.pendingAuras then BNP.pendingAuras[guid] = nil end - if BNP.pvpAuraProtection then BNP.pvpAuraProtection[guid] = nil end - if BNP.darkHarvest and BNP.darkHarvest.guid == guid then BNP.darkHarvest = nil end - if BNP.guidNames then BNP.guidNames[guid] = nil end -end - --- Full-immunity cleanse bridge ---------------------------------------------- --- Divine Shield and Ice Block remove harmful effects. BNP tracks some of --- those effects from cast events/timers, so a server cleanse must also wipe --- every local harmful-aura cache for that exact GUID. While the immunity is --- active, keep a short renewable suppression window so stale UnitDebuff / --- pending-cast data cannot repopulate icons for one frame. -BNP.guidHarmfulAuraSuppressionUntil = BNP.guidHarmfulAuraSuppressionUntil or {} - -local function ClearHarmfulAuraStateOnly(guid) - if not guid then return end - BNP.guidAuras[guid] = nil - if BNP.svStateByGUID then BNP.svStateByGUID[guid] = nil end - if BNP.svExpiryByGUID then BNP.svExpiryByGUID[guid] = nil end - if BNP.svRenderRevisionByGUID then BNP.svRenderRevisionByGUID[guid] = nil end - if BNP.guidLiveCCs then BNP.guidLiveCCs[guid] = nil end - if BNP.pendingAuras then BNP.pendingAuras[guid] = nil end - if BNP.pvpAuraProtection then BNP.pvpAuraProtection[guid] = nil end - if BNP.darkHarvest and BNP.darkHarvest.guid == guid then BNP.darkHarvest = nil end - - -- A failed/immune cast rollback must never restore pre-cleanse timers after - -- Bubble/Ice Block. Drop all outstanding attempts tied to this GUID. - if BNP.auraCastAttempts then - local i - for i = table.getn(BNP.auraCastAttempts), 1, -1 do - local attempt = BNP.auraCastAttempts[i] - if attempt and attempt.guid == guid then - table.remove(BNP.auraCastAttempts, i) - end - end - end -end - -local function HarmfulAuraSuppressed(guid, now) - if not guid then return false end - local untilTime = BNP.guidHarmfulAuraSuppressionUntil[guid] - if not untilTime then return false end - now = now or GetTime() - if now < untilTime then return true end - BNP.guidHarmfulAuraSuppressionUntil[guid] = nil - return false -end - -function BNP:SuppressHarmfulAurasForImmunity(guid, seconds) - if not guid then return end - local now = GetTime() - local untilTime = now + math.max(tonumber(seconds) or 0.35, 0.10) - local current = self.guidHarmfulAuraSuppressionUntil[guid] or 0 - if untilTime > current then - self.guidHarmfulAuraSuppressionUntil[guid] = untilTime - end - ClearHarmfulAuraStateOnly(guid) -end - -function BNP:IsHarmfulAuraSuppressed(guid, now) - return HarmfulAuraSuppressed(guid, now) -end - -local function CleanupDeadUnit(unit, guid, now) - if not unit or not guid or not UnitIsDeadOrGhost then return false end - if AuraCacheProtected and AuraCacheProtected(guid, now) then return false end - if UnitIsDeadOrGhost(unit) then - ClearGUIDAuraState(guid) - return true - end - return false -end - - --- Fallback for players/NPCs whose nameplate disappears before the periodic --- dead-state check sees them. Vanilla combat chat reports " dies." only, --- so this fallback must NEVER clear multiple same-name GUIDs. -local function ClearAuraStateByName(name) - if not name or name == "" then return end - - -- Combat death chat gives us only a name, never the exact GUID. - -- Count ALL known GUIDs with this name, not only GUIDs that currently have - -- tracked auras. Otherwise, if two same-name mobs exist but only one has our - -- debuffs, the death of the other mob could wrongly clear the survivor. - local matchGUID = nil - local matches = 0 - local guid, knownName - - for guid, knownName in pairs(BNP.guidNames or {}) do - if knownName == name then - matches = matches + 1 - matchGUID = guid - - if matches > 1 then - -- Ambiguous same-name death: never clear anything by name. - -- Exact GUID-based target death cleanup remains authoritative. - return - end - end - end - - if matches == 1 and matchGUID then - ClearGUIDAuraState(matchGUID) - end -end - - --- Boss / NPC evade-reset cleanup -------------------------------------------- --- On a real reset the server restores the NPC to full health and drops combat, --- while BNP's local aura timers would otherwise keep running. Track only the --- current hostile NPC and clear its exact GUID cache when that reset signature --- is observed. -local resetWatch = {} -local resetElapsed = 0 - -local function CheckCurrentTargetReset(now) - local exists, guid = UnitExists("target") - if not exists or not guid then - resetWatch.guid = nil - resetWatch.wasDamaged = nil - return - end - - if UnitIsPlayer and UnitIsPlayer("target") then - resetWatch.guid = guid - resetWatch.wasDamaged = nil - return - end - - if resetWatch.guid ~= guid then - resetWatch.guid = guid - resetWatch.wasDamaged = nil - end - - if not UnitHealth or not UnitHealthMax then return end - local hp = tonumber(UnitHealth("target")) - local maxhp = tonumber(UnitHealthMax("target")) - if not hp or not maxhp or maxhp <= 0 then return end - - local pct = hp / maxhp - if pct < 0.95 then - resetWatch.wasDamaged = true - end - - if not resetWatch.wasDamaged then return end - if pct < 0.995 then return end - - -- Prefer the target's combat state when available. If the API is missing, - -- require the player to be out of combat as a conservative fallback. - local outOfCombat = false - if UnitAffectingCombat then - outOfCombat = not UnitAffectingCombat("target") - elseif PlayerFrame and PlayerFrame.inCombat ~= nil then - outOfCombat = not PlayerFrame.inCombat - end - - if not outOfCombat then return end - - if BNP.guidAuras and BNP.guidAuras[guid] then - ClearGUIDAuraState(guid) - if RaidTrace then RaidTrace("RESET", guid, "-", nil) end - end - - resetWatch.wasDamaged = nil -end - -local resetFrame = CreateFrame("Frame") -resetFrame:SetScript("OnUpdate", function() - resetElapsed = resetElapsed + arg1 - if resetElapsed < 0.10 then return end - resetElapsed = 0 - CheckCurrentTargetReset(GetTime()) -end) - -local deathChatFrame = CreateFrame("Frame") -deathChatFrame:RegisterEvent("CHAT_MSG_COMBAT_HOSTILE_DEATH") -deathChatFrame:RegisterEvent("CHAT_MSG_COMBAT_FRIENDLY_DEATH") -deathChatFrame:SetScript("OnEvent", function() - local raw = arg1 - if not raw then return end - local _, _, deadName = string.find(raw, "^(.+) dies%.$") - if deadName then ClearAuraStateByName(deadName) end -end) - --- Only successful casts by the local player are allowed to create aura --- entries. This prevents target scans from importing debuffs cast by other --- players. SuperWoW supplies caster GUID, target GUID and spell ID. -local function GetPlayerGUID() - local exists, guid = UnitExists("player") - if exists and guid then return guid end - return nil -end - -local function DefHasSpellID(def, spellID) - if not def or not spellID then return false end - - local i - if def.spellIDs then - for i = 1, table.getn(def.spellIDs) do - if def.spellIDs[i] == spellID then return true end - end - end - - if def.durations and def.durations[spellID] then return true end - return false -end - -local function FindAuraDef(spellID) - if not spellID or not SpellInfo then return nil, nil end - local spellName, _, texture = SpellInfo(spellID) - if not spellName and not texture then return nil, nil end - - -- Exact IDs are authoritative and must win over shared spell textures. - local _, def - for _, def in ipairs(AURA_DEFS) do - if DefHasSpellID(def, spellID) then - return def, texture, spellID - end - end - - for _, def in ipairs(AURA_DEFS) do - if NameMatches(def, spellName) then - return def, texture, spellID - end - end - - for _, def in ipairs(AURA_DEFS) do - if texture and def.textureMatch and string.find(string.lower(texture), def.textureMatch) then - return def, texture, spellID - end - end - - return nil, nil -end - - --- Paladin Judgements -------------------------------------------------------- --- SuperWoW reports the button press as Judgement [20271], while the target --- receives a different aura ID (e.g. Light 20344, Justice 20184, Crusader --- 20302, Wisdom 20353). Therefore normal CAST-ID == AURA-ID confirmation --- cannot work for Judgements. -local PALADIN_JUDGEMENT_CAST_ID = 20271 - -local function IsJudgementDef(def) - if not def or not def.key then return false end - return string.find(def.key, "^judgement_") and true or false -end - -local function FindJudgementAuraOnUnit(unit) - if playerClass ~= "PALADIN" or not unit then return nil end - - local i, texture, stacks, dtype, auraSpellID - for i = 1, 64 do - texture, stacks, dtype, auraSpellID = UnitDebuff(unit, i) - if not texture then break end - - if auraSpellID then - local auraName = nil - if SpellInfo then auraName = SpellInfo(auraSpellID) end - - local _, def - for _, def in ipairs(AURA_DEFS) do - if IsJudgementDef(def) then - local explicitMatch = false - local j - if def.spellIDs then - for j = 1, table.getn(def.spellIDs) do - if def.spellIDs[j] == auraSpellID then - explicitMatch = true - break - end - end - end - - if explicitMatch or NameMatches(def, auraName) then - return def, auraSpellID, texture - end - end - end - end - end - - return nil -end - - --- Turtle WoW Dark Harvest (spell 52552). --- It is reported by SuperWoW as CHANNEL; arg5 is channel length in ms. --- A 30% shorter tick interval means aura time advances at 1/0.70 speed. -local DARK_HARVEST_SPELL_ID = 52552 -local DARK_HARVEST_SPEED = 1 / 0.70 -local DARK_HARVEST_AURAS = { - corruption=true, curse_of_agony=true, siphon_life=true, - curse_of_doom=true, drain_life=true, drain_soul=true, -} -BNP.darkHarvest = nil - -local function StartDarkHarvest(guid, durationMS) - local now=GetTime() - local duration=tonumber(durationMS) or 8000 - if duration > 100 then duration=duration/1000 end - BNP.darkHarvest={guid=guid, lastUpdate=now, ends=now+duration} -end - -local function UpdateDarkHarvest(now) - local st=BNP.darkHarvest - if not st then return end - local untilTime=now - if untilTime > st.ends then untilTime=st.ends end - local delta=untilTime-st.lastUpdate - if delta > 0 then - local cache=BNP.guidAuras[st.guid] - if cache then - local bonus=delta*(DARK_HARVEST_SPEED-1) - local key,aura - for key,aura in pairs(cache) do - if DARK_HARVEST_AURAS[key] and type(aura)=="table" and aura.expires and aura.expires > st.lastUpdate then - aura.expires=aura.expires-bonus - end - end - end - st.lastUpdate=untilTime - end - if now >= st.ends then BNP.darkHarvest=nil end -end - - - --- Raid confirmation trace ---------------------------------------------------- --- Tiny always-on ring buffer for the last tracked own-aura transitions. --- No chat output and no scanning; entries are only added at existing events. -BNP.raidTrace = BNP.raidTrace or {} -BNP.raidTraceMax = 30 - -local function RaidTrace(stage, guid, key, spellID) - local t = BNP.raidTrace - table.insert(t, { - time = GetTime(), - stage = stage, - guid = guid, - key = key, - spellID = spellID, - }) - while table.getn(t) > BNP.raidTraceMax do - table.remove(t, 1) - end -end - --- Cast confirmation --------------------------------------------------------- --- UNIT_CASTEVENT "CAST" means the player finished the cast, but it does not --- guarantee that the hostile aura actually landed. Resist/Miss/Immune must --- therefore never create a visible timer. --- --- We first remember the cast as "pending". It becomes a real aura only after --- UnitDebuff confirms the matching aura on that exact SuperWoW GUID. --- Never infer success from CAST alone: melee abilities can still miss/dodge, --- and hostile targets may resist or be immune after the cast event. -BNP.pendingAuras = BNP.pendingAuras or {} -BNP.pendingAoEAuras = BNP.pendingAoEAuras or {} - --- Pending failures are matched by exact SuperWoW UNIT_CASTEVENT data. --- Never use global combat-text "miss/resist" lines here: in a raid those can --- belong to another spell and previously caused unrelated pending DoTs to die. - -local function FailPendingAura(targetGUID, spellID) - if not targetGUID or not spellID then return end - local entries = BNP.pendingAuras[targetGUID] - if not entries then return end - - local def = FindAuraDef(spellID) - if def and entries[def.key] then - entries[def.key] = nil - end - - -- Generic Paladin Judgement failure. - if playerClass == "PALADIN" and spellID == PALADIN_JUDGEMENT_CAST_ID then - entries.__judgement = nil - end -end - -local PENDING_TIMEOUT = 4.0 - --- Shagu-style authoritative own-cast tracking -------------------------------- --- Exact own SuperWoW CAST on an exact GUID creates normal direct single-target --- debuffs immediately. The application remains provisional for a short window: --- SuperWoW FAIL and localized combat-log miss/resist/immune messages can roll it --- back, including restoring the old timer after a resisted refresh. -local STRICT_AURA_KEYS = { - -- Shared / proc / secondary effects - shadow_vulnerability=true, hunters_mark=true, - - -- Warlock CC - fear=true, howl_of_terror=true, banish=true, death_coil=true, - - -- Hunter traps / CC / talent procs - immolation_trap_effect=true, explosive_trap_effect=true, freezing_trap_effect=true, - improved_scorpid_sting=true, improved_wing_clip=true, improved_concussive_shot=true, - scatter_shot=true, wyvern_sting=true, entrapment=true, counterattack=true, - intimidation=true, scare_beast=true, - - -- Mage CC / proc effects - polymorph=true, frost_nova=true, counterspell=true, counterspell_silence=true, - ignite=true, impact=true, frostbite=true, fire_vulnerability=true, winters_chill=true, - - -- Priest CC / proc effects - psychic_scream=true, silence=true, blackout=true, touch_of_weakness=true, - shackle_undead=true, mind_control=true, - - -- Paladin CC - hammer_of_justice=true, repentance=true, turn_undead=true, - - -- Rogue CC / poison procs - gouge=true, kidney_shot=true, cheap_shot=true, sap=true, blind=true, - kick_silenced=true, crippling_poison=true, mind_numbing_poison=true, - wound_poison=true, deadly_poison=true, - - -- Druid CC / secondary effects - entangling_roots=true, bash=true, pounce=true, pounce_bleed=true, - hibernate=true, feral_charge_effect=true, - - -- Warrior CC / talent procs - improved_hamstring=true, intimidating_shout=true, disarm=true, - charge_stun=true, intercept_stun=true, concussion_blow=true, - revenge_stun=true, pummel=true, shield_bash=true, deep_wound=true, - - -- Shaman totem / proc effects - earthbind=true, earthgrab=true, frostbrand_attack=true, -} - -local function GetTrackingMode(def) - if not def or not def.key then return "AURA" end - if def.category == "CURSE" then return "AURA" end - if def.aoe then return "AURA" end - local cat = string.upper(tostring(def.category or "")) - if cat == "CC" or cat == "ROOT" or cat == "STUN" - or cat == "TRAP" or cat == "SILENCE" or cat == "PROC" then - return "AURA" - end - if STRICT_AURA_KEYS[def.key] then return "AURA" end - return "CAST" -end - -local function IsCastPrimary(def) - return GetTrackingMode(def) == "CAST" -end - -local AURA_ATTEMPT_WINDOW = 5.0 -BNP.auraCastAttempts = BNP.auraCastAttempts or {} - -local function CopyAuraState(aura) - if type(aura) ~= "table" then return nil end - local copy = {} - local k, v - for k, v in pairs(aura) do copy[k] = v end - return copy -end - -local function BuildAffectedKeys(def) - local keys = {} - if not def or not def.key then return keys end - - keys[def.key] = true - - -- Any Warlock curse cast can change the target's complete curse state. - -- Snapshot every tracked curse so a later resist/fail can restore exactly - -- what was active before the attempt. This also supports Turtle mechanics - -- where Curse of Agony may coexist with one regular curse. - if def.category == "CURSE" then - local _, oldDef - for _, oldDef in ipairs(AURA_DEFS) do - if oldDef.category == "CURSE" then - keys[oldDef.key] = true - end - end - end - - return keys -end - -local function StartAuraCastAttempt(guid, def, spellID, affectedKeys) - if not guid or not spellID then return nil end - - local keys = affectedKeys or BuildAffectedKeys(def) - local cache = BNP.guidAuras[guid] - local before = {} - local key - - for key in pairs(keys) do - local aura = cache and cache[key] - before[key] = { - exists = type(aura) == "table" and true or false, - aura = CopyAuraState(aura), - } - end - - local spellName = nil - if SpellInfo then spellName = SpellInfo(spellID) end - if not spellName and def and def.localizedName then spellName = def.localizedName end - if not spellName and def and def.names then spellName = def.names[1] end - - local attempt = { - guid = guid, - key = def and def.key or "__special", - spellID = spellID, - spellName = spellName, - time = GetTime(), - before = before, - done = false, - } - - table.insert(BNP.auraCastAttempts, attempt) - while table.getn(BNP.auraCastAttempts) > 32 do - table.remove(BNP.auraCastAttempts, 1) - end - - return attempt -end - -local function RollbackAuraCastAttempt(attempt, reason) - if not attempt or attempt.done then return false end - - local guid = attempt.guid - local cache = BNP.guidAuras[guid] - if not cache then - cache = {} - BNP.guidAuras[guid] = cache - end - - local key, state - for key, state in pairs(attempt.before or {}) do - if state.exists and state.aura then - cache[key] = CopyAuraState(state.aura) - else - cache[key] = nil - end - end - - attempt.done = true - RaidTrace("ROLLBACK", guid, attempt.key, attempt.spellID) - return true -end - -local function RollbackAuraAttemptBySpell(guid, spellID, reason) - if not spellID then return false end - local now = GetTime() - local i - - for i = table.getn(BNP.auraCastAttempts), 1, -1 do - local attempt = BNP.auraCastAttempts[i] - if attempt and not attempt.done - and attempt.spellID == spellID - and (not guid or attempt.guid == guid) - and now - (attempt.time or 0) <= AURA_ATTEMPT_WINDOW then - return RollbackAuraCastAttempt(attempt, reason) - end - end - - return false -end - -local function CleanupAuraCastAttempts(now) - local i - for i = table.getn(BNP.auraCastAttempts), 1, -1 do - local attempt = BNP.auraCastAttempts[i] - if not attempt or attempt.done or now - (attempt.time or 0) > AURA_ATTEMPT_WINDOW then - table.remove(BNP.auraCastAttempts, i) - end - end -end - --- ShaguPlates does more than just seed debuffs from UNIT_CASTEVENT CAST: --- libdebuff also watches the local player's failure combat messages and --- reverts the last application on miss/resist/immune/evade/etc. BNP mirrors --- that safety, but keeps the exact SuperWoW GUID and can restore the previous --- timer when a refresh fails. -local failurePatterns = {} - -local function EscapeLuaPattern(text) - return string.gsub(text, "([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1") -end - -local function CombatFormatToPattern(formatText) - if not formatText or formatText == "" then return nil end - - local text = formatText - text = string.gsub(text, "%%%%", "__BNP_PERCENT__") - text = string.gsub(text, "%%%d+%$s", "__BNP_STRING__") - text = string.gsub(text, "%%s", "__BNP_STRING__") - text = string.gsub(text, "%%%d+%$d", "__BNP_NUMBER__") - text = string.gsub(text, "%%d", "__BNP_NUMBER__") - text = EscapeLuaPattern(text) - text = string.gsub(text, "__BNP_STRING__", ".+") - text = string.gsub(text, "__BNP_NUMBER__", "%%d+") - text = string.gsub(text, "__BNP_PERCENT__", "%%%%") - return "^" .. text .. "$" -end - -local function AddFailurePattern(formatText) - local pattern = CombatFormatToPattern(formatText) - if pattern then table.insert(failurePatterns, pattern) end -end - -AddFailurePattern(SPELLIMMUNESELFOTHER) -AddFailurePattern(IMMUNEDAMAGECLASSSELFOTHER) -AddFailurePattern(SPELLMISSSELFOTHER) -AddFailurePattern(SPELLRESISTSELFOTHER) -AddFailurePattern(SPELLEVADEDSELFOTHER) -AddFailurePattern(SPELLDODGEDSELFOTHER) -AddFailurePattern(SPELLDEFLECTEDSELFOTHER) -AddFailurePattern(SPELLREFLECTSELFOTHER) -AddFailurePattern(SPELLPARRIEDSELFOTHER) -AddFailurePattern(SPELLFAILCASTSELF) - -local function IsLocalFailureMessage(raw) - if not raw or raw == "" then return false end - local i - for i = 1, table.getn(failurePatterns) do - if string.find(raw, failurePatterns[i]) then return true end - end - return false -end - -local function RollbackAuraAttemptFromCombatMessage(raw) - if not IsLocalFailureMessage(raw) then return false end - - local now = GetTime() - local i - for i = table.getn(BNP.auraCastAttempts), 1, -1 do - local attempt = BNP.auraCastAttempts[i] - if attempt and not attempt.done - and now - (attempt.time or 0) <= AURA_ATTEMPT_WINDOW - and attempt.spellName - and string.find(raw, attempt.spellName, 1, true) then - return RollbackAuraCastAttempt(attempt, "COMBAT_FAIL") - end - end - - return false -end - -local auraFailureFrame = CreateFrame("Frame") -auraFailureFrame:RegisterEvent("CHAT_MSG_SPELL_FAILED_LOCALPLAYER") -auraFailureFrame:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE") -auraFailureFrame:SetScript("OnEvent", function() - RollbackAuraAttemptFromCombatMessage(arg1) -end) - -local function HasPendingAuras() - local guid, entries, key, pending - for guid, entries in pairs(BNP.pendingAuras) do - for key, pending in pairs(entries) do - if type(pending) == "table" then - return true - end - end - end - return false -end - -local function GetUnitTokenForPlate(plate) - if not plate or not plate.GetName then return nil end - return plate:GetName(1) -end - -local function FindMatchingAuraOnUnit(unit, def, castSpellID) - if not unit or not def then return nil, nil, nil end - - -- SuperWoW can expose more than the stock 16/32 debuff slots. In a 40-player - -- raid our own debuff may therefore sit beyond slot 32. Stopping at 32 made - -- valid casts time out instead of entering guidAuras. - local i = 1 - while i <= 64 do - local texture, stacks, dtype, auraSpellID = UnitDebuff(unit, i) - if not texture then break end - - if auraSpellID then - if auraSpellID == castSpellID or DefHasSpellID(def, auraSpellID) then - return auraSpellID, texture, tonumber(stacks) or 0 - end - - local auraName = SpellInfo and SpellInfo(auraSpellID) or nil - if NameMatches(def, auraName) then - return auraSpellID, texture, tonumber(stacks) or 0 - end - end - - if texture and def.textureMatch and string.find(string.lower(texture), def.textureMatch) then - return auraSpellID or castSpellID, texture, tonumber(stacks) or 0 - end - - i = i + 1 - end - - return nil, nil, nil -end - -local function ReconcileLiveCurseCache(unit, guid) - if playerClass ~= "WARLOCK" or not unit or not guid then return end - - local cache = BNP.guidAuras[guid] - if not cache then return end - - -- Curse casts are already strict aura-confirmed. Once a new curse has really - -- appeared, use that same live target aura state to decide which older curse - -- timers are still valid. This avoids hard-coding whether CoA is exclusive: - -- if the server keeps two curses, BNP keeps two; if one was replaced, BNP - -- removes only the aura that is actually gone. - local _, curseDef - for _, curseDef in ipairs(AURA_DEFS) do - if curseDef.category == "CURSE" and cache[curseDef.key] then - local cachedAura = cache[curseDef.key] - local liveSpellID = FindMatchingAuraOnUnit( - unit, - curseDef, - cachedAura and cachedAura.spellID or nil - ) - - if not liveSpellID then - RaidTrace("CURSE_REMOVE", guid, curseDef.key, cachedAura and cachedAura.spellID) - cache[curseDef.key] = nil - end - end - end -end - -local function CommitPendingAura(guid, key, pending, unit) - if not guid or not pending then return end - - local def = pending.def - local cache = BNP.guidAuras[guid] - if not cache then - cache = {} - BNP.guidAuras[guid] = cache - end - - -- Do not guess curse exclusivity here. Warlock curses are strict aura- - -- confirmed and are reconciled against the live target state after commit. - -- This supports both normal Vanilla replacement and Turtle dual-curse rules. - - cache._nextOrder = (cache._nextOrder or 0) + 1 - local aura = cache[key] or {} - aura.order = cache._nextOrder - aura.duration = pending.duration - aura.expires = GetTime() + pending.duration - aura.spellID = pending.spellID - aura.texture = pending.texture or def.texture - aura.stacks = tonumber(pending.stacks) or 0 - aura.confirmedAt = GetTime() - aura.missingScans = nil - - if pending.liveConfirmed then - aura.castPrimary = nil - aura.castPrimaryAt = nil - aura.liveConfirmed = true - elseif pending.castPrimary then - aura.castPrimary = true - aura.castPrimaryAt = GetTime() - aura.liveConfirmed = nil - end - - cache[key] = aura - - if def.category == "CURSE" and unit then - ReconcileLiveCurseCache(unit, guid) - end - - RaidTrace("CONFIRM", guid, key, aura.spellID) -end - -local function ConfirmPendingForUnit(unit, guid, now) - if not unit or not guid then return end - local pendingForGUID = BNP.pendingAuras[guid] - if not pendingForGUID then return end - - local key, pending - for key, pending in pairs(pendingForGUID) do - if type(pending) == "table" then - if pending.judgementProbe then - local def, auraSpellID, texture = FindJudgementAuraOnUnit(unit) - - if def and auraSpellID then - local duration = def.duration - if BNP.GetSpellbookDurationForDef then - duration = BNP:GetSpellbookDurationForDef(def) or duration - end - - local confirmed = { - def = def, - spellID = auraSpellID, - texture = texture or def.texture, - duration = duration, - created = pending.created, - liveConfirmed = true, - } - - CommitPendingAura(guid, def.key, confirmed, unit) - pendingForGUID[key] = nil - elseif now - pending.created > PENDING_TIMEOUT then - RaidTrace("TIMEOUT", guid, key, pending.spellID) - -- No Judgement aura appeared: resist, miss, immune or failed cast. - pendingForGUID[key] = nil - end - else - local auraSpellID, auraTexture, auraStacks = FindMatchingAuraOnUnit(unit, pending.def, pending.spellID) - if auraSpellID then - local confirmed = { - def = pending.def, - spellID = auraSpellID, - texture = auraTexture or pending.texture, - duration = pending.duration, - created = pending.created, - stacks = auraStacks or 0, - liveConfirmed = true, - } - CommitPendingAura(guid, key, confirmed, unit) - pendingForGUID[key] = nil - elseif now - pending.created > PENDING_TIMEOUT then - RaidTrace("TIMEOUT", guid, key, pending.spellID) - -- Effects not approved for the raid fallback remain strict. - pendingForGUID[key] = nil - end - end - end - end -end - -local function CleanupPending(now) - local guid, entries, key, pending - for guid, entries in pairs(BNP.pendingAuras) do - local any = false - for key, pending in pairs(entries) do - if type(pending) == "table" then - if now - pending.created > PENDING_TIMEOUT then - entries[key] = nil - else - any = true - end - end - end - if not any then BNP.pendingAuras[guid] = nil end - end -end - - -local AOE_PENDING_TIMEOUT = 1.5 - -local function QueueAoEAura(def, spellID, texture, duration) - if not def or not spellID then return end - BNP.pendingAoEAuras[def.key] = { - def = def, - spellID = spellID, - texture = texture or def.texture, - duration = duration, - created = GetTime(), - } -end - -local function ConfirmAoEAurasOnUnit(unit, guid) - if not unit or not guid then return end - local key, pending - for key, pending in pairs(BNP.pendingAoEAuras) do - if type(pending) == "table" then - local auraSpellID, auraTexture, auraStacks = FindMatchingAuraOnUnit(unit, pending.def, pending.spellID) - if auraSpellID then - CommitPendingAura(guid, pending.def.key, { - def = pending.def, - spellID = auraSpellID, - texture = auraTexture or pending.texture, - duration = pending.duration, - created = pending.created, - stacks = auraStacks or 0, - liveConfirmed = true, - }, unit) - end - end - end -end - -local function HasPendingAoEAuras() - local key, pending - for key, pending in pairs(BNP.pendingAoEAuras) do - if type(pending) == "table" then return true end - end - return false -end - -local function CleanupPendingAoE(now) - local key, pending - for key, pending in pairs(BNP.pendingAoEAuras) do - if type(pending) == "table" and now - (pending.created or 0) > AOE_PENDING_TIMEOUT then - BNP.pendingAoEAuras[key] = nil - end - end -end - -local recentChannelAuras = {} - -local function ChannelKey(guid, spellID) - return tostring(guid or "-") .. ":" .. tostring(spellID or 0) -end - -local castEvents = CreateFrame("Frame") -castEvents:RegisterEvent("UNIT_CASTEVENT") -castEvents:SetScript("OnEvent", function() - local casterGUID = arg1 - local targetGUID = arg2 - local eventType = arg3 - local spellID = arg4 - local playerGUID = GetPlayerGUID() - - if not playerGUID or casterGUID ~= playerGUID then return end - - if eventType == "FAIL" then - RaidTrace("FAIL", targetGUID, "-", spellID) - FailPendingAura(targetGUID, spellID) - RollbackAuraAttemptBySpell(targetGUID, spellID, "UNIT_FAIL") - return - end - - -- Dark Harvest has its own channel implementation and must not enter the - -- generic aura pipeline below. - if eventType == "CHANNEL" and spellID == DARK_HARVEST_SPELL_ID then - StartDarkHarvest(targetGUID, arg5) - return - end - - if eventType ~= "CAST" and eventType ~= "CHANNEL" then return end - - -- Shadow audit only: this never changes the existing lookup or renderer. - if BNP.AuditSpellDB then BNP:AuditSpellDB(spellID) end - - -- Paladin Judgement is a generic cast (20271). The actual debuff ID is only - -- known after it lands on the target, so defer identification to UnitDebuff. - if playerClass == "PALADIN" and spellID == PALADIN_JUDGEMENT_CAST_ID then - if not targetGUID then return end - - if targetGUID then - local judgementKeys = {} - local _, judgementDef - for _, judgementDef in ipairs(AURA_DEFS) do - if IsJudgementDef(judgementDef) then judgementKeys[judgementDef.key] = true end - end - StartAuraCastAttempt(targetGUID, { key = "__judgement" }, spellID, judgementKeys) - end - - local pendingForGUID = BNP.pendingAuras[targetGUID] - if not pendingForGUID then - pendingForGUID = {} - BNP.pendingAuras[targetGUID] = pendingForGUID - end - - pendingForGUID.__judgement = { - judgementProbe = true, - created = GetTime(), - } - return - end - - -- Shadow Vulnerability uses one aura ID per talent rank. It is a shared - -- raid debuff and has its own any-Warlock path below, so none of its ranks - -- should enter the normal own-cast pending pipeline. - local sid = tonumber(spellID) - if playerClass == "WARLOCK" and (sid == 17794 or sid == 17797 or sid == 17798 or sid == 17799 or sid == 17800) then return end - - local def, texture = FindAuraDef(spellID) - if not def then return end - - -- A few SuperWoW channel paths can omit arg2 briefly. A channel cannot move to - -- a different target after it starts, so the real current target is a safe - -- fallback here (unlike blindly doing this for every normal CAST event). - if eventType == "CHANNEL" and not targetGUID then - targetGUID = GetTargetGUID() - end - - -- Channeled hostile effects are applied when SuperWoW emits CHANNEL, not when - -- the channel later ends. Some clients may additionally emit CAST at the end; - -- suppress that duplicate so the timer cannot restart after the channel. If a - -- server exposes only CAST, the fallback remains available. - if eventType == "CHANNEL" then - if not def.channel then return end - local channelMS = tonumber(arg5) or 0 - local hold = channelMS > 0 and (channelMS / 1000) or (def.duration or 1) - recentChannelAuras[ChannelKey(targetGUID, spellID)] = GetTime() + hold + 0.75 - elseif def.channel then - local untilTime = recentChannelAuras[ChannelKey(targetGUID, spellID)] - if untilTime and GetTime() <= untilTime then return end - end - - local duration = nil - -- Resolve the exact rank that was cast. This preserves tooltip-derived talent - -- or server modifications for that rank, while still falling back to the - -- explicit rank table when the exact spellbook slot cannot be resolved. - if BNP.GetSpellbookDurationForSpellID then - duration = BNP:GetSpellbookDurationForSpellID(def, spellID) - end - duration = duration or (def.durations and def.durations[spellID]) - if not duration and BNP.GetSpellbookDurationForDef then - duration = BNP:GetSpellbookDurationForDef(def) - end - duration = duration or def.duration - - if def.aoe then - QueueAoEAura(def, spellID, texture, duration) - return - end - - if not targetGUID then return end - - StartAuraCastAttempt(targetGUID, def, spellID) - - local pendingForGUID = BNP.pendingAuras[targetGUID] - if not pendingForGUID then - pendingForGUID = {} - BNP.pendingAuras[targetGUID] = pendingForGUID - end - - pendingForGUID[def.key] = { - def = def, - spellID = spellID, - texture = texture or def.texture, - duration = duration, - created = GetTime(), - } - - RaidTrace("CAST", targetGUID, def.key, spellID) - - if IsCastPrimary(def) then - local direct = { - def = def, - spellID = spellID, - texture = texture or def.texture, - duration = duration, - created = GetTime(), - stacks = 0, - castPrimary = true, - } - CommitPendingAura(targetGUID, def.key, direct) - local cache = BNP.guidAuras and BNP.guidAuras[targetGUID] - if cache and cache[def.key] then - cache[def.key].castPrimary = true - cache[def.key].castPrimaryAt = GetTime() - cache[def.key].liveConfirmed = nil - end - pendingForGUID[def.key] = nil - RaidTrace("DIRECT", targetGUID, def.key, spellID) - return - end - - -- Fast path: if the cast target is still our current target, try to confirm - -- immediately. The normal pending loop remains as the retry path. - local currentTargetGUID = GetTargetGUID() - if currentTargetGUID and currentTargetGUID == targetGUID then - ConfirmPendingForUnit("target", targetGUID, GetTime()) - end -end) - - - --- Paladin Judgement melee refresh ------------------------------------------ --- Vanilla Judgement debuffs refresh when the judging Paladin lands a melee --- strike on that target. The aura itself does not get re-cast, so UNIT_CASTEVENT --- cannot update our timer. Refresh only an already-confirmed OWN Judgement. -local function RefreshOwnJudgementForGUID(guid) - if playerClass ~= "PALADIN" or not guid then return end - - local cache = BNP.guidAuras[guid] - if not cache then return end - - local _, def - for _, def in ipairs(AURA_DEFS) do - if IsJudgementDef(def) then - local aura = cache[def.key] - if type(aura) == "table" and aura.expires then - local duration = aura.duration or def.duration or 10 - aura.duration = duration - aura.expires = GetTime() + duration - aura.confirmedAt = GetTime() - aura.missingScans = nil - end - end - end -end - --- SuperWoW gives us the target GUID of the player's current target. For normal --- auto-attacks, the combat text event is sufficient to know a successful melee --- hit occurred; matching the current target GUID keeps the refresh scoped to --- the unit actually being attacked. -local judgementMeleeFrame = CreateFrame("Frame") -judgementMeleeFrame:RegisterEvent("CHAT_MSG_COMBAT_SELF_HITS") -judgementMeleeFrame:SetScript("OnEvent", function() - if playerClass ~= "PALADIN" then return end - - local guid = GetTargetGUID() - if not guid then return end - - -- CHAT_MSG_COMBAT_SELF_HITS only fires for landed melee attacks (hit/crit). - RefreshOwnJudgementForGUID(guid) -end) - - - --- Holy Strike also counts as a melee-style Judgement refresh on Turtle/Octo. --- It is reported via CHAT_MSG_SPELL_SELF_DAMAGE rather than the normal melee --- hit event and does not expose a useful spell ID in this client. -local holyStrikeRefreshFrame = CreateFrame("Frame") -holyStrikeRefreshFrame:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE") -holyStrikeRefreshFrame:SetScript("OnEvent", function() - if playerClass ~= "PALADIN" then return end - - local raw = arg1 - if not raw then return end - - -- Only successful Holy Strike hit messages refresh the timer. - -- Miss/resist/immune lines do not match this pattern. - if not string.find(raw, "^Your Holy Strike hits ") then return end - - local guid = GetTargetGUID() - if not guid then return end - - -- Extra safety: when the target name is available, require it to be present - -- in the combat-log line so a stale/current target cannot refresh another GUID. - local targetName = UnitName("target") - if targetName and targetName ~= "" and not string.find(raw, targetName, 1, true) then - return - end - - RefreshOwnJudgementForGUID(guid) -end) - - --- Enemy Feign Death / Vanish protection. -local pvpDisappearFrame = CreateFrame("Frame") -pvpDisappearFrame:RegisterEvent("UNIT_CASTEVENT") -pvpDisappearFrame:SetScript("OnEvent", function() - local casterGUID = arg1 - local eventType = arg3 - local spellID = arg4 - - if not casterGUID or not spellID or not SpellInfo then return end - if eventType ~= "CAST" and eventType ~= "START" then return end - - local spellName = SpellInfo(spellID) - - -- Abilities that can temporarily drop target/nameplate visibility without - -- actually cleansing existing harmful effects. - local TEMPORARY_DISAPPEAR = { - ["Feign Death"] = true, - ["Vanish"] = true, - ["Shadowmeld"] = true, - } - - if spellName and TEMPORARY_DISAPPEAR[spellName] then - ProtectAuraCache(casterGUID, spellName) - end -end) - - - --- Shared/global aura exceptions --------------------------------------------- --- Most BNP debuffs are strictly "own casts only". A very small number of --- effects are globally unique on a target and are useful even when applied by --- another player. These are explicitly opted in via def.shared = true. -local LIVE_DEBUFF_SNAPSHOT = { entries = {}, count = 0, time = 0 } - --- Capture UnitDebuff once and reuse that exact snapshot for shared-aura sync --- and removal sync in the same 0.10s scan. This avoids scanning the same --- SuperWoW unit twice (or more) per plate while keeping behavior identical. -local function CaptureLiveDebuffSnapshot(unit, now) - local snap = LIVE_DEBUFF_SNAPSHOT - local entries = snap.entries - local count = 0 - local i - - snap.time = now or GetTime() - - for i = 1, 64 do - local a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 = UnitDebuff(unit, i) - if not a1 then break end - - count = count + 1 - local entry = entries[count] - if not entry then - entry = {} - entries[count] = entry - end - - entry[1] = a1 - entry[2] = a2 - entry[3] = a3 - entry[4] = a4 - entry[5] = a5 - entry[6] = a6 - entry[7] = a7 - entry[8] = a8 - entry[9] = a9 - entry[10] = a10 - end - - snap.count = count - return snap -end - -local function ClassicAPIAuraDataAvailable() - return type(C_UnitAuras) == "table" and type(C_UnitAuras.GetDebuffDataByIndex) == "function" -end - --- ClassicAPI exposes the real hostile-aura duration and absolute expiration. --- Use it only for the isolated shared Shadow Vulnerability path; all normal own --- debuffs keep their existing proven tracking pipeline unchanged. -local function FindClassicAPISharedAura(unit, def) - if not ClassicAPIAuraDataAvailable() then return nil, nil, nil, nil, nil, false end - - local i - for i = 1, 64 do - local ok, aura = pcall(C_UnitAuras.GetDebuffDataByIndex, unit, i) - if not ok then return nil, nil, nil, nil, nil, false end - if not aura then break end - - local spellID = tonumber(aura.spellId or aura.spellID) - local matches = false - if def.sharedSpellIDs and spellID then - local _, knownID - for _, knownID in ipairs(def.sharedSpellIDs) do - if spellID == knownID then - matches = true - break - end - end - end - if not matches and aura.name then matches = NameMatches(def, aura.name) end - - if matches then - return spellID, aura.icon, tonumber(aura.duration), tonumber(aura.expirationTime), - tonumber(aura.applications) or 0, true - end - end - - -- A successful empty ClassicAPI scan is authoritative for absence. The - -- existing short missing grace protects against transient token updates. - return nil, nil, nil, nil, nil, true -end - -local function FindSharedAuraOnUnit(unit, def, snapshot) - if not unit or not def then return nil, nil, nil, nil, nil, false end - - if def.key == "shadow_vulnerability" then - local spellID, texture, duration, expires, stacks, authoritative = - FindClassicAPISharedAura(unit, def) - if authoritative then return spellID, texture, duration, expires, stacks, true end - end - - local maxIndex = snapshot and snapshot.count or 64 - local now = snapshot and snapshot.time or GetTime() - local i - for i = 1, maxIndex do - local a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 - - if snapshot then - local entry = snapshot.entries[i] - if not entry then break end - a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 = - entry[1], entry[2], entry[3], entry[4], entry[5], - entry[6], entry[7], entry[8], entry[9], entry[10] - else - a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 = UnitDebuff(unit, i) - if not a1 then break end - end - - local texture = a1 - local auraSpellID = a4 - if not texture then break end - - local auraName = nil - if auraSpellID and SpellInfo then - auraName = SpellInfo(auraSpellID) - end - - local sharedMatches = nil - if def.sharedSpellIDs and auraSpellID then - sharedMatches = false - local _, sharedSpellID - for _, sharedSpellID in ipairs(def.sharedSpellIDs) do - if auraSpellID == sharedSpellID then - sharedMatches = true - break - end - end - end - - -- Some different class talents use the same localized aura name. Shared - -- effects may therefore opt into exact aura IDs instead of name matching. - if sharedMatches == true or (sharedMatches == nil and AuraMatches(def, auraName, texture)) then - -- SuperWoW/extended clients may append duration/expiration data. - -- Probe plausible numeric return pairs without assuming one exact fork. - local duration, expires - local vals = { a5, a6, a7, a8, a9, a10 } - local n - for n = 1, table.getn(vals) - 1 do - local d = vals[n] - local e = vals[n + 1] - if type(d) == "number" and type(e) == "number" - and d > 0 and d <= 3600 and e > now then - duration = d - expires = e - break - end - end - - return auraSpellID, texture, duration, expires, tonumber(a2) or 0, false - end - end - - return nil, nil, nil, nil, nil, false -end - -local SV_SHARED_MISSING_GRACE = 0.45 - -local function SyncShadowVulnerabilityPresence(unit, guid, now, def, auraSpellID, texture, liveStacks, liveDuration, liveExpires, classicAPIAuthoritative) - local cache = BNP.guidAuras[guid] - local state = BNP.svStateByGUID[guid] - - -- ClassicAPI can keep an expired AuraData row around briefly before the - -- next UNIT_AURA cleanup. A positive expirationTime in the past is proof - -- that this row is stale; never turn it into a fresh estimated 10 seconds. - -- A newer RAW_COMBATLOG application may temporarily outrun the API update, - -- so preserve that independently proven future expiration when present. - local previousExpires = state and tonumber(state.expires) or nil - local explicitExpired = liveExpires and liveExpires > 0 and liveExpires <= now - local provenFuture = state and state.provenExpires and state.provenExpires > now - if explicitExpired and not provenFuture then - BNP.svStateByGUID[guid] = nil - BNP.svExpiryByGUID[guid] = nil - if BNP.svRenderRevisionByGUID then BNP.svRenderRevisionByGUID[guid] = nil end - if cache then cache[def.key] = nil end - return - end - - -- Some ClassicAPI builds replace expirationTime with 0/nil while retaining - -- the expired AuraData row for another update or two. Once ClassicAPI has - -- answered authoritatively, never manufacture a timer from that incomplete - -- row. Keep only an already-known future expiry or a proven raw reapply. - local classicRowWithoutFutureExpiry = classicAPIAuthoritative and - (auraSpellID or texture) and not (liveExpires and liveExpires > now) - local previousFuture = previousExpires and previousExpires > now - if classicRowWithoutFutureExpiry and not provenFuture and not previousFuture then - BNP.svStateByGUID[guid] = nil - BNP.svExpiryByGUID[guid] = nil - if BNP.svRenderRevisionByGUID then BNP.svRenderRevisionByGUID[guid] = nil end - if cache then cache[def.key] = nil end - return - end - - if auraSpellID or texture then - if not state then - state = { - present = true, - lastSeen = now, - } - BNP.svStateByGUID[guid] = state - end - - state.present = true - state.lastSeen = now - state.duration = (liveDuration and liveDuration > 0 and liveDuration) or state.duration or def.duration or 10 - - if state.provenExpires and state.provenExpires <= now then state.provenExpires = nil end - local apiExpires = liveExpires and liveExpires > now and liveExpires or nil - local chosenExpires = apiExpires - if state.provenExpires and (not chosenExpires or state.provenExpires > chosenExpires) then - chosenExpires = state.provenExpires - end - if not chosenExpires and previousExpires and previousExpires > now then chosenExpires = previousExpires end - if not chosenExpires then - chosenExpires = now + state.duration - state.estimated = true - else - state.estimated = nil - end - - state.expires = chosenExpires - state.apiExpires = apiExpires - if previousExpires and chosenExpires > previousExpires + 0.05 then - state.lastRefresh = now - BNP.svRenderRevisionByGUID[guid] = (BNP.svRenderRevisionByGUID[guid] or 0) + 1 - state.renderRevision = BNP.svRenderRevisionByGUID[guid] - RaidTrace("SV_CLASSICAPI_REFRESH", guid, def.key, auraSpellID or 17794) - end - - state.spellID = auraSpellID or state.spellID or 17794 - state.texture = texture or state.texture or def.texture - state.stacks = tonumber(liveStacks) or state.stacks or 0 - BNP.svExpiryByGUID[guid] = state.expires - - if not cache then - cache = {} - BNP.guidAuras[guid] = cache - end - local aura = cache[def.key] - if not aura then - cache._nextOrder = (cache._nextOrder or 0) + 1 - aura = { order = cache._nextOrder } - cache[def.key] = aura - end - - aura.sharedLive = true - aura.spellID = state.spellID - aura.texture = state.texture - aura.stacks = state.stacks - aura.duration = state.duration - aura.expires = state.expires - aura.confirmedAt = aura.confirmedAt or now - aura.sharedLastSeen = now - aura.sharedEstimated = state.estimated and true or nil - return - end - - -- One empty SuperWoW/UnitDebuff snapshot is not authoritative. Only remove - -- the shared state after no token has positively seen it for a short grace. - if state and now - (state.lastSeen or 0) >= SV_SHARED_MISSING_GRACE then - BNP.svStateByGUID[guid] = nil - BNP.svExpiryByGUID[guid] = nil - if BNP.svRenderRevisionByGUID then BNP.svRenderRevisionByGUID[guid] = nil end - if cache then cache[def.key] = nil end - end -end - -local function SyncSharedAuras(unit, guid, now, snapshot) - if not unit or not guid then return end - - local _, def - for _, def in ipairs(AURA_DEFS) do - if def.shared then - local auraSpellID, texture, liveDuration, liveExpires, liveStacks, classicAPIAuthoritative = - FindSharedAuraOnUnit(unit, def, snapshot) - - -- Shadow Vulnerability is deliberately isolated from the generic shared - -- aura timer path. Its GUID state is the only owner of its countdown. - if def.key == "shadow_vulnerability" then - SyncShadowVulnerabilityPresence( - unit, guid, now, def, auraSpellID, texture, liveStacks, liveDuration, - liveExpires, classicAPIAuthoritative - ) - else - local cache = BNP.guidAuras[guid] - - if auraSpellID or texture then - if not cache then - cache = {} - BNP.guidAuras[guid] = cache - end - - local aura = cache[def.key] - -- Never overwrite an accurately timed own aura with the shared/live - -- fallback. Shared entries exist only when we don't own the timer. - if not aura or aura.sharedLive then - if not aura then - cache._nextOrder = (cache._nextOrder or 0) + 1 - aura = { order = cache._nextOrder } - end - - aura.sharedLive = true - aura.spellID = auraSpellID - aura.texture = texture or def.texture - aura.confirmedAt = now - aura.sharedLastSeen = now - - local previousStacks = tonumber(aura.stacks) or 0 - local currentStacks = tonumber(liveStacks) or 0 - if currentStacks > previousStacks and aura.expires then - aura.duration = def.duration or aura.duration or 10 - aura.expires = now + aura.duration - aura.sharedEstimated = true - RaidTrace("SHARED_STACK_REFRESH", guid, def.key, auraSpellID) - end - aura.stacks = currentStacks - - if liveExpires and liveExpires > now then - aura.duration = liveDuration - aura.expires = liveExpires - aura.sharedEstimated = nil - elseif not aura.expires then - aura.duration = def.duration or 120 - aura.expires = now + aura.duration - aura.sharedEstimated = true - end - - cache[def.key] = aura - end - elseif cache and cache[def.key] and cache[def.key].sharedLive then - local aura = cache[def.key] - local lastSeen = aura.sharedLastSeen or aura.confirmedAt or 0 - if now - lastSeen >= 0.40 then cache[def.key] = nil end - end - end - end - end -end - - --- Live aura removal --------------------------------------------------------- --- Timers alone are not enough for CC/roots/traps: effects may be dispelled, --- broken by damage, removed by trinkets, or otherwise end early. --- --- We NEVER create auras from this scan. It may only remove already-confirmed --- own auras from the GUID cache when their exact spell ID is no longer present. -local REMOVAL_SCAN_INTERVAL = 0.10 -local REMOVAL_GRACE = 0.35 -local NORMAL_REMOVAL_MISSING_SCANS = 4 --- Projected non-target nameplate tokens can briefly return an incomplete --- UnitDebuff list when many plates overlap in raids. CC used to be deleted on --- the first such miss, which made Banish/roots/stuns blink. Require a few --- consecutive misses for non-target CC while keeping the real target strict. -local EXACT_NON_TARGET_MISSING_SCANS = 3 - -local function CacheHasTrackedAuras(cache) - if not cache then return false end - local key, aura - for key, aura in pairs(cache) do - if type(aura) == "table" and aura.spellID and aura.expires then - return true - end - end - return false -end - -local CHANNEL_LIVE_REMOVAL = { - drain_life=true, drain_soul=true, - mind_flay=true, starshards=true, mind_control=true, -} - -local EXACT_LIVE_REMOVAL = { - -- Warlock - fear=true, howl_of_terror=true, banish=true, death_coil=true, - -- Hunter - freezing_trap_effect=true, improved_wing_clip=true, improved_concussive_shot=true, - scatter_shot=true, wyvern_sting=true, entrapment=true, counterattack=true, - intimidation=true, scare_beast=true, - -- Mage - polymorph=true, frost_nova=true, counterspell_silence=true, impact=true, frostbite=true, - -- Priest - psychic_scream=true, silence=true, blackout=true, shackle_undead=true, mind_control=true, - -- Paladin - hammer_of_justice=true, repentance=true, turn_undead=true, - -- Rogue - gouge=true, kidney_shot=true, cheap_shot=true, sap=true, blind=true, kick_silenced=true, - -- Druid - entangling_roots=true, bash=true, pounce=true, hibernate=true, feral_charge_effect=true, - -- Warrior - improved_hamstring=true, intimidating_shout=true, disarm=true, charge_stun=true, - intercept_stun=true, concussion_blow=true, revenge_stun=true, pummel=true, shield_bash=true, - -- Shaman / custom roots - earthgrab=true, -} - -local function NeedsExactLiveRemoval(key) - return EXACT_LIVE_REMOVAL[key] and true or false -end - -local function IsCrowdControlDef(def) - return def and EXACT_LIVE_REMOVAL[def.key] and true or false -end - -local function GetEntryIconSize(entry, forceCC) - if forceCC or (entry and IsCrowdControlDef(entry.def)) then - return GetCCIconSize() - end - return GetIconSize() -end - -local function ApplyIconDimensions(icon, size) - if not icon or not size then return end - if icon.BNPLayoutSize ~= size then - icon:SetWidth(size) - icon:SetHeight(size) - icon.BNPLayoutSize = size - end -end - -local function LayoutActiveIcons(container, active, visibleCount, forceCC) - if not container then return end - local position = forceCC and GetCCPosition() or GetDebuffPosition() - local sizes = {} - local totalWidth = 0 - local maxSize = 0 - local i - - for i = 1, visibleCount do - local size = GetEntryIconSize(active[i], forceCC) - sizes[i] = size - totalWidth = totalWidth + size - if size > maxSize then maxSize = size end - end - if visibleCount > 1 then totalWidth = totalWidth + ICON_SPACING * (visibleCount - 1) end - - local maxConfigured = math.max(GetIconSize(), GetCCIconSize()) - local maxWidth = maxConfigured * MAX_VISIBLE_ICONS + ICON_SPACING * (MAX_VISIBLE_ICONS - 1) - container:SetWidth(maxWidth) - container:SetHeight(maxSize > 0 and maxSize or maxConfigured) - - local cursor - if position == "left" or position == "bottom_right" or position == "top_right" then - cursor = 0 - for i = 1, visibleCount do - local icon = container.icons[i] - local size = sizes[i] - ApplyIconDimensions(icon, size) - icon:ClearAllPoints() - icon:SetPoint("RIGHT", container, "RIGHT", -cursor, 0) - cursor = cursor + size + ICON_SPACING - end - elseif position == "right" or position == "bottom_left" or position == "top_left" then - cursor = 0 - for i = 1, visibleCount do - local icon = container.icons[i] - local size = sizes[i] - ApplyIconDimensions(icon, size) - icon:ClearAllPoints() - icon:SetPoint("LEFT", container, "LEFT", cursor, 0) - cursor = cursor + size + ICON_SPACING - end - else - cursor = (maxWidth - totalWidth) / 2 - for i = 1, visibleCount do - local icon = container.icons[i] - local size = sizes[i] - ApplyIconDimensions(icon, size) - icon:ClearAllPoints() - icon:SetPoint("LEFT", container, "LEFT", cursor, 0) - cursor = cursor + size + ICON_SPACING - end - end -end - -local function ShouldDisplayAuraDef(def) - if IsCrowdControlDef(def) then - if BNP.AreCrowdControlEnabled and not BNP:AreCrowdControlEnabled() then return false end - if UseDedicatedCCContainer() then return false end - return true - end - return not BNP.AreDebuffsEnabled or BNP:AreDebuffsEnabled() -end - - --- Separate CC display ------------------------------------------------------- --- This layer is intentionally independent from guidAuras. It may display live --- CCs from other players, but it can never create, refresh, remove or alter an --- own DoT/debuff timer. -local GLOBAL_CC_DEFS = {} -local GLOBAL_CC_BY_ID = {} -local GLOBAL_CC_NEGATIVE = {} -local seenGlobalCC = {} - -local function BuildGlobalCCDefs() - local _, defs, _, ccDef, _, sid - for _, defs in pairs(CLASS_AURAS) do - for _, ccDef in ipairs(defs or {}) do - if IsCrowdControlDef(ccDef) and not seenGlobalCC[ccDef.key] then - seenGlobalCC[ccDef.key] = true - table.insert(GLOBAL_CC_DEFS, ccDef) - if ccDef.spellIDs then - for _, sid in ipairs(ccDef.spellIDs) do GLOBAL_CC_BY_ID[sid] = ccDef end - end - if ccDef.durations then - for sid in pairs(ccDef.durations) do GLOBAL_CC_BY_ID[sid] = ccDef end - end - end - end - end -end -BuildGlobalCCDefs() - -local function ResolveGlobalCCDef(spellID, texture) - if spellID and GLOBAL_CC_BY_ID[spellID] then return GLOBAL_CC_BY_ID[spellID] end - if spellID and GLOBAL_CC_NEGATIVE[spellID] then return nil end - - local auraName = spellID and SpellInfo and SpellInfo(spellID) or nil - local _, ccDef - for _, ccDef in ipairs(GLOBAL_CC_DEFS) do - if NameMatches(ccDef, auraName) - or (texture and ccDef.textureMatch and string.find(string.lower(texture), ccDef.textureMatch)) then - if spellID then GLOBAL_CC_BY_ID[spellID] = ccDef end - return ccDef - end - end - - if spellID then GLOBAL_CC_NEGATIVE[spellID] = true end - return nil -end - -local function WantsForeignCCs() - return BNP.AreCrowdControlEnabled and BNP:AreCrowdControlEnabled() - and BNP.ShowOtherPlayersCCs and BNP:ShowOtherPlayersCCs() -end - -local function SyncForeignCCs(unit, guid, now) - if not unit or not guid then return end - if not WantsForeignCCs() then - BNP.guidLiveCCs[guid] = nil - return - end - - local seen = {} - local live = BNP.guidLiveCCs[guid] - local i - for i = 1, 64 do - local texture, stacks, dtype, auraSpellID = UnitDebuff(unit, i) - if not texture then break end - - local ccDef = ResolveGlobalCCDef(auraSpellID, texture) - if ccDef then - seen[ccDef.key] = true - if not live then - live = {} - BNP.guidLiveCCs[guid] = live - end - - local aura = live[ccDef.key] - if not aura then - aura = { firstSeen = now, order = now } - live[ccDef.key] = aura - end - aura.def = ccDef - aura.spellID = auraSpellID - aura.texture = texture or ccDef.texture - aura.stacks = tonumber(stacks) or 0 - aura.lastSeen = now - aura.missingScans = nil - -- Vanilla/SuperWoW does not reliably provide foreign application time. - -- Use a conservative local estimate for text only; live aura presence is - -- authoritative for appearance/removal. - if not aura.expires and ccDef.duration then - aura.expires = now + ccDef.duration - end - end - end - - if live then - local key, aura - for key, aura in pairs(live) do - if not seen[key] then - aura.missingScans = (aura.missingScans or 0) + 1 - -- A single empty projected UnitDebuff scan is common with tightly - -- stacked raid nameplates. Keep the last positive CC sighting through - -- two misses so foreign Banish/CC does not visibly blink. - if aura.missingScans >= 3 and now - (aura.lastSeen or 0) >= 0.40 then - live[key] = nil - end - else - aura.missingScans = nil - end - end - end -end - -local function HideCCRow(plate) - local container = plate and plate.BNPCCContainer - if not container then return end - local i - for i = 1, table.getn(container.icons or {}) do - local icon = container.icons[i] - icon.lastTimerText = nil - icon.timer:SetText("") - if icon.stack then icon.stack:SetText("") end - icon:Hide() - end - container:Hide() -end - -local function FormatCCTimer(remaining) - if not remaining or remaining <= 0 then return "" end - if remaining >= 3600 then - return math.ceil(remaining / 3600) .. "h" - elseif remaining >= 60 then - return math.ceil(remaining / 60) .. "m" - end - return tostring(math.ceil(remaining)) -end - -local function UpdateCCRow(plate, guid, cache, now) - if not UseDedicatedCCContainer() - or (BNP.AreCrowdControlEnabled and not BNP:AreCrowdControlEnabled()) then - HideCCRow(plate) - return - end - - if not plate.BNPCCContainer then CreateCCContainer(plate) end - local container = plate.BNPCCContainer - if not container then return end - - local active = container.activeAuras - if not active then active = {}; container.activeAuras = active end - local pool = container.activeAuraPool - if not pool then - pool = {} - for i = 1, MAX_VISIBLE_ICONS do pool[i] = {} end - container.activeAuraPool = pool - end - local oldCount = table.getn(active) - for i = 1, oldCount do active[i] = nil end - local count = 0 - local seenOwn = {} - - local _, ccDef - for _, ccDef in ipairs(AURA_DEFS) do - if IsCrowdControlDef(ccDef) then - local aura = cache and cache[ccDef.key] - local remaining = aura and aura.expires and (aura.expires - now) or nil - local sharedLive = aura and aura.sharedLive - if aura and (sharedLive or (remaining and remaining > 0)) then - count = count + 1 - if count <= MAX_VISIBLE_ICONS then - local entry = pool[count] - active[count] = entry - entry.def = ccDef - entry.aura = aura - entry.remaining = remaining - seenOwn[ccDef.key] = true - end - end - end - end - - if WantsForeignCCs() then - local live = guid and BNP.guidLiveCCs[guid] - local key, aura - for key, aura in pairs(live or {}) do - if count >= MAX_VISIBLE_ICONS then break end - if aura.def and not seenOwn[key] and now - (aura.lastSeen or 0) <= 0.60 then - count = count + 1 - local entry = pool[count] - active[count] = entry - entry.def = aura.def - entry.aura = aura - entry.remaining = aura.expires and (aura.expires - now) or nil - end - end - end - - table.sort(active, function(a, b) - return (a.aura.order or a.aura.firstSeen or 0) < (b.aura.order or b.aura.firstSeen or 0) - end) - - local visibleCount = table.getn(active) - local iconSize = GetCCIconSize() - local position = GetCCPosition() - local layoutChanged = container.BNPLastVisibleCount ~= visibleCount - or container.BNPLastCCIconSize ~= iconSize - or container.BNPLastPosition ~= position - if layoutChanged then - container.BNPLastVisibleCount = visibleCount - container.BNPLastCCIconSize = iconSize - container.BNPLastPosition = position - if visibleCount > 0 then LayoutActiveIcons(container, active, visibleCount, true) end - end - - local i - for i = 1, table.getn(container.icons) do - local icon = container.icons[i] - local entry = active[i] - if entry then - icon.texture:SetTexture(entry.aura.texture or entry.def.texture) - local stackCount = tonumber(entry.aura.stacks) or 0 - if icon.stack then - if stackCount > 1 then icon.stack:SetText(tostring(stackCount)) else icon.stack:SetText("") end - end - local timerText = FormatCCTimer(entry.remaining) - if icon.lastTimerText ~= timerText then icon.timer:SetText(timerText); icon.lastTimerText = timerText end - icon:Show() - else - icon.lastTimerText = nil - icon.timer:SetText("") - if icon.stack then icon.stack:SetText("") end - icon:Hide() - end - end - - if visibleCount > 0 then container:Show() else container:Hide() end -end - -local function SyncAuraRemoval(unit, guid, now, snapshot) - if not unit or not guid then return end - if AuraCacheProtected(guid, now) then return end - local cache = BNP.guidAuras[guid] - if not CacheHasTrackedAuras(cache) then return end - - local present = {} - local i - local maxIndex = snapshot and snapshot.count or 64 - for i = 1, maxIndex do - local texture, stacks, dtype, auraSpellID - if snapshot then - local entry = snapshot.entries[i] - if not entry then break end - texture, stacks, dtype, auraSpellID = entry[1], entry[2], entry[3], entry[4] - else - texture, stacks, dtype, auraSpellID = UnitDebuff(unit, i) - if not texture then break end - end - - if auraSpellID then - present[auraSpellID] = true - - local trackedKey, trackedAura - for trackedKey, trackedAura in pairs(cache) do - if type(trackedAura) == "table" and trackedAura.spellID == auraSpellID then - trackedAura.stacks = tonumber(stacks) or 0 - end - end - end - end - - -- Resolve current-target identity once per scan, not once per tracked aura. - -- Knowing the target GUID for projected units also lets exact CC removal stay - -- immediate on the real target but conservative on crowded non-target plates. - local currentTargetGUID = GetTargetGUID() - local playerRemoval = false - if unit == "target" and UnitIsPlayer and UnitIsPlayer("target") then - playerRemoval = currentTargetGUID and currentTargetGUID == guid - end - - local key, aura - for key, aura in pairs(cache) do - if type(aura) == "table" and aura.spellID and aura.expires and key ~= "shadow_vulnerability" then - -- Give the client a tiny grace period immediately after confirmation so - -- transient aura-list updates cannot remove a freshly-landed effect. - local age = now - (aura.confirmedAt or 0) - local exactRemoval = NeedsExactLiveRemoval(key) - - -- Normal DoTs on player nameplates are only safe to live-remove from the - -- CURRENT target. During target switches, non-target SuperWoW nameplate - -- tokens can briefly expose an incomplete/empty UnitDebuff list. Treating - -- that as a dispel caused valid DoTs to disappear from the GUID cache. - -- - -- CC/root/trap effects still use live removal on every visible plate. The - -- real target is immediate; projected non-targets require a few misses so - -- one crowded-raid scan cannot make Banish/CC blink. - -- Channeled debuffs should end when the channel is interrupted/cancelled, - -- but only trust the real current-target UnitDebuff list for this. Busy - -- non-target nameplate tokens remain intentionally non-authoritative. - local channelRemoval = CHANNEL_LIVE_REMOVAL[key] - and currentTargetGUID and currentTargetGUID == guid - - if age >= REMOVAL_GRACE then - if present[aura.spellID] then - -- Positive live sighting upgrades a CAST-seeded timer. Once upgraded, - -- normal dispel/early-removal logic is safe again. - aura.missingScans = nil - aura.exactMissingScans = nil - if aura.castPrimary then - aura.castPrimary = nil - aura.castPrimaryAt = nil - aura.liveConfirmed = true - end - elseif channelRemoval then - RaidTrace("CHANNEL_REMOVE", guid, key, aura.spellID) - cache[key] = nil - elseif exactRemoval then - if currentTargetGUID and currentTargetGUID == guid then - RaidTrace("REMOVE", guid, key, aura.spellID) - -- The real target unit is authoritative, so CC can still disappear - -- immediately when it is broken/dispelled. - cache[key] = nil - else - -- Non-target SuperWoW tokens can return one empty/incomplete aura - -- list while plates overlap. Do not turn that one bad sample into - -- a visible Banish/CC blink. - aura.exactMissingScans = (aura.exactMissingScans or 0) + 1 - if aura.exactMissingScans >= EXACT_NON_TARGET_MISSING_SCANS then - RaidTrace("REMOVE", guid, key, aura.spellID) - cache[key] = nil - end - end - elseif playerRemoval then - if aura.castPrimary then - -- CAST-seeded auras may be omitted by crowded raid UnitDebuff lists. - -- Do not use that same missing list to remove it early. - aura.missingScans = nil - else - -- Normal positively-confirmed player DoTs/curses use conservative - -- removal and still react to real dispels. - aura.missingScans = (aura.missingScans or 0) + 1 - - if aura.missingScans >= NORMAL_REMOVAL_MISSING_SCANS then - RaidTrace("REMOVE", guid, key, aura.spellID) - cache[key] = nil - end - end - end - end - end - end -end - -local function FormatTimer(remaining) - if not remaining or remaining <= 0 then return "" end - - -- Keep long durations compact on the small nameplate icons. - -- Examples: 300s -> 5m, 61s -> 2m, 59s -> 59. - if remaining >= 3600 then - return math.ceil(remaining / 3600) .. "h" - elseif remaining >= 60 then - return math.ceil(remaining / 60) .. "m" - end - - return tostring(math.ceil(remaining)) -end - --- Nameplate GUID stability --------------------------------------------------- --- Blizzard/SuperWoW can briefly recycle a visible plate while LOS/occlusion --- changes (for example when a wall is between the player and nearby mobs). --- During that tiny transition plate:GetName(1) may still expose the previous --- unit GUID. Rendering that GUID's cache causes "ghost DoTs" on an unrelated --- mob for a frame or two. --- --- Require the same GUID on two consecutive renderer updates before using its --- aura cache. This does not change aura tracking itself; it only gates display. -local AURA_GUID_TRANSIENT_HOLD = 0.75 -local AURA_GUID_CONFIRM_UPDATES = 2 -local AURA_GUID_SWITCH_CONFIRM_UPDATES = 8 - -local function PinExactTargetPlate(plate, now) - if not plate or not C_NamePlate or - type(C_NamePlate.GetNamePlateForUnit) ~= "function" then - return nil - end - - local targetGUID = GetTargetGUID() - if not targetGUID then return nil end - - local ok, targetPlate = pcall(C_NamePlate.GetNamePlateForUnit, "target") - if not ok or not targetPlate or targetPlate ~= plate then return nil end - - -- The real target is the one projected unit ClassicAPI can resolve exactly. - -- Pinning it here prevents a crowded SuperWoW plate token from replacing the - -- target GUID for a few renderer ticks. When the player switches away, this - -- stable mapping remains useful for that now-non-target plate. - plate.BNPAuraGUIDStable = targetGUID - plate.BNPAuraGUIDCandidate = targetGUID - plate.BNPAuraGUIDCandidateCount = AURA_GUID_SWITCH_CONFIRM_UPDATES - plate.BNPAuraGUIDLastGoodAt = now - plate.BNPAuraForceFreshIdentity = nil - return targetGUID -end - -local function StableNameStillMatches(plate, stableGUID) - if not plate or not stableGUID or not plate.name or not plate.name.GetText then - return true - end - local oldName = BNP.guidNames and BNP.guidNames[stableGUID] - local visibleName = plate.name:GetText() - if oldName and visibleName and oldName ~= "" and visibleName ~= "" then - return oldName == visibleName - end - return true -end - -local function GetStablePlateGUID(plate, now) - if not plate then return nil end - now = now or GetTime() - - -- Exact current-target frame identity always wins over projected token data. - local exactTargetGUID = PinExactTargetPlate(plate, now) - if exactTargetGUID then return exactTargetGUID end - - local guid = GetPlateGUID(plate) - local stable = plate.BNPAuraGUIDStable - - -- Dense 40-player stacks can briefly lose the projected token. Keep the last - -- confirmed identity long enough to bridge those gaps instead of hiding the - -- whole icon row for one or two renderer ticks. - if not guid then - plate.BNPAuraGUIDCandidate = nil - plate.BNPAuraGUIDCandidateCount = 0 - if stable and plate.BNPAuraGUIDLastGoodAt and - now - plate.BNPAuraGUIDLastGoodAt <= AURA_GUID_TRANSIENT_HOLD and - not plate.BNPAuraForceFreshIdentity then - return stable - end - return nil - end - - if stable and guid == stable then - plate.BNPAuraGUIDCandidate = guid - plate.BNPAuraGUIDCandidateCount = AURA_GUID_SWITCH_CONFIRM_UPDATES - plate.BNPAuraGUIDLastGoodAt = now - plate.BNPAuraForceFreshIdentity = nil - return stable - end - - if plate.BNPAuraGUIDCandidate ~= guid then - plate.BNPAuraGUIDCandidate = guid - plate.BNPAuraGUIDCandidateCount = 1 - - -- If Blizzard really reused the frame for a differently named unit, switch - -- quickly. Same-name raid mobs are exactly where transient GUID/token swaps - -- happen, so keep the previous stable GUID until the new candidate persists. - if stable and not plate.BNPAuraForceFreshIdentity and - StableNameStillMatches(plate, stable) then - return stable - end - return nil - end - - plate.BNPAuraGUIDCandidateCount = (plate.BNPAuraGUIDCandidateCount or 1) + 1 - - local required = AURA_GUID_SWITCH_CONFIRM_UPDATES - if plate.BNPAuraForceFreshIdentity or - (stable and not StableNameStillMatches(plate, stable)) then - required = AURA_GUID_CONFIRM_UPDATES - end - - if plate.BNPAuraGUIDCandidateCount < required then - if stable and not plate.BNPAuraForceFreshIdentity and - StableNameStillMatches(plate, stable) then - return stable - end - return nil - end - - plate.BNPAuraGUIDStable = guid - plate.BNPAuraGUIDLastGoodAt = now - plate.BNPAuraForceFreshIdentity = nil - return guid -end - --- Shared identity helper for sibling nameplate modules (for example PvP --- immunities). Reusing the same stabilized GUID prevents those modules from --- reintroducing the dense-raid/nameplate-token flicker fixed above. -BNP.GetStablePlateAuraGUID = GetStablePlateGUID - -local function UpdatePlate(plate, now) - if BNP.AreAnyAurasEnabled and not BNP:AreAnyAurasEnabled() then - if plate and plate.BNPAuraContainer then - local i - for i = 1, MAX_VISIBLE_ICONS do - local icon = plate.BNPAuraContainer.icons and plate.BNPAuraContainer.icons[i] - if icon then icon:Hide() end - end - plate.BNPAuraContainer:Hide() - end - HideCCRow(plate) - return - end - - if not plate or not plate:IsShown() then - HidePlateAuraFrames(plate) - return - end - if not plate.BNPAuraContainer then CreateAuraContainer(plate) end - - local container = plate.BNPAuraContainer - if not container then return end - - -- Nameplate frames are recycled. Re-anchor only when the hidden-name state - -- changes so Player/NPC hide options immediately compact Top aura rows. - local nameHidden = (BNP.IsPlateNameHidden and BNP:IsPlateNameHidden(plate)) or (plate.BNPNameHidden and true or false) - if plate.BNPAuraLastNameHidden ~= nameHidden then - plate.BNPAuraLastNameHidden = nameHidden - AnchorAuraContainer(plate, plate.BNPAuraContainer, false) - if plate.BNPCCContainer then AnchorAuraContainer(plate, plate.BNPCCContainer, true) end - end - - local guid = GetStablePlateGUID(plate, now) - if guid and plate.name and plate.name.GetText then - local visibleName = plate.name:GetText() - if visibleName and visibleName ~= "" then BNP.guidNames[guid] = visibleName end - end - now = now or GetTime() - - -- A full-immunity cleanse (Divine Shield / Ice Block) is authoritative. - -- Hide both harmful rows immediately and do not let timer/cast caches render - -- again until the short suppression window is no longer being renewed by - -- the immunity module. - if guid and HarmfulAuraSuppressed(guid, now) then - ClearHarmfulAuraStateOnly(guid) - HidePlateAuraFrames(plate) - return - end - - local cache = guid and BNP.guidAuras[guid] or nil - - -- Reuse one small table per plate. Active auras are packed without gaps and - -- sorted by the order in which they were first applied to this GUID. - local active = container.activeAuras - if not active then - active = {} - container.activeAuras = active - end - local activePool = container.activeAuraPool - if not activePool then - activePool = {} - for i = 1, MAX_VISIBLE_ICONS do activePool[i] = {} end - container.activeAuraPool = activePool - end - local previousCount = table.getn(active) - for i = 1, previousCount do active[i] = nil end - local activeCount = 0 - - for _, def in ipairs(AURA_DEFS) do - if ShouldDisplayAuraDef(def) then - local aura = cache and cache[def.key] - local remaining = aura and aura.expires and (aura.expires - now) or nil - local sharedLive = aura and aura.sharedLive - local svRenderRevision = nil - - if def.key == "shadow_vulnerability" and guid then - local svState = BNP.svStateByGUID[guid] - if svState and svState.present then - -- Shadow Vulnerability rendering is owned entirely by svState. - -- Do not require the mirrored generic cache entry to already exist: - -- rebuild it from state if another cleanup path removed it. - if not cache then - cache = {} - BNP.guidAuras[guid] = cache - end - aura = cache[def.key] - if not aura then - cache._nextOrder = (cache._nextOrder or 0) + 1 - aura = { order = cache._nextOrder } - cache[def.key] = aura - end - - aura.sharedLive = true - aura.sharedEstimated = true - aura.spellID = svState.spellID or aura.spellID or 17794 - aura.texture = svState.texture or aura.texture or def.texture - aura.stacks = tonumber(svState.stacks) or aura.stacks or 0 - aura.duration = def.duration or 10 - aura.expires = svState.expires - - remaining = svState.expires and (svState.expires - now) or nil - sharedLive = true - svRenderRevision = BNP.svRenderRevisionByGUID[guid] or 0 - else - remaining = nil - sharedLive = false - aura = nil - end - end - - if sharedLive or (remaining and remaining > 0) then - if sharedLive and remaining and remaining <= 0 then - remaining = nil - end - if activeCount < MAX_VISIBLE_ICONS then - activeCount = activeCount + 1 - local entry = activePool[activeCount] - active[activeCount] = entry - entry.def = def - entry.aura = aura - entry.remaining = remaining - entry.svRenderRevision = svRenderRevision - end - elseif cache and aura then - cache[def.key] = nil - end - end - end - - if activeCount > 1 then - table.sort(active, function(a, b) - return (a.aura.order or 0) < (b.aura.order or 0) - end) - end - - local visibleCount = activeCount - local iconSize = GetIconSize() - local ccIconSize = GetCCIconSize() - local position = GetDebuffPosition() - local layoutMask = 0 - local maskValue = 1 - for i = 1, visibleCount do - if active[i] and IsCrowdControlDef(active[i].def) then - layoutMask = layoutMask + maskValue - end - maskValue = maskValue * 2 - end - - local layoutChanged = container.BNPLastVisibleCount ~= visibleCount - or container.BNPLastIconSize ~= iconSize - or container.BNPLastCCIconSize ~= ccIconSize - or container.BNPLastPosition ~= position - or container.BNPLastLayoutMask ~= layoutMask - if layoutChanged then - container.BNPLastVisibleCount = visibleCount - container.BNPLastIconSize = iconSize - container.BNPLastCCIconSize = ccIconSize - container.BNPLastPosition = position - container.BNPLastLayoutMask = layoutMask - if visibleCount > 0 then - -- CCs may share the normal row for Left/Right (and optionally Top), so - -- lay out each icon using its own configured size without touching tracking. - LayoutActiveIcons(container, active, visibleCount, false) - end - end - - for i = 1, table.getn(container.icons) do - local icon = container.icons[i] - local entry = active[i] - - if entry then - local texture = entry.aura.texture or entry.def.texture - if icon.BNPLastTexture ~= texture then - icon.texture:SetTexture(texture) - icon.BNPLastTexture = texture - end - - local stackCount = tonumber(entry.aura.stacks) or 0 - local stackText = stackCount > 1 and tostring(stackCount) or "" - if icon.stack and icon.BNPLastStackText ~= stackText then - icon.stack:SetText(stackText) - icon.BNPLastStackText = stackText - end - - local timerText = FormatTimer(entry.remaining) - if entry.def and entry.def.key == "shadow_vulnerability" then - local revision = entry.svRenderRevision or 0 - if icon.BNPSVRenderRevision ~= revision then - -- A confirmed proc/refresh happened. Invalidate the text cache so the - -- refreshed countdown is written by the normal renderer, never from - -- inside a combat-log event callback. - icon.lastTimerText = nil - icon.BNPSVRenderRevision = revision - end - icon.BNPSVRenderedExpires = entry.aura and entry.aura.expires or nil - icon.BNPSVRenderedRemaining = entry.remaining - else - icon.BNPSVRenderRevision = nil - icon.BNPSVRenderedExpires = nil - icon.BNPSVRenderedRemaining = nil - end - - if icon.lastTimerText ~= timerText then - icon.timer:SetText(timerText) - icon.lastTimerText = timerText - end - - icon:Show() - else - icon.lastTimerText = nil - icon.BNPLastTexture = nil - icon.BNPLastStackText = nil - icon.timer:SetText("") - if icon.stack then icon.stack:SetText("") end - icon:Hide() - end - end - - if visibleCount > 0 then - container:Show() - else - container:Hide() - end - - UpdateCCRow(plate, guid, cache, now) - ApplyAuraFrameLevel(plate, container) - ApplyAuraFrameLevel(plate, plate.BNPCCContainer) - ApplyTargetAuraPriority(plate, container) - ApplyTargetAuraPriority(plate, plate.BNPCCContainer) -end - - -function BNP:RefreshAuraLayout(plate) - if not plate then return end - local auraSize = GetIconSize() - local ccSize = GetCCIconSize() - local maxSize = math.max(auraSize, ccSize) - local width = maxSize * MAX_VISIBLE_ICONS + ICON_SPACING * (MAX_VISIBLE_ICONS - 1) - local containers = { plate.BNPAuraContainer, plate.BNPCCContainer } - local index, container - for index, container in ipairs(containers) do - if container then - ApplyAuraFrameLevel(plate, container) - local defaultSize = (index == 2) and ccSize or auraSize - container:SetWidth(width) - container:SetHeight(defaultSize) - container.BNPLastVisibleCount = nil - container.BNPLastIconSize = nil - container.BNPLastCCIconSize = nil - container.BNPLastPosition = nil - container.BNPLastLayoutMask = nil - local i - for i = 1, table.getn(container.icons or {}) do - ApplyIconDimensions(container.icons[i], defaultSize) - end - end - end -end - -function BNP:RefreshAllAuraLayouts() - local plate - for plate in pairs(BNP.plates or {}) do - if plate then - if plate.BNPAuraContainer then - AnchorAuraContainer(plate, plate.BNPAuraContainer, false) - end - if plate.BNPCCContainer then - AnchorAuraContainer(plate, plate.BNPCCContainer, true) - end - end - self:RefreshAuraLayout(plate) - end -end - -ScanSpellbook() -table.insert(BNP.libnameplate.OnInit, CreateAuraContainer) -table.insert(BNP.libnameplate.OnInit, CreateCCContainer) -table.insert(BNP.libnameplate.OnShow, CreateAuraContainer) -table.insert(BNP.libnameplate.OnShow, CreateCCContainer) - --- Keep the proven central update loop and the existing icon implementation. --- No CooldownFrame templates and no frames or tables are created here. -local renderer = CreateFrame("Frame") -local elapsedTotal = 0 -local removalElapsed = 0 -renderer:SetScript("OnUpdate", function() - elapsedTotal = elapsedTotal + arg1 - removalElapsed = removalElapsed + arg1 - if elapsedTotal < UPDATE_INTERVAL then return end - elapsedTotal = 0 - - local now = GetTime() - UpdateDarkHarvest(now) - RefreshTargetPriorityIdentity() - - CleanupAuraCastAttempts(now) - - local hasPending = HasPendingAuras() - local hasPendingAoE = HasPendingAoEAuras() - local doRemovalScan = removalElapsed >= REMOVAL_SCAN_INTERVAL - if doRemovalScan then removalElapsed = 0 end - - local targetGUID = nil - if hasPending or hasPendingAoE or doRemovalScan then - targetGUID = GetTargetGUID() - end - - local targetSuppressed = targetGUID and HarmfulAuraSuppressed(targetGUID, now) - if targetSuppressed then - ClearHarmfulAuraStateOnly(targetGUID) - end - - if hasPending and targetGUID and not targetSuppressed then - -- Current target is the most reliable confirmation source. - ConfirmPendingForUnit("target", targetGUID, now) - end - - if targetGUID and not targetSuppressed then - CleanupDeadUnit("target", targetGUID, now) - end - - if doRemovalScan and targetGUID and not targetSuppressed then - local targetSnapshot = nil - if HAS_SHARED_AURAS or CacheHasTrackedAuras(BNP.guidAuras[targetGUID]) then - targetSnapshot = CaptureLiveDebuffSnapshot("target", now) - end - - SyncSharedAuras("target", targetGUID, now, targetSnapshot) - - if BNP.guidAuras[targetGUID] then - SyncAuraRemoval("target", targetGUID, now, targetSnapshot) - end - end - - -- The normal renderer always runs. Extra GUID/token/UnitDebuff work is only - -- performed while pending casts exist or during the throttled removal scan. - for plate in pairs(BNP.plates) do - if plate:IsShown() and (hasPending or hasPendingAoE or doRemovalScan) then - local guid = GetPlateGUID(plate) - local token = GetUnitTokenForPlate(plate) - - if guid and token then - local guidSuppressed = HarmfulAuraSuppressed(guid, now) - if guidSuppressed then - ClearHarmfulAuraStateOnly(guid) - end - - -- Do NOT call UnitIsDeadOrGhost() on SuperWoW nameplate tokens. - -- During target changes/recycling those tokens can briefly report an - -- invalid dead state and wipe the entire GUID aura cache. - local isCurrentTarget = targetGUID and targetGUID == guid - - -- The real "target" unit was already confirmed above and is more - -- authoritative than its projected nameplate token. Avoid duplicate - -- confirmation work for that same GUID. - if hasPending and not isCurrentTarget and not guidSuppressed then - ConfirmPendingForUnit(token, guid, now) - end - - if hasPendingAoE and not guidSuppressed then - ConfirmAoEAurasOnUnit(token, guid) - end - - if doRemovalScan and not isCurrentTarget and not guidSuppressed then - -- Removal/shared scans are destructive and therefore must never use - -- a raw projected GUID while the renderer still considers the plate - -- identity unstable. This is the key raid-stack safeguard: one stale - -- SuperWoW token can no longer remove auras from the wrong GUID. - local stableGUID = plate.BNPAuraGUIDStable - local identityTrusted = stableGUID and stableGUID == guid - if identityTrusted then - local snapshot = nil - if HAS_SHARED_AURAS or CacheHasTrackedAuras(BNP.guidAuras[stableGUID]) then - snapshot = CaptureLiveDebuffSnapshot(token, now) - end - - SyncSharedAuras(token, stableGUID, now, snapshot) - - -- Non-target exact-removal also requires multiple misses below, so - -- crowded raid aura lists cannot make Banish/CC blink. - SyncAuraRemoval(token, stableGUID, now, snapshot) - end - end - end - end - - UpdatePlate(plate, now) - end - - if hasPending then - CleanupPending(now) - end - - if hasPendingAoE then - CleanupPendingAoE(now) - end -end) - - --- Foreign CC scanner --------------------------------------------------------- --- Runs on its own frame so this optional feature can never interrupt the proven --- own-debuff renderer/update loop. -local FOREIGN_CC_SCAN_INTERVAL = 0.15 -local foreignCCElapsed = 0 -local foreignCCScanner = CreateFrame("Frame") -foreignCCScanner:SetScript("OnUpdate", function() - if not WantsForeignCCs() then return end - foreignCCElapsed = foreignCCElapsed + arg1 - if foreignCCElapsed < FOREIGN_CC_SCAN_INTERVAL then return end - foreignCCElapsed = 0 - - local now = GetTime() - local targetGUID = GetTargetGUID() - if targetGUID then pcall(SyncForeignCCs, "target", targetGUID, now) end - - local plate - for plate in pairs(BNP.plates or {}) do - if plate and plate:IsShown() then - local guid = GetPlateGUID(plate) - if guid and guid ~= targetGUID then - local token = GetUnitTokenForPlate(plate) - if token then pcall(SyncForeignCCs, token, guid, now) end - end - end - end -end) - - --- Shadow Vulnerability [17794/17797/17798/17799/17800]: shared Warlock raid debuff ------------------- --- Improved Shadow Bolt's Shadow Vulnerability belongs to the TARGET, not to a --- single BNP player. If any Warlock procs any Shadow Vulnerability rank, every Warlock running BNP --- should display the same aura. --- --- Tracking sources: --- * SyncSharedAuras() is authoritative for appearance/removal and scans the --- real target plus visible SuperWoW nameplate tokens every 0.10s. --- * RAW_COMBATLOG is the primary refresh source for other Warlocks. SuperWoW --- puts GUIDs into the raw combat text, so an actual Shadow Vulnerability --- application can be tied to the exact target without guessing by name. --- * UNIT_CASTEVENT for any SV rank remains as a harmless compatibility --- fast-path for clients/servers that publish the proc itself as a cast event. --- * Charge/stack increases are used as an additional live fallback. --- --- No caster ownership is stored or required here. -local SHADOW_VULNERABILITY_IDS = { - [17794] = true, -- Rank 1 - [17797] = true, -- Rank 2 - [17798] = true, -- Rank 3 - [17799] = true, -- Rank 4 - [17800] = true, -- Rank 5 -} - -local function IsShadowVulnerabilitySpellID(spellID) - local sid = tonumber(spellID) - if sid and SHADOW_VULNERABILITY_IDS[sid] then return true end - - -- Custom 1.12 servers may keep the visible aura on the classic ID while - -- publishing a hidden proc/cast event on a custom spell ID. Accept that - -- event by its exact localized spell name as well. - if sid and SpellInfo then - local name = SpellInfo(sid) - if name == "Shadow Vulnerability" or name == "Schattenverwundbarkeit" then - return true - end - end - return false -end - --- Small diagnostic state for the one thing vanilla does not expose directly: --- the remaining duration of hostile debuffs. This lets /bnp raidcheck show --- whether a refresh produced any observable client-side signal (stack change, --- UNIT_AURA, hidden UNIT_CASTEVENT, or changed UnitDebuff return values). -BNP.svProbe = BNP.svProbe or { events = {} } -BNP.svProbe.events = BNP.svProbe.events or {} -BNP.svProbe.raw = BNP.svProbe.raw or {} -local function SVProbeEvent(stage, guid, spellID, extra) - local t = BNP.svProbe.events - table.insert(t, { - time = GetTime(), stage = stage, guid = guid, spellID = spellID, extra = extra, - }) - while table.getn(t) > 18 do table.remove(t, 1) end -end - -local function SnapshotSVUnit(unit, guid, reason) - if not unit then return end - local i - for i = 1, 64 do - local a1,a2,a3,a4,a5,a6,a7,a8,a9,a10 = UnitDebuff(unit, i) - if not a1 then break end - if IsShadowVulnerabilitySpellID(a4) then - BNP.svProbe.last = { - time = GetTime(), reason = reason, unit = unit, guid = guid, index = i, - spellID = a4, stacks = a2, a3 = a3, a5 = a5, a6 = a6, a7 = a7, - a8 = a8, a9 = a9, a10 = a10, - } - return - end - end -end - -local function SVDef() - local i - for i = 1, table.getn(AURA_DEFS) do - if AURA_DEFS[i] and AURA_DEFS[i].key == "shadow_vulnerability" then - return AURA_DEFS[i] - end - end -end - -local function FindSharedShadowVulnerabilityByGUID(guid, def) - if not guid or not def then return nil, nil, nil, nil end - - local targetGUID = GetTargetGUID() - if targetGUID and targetGUID == guid then - local spellID, texture, duration, expires, stacks = FindSharedAuraOnUnit("target", def) - if spellID or texture then return spellID, texture, duration, expires, stacks end - end - - local plate - for plate in pairs(BNP.plates or {}) do - if plate and plate:IsShown() and GetPlateGUID(plate) == guid then - local token = GetUnitTokenForPlate(plate) - if token then - local spellID, texture, duration, expires, stacks = FindSharedAuraOnUnit(token, def) - if spellID or texture then return spellID, texture, duration, expires, stacks end - end - end - end - - return nil, nil, nil, nil, nil -end - -local function RefreshSharedShadowVulnerability(guid) - if playerClass ~= "WARLOCK" or not guid then return false end - local def = SVDef() - if not def then return false end - - local now = GetTime() - local liveSpellID, texture, liveDuration, liveExpires, liveStacks = - FindSharedShadowVulnerabilityByGUID(guid, def) - - BNP.guidAuras[guid] = BNP.guidAuras[guid] or {} - local cache = BNP.guidAuras[guid] - local aura = cache[def.key] - if not aura then - cache._nextOrder = (cache._nextOrder or 0) + 1 - aura = { order = cache._nextOrder } - cache[def.key] = aura - end - - aura.sharedLive = true - aura.sharedEstimated = nil - aura.spellID = liveSpellID or aura.spellID or 17794 - aura.texture = texture or aura.texture or def.texture - aura.confirmedAt = now - aura.stacks = tonumber(liveStacks) or aura.stacks or 0 - if liveSpellID or texture then aura.sharedLastSeen = now end - - -- A real RAW_COMBATLOG re-application is proof of a fresh duration. Prefer - -- ClassicAPI's absolute expiration when it has already updated; otherwise - -- keep the proven local now+duration value until the API catches up. - aura.duration = (liveDuration and liveDuration > 0 and liveDuration) or def.duration or 10 - local state = BNP.svStateByGUID[guid] - if not state then - state = {} - BNP.svStateByGUID[guid] = state - end - state.present = true - state.duration = aura.duration - state.provenExpires = now + aura.duration - state.rawRefreshAt = now - local apiExpires = liveExpires and liveExpires > now and liveExpires or nil - state.apiExpires = apiExpires - if apiExpires and apiExpires > state.provenExpires then - state.expires = apiExpires - else - state.expires = state.provenExpires - end - state.lastRefresh = now - BNP.svRenderRevisionByGUID[guid] = (BNP.svRenderRevisionByGUID[guid] or 0) + 1 - state.renderRevision = BNP.svRenderRevisionByGUID[guid] - if liveSpellID or texture then state.lastSeen = now end - state.spellID = liveSpellID or state.spellID or aura.spellID or 17794 - state.texture = texture or state.texture or aura.texture or def.texture - state.stacks = tonumber(liveStacks) or state.stacks or 0 - - BNP.svExpiryByGUID[guid] = state.expires - aura.spellID = state.spellID - aura.texture = state.texture - aura.stacks = state.stacks - aura.expires = state.expires - aura.sharedEstimated = nil - - RaidTrace("SV_SHARED_REFRESH", guid, def.key, aura.spellID or 17794) - return true -end - --- SuperWoW's raw event contains the exact affected GUID. Unlike cast/channel --- heuristics, an explicit "is afflicted by Shadow Vulnerability" line proves --- that the proc really happened, including procs caused by another Warlock. -local svRawEvent = CreateFrame("Frame") -pcall(svRawEvent.RegisterEvent, svRawEvent, "RAW_COMBATLOG") -svRawEvent:SetScript("OnEvent", function() - if playerClass ~= "WARLOCK" or event ~= "RAW_COMBATLOG" then return end - local raw = tostring(arg2 or "") - local lower = string.lower(raw) - local hasName = string.find(lower, "shadow vulnerability", 1, true) or - string.find(lower, "schattenverwundbarkeit", 1, true) - if not hasName then return end - if not string.find(lower, "is afflicted", 1, true) and - not string.find(lower, "wird von", 1, true) then return end - - local _, _, guid = string.find(raw, "^(0x%x+)") - if not guid then return end - if RefreshSharedShadowVulnerability(guid) then - SVProbeEvent("SV_RAW_REFRESH", guid, 17794, "proven application") - RaidTrace("SV_RAW_REFRESH", guid, "shadow_vulnerability", 17794) - end -end) - --- Shadow Vulnerability refresh: legacy compatibility fallback --------------- --- --- The original v1.0.5 build was the most reliable version on this client. --- It did not try to prove the hidden proc itself. Instead, every successful --- Shadow Bolt CAST / Drain Soul CHANNEL opened a very short verification --- window. If Shadow Vulnerability was live on that exact target GUID, the --- local countdown was restarted at 10 seconds. --- --- Keep that behavior only as a compatibility fallback when ClassicAPI aura --- data is unavailable. ClassicAPI users must never refresh from a cast alone: --- the report proved that this can reset the local clock without a real proc. --- Unlike old v1.0.5, this listener accepts the trigger from ANY Warlock, not --- only the local player, as long as UNIT_CASTEVENT supplies the target GUID. -local SHADOW_BOLT_IDS = { - [686]=true, [695]=true, [705]=true, [1088]=true, [1106]=true, - [7641]=true, [11659]=true, [11660]=true, [11661]=true, [25307]=true, -} - -local DRAIN_SOUL_IDS = { - [1120]=true, [8288]=true, [8289]=true, [11675]=true, - [51687]=true, -- Octo/custom rank observed in UNIT_CASTEVENT -} - -local function IsShadowBoltSpellID(spellID) - spellID = tonumber(spellID) - if spellID and SHADOW_BOLT_IDS[spellID] then return true end - if not spellID or not SpellInfo then return false end - local spellName = SpellInfo(spellID) - return spellName == "Shadow Bolt" or spellName == "Schattenblitz" -end - -local function IsDrainSoulSpellID(spellID) - spellID = tonumber(spellID) - if spellID and DRAIN_SOUL_IDS[spellID] then return true end - if not spellID or not SpellInfo then return false end - local spellName = SpellInfo(spellID) - return spellName == "Drain Soul" or spellName == "Seelendieb" -end - -local function ShadowVulnerabilityIsLive(guid) - if not guid then return false end - local def = SVDef() - if not def then return false end - local spellID, texture = FindSharedShadowVulnerabilityByGUID(guid, def) - return IsShadowVulnerabilitySpellID(spellID) or texture ~= nil -end - --- One pending verification per target GUID so several Warlocks can cast at --- different mobs during the same frame without overwriting each other. -local sv105Pending = {} -local sv105Elapsed = 0 -local SV105_VERIFY_WINDOW = 0.80 -local SV105_VERIFY_INTERVAL = 0.05 - -local function QueueSV105Refresh(guid, spellID, casterGUID, eventType) - if not guid then return end - sv105Pending[guid] = GetTime() + SV105_VERIFY_WINDOW - SVProbeEvent("SV_105_ARM", guid, spellID, tostring(eventType or "?") .. ":" .. tostring(casterGUID or "?")) - - -- Mid-duration refreshes already have the aura visible. Handle them in the - -- same event immediately so the icon number jumps back to 10 without waiting - -- for the next scanner tick. - if ShadowVulnerabilityIsLive(guid) then - if RefreshSharedShadowVulnerability(guid) then - sv105Pending[guid] = nil - SVProbeEvent("SV_105_REFRESH", guid, spellID, "immediate") - RaidTrace("SV_105_REFRESH", guid, "shadow_vulnerability", spellID) - end - end -end - -local sv105Event = CreateFrame("Frame") -sv105Event:RegisterEvent("UNIT_CASTEVENT") -sv105Event:SetScript("OnEvent", function() - if playerClass ~= "WARLOCK" then return end - if ClassicAPIAuraDataAvailable() then return end - - local casterGUID = arg1 - local targetGUID = arg2 - local eventType = arg3 - local spellID = tonumber(arg4) - - if eventType ~= "CAST" and eventType ~= "CHANNEL" then return end - if not IsShadowBoltSpellID(spellID) and not IsDrainSoulSpellID(spellID) then return end - - -- Old v1.0.5 used only the local player's exact target GUID. For the shared - -- version we trust arg2 for any Warlock. Only our own cast may fall back to - -- the current target when this old SuperWoW build omits arg2. - if not targetGUID then - local playerGUID = GetPlayerGUID() - if playerGUID and casterGUID == playerGUID then targetGUID = GetTargetGUID() end - end - if not targetGUID then return end - - QueueSV105Refresh(targetGUID, spellID, casterGUID, eventType) -end) - -local sv105RefreshFrame = CreateFrame("Frame") -sv105RefreshFrame:SetScript("OnUpdate", function() - if playerClass ~= "WARLOCK" or not next(sv105Pending) then return end - if ClassicAPIAuraDataAvailable() then - sv105Pending = {} - return - end - - sv105Elapsed = sv105Elapsed + arg1 - if sv105Elapsed < SV105_VERIFY_INTERVAL then return end - sv105Elapsed = 0 - - local now = GetTime() - local guid, untilTime - for guid, untilTime in pairs(sv105Pending) do - if now > untilTime then - sv105Pending[guid] = nil - elseif ShadowVulnerabilityIsLive(guid) then - if RefreshSharedShadowVulnerability(guid) then - sv105Pending[guid] = nil - SVProbeEvent("SV_105_REFRESH", guid, 17794, "verified") - RaidTrace("SV_105_REFRESH", guid, "shadow_vulnerability", 17794) - end - end - end -end) - -function BNP:RefreshDebuffVisibility() - if not self:AreCrowdControlEnabled() or not self:ShowOtherPlayersCCs() then - BNP.guidLiveCCs = {} - end - self:RefreshAllAuraLayouts() - - local plate - for plate in pairs(BNP.plates or {}) do - if plate and plate.BNPAuraContainer then - if self:AreAnyAurasEnabled() then - UpdatePlate(plate) - else - local i - for i = 1, MAX_VISIBLE_ICONS do - local icon = plate.BNPAuraContainer.icons and plate.BNPAuraContainer.icons[i] - if icon then icon:Hide() end - end - plate.BNPAuraContainer:Hide() - HideCCRow(plate) - end - end - end -end diff --git a/modules/castbar.lua b/modules/castbar.lua deleted file mode 100644 index 44283dd..0000000 --- a/modules/castbar.lua +++ /dev/null @@ -1,386 +0,0 @@ -BNP = BNP or {} - -local casts = {} -local BAR_HEIGHT = 6 - --- Runtime-only visual preview. This is intentionally not saved in BNP_DB so --- a /reload always returns to normal cast detection. -local castbarTestMode = false -local testCastStart = 0 -local TEST_CAST_DURATION = 4 -local TEST_CAST_TEXTURE = "Interface\\Icons\\Spell_Fire_Fireball02" - -local testCast = { - name = "Test Cast", - texture = TEST_CAST_TEXTURE, - startTime = 0, - endTime = 0, - duration = TEST_CAST_DURATION, - channel = false, -} - -local function ResetTestCast(now) - now = now or GetTime() - testCastStart = now - testCast.startTime = now - testCast.endTime = now + TEST_CAST_DURATION - testCast.duration = TEST_CAST_DURATION -end - -local function GetTestCast(now) - if testCastStart == 0 or now >= testCast.endTime then - ResetTestCast(now) - end - return testCast -end - -local function GetCastbarHeight() - if BNP.GetCastbarHeight then return BNP:GetCastbarHeight() end - return BAR_HEIGHT -end -local BAR_GAP = 4 - -local function GetCastbarSpacing() - if BNP.GetCastbarSpacing then return BNP:GetCastbarSpacing() end - return BAR_GAP -end - -local BOTTOM_AURA_EDGE_GAP = 4 -local BOTTOM_AURA_ROW_SPACING = 2 - -local function FrameShown(frame) - return frame and frame.IsShown and frame:IsShown() and true or false -end - -local function IsBottomDebuffPosition(position) - return position == "bottom_mid" or position == "bottom_left" or position == "bottom_right" -end - -local function GetBottomAuraClearance(plate) - if not plate then return 0 end - - local debuffPosition = BNP.GetDebuffPosition and BNP:GetDebuffPosition() or "top" - local debuffsEnabled = not BNP.AreDebuffsEnabled or BNP:AreDebuffsEnabled() - local auraSize = BNP.GetIconSize and BNP:GetIconSize() or 18 - - if debuffsEnabled and IsBottomDebuffPosition(debuffPosition) and FrameShown(plate.BNPAuraContainer) then - return BOTTOM_AURA_EDGE_GAP + auraSize - end - return 0 -end - -local function ApplyCastbarLayout(plate, bar) - if not plate or not plate.healthbar or not bar then return end - local gap = GetCastbarSpacing() + GetBottomAuraClearance(plate) - - bar:ClearAllPoints() - bar:SetPoint("TOPLEFT", plate.healthbar, "BOTTOMLEFT", 15, -gap) - bar:SetPoint("TOPRIGHT", plate.healthbar, "BOTTOMRIGHT", 14, -gap) - - if bar.icon then - bar.icon:ClearAllPoints() - bar.icon:SetPoint("TOPLEFT", plate.healthbar, "BOTTOMLEFT", 0, -gap) - end - bar.BNPBottomAuraClearance = GetBottomAuraClearance(plate) -end - -local function Enabled() - if BNP.AreCastbarsEnabled then return BNP:AreCastbarsEnabled() end - return not BNP_DB or BNP_DB.castbars ~= false -end - -local function GetPlateGUID(plate) - if not plate or not plate.GetName then return nil end - - local token = plate:GetName(1) - if not token then return nil end - - local exists, guid = UnitExists(token) - if exists and guid then return guid end - - -- On SuperWoW the plate token itself can already be the GUID. - return token -end - -local function SpellData(spellID) - if SpellInfo and spellID then - local name, _, texture = SpellInfo(spellID) - return name or "Casting", texture - end - return "Casting", nil -end - -local function CreateCastbar(plate) - if plate.BNPCastbar then return plate.BNPCastbar end - if not plate.healthbar then return nil end - - local parent = plate.BNPScaleWrapper or plate - local bar = CreateFrame("StatusBar", nil, parent) - bar:SetHeight(GetCastbarHeight()) - -- Center the castbar under the complete Blizzard nameplate. - -- Extend beneath the level badge instead of stopping at the healthbar edge. - -- Compact row: icon + castbar together span the complete plate width. - -- The icon begins at the healthbar's left edge; the bar continues directly - -- beside it and ends beneath the level badge. - local gap = GetCastbarSpacing() + GetBottomAuraClearance(plate) - bar:SetPoint("TOPLEFT", plate.healthbar, "BOTTOMLEFT", 15, -gap) - bar:SetPoint("TOPRIGHT", plate.healthbar, "BOTTOMRIGHT", 14, -gap) - bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar") - bar:SetStatusBarColor(1.0, 0.8, 0.0) - bar:SetMinMaxValues(0, 1) - bar:SetValue(0) - bar:SetFrameLevel((plate.healthbar:GetFrameLevel() or 1) + 1) - bar:Hide() - - local bg = bar:CreateTexture(nil, "BACKGROUND") - bg:SetTexture("Interface\\TargetingFrame\\UI-StatusBar") - bg:SetVertexColor(0.10, 0.10, 0.00, 0.85) - bg:SetAllPoints(bar) - - local text = bar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") - text:SetPoint("CENTER", bar, "CENTER", 0, 0) - text:SetTextColor(1, 1, 1) - bar.text = text - - -- Spell icon to the left of the castbar. - local icon = CreateFrame("Frame", nil, parent) - icon:SetWidth(14) - icon:SetHeight(14) - icon:SetPoint("TOPLEFT", plate.healthbar, "BOTTOMLEFT", 0, -gap) - icon:SetFrameLevel(bar:GetFrameLevel() + 1) - - local texture = icon:CreateTexture(nil, "ARTWORK") - texture:SetAllPoints(icon) - icon.texture = texture - - local border = icon:CreateTexture(nil, "OVERLAY") - border:SetTexture("Interface\\Buttons\\UI-Quickslot2") - border:SetPoint("CENTER", icon, "CENTER", 0, 0) - border:SetWidth(23) - border:SetHeight(23) - icon.border = border - - icon:Hide() - bar.icon = icon - - plate.BNPCastbar = bar - return bar -end - -local function Hide(plate) - if plate and plate.BNPCastbar then - plate.BNPCastbar:Hide() - if plate.BNPCastbar.icon then - plate.BNPCastbar.icon:Hide() - end - end -end - --- Exact SuperWoW event path used by ShaguTweaks' superwow module: --- START / CAST / CHANNEL create cast state; FAIL removes it. -local eventFrame = CreateFrame("Frame") -eventFrame:RegisterEvent("UNIT_CASTEVENT") -eventFrame:SetScript("OnEvent", function() - local guid = arg1 - local eventType = arg3 - local spellID = arg4 - local timerMS = tonumber(arg5) or 0 - - if not guid then return end - - if eventType == "START" or eventType == "CAST" or eventType == "CHANNEL" then - if timerMS <= 0 then - -- Instant casts do not need a visible castbar. - casts[guid] = nil - return - end - - local name, texture = SpellData(spellID) - local now = GetTime() - local duration = timerMS / 1000 - - casts[guid] = { - name = name, - texture = texture, - startTime = now, - endTime = now + duration, - duration = duration, - channel = eventType == "CHANNEL", - } - elseif eventType == "FAIL" then - casts[guid] = nil - end -end) - -local activeCastPlates = {} - -local function MarkCastPlateActive(plate) - if not plate then return end - activeCastPlates[plate] = true -end - -local function MarkCastPlateInactive(plate) - if not plate then return end - activeCastPlates[plate] = nil - Hide(plate) -end - --- Lightweight discovery pass. This does NOT animate bars; it only checks --- visible plates for active cast state at 20 Hz. -local discovery = CreateFrame("Frame") -local discoveryElapsed = 0 -discovery:SetScript("OnUpdate", function() - discoveryElapsed = discoveryElapsed + arg1 - if discoveryElapsed < 0.05 then return end - discoveryElapsed = 0 - - if not Enabled() and not castbarTestMode then - local plate - for plate in pairs(activeCastPlates) do - MarkCastPlateInactive(plate) - end - return - end - - local now = GetTime() - local plate - for plate in pairs(BNP.plates) do - if plate:IsShown() then - if castbarTestMode then - -- Preview every visible nameplate. No GUID/cast lookup is needed. - MarkCastPlateActive(plate) - else - local guid = GetPlateGUID(plate) - local cast = guid and casts[guid] - - if cast and now < cast.endTime then - MarkCastPlateActive(plate) - elseif activeCastPlates[plate] then - MarkCastPlateInactive(plate) - end - end - elseif activeCastPlates[plate] then - MarkCastPlateInactive(plate) - end - end -end) - --- Frame-perfect animation remains unchanged visually, but only active --- castbars are updated every rendered frame. -local updater = CreateFrame("Frame") -updater:SetScript("OnUpdate", function() - if not Enabled() and not castbarTestMode then return end - - local now = GetTime() - local plate - for plate in pairs(activeCastPlates) do - if not plate:IsShown() then - MarkCastPlateInactive(plate) - else - local guid - local cast - - if castbarTestMode then - cast = GetTestCast(now) - else - guid = GetPlateGUID(plate) - cast = guid and casts[guid] - end - - if not cast or now >= cast.endTime then - if (not castbarTestMode) and guid and cast and now >= cast.endTime then - casts[guid] = nil - end - MarkCastPlateInactive(plate) - else - local bar = CreateCastbar(plate) - if bar then - local bottomClearance = GetBottomAuraClearance(plate) - if bar.BNPBottomAuraClearance ~= bottomClearance then - ApplyCastbarLayout(plate, bar) - end - - local current = now - cast.startTime - if current < 0 then current = 0 end - if current > cast.duration then current = cast.duration end - - bar:SetMinMaxValues(0, cast.duration) - - if cast.channel then - bar:SetValue(cast.duration - current) - else - bar:SetValue(current) - end - - bar.text:SetText(cast.name) - bar:SetAlpha(1) - - if bar.icon then - if cast.texture then - bar.icon.texture:SetTexture(cast.texture) - bar.icon:SetAlpha(1) - bar.icon:Show() - else - bar.icon:Hide() - end - end - - bar:Show() - end - end - end - end -end) - - -function BNP:IsCastbarTestMode() - return castbarTestMode and true or false -end - -function BNP:SetCastbarTestMode(enabled) - castbarTestMode = enabled and true or false - - if castbarTestMode then - ResetTestCast(GetTime()) - else - -- Remove every preview immediately. Real casts are rediscovered on the - -- next lightweight discovery pass without touching their stored state. - local plate - for plate in pairs(activeCastPlates) do - MarkCastPlateInactive(plate) - end - end - - if self.optionsFrame and self.optionsFrame.castbarTestCheck then - self.optionsFrame.castbarTestCheck:SetChecked(castbarTestMode) - end -end - - -function BNP:RefreshCastbarLayout() - local plate - local height = GetCastbarHeight() - - for plate in pairs(BNP.plates or {}) do - if plate and plate.BNPCastbar then - plate.BNPCastbar:SetHeight(height) - ApplyCastbarLayout(plate, plate.BNPCastbar) - end - end -end - -function BNP:RefreshCastbarHeights() - self:RefreshCastbarLayout() -end - -function BNP:SetCastbarsEnabled(enabled) - BNP_DB = BNP_DB or {} - BNP_DB.castbars = enabled and true or false - - if not enabled then - local plate - for plate in pairs(BNP.plates) do Hide(plate) end - end - - self:Print("Nameplate castbars " .. (enabled and "enabled." or "disabled.")) -end diff --git a/modules/classcolors.lua b/modules/classcolors.lua deleted file mode 100644 index 90e243d..0000000 --- a/modules/classcolors.lua +++ /dev/null @@ -1,260 +0,0 @@ -BNP = BNP or {} - -local fallbackColors = { - WARRIOR = { 0.78, 0.61, 0.43 }, - MAGE = { 0.41, 0.80, 0.94 }, - ROGUE = { 1.00, 0.96, 0.41 }, - DRUID = { 1.00, 0.49, 0.04 }, - HUNTER = { 0.67, 0.83, 0.45 }, - SHAMAN = { 0.00, 0.44, 0.87 }, - PRIEST = { 1.00, 1.00, 1.00 }, - WARLOCK = { 0.58, 0.51, 0.79 }, - PALADIN = { 0.96, 0.55, 0.73 }, -} - -local function GetColor(class) - if RAID_CLASS_COLORS and RAID_CLASS_COLORS[class] then - local c = RAID_CLASS_COLORS[class] - return c.r, c.g, c.b - end - - local c = fallbackColors[class] - if c then return c[1], c[2], c[3] end -end - -local function Enabled() - if BNP.AreClassColorsEnabled then return BNP:AreClassColorsEnabled() end - return not BNP_DB or BNP_DB.classColors ~= false -end - -local function ClearClassCache(plate) - if not plate then return end - plate.BNPClassColorApplied = nil - plate.BNPClassColorToken = nil - plate.BNPClassGUID = nil - plate.BNPClassR = nil - plate.BNPClassG = nil - plate.BNPClassB = nil -end - -local function ClearNativeColor(plate) - if not plate then return end - plate.BNPClassNativeGUID = nil - plate.BNPClassNativeR = nil - plate.BNPClassNativeG = nil - plate.BNPClassNativeB = nil - plate.BNPClassNativeA = nil -end - -local function ResolvePlateUnit(plate) - if not plate or not plate.GetName then return nil, nil end - - local token = plate:GetName(1) - if not token or not UnitExists(token) then - return token, nil - end - - local exists, guid = UnitExists(token) - if exists then - return token, guid - end - - return token, nil -end - -local function CaptureNativeColor(plate, guid) - if not plate or not plate.healthbar or not guid then return end - if plate.BNPClassNativeGUID == guid and plate.BNPClassNativeR then return end - - local r, g, b, a = plate.healthbar:GetStatusBarColor() - plate.BNPClassNativeGUID = guid - plate.BNPClassNativeR = r - plate.BNPClassNativeG = g - plate.BNPClassNativeB = b - plate.BNPClassNativeA = a or 1 -end - -local function GetFallbackNativeColor(token) - if not token or not UnitExists(token) then return nil end - - if UnitCanAttack and UnitCanAttack("player", token) then - return 1, 0, 0, 1 - end - - if UnitIsFriend and UnitIsFriend("player", token) then - return 0, 1, 0, 1 - end - - return 1, 1, 0, 1 -end - -local function RestoreNativeColor(plate) - if not plate or not plate.healthbar then - ClearClassCache(plate) - return false - end - - local token, guid = ResolvePlateUnit(plate) - - if token and guid and UnitIsPlayer(token) then - if plate.BNPClassNativeGUID == guid and plate.BNPClassNativeR then - plate.healthbar:SetStatusBarColor( - plate.BNPClassNativeR, - plate.BNPClassNativeG, - plate.BNPClassNativeB, - plate.BNPClassNativeA or 1 - ) - else - local r, g, b, a = GetFallbackNativeColor(token) - if r then plate.healthbar:SetStatusBarColor(r, g, b, a or 1) end - end - end - - ClearClassCache(plate) - return true -end - -local function ApplyClassColor(plate) - if not Enabled() then - RestoreNativeColor(plate) - return false - end - - if not plate or not plate.healthbar then return false end - - local token, guid = ResolvePlateUnit(plate) - - -- BNP only owns PLAYER class colors. - -- NPC/neutral/friendly-NPC colors stay completely under Blizzard control. - if not token or not guid or not UnitIsPlayer(token) then - ClearClassCache(plate) - return false - end - - local _, class = UnitClass(token) - local r, g, b = GetColor(class) - if not r then - ClearClassCache(plate) - return false - end - - -- Remember Blizzard's original player color before BNP overwrites it so the - -- Class Colors checkbox can restore the plate immediately without /reload. - CaptureNativeColor(plate, guid) - - plate.BNPClassColorApplied = true - plate.BNPClassColorToken = token - plate.BNPClassGUID = guid - plate.BNPClassR = r - plate.BNPClassG = g - plate.BNPClassB = b - - plate.healthbar:SetStatusBarColor(r, g, b) - return true -end - -BNP.ApplyClassColor = ApplyClassColor -BNP.RestoreClassColor = RestoreNativeColor - -function BNP:RefreshClassColors() - local plate - for plate in pairs(self.plates or {}) do - if plate and plate:IsShown() and plate.healthbar then - if Enabled() then - ClearClassCache(plate) - ApplyClassColor(plate) - else - RestoreNativeColor(plate) - end - end - end -end - -local function EnforceClassColor(plate) - if not Enabled() then return end - if not plate or not plate.healthbar or not plate.BNPClassGUID then return end - - local token, guid = ResolvePlateUnit(plate) - - -- Never enforce a cached color after Blizzard recycled the frame. - if not token or not guid or guid ~= plate.BNPClassGUID or not UnitIsPlayer(token) then - ClearClassCache(plate) - ClearNativeColor(plate) - return - end - - local r, g, b, a = plate.healthbar:GetStatusBarColor() - if math.abs(r - plate.BNPClassR) > 0.01 - or math.abs(g - plate.BNPClassG) > 0.01 - or math.abs(b - plate.BNPClassB) > 0.01 then - -- Blizzard can rewrite a player plate back to its native reaction color. - -- Keep that latest native value so disabling class colors restores cleanly. - plate.BNPClassNativeGUID = guid - plate.BNPClassNativeR = r - plate.BNPClassNativeG = g - plate.BNPClassNativeB = b - plate.BNPClassNativeA = a or 1 - - plate.healthbar:SetStatusBarColor( - plate.BNPClassR, - plate.BNPClassG, - plate.BNPClassB - ) - end -end - -if BNP.libnameplate then - table.insert(BNP.libnameplate.OnInit, function(plate) - local current = plate or this - if not current then return end - - ClearClassCache(current) - ClearNativeColor(current) - ApplyClassColor(current) - - local bar = current.healthbar - if bar and not current.BNPClassColorHooked then - local oldValueChanged = bar:GetScript("OnValueChanged") - bar:SetScript("OnValueChanged", function() - if oldValueChanged then oldValueChanged() end - EnforceClassColor(current) - end) - current.BNPClassColorHooked = true - end - end) - - table.insert(BNP.libnameplate.OnShow, function(plate) - local current = plate or this - if not current then return end - ClearClassCache(current) - ClearNativeColor(current) - ApplyClassColor(current) - end) -end - --- Blizzard may rewrite player colors during combat/threat updates. --- Re-apply only while the plate still resolves to the SAME player GUID. --- If a recycled frame now represents an NPC/neutral unit, drop the cache --- immediately and never touch its Blizzard color. -local elapsed = 0 -local updater = CreateFrame("Frame") -updater:SetScript("OnUpdate", function() - elapsed = elapsed + arg1 - if elapsed < 0.10 then return end - elapsed = 0 - - if not Enabled() then return end - - local plate - for plate in pairs(BNP.plates or {}) do - if plate:IsShown() and plate.healthbar then - local token, guid = ResolvePlateUnit(plate) - - if plate.BNPClassGUID then - EnforceClassColor(plate) - elseif token and guid and UnitIsPlayer(token) then - ApplyClassColor(plate) - end - end - end -end) diff --git a/modules/combopoints.lua b/modules/combopoints.lua deleted file mode 100644 index 2f0805a..0000000 --- a/modules/combopoints.lua +++ /dev/null @@ -1,299 +0,0 @@ -BNP = BNP or {} - -local _, playerClass = UnitClass("player") -local COMBO_CLASS = (playerClass == "ROGUE" or playerClass == "DRUID") - -local MAX_POINTS = 5 -local POINT_GAP = 1 - -local function Enabled() - return COMBO_CLASS and BNP.AreComboPointsEnabled and BNP:AreComboPointsEnabled() -end - -local function GetTargetGUID() - local exists, guid = UnitExists("target") - if exists and guid then return guid end - return nil -end - -local function GetComboCount() - if not GetComboPoints then return 0 end - local count = tonumber(GetComboPoints()) or 0 - if count < 0 then count = 0 end - if count > MAX_POINTS then count = MAX_POINTS end - return count -end - -local function GetSourcePoint() - if getglobal then - return getglobal("TargetFrameComboPoint1") - end - return nil -end - -local function GetSourceSize() - local source = GetSourcePoint() - local w, h = 12, 16 - - if source then - if source.GetWidth then w = tonumber(source:GetWidth()) or w end - if source.GetHeight then h = tonumber(source:GetHeight()) or h end - end - - if w <= 0 then w = 12 end - if h <= 0 then h = 16 end - return w, h -end - -local function ApplyTargetFrameScale(holder) - if not holder then return end - - local source = GetSourcePoint() - local parent = holder:GetParent() - - local sourceScale = 1.0 - local parentScale = 1.0 - - -- Match the actual on-screen scale of Blizzard's TargetFrame combo points. - if source and source.GetEffectiveScale then - sourceScale = tonumber(source:GetEffectiveScale()) or 1.0 - elseif UIParent and UIParent.GetEffectiveScale then - sourceScale = tonumber(UIParent:GetEffectiveScale()) or 1.0 - elseif UIParent and UIParent.GetScale then - sourceScale = tonumber(UIParent:GetScale()) or 1.0 - end - - if parent and parent.GetEffectiveScale then - parentScale = tonumber(parent:GetEffectiveScale()) or 1.0 - end - - if sourceScale <= 0 then sourceScale = 1.0 end - if parentScale <= 0 then parentScale = 1.0 end - - holder:SetScale(sourceScale / parentScale) -end - -local function CopySourceTextureAppearance(dst, src) - if not dst or not src then return end - - if src.GetTexture and dst.SetTexture then - dst:SetTexture(src:GetTexture()) - end - - if src.GetTexCoord and dst.SetTexCoord then - local l, r, t, b = src:GetTexCoord() - if l then dst:SetTexCoord(l, r, t, b) end - end - - if src.GetVertexColor and dst.SetVertexColor then - local r, g, b, a = src:GetVertexColor() - if r then dst:SetVertexColor(r, g, b, a or 1) end - end - - if src.GetBlendMode and dst.SetBlendMode then - local mode = src:GetBlendMode() - if mode then dst:SetBlendMode(mode) end - end -end - -local function CreateExactPoint(parent) - local source = GetSourcePoint() - local pointW, pointH = GetSourceSize() - - local point = CreateFrame("Frame", nil, parent) - point:SetWidth(pointW) - point:SetHeight(pointH) - - local copied = false - - -- Clone the actual texture regions from Blizzard's TargetFrame combo point. - -- This guarantees the same client artwork/skin instead of guessing atlas cuts. - if source and source.GetRegions then - local regions = { source:GetRegions() } - local i - - for i = 1, table.getn(regions) do - local src = regions[i] - if src and src.GetObjectType and src:GetObjectType() == "Texture" then - local tex = point:CreateTexture(nil, "ARTWORK") - CopySourceTextureAppearance(tex, src) - - local w = src.GetWidth and tonumber(src:GetWidth()) or pointW - local h = src.GetHeight and tonumber(src:GetHeight()) or pointH - if not w or w <= 0 then w = pointW end - if not h or h <= 0 then h = pointH end - - tex:SetWidth(w) - tex:SetHeight(h) - - -- Preserve the standard TargetFrame relative offsets where possible. - local anchored = false - if src.GetPoint then - local pnt, rel, relp, x, y = src:GetPoint(1) - if pnt then - -- Source textures are normally anchored to their own combo frame. - -- Re-anchor them to our clone with the same point/offset. - tex:SetPoint(pnt, point, relp or pnt, x or 0, y or 0) - anchored = true - end - end - - if not anchored then - tex:SetPoint("CENTER", point, "CENTER", 0, 0) - end - - copied = true - end - end - end - - -- Fallback only if the client does not expose TargetFrameComboPoint1 regions. - if not copied then - local base = point:CreateTexture(nil, "BACKGROUND") - base:SetTexture("Interface\\ComboFrame\\ComboPoint") - base:SetWidth(12) - base:SetHeight(16) - base:SetPoint("TOPLEFT", point, "TOPLEFT", 0, 0) - base:SetTexCoord(0, 0.375, 0, 1) - - local highlight = point:CreateTexture(nil, "ARTWORK") - highlight:SetTexture("Interface\\ComboFrame\\ComboPoint") - highlight:SetWidth(8) - highlight:SetHeight(16) - highlight:SetPoint("TOPLEFT", point, "TOPLEFT", 2, 0) - highlight:SetTexCoord(0.375, 0.5625, 0, 1) - end - - point:Hide() - return point -end - -local function EnsurePoints(plate) - if plate.BNPComboPoints then - ApplyTargetFrameScale(plate.BNPComboPoints) - return plate.BNPComboPoints - end - - if not plate or not plate.name then return nil end - - local pointW, pointH = GetSourceSize() - local parent = plate.BNPScaleWrapper or plate - - local holder = CreateFrame("Frame", nil, parent) - holder:SetWidth(MAX_POINTS * pointW + (MAX_POINTS - 1) * POINT_GAP) - holder:SetHeight(pointH) - - -- Keep the position the user already approved. - holder:SetPoint("BOTTOM", plate.name, "TOP", 0, 2) - holder:SetFrameLevel((plate:GetFrameLevel() or 1) + 12) - holder.points = {} - holder:Hide() - - local i - for i = 1, MAX_POINTS do - local point = CreateExactPoint(holder) - - if i == 1 then - point:SetPoint("LEFT", holder, "LEFT", 0, 0) - else - point:SetPoint("LEFT", holder.points[i - 1], "RIGHT", POINT_GAP, 0) - end - - holder.points[i] = point - end - - ApplyTargetFrameScale(holder) - plate.BNPComboPoints = holder - return holder -end - -local function HidePoints(plate) - local holder = plate and plate.BNPComboPoints - if not holder then return end - - local i - for i = 1, MAX_POINTS do - holder.points[i]:Hide() - end - holder:Hide() -end - -local function UpdatePlate(plate) - if not plate or not plate:IsShown() then return end - - if not Enabled() then - HidePoints(plate) - return - end - - local targetGUID = GetTargetGUID() - local plateGUID = plate.GetName and plate:GetName(1) or nil - - if not targetGUID or not plateGUID or targetGUID ~= plateGUID then - HidePoints(plate) - return - end - - local count = GetComboCount() - if count <= 0 then - HidePoints(plate) - return - end - - local holder = EnsurePoints(plate) - if not holder then return end - ApplyTargetFrameScale(holder) - - local i - for i = 1, MAX_POINTS do - if i <= count then - holder.points[i]:Show() - else - holder.points[i]:Hide() - end - end - - holder:Show() -end - -function BNP:RefreshComboPoints() - local plate - for plate in pairs(BNP.plates or {}) do - if plate:IsShown() then - UpdatePlate(plate) - else - HidePoints(plate) - end - end - - if BNP.RefreshAllAuraLayouts then - BNP:RefreshAllAuraLayouts() - end -end - -table.insert(BNP.libnameplate.OnInit, function(plate) - if COMBO_CLASS then - EnsurePoints(plate) - HidePoints(plate) - end -end) - -table.insert(BNP.libnameplate.OnShow, function(plate) - if COMBO_CLASS then - HidePoints(plate) - UpdatePlate(plate) - end -end) - -table.insert(BNP.libnameplate.OnUpdate, function(plate) - if COMBO_CLASS then UpdatePlate(plate) end -end) - -if COMBO_CLASS then - local events = CreateFrame("Frame") - events:RegisterEvent("PLAYER_TARGET_CHANGED") - events:RegisterEvent("PLAYER_COMBO_POINTS") - events:SetScript("OnEvent", function() - BNP:RefreshComboPoints() - end) -end diff --git a/modules/commands.lua b/modules/commands.lua deleted file mode 100644 index c30698b..0000000 --- a/modules/commands.lua +++ /dev/null @@ -1,592 +0,0 @@ --- Elite dragon render probe --------------------------------------------------- --- Kept directly in commands.lua on purpose so /bnp dragontest cannot depend --- on a separate module loading successfully. This is render-only: it does not --- alter level text, classification state, nameplate discovery, or saved data. -local function BNP_FindTargetPlateForDragonTest() - if C_NamePlate and type(C_NamePlate.GetNamePlateForUnit) == "function" then - local ok, plate = pcall(C_NamePlate.GetNamePlateForUnit, "target") - if ok and plate then return plate end - end - - local exists, guid = UnitExists("target") - if not exists or not guid then return nil end - local plate - for plate in pairs(BNP.plates or {}) do - if plate and plate:IsShown() and plate.GetName then - local ok, token = pcall(plate.GetName, plate, 1) - if ok and token == guid then return plate end - end - end - return nil -end - -local function BNP_ToggleDirectDragonRenderTest() - if not UnitExists("target") then - BNP:Print("Target a visible unit first.") - return - end - - local plate = BNP_FindTargetPlateForDragonTest() - if not plate then - BNP:Print("Target nameplate not found. Keep the target nameplate visible and try again.") - return - end - - local healthbar = plate.healthbar - if not healthbar and plate.GetChildren then - healthbar = plate:GetChildren() - plate.healthbar = healthbar - end - if not healthbar then - BNP:Print("Target healthbar not found.") - return - end - - if not plate.BNPDirectDragonTestFrame then - -- Put the dragon directly on the healthbar BACKGROUND layer. - -- This keeps the exact size/position/scaling, but lets the bar, border - -- and level text draw in front of it at all times. - local t = healthbar:CreateTexture(nil, "BACKGROUND") - t:SetWidth(37) - t:SetHeight(28) - t:SetPoint("CENTER", healthbar, "RIGHT", 20, -1) - t:SetTexture("Interface\\TargetingFrame\\UI-TargetingFrame-Elite") - t:SetTexCoord(0.58, 1.00, 0.00, 0.78125) - - t:Hide() - plate.BNPDirectDragonTestFrame = t - end - - local f = plate.BNPDirectDragonTestFrame - if f:IsShown() then - f:Hide() - BNP:Print("Dragon test OFF.") - else - f:Show() - local classification = UnitClassification and UnitClassification("target") or "?" - BNP:Print("Dragon test ON. Classification: " .. tostring(classification) .. ".") - end -end - - --- Automatic Elite / World Boss dragon --------------------------------------- --- Keep this in commands.lua deliberately: /bnp dragontest and /bnp eliteinfo --- prove that this file and this exact plate/render path work on the 1.12 client. --- The automatic path reuses the SAME frame created by dragontest. -local BNP_EliteAutoNextScan = 0 -local BNP_EliteAutoState = { - ticks = 0, - visible = 0, - elite = 0, - shown = 0, - lastClass = "nil", - lastError = "none", -} - -local function BNP_GetOrCreateAutoDragon(plate) - if not plate then return nil end - - -- Reuse the exact proven dragontest frame if it already exists. - if plate.BNPDirectDragonTestFrame then - return plate.BNPDirectDragonTestFrame - end - - local healthbar = plate.healthbar - if not healthbar and plate.GetChildren then - healthbar = plate:GetChildren() - plate.healthbar = healthbar - end - if not healthbar then return nil end - - -- Same render path as /bnp dragontest: a BACKGROUND texture directly - -- on the healthbar, so the dragon always stays behind bar/level elements. - local t = healthbar:CreateTexture(nil, "BACKGROUND") - t:SetWidth(37) - t:SetHeight(28) - t:SetPoint("CENTER", healthbar, "RIGHT", 20, -1) - t:SetTexture("Interface\\TargetingFrame\\UI-TargetingFrame-Elite") - t:SetTexCoord(0.58, 1.00, 0.00, 0.78125) - - t:Hide() - plate.BNPDirectDragonTestFrame = t - return t -end - -local function BNP_UpdateEliteDragonPlate(plate, unit) - if not plate or not plate:IsShown() then - if plate and plate.BNPDirectDragonTestFrame then - plate.BNPDirectDragonTestFrame:Hide() - end - return - end - - local token = unit - if not token and plate.GetName then - token = plate:GetName(1) - end - - local classification = nil - if token and UnitClassification then - classification = UnitClassification(token) - end - - local dragon = BNP_GetOrCreateAutoDragon(plate) - if not dragon then return end - - if classification == "elite" or classification == "worldboss" then - dragon:Show() - else - dragon:Hide() - end -end - -local function BNP_RunEliteAutoScan() - local visible = 0 - local elite = 0 - local shown = 0 - local lastClass = "nil" - - local plate - for plate in pairs(BNP.plates or {}) do - if plate and plate:IsShown() then - visible = visible + 1 - - local classification = nil - local token = nil - if plate.GetName then - token = plate:GetName(1) - end - if token and UnitClassification then - classification = UnitClassification(token) - end - lastClass = tostring(classification or "nil") - - local dragon = BNP_GetOrCreateAutoDragon(plate) - if dragon then - if classification == "elite" or classification == "worldboss" then - elite = elite + 1 - dragon:Show() - shown = shown + 1 - else - dragon:Hide() - end - end - elseif plate and plate.BNPDirectDragonTestFrame then - plate.BNPDirectDragonTestFrame:Hide() - end - end - - BNP_EliteAutoState.visible = visible - BNP_EliteAutoState.elite = elite - BNP_EliteAutoState.shown = shown - BNP_EliteAutoState.lastClass = lastClass -end - --- Instant path --------------------------------------------------------------- --- 1) BNP's own OnShow fires immediately when an already-known Blizzard plate --- is shown/reused. -if BNP.libnameplate and BNP.libnameplate.OnShow then - table.insert(BNP.libnameplate.OnShow, function(plate) - BNP_UpdateEliteDragonPlate(plate) - end) -end - --- 2) ClassicAPI's nameplate event can resolve the unit token directly on the --- frame it belongs to. This is the fastest path for newly appearing plates. -local BNP_EliteInstantEvents = CreateFrame("Frame", nil, UIParent) -local addedOK = pcall(BNP_EliteInstantEvents.RegisterEvent, BNP_EliteInstantEvents, "NAME_PLATE_UNIT_ADDED") -local removedOK = pcall(BNP_EliteInstantEvents.RegisterEvent, BNP_EliteInstantEvents, "NAME_PLATE_UNIT_REMOVED") - -if addedOK or removedOK then - BNP_EliteInstantEvents:SetScript("OnEvent", function() - local unit = arg1 - if not unit then return end - - if event == "NAME_PLATE_UNIT_ADDED" then - if C_NamePlate and type(C_NamePlate.GetNamePlateForUnit) == "function" then - local ok, plate = pcall(C_NamePlate.GetNamePlateForUnit, unit) - if ok and plate then - BNP_UpdateEliteDragonPlate(plate, unit) - end - end - elseif event == "NAME_PLATE_UNIT_REMOVED" then - -- The fallback pass clears hidden/recycled frames. If ClassicAPI still - -- exposes the frame at removal time, hide it immediately as well. - if C_NamePlate and type(C_NamePlate.GetNamePlateForUnit) == "function" then - local ok, plate = pcall(C_NamePlate.GetNamePlateForUnit, unit) - if ok and plate and plate.BNPDirectDragonTestFrame then - plate.BNPDirectDragonTestFrame:Hide() - end - end - end - end) -end - --- Apply immediately to any plates that were already visible when this module --- loaded. Afterwards the 0.50s scan is only a safety net. -pcall(BNP_RunEliteAutoScan) - -local BNP_EliteAutoFrame = CreateFrame("Frame", nil, UIParent) -BNP_EliteAutoFrame:SetScript("OnUpdate", function() - local now = GetTime() - if now < BNP_EliteAutoNextScan then return end - BNP_EliteAutoNextScan = now + 0.50 - BNP_EliteAutoState.ticks = BNP_EliteAutoState.ticks + 1 - - local ok, err = pcall(BNP_RunEliteAutoScan) - if not ok then - BNP_EliteAutoState.lastError = tostring(err or "unknown") - else - BNP_EliteAutoState.lastError = "none" - end -end) -BNP_EliteAutoFrame:Show() - - -local function BNP_PrintEliteInfo() - DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffBNP ELITEINFO|r") - - local targetExists, targetGUID = UnitExists("target") - local targetName = UnitName("target") or "?" - local targetClass = UnitClassification and UnitClassification("target") or "?" - local targetLevel = UnitLevel and UnitLevel("target") or "?" - - DEFAULT_CHAT_FRAME:AddMessage( - "TARGET | "..tostring(targetName).. - " | level "..tostring(targetLevel).. - " | class "..tostring(targetClass).. - " | guid "..tostring(targetGUID or "nil") - ) - - if not targetExists then - DEFAULT_CHAT_FRAME:AddMessage("No target. Target a visible Elite and run /bnp eliteinfo again.") - return - end - - local apiTargetPlate = nil - if C_NamePlate and type(C_NamePlate.GetNamePlateForUnit) == "function" then - local ok, frame = pcall(C_NamePlate.GetNamePlateForUnit, "target") - if ok then apiTargetPlate = frame end - end - DEFAULT_CHAT_FRAME:AddMessage("C_NamePlate(target) | "..tostring(apiTargetPlate and "FOUND" or "nil")) - - local matchedPlate = nil - local visible = 0 - local plate - for plate in pairs(BNP.plates or {}) do - if plate and plate:IsShown() then - visible = visible + 1 - local rawToken = nil - if plate.GetName then - local ok, value = pcall(plate.GetName, plate, 1) - if ok then rawToken = value end - end - - local directMatch = apiTargetPlate and plate == apiTargetPlate - local guidMatch = targetGUID and rawToken == targetGUID - if directMatch or guidMatch then - matchedPlate = plate - local rawExists, rawGUID = false, nil - if rawToken then rawExists, rawGUID = UnitExists(rawToken) end - local rawClass = "nil" - if rawToken and UnitClassification then - local ok, value = pcall(UnitClassification, rawToken) - if ok then rawClass = value end - end - DEFAULT_CHAT_FRAME:AddMessage( - "BNP MATCH | direct "..tostring(directMatch and 1 or 0).. - " | guid "..tostring(guidMatch and 1 or 0).. - " | token "..tostring(rawToken or "nil") - ) - DEFAULT_CHAT_FRAME:AddMessage( - "BNP TOKEN | exists "..tostring(rawExists and 1 or 0).. - " | guid "..tostring(rawGUID or "nil").. - " | class "..tostring(rawClass or "nil") - ) - break - end - end - end - DEFAULT_CHAT_FRAME:AddMessage("BNP visible plates | "..tostring(visible).." | target match "..tostring(matchedPlate and "YES" or "NO")) - - local existing = 0 - local guidMatchUnit = nil - local frameMatchUnit = nil - local i - for i = 1, 40 do - local unit = "nameplate"..tostring(i) - local exists, guid = UnitExists(unit) - if exists then - existing = existing + 1 - local class = UnitClassification and UnitClassification(unit) or "?" - local frame = nil - if C_NamePlate and type(C_NamePlate.GetNamePlateForUnit) == "function" then - local ok, value = pcall(C_NamePlate.GetNamePlateForUnit, unit) - if ok then frame = value end - end - - if targetGUID and guid == targetGUID then - guidMatchUnit = unit - DEFAULT_CHAT_FRAME:AddMessage( - "TOKEN GUID MATCH | "..unit.. - " | class "..tostring(class).. - " | frame "..tostring(frame and "FOUND" or "nil") - ) - end - - if apiTargetPlate and frame == apiTargetPlate then - frameMatchUnit = unit - DEFAULT_CHAT_FRAME:AddMessage( - "TOKEN FRAME MATCH | "..unit.. - " | guid "..tostring(guid or "nil").. - " | class "..tostring(class) - ) - end - end - end - - DEFAULT_CHAT_FRAME:AddMessage( - "nameplateN | existing "..tostring(existing).. - " | guidMatch "..tostring(guidMatchUnit or "NONE").. - " | frameMatch "..tostring(frameMatchUnit or "NONE") - ) -end - -SLASH_BLIZZNAMEPLATESPLUS1 = "/bnp" -SlashCmdList["BLIZZNAMEPLATESPLUS"] = function(msg) - msg = string.lower(msg or "") - - if msg == "" or msg == "config" or msg == "options" then - if BNP.ToggleOptions then BNP:ToggleOptions() end - elseif msg == "spellprobe" or msg == "spellbook" then - if BNP.SpellbookProbe then BNP:SpellbookProbe() else BNP:Print("Spellbook probe is not loaded.") end - elseif msg == "spellprobe corruption" then - if BNP.SpellbookProbe then BNP:SpellbookProbe("corruption") end - elseif msg == "spellprobe agony" then - if BNP.SpellbookProbe then BNP:SpellbookProbe("agony") end - elseif msg == "spellprobe siphon" then - if BNP.SpellbookProbe then BNP:SpellbookProbe("siphon") end - elseif msg == "unknown" or msg == "learn" then - if BNP.PrintUnknownBeta then BNP:PrintUnknownBeta(false) else BNP:Print("Unknown collector is not loaded.") end - elseif msg == "unknown verbose" or msg == "learn verbose" then - if BNP.PrintUnknownBeta then BNP:PrintUnknownBeta(true) else BNP:Print("Unknown collector is not loaded.") end - elseif msg == "unknown reset" or msg == "learn reset" then - if BNP.ResetUnknownBeta then BNP:ResetUnknownBeta() end - BNP.unknownDirectEvents = {} - elseif msg == "auras" or msg == "targetauras" then - if BNP.PrintTargetAuraProbe then - BNP:PrintTargetAuraProbe() - else - BNP:Print("Target aura probe is not loaded.") - end - elseif msg == "raidcheck" then - local exists, guid = UnitExists("target") - DEFAULT_CHAT_FRAME:AddMessage("|cff66ccffBNP RAIDCHECK|r") - if not exists or not guid then - DEFAULT_CHAT_FRAME:AddMessage("No target / no target GUID.") - return - end - DEFAULT_CHAT_FRAME:AddMessage("Target: "..tostring(UnitName("target") or "?").." | GUID: "..tostring(guid)) - local cache=BNP.guidAuras and BNP.guidAuras[guid] - local n=0 - if cache then - local key,aura - for key,aura in pairs(cache) do - if type(aura)=="table" and aura.spellID then - n=n+1 - local mode = aura.castPrimary and "CAST" or (aura.liveConfirmed and "LIVE" or "?") - DEFAULT_CHAT_FRAME:AddMessage( - "CACHE | "..tostring(key).." | ID "..tostring(aura.spellID).." | "..mode.. - " | stacks "..tostring(aura.stacks or 0).. - " | left "..(aura.expires and string.format("%.1f", aura.expires-GetTime()) or "?") - ) - end - end - end - if n==0 then DEFAULT_CHAT_FRAME:AddMessage("CACHE: EMPTY") end - local svState = BNP.svStateByGUID and BNP.svStateByGUID[guid] - if svState then - DEFAULT_CHAT_FRAME:AddMessage( - "SV STATE | present "..tostring(svState.present and 1 or 0).. - " | ID "..tostring(svState.spellID or "?").. - " | left "..(svState.expires and string.format("%.1f", svState.expires-GetTime()) or "?").. - " | refreshed "..(svState.lastRefresh and string.format("%.1fs ago", GetTime()-svState.lastRefresh) or "?") - ) - else - DEFAULT_CHAT_FRAME:AddMessage("SV STATE: none") - end - - -- Renderer-side SV diagnostics: tells us what the actual visible icon has - -- consumed from the GUID state, not just what the cache contains. - local rendered = false - local plate - for plate in pairs(BNP.plates or {}) do - if plate and plate:IsShown() and plate.GetName and plate:GetName(1) == guid then - local c = plate.BNPAuraContainer - if c and c.icons then - local ri - for ri = 1, table.getn(c.icons) do - local icon = c.icons[ri] - if icon and icon.BNPSVRenderedExpires then - DEFAULT_CHAT_FRAME:AddMessage( - "SV RENDER | icon "..tostring(ri).. - " | text "..tostring(icon.timer and icon.timer:GetText() or "?").. - " | left "..string.format("%.1f", (icon.BNPSVRenderedExpires or GetTime())-GetTime()).. - " | rev "..tostring(icon.BNPSVRenderRevision or 0) - ) - rendered = true - break - end - end - end - end - end - if not rendered then DEFAULT_CHAT_FRAME:AddMessage("SV RENDER: no visible SV icon") end - - local pending = BNP.pendingAuras and BNP.pendingAuras[guid] - if pending then - DEFAULT_CHAT_FRAME:AddMessage("-- PENDING FOR TARGET --") - local pk,pv - for pk,pv in pairs(pending) do - if type(pv)=="table" then - DEFAULT_CHAT_FRAME:AddMessage( - "PENDING | "..tostring(pk).. - " | ID "..tostring(pv.spellID or "?").. - " | age "..string.format("%.1fs", GetTime()-(pv.created or GetTime())) - ) - end - end - else - DEFAULT_CHAT_FRAME:AddMessage("PENDING: none") - end - - DEFAULT_CHAT_FRAME:AddMessage("-- LAST TRACKED CAST EVENTS --") - local trace=BNP.raidTrace or {} - local start=math.max(1,table.getn(trace)-14) - local ti - for ti=start,table.getn(trace) do - local e=trace[ti] - DEFAULT_CHAT_FRAME:AddMessage( - tostring(e.stage).. - " | "..tostring(e.key or "-").. - " | ID "..tostring(e.spellID or "?").. - " | GUID "..tostring(e.guid or "nil").. - " | "..string.format("%.1fs ago",GetTime()-(e.time or GetTime())) - ) - end - - DEFAULT_CHAT_FRAME:AddMessage("-- LIVE TARGET AURAS (up to 64) --") - local i - for i=1,64 do - local tex,stacks,dtype,id=UnitDebuff("target",i) - if not tex then break end - local name=(id and SpellInfo and SpellInfo(id)) or "?" - DEFAULT_CHAT_FRAME:AddMessage("LIVE "..tostring(i).." | ID "..tostring(id or "?").." | "..tostring(name).." | stacks "..tostring(stacks or 0)) - end - - local svp = BNP.svProbe - if svp then - DEFAULT_CHAT_FRAME:AddMessage("-- SV PROBE --") - local last = svp.last - if last then - DEFAULT_CHAT_FRAME:AddMessage( - "SV LIVE | "..tostring(last.reason or "?").." | unit "..tostring(last.unit or "?").. - " | idx "..tostring(last.index or "?").." | ID "..tostring(last.spellID or "?").. - " | stacks "..tostring(last.stacks or 0) - ) - DEFAULT_CHAT_FRAME:AddMessage( - "RET a3="..tostring(last.a3).." a5="..tostring(last.a5).." a6="..tostring(last.a6).. - " a7="..tostring(last.a7).." a8="..tostring(last.a8).." a9="..tostring(last.a9).." a10="..tostring(last.a10) - ) - else - DEFAULT_CHAT_FRAME:AddMessage("SV LIVE: no captured snapshot") - end - local ev = svp.events or {} - local es = math.max(1, table.getn(ev)-9) - local ei - for ei=es,table.getn(ev) do - local e=ev[ei] - DEFAULT_CHAT_FRAME:AddMessage( - tostring(e.stage).." | ID "..tostring(e.spellID or "?").." | GUID "..tostring(e.guid or "nil").. - " | "..tostring(e.extra or "-").." | "..string.format("%.1fs ago",GetTime()-(e.time or GetTime())) - ) - end - - DEFAULT_CHAT_FRAME:AddMessage("-- SV RAW WINDOW --") - local raw = svp.raw or {} - if table.getn(raw) == 0 then - DEFAULT_CHAT_FRAME:AddMessage("RAW: no matching lines captured") - else - local rs = math.max(1, table.getn(raw)-13) - local ri - for ri=rs,table.getn(raw) do - local r=raw[ri] - DEFAULT_CHAT_FRAME:AddMessage( - "RAW "..string.format("%.1fs",GetTime()-(r.time or GetTime())).. - " | "..tostring(r.event or "?").." | "..tostring(r.raw or "") - ) - end - end - end - elseif msg == "direct" then - if BNP.PrintUnknownDirectEvents then - BNP:PrintUnknownDirectEvents() - else - BNP:Print("Direct event collector is not loaded.") - end - elseif msg == "spelldb" or msg == "audit" then - if BNP.PrintSpellDBAudit then BNP:PrintSpellDBAudit() else BNP:Print("SpellDB audit is not loaded.") end - elseif msg == "spelldb reset" or msg == "audit reset" then - if BNP.ResetSpellDBAudit then BNP:ResetSpellDBAudit() end - elseif msg == "guid" or msg == "guiddebug" or msg == "diag" then - if BNP.PrintGUIDDiagnostic then BNP:PrintGUIDDiagnostic() else BNP:Print("GUID diagnostics are not loaded.") end - elseif msg == "dragontest" then - BNP_ToggleDirectDragonRenderTest() - elseif msg == "eliteinfo" then - BNP_PrintEliteInfo() - elseif msg == "eliteauto" then - BNP:Print("Elite auto | ticks " .. tostring(BNP_EliteAutoState.ticks) .. " | visible " .. tostring(BNP_EliteAutoState.visible) .. " | elite " .. tostring(BNP_EliteAutoState.elite) .. " | shown " .. tostring(BNP_EliteAutoState.shown) .. " | lastClass " .. tostring(BNP_EliteAutoState.lastClass) .. " | error " .. tostring(BNP_EliteAutoState.lastError)) - elseif msg == "status" then - local visible = 0 - local plate - for plate in pairs(BNP.plates) do - if plate:IsShown() then visible = visible + 1 end - end - BNP:Print("registered: " .. BNP.detected .. ", visible: " .. visible .. ", test spell: " .. tostring(BNP.corruptionName or "not found")) - elseif msg == "cache" then - local name = UnitName("target") - if not name then - BNP:Print("No target selected.") - return - end - local remaining = BNP.FindCachedCorruption and BNP.FindCachedCorruption(name) - BNP:Print("Cache for " .. name .. ": " .. tostring(remaining or "no Corruption found")) - elseif msg == "castbars on" then - if BNP.SetCastbarsEnabled then BNP:SetCastbarsEnabled(true) end - elseif msg == "castbars off" then - if BNP.SetCastbarsEnabled then BNP:SetCastbarsEnabled(false) end - elseif msg == "scale" then - BNP:Print("Current nameplate scale: " .. tostring(BNP:GetNameplateScale())) - elseif msg == "scale reset" then - if BNP.ResetNameplateScale then BNP:ResetNameplateScale() end - elseif string.find(msg, "^scale%s+") then - local _, _, value = string.find(msg, "^scale%s+([0-9%.]+)") - if BNP.SetNameplateScale then BNP:SetNameplateScale(value) end - elseif msg == "classcolors on" then - BNP_DB = BNP_DB or {} - BNP_DB.classColors = true - BNP:Print("Class colors enabled.") - elseif msg == "classcolors off" then - BNP_DB = BNP_DB or {} - BNP_DB.classColors = false - BNP:Print("Class colors disabled.") - elseif msg == "debug on" then - BNP:SetDebug(true) - elseif msg == "debug off" then - BNP:SetDebug(false) - elseif msg == "debug" then - BNP:SetDebug(not BNP.debugEnabled) - else - BNP:Print("Commands: /bnp unknown, /bnp direct, /bnp spellprobe, /bnp spelldb, /bnp guid, /bnp dragontest, /bnp eliteinfo, /bnp eliteauto, /bnp status, /bnp castbars on/off, /bnp scale <0.5-1.5>, /bnp scale reset, /bnp classcolors on/off, /bnp debug on, /bnp debug off") - end -end diff --git a/modules/debug.lua b/modules/debug.lua deleted file mode 100644 index 7cd623a..0000000 --- a/modules/debug.lua +++ /dev/null @@ -1,43 +0,0 @@ -if not BNP.libnameplate then return end - -local function CreateMarker(plate) - if not plate then return end - BNP:RegisterPlate(plate) - - if not plate.BNPMarker then - local marker = CreateFrame("Frame", nil, plate) - marker:SetWidth(14) - marker:SetHeight(14) - marker:SetPoint("BOTTOM", plate, "TOP", 0, 34) - marker:SetFrameLevel(30) - - marker.texture = marker:CreateTexture(nil, "OVERLAY") - marker.texture:SetAllPoints(marker) - marker.texture:SetTexture("Interface\\Buttons\\WHITE8X8") - marker.texture:SetVertexColor(1, 0, 0, 1) - - marker.text = marker:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") - marker.text:SetPoint("CENTER", marker, "CENTER", 0, 0) - marker.text:SetText("+") - marker.text:SetTextColor(1, 1, 1) - plate.BNPMarker = marker - end - - if BNP.debugEnabled then plate.BNPMarker:Show() else plate.BNPMarker:Hide() end -end - -table.insert(BNP.libnameplate.OnInit, CreateMarker) -table.insert(BNP.libnameplate.OnShow, CreateMarker) - -function BNP:SetDebug(enabled) - BNP.debugEnabled = enabled - BNP_DB = BNP_DB or {} - BNP_DB.debugEnabled = enabled - local plate - for plate in pairs(BNP.plates) do - if plate.BNPMarker then - if enabled then plate.BNPMarker:Show() else plate.BNPMarker:Hide() end - end - end - BNP:Print("Debug-Marker " .. (enabled and "aktiviert" or "deaktiviert") .. ".") -end diff --git a/modules/fullalpha.lua b/modules/fullalpha.lua deleted file mode 100644 index b9cd387..0000000 --- a/modules/fullalpha.lua +++ /dev/null @@ -1,728 +0,0 @@ -BNP = BNP or {} - -local NON_TARGET_ALPHA = 0.55 -local TARGET_RESOLVE_CACHE_TIME = 0.05 -local ALPHA_IDENTITY_HOLD_TIME = 0.20 - -local targetResolveCacheGUID = nil -local targetResolveCacheAt = -100 -local targetResolveCacheFound = false -local classicTargetCacheGUID = nil -local classicTargetCacheAt = -100 -local classicTargetCachePlate = nil -local classicTargetCacheAvailable = false - -local function GetPlateGUID(plate) - if not plate or not plate.GetName then return nil end - local token = plate:GetName(1) - if not token then return nil end - local exists, guid = UnitExists(token) - if exists and guid then return guid end - return token -end - -local function GetVerifiedPlateGUID(plate) - if not plate or not plate.GetName then return nil end - local token = plate:GetName(1) - if not token then return nil end - - -- Alpha must only be lowered after SuperWoW has positively resolved the - -- projected plate token. While rotating the camera Blizzard can recycle and - -- re-show a plate before its token is live again. During that short window - -- treating nil/stale identity as "non-target" caused random low-alpha pops. - local exists, guid = UnitExists(token) - if exists and guid then return guid end - return nil -end - -local function GetTargetGUID() - local exists, guid = UnitExists("target") - if exists and guid then return guid end - return nil -end - -local function InvalidateTargetResolution() - targetResolveCacheGUID = nil - targetResolveCacheAt = -100 - targetResolveCacheFound = false - classicTargetCacheGUID = nil - classicTargetCacheAt = -100 - classicTargetCachePlate = nil - classicTargetCacheAvailable = false -end - -local function GetClassicAPITargetPlate(targetGUID) - local now = GetTime() - if classicTargetCacheGUID == targetGUID and - now - classicTargetCacheAt < TARGET_RESOLVE_CACHE_TIME then - return classicTargetCachePlate, classicTargetCacheAvailable - end - - local available = C_NamePlate and - type(C_NamePlate.GetNamePlateForUnit) == "function" - local targetPlate = nil - - if available then - local ok, plate = pcall(C_NamePlate.GetNamePlateForUnit, "target") - if ok then targetPlate = plate end - end - - classicTargetCacheGUID = targetGUID - classicTargetCacheAt = now - classicTargetCachePlate = targetPlate - classicTargetCacheAvailable = available and true or false - return targetPlate, classicTargetCacheAvailable -end - -local function GetPlateToken(plate) - if not plate or not plate.GetName then return nil end - return plate:GetName(1) -end - -local function GetRegionText(region) - if not region or not region.GetText then return nil end - return region:GetText() -end - -local function PlateMatchesTargetVisual(plate) - if not UnitName then return false end - - local targetName = UnitName("target") - local plateName = GetRegionText(plate and plate.name) - if not targetName or not plateName or targetName ~= plateName then - return false - end - - -- Same-name mobs are common. A known, different level therefore rules the - -- plate out. If the level is hidden/"??", keeping another same-name plate at - -- full alpha is the safe failure mode; it is preferable to dimming the real - -- target because a projected token still carries its previous GUID. - if UnitLevel then - local targetLevel = UnitLevel("target") - local plateLevel = tonumber(GetRegionText(plate and plate.level)) - if targetLevel and targetLevel > 0 and plateLevel and plateLevel > 0 and - targetLevel ~= plateLevel then - return false - end - end - - return true -end - -local function IsPlateCurrentTarget(plate, targetGUID) - if not plate or not targetGUID then return false end - - local token = GetPlateToken(plate) - if token and UnitIsUnit then - local ok, sameUnit = pcall(UnitIsUnit, token, "target") - if ok and sameUnit then return true end - end - - local plateGUID = GetVerifiedPlateGUID(plate) - if plateGUID and plateGUID == targetGUID then return true end - - -- SuperWoW can briefly return the old, but still syntactically valid, GUID - -- for a recycled projected nameplate. The displayed Blizzard name/level has - -- already switched by then and protects the actual target from being dimmed. - return PlateMatchesTargetVisual(plate) -end - -local function HasResolvedVisibleTarget(targetGUID) - local now = GetTime() - if targetResolveCacheGUID == targetGUID and - now - targetResolveCacheAt < TARGET_RESOLVE_CACHE_TIME then - return targetResolveCacheFound - end - - local found = false - local plate - for plate in pairs(BNP.plates or {}) do - if plate and plate.IsShown and plate:IsShown() and - IsPlateCurrentTarget(plate, targetGUID) then - found = true - break - end - end - - targetResolveCacheGUID = targetGUID - targetResolveCacheAt = now - targetResolveCacheFound = found - return found -end - -local function IsForeignTagged(plate) - if not plate or not plate.GetName then return false end - local token = plate:GetName(1) - if not token then return false end - - local exists = UnitExists(token) - if not exists then return false end - - if UnitCanAttack and not UnitCanAttack("player", token) then - return false - end - - if not UnitIsTapped or not UnitIsTapped(token) then - return false - end - - if UnitIsTappedByPlayer and UnitIsTappedByPlayer(token) then - return false - end - - return true -end - -local function DesiredAlpha(plate) - -- No selected target: keep all nameplates fully visible. - local targetGUID = GetTargetGUID() - if not targetGUID then return 1 end - - local classicTargetPlate, classicTargetAvailable = - GetClassicAPITargetPlate(targetGUID) - - if classicTargetAvailable then - -- Exact ClassicAPI frame resolution makes the old transition hold - -- unnecessary. More importantly, it lets us keep alpha stable even when a - -- crowded projected nameplate token briefly stops resolving. The previous - -- code treated an unresolved non-target token as alpha 1 and then faded it - -- again on the next tick, which made both the plate and its aura children - -- visibly pulse/flash in large stacked raids. - if not classicTargetPlate then return 1 end - - -- The exact target frame is authoritative and must always stay opaque, - -- even while its SuperWoW token is transiently unavailable. - if plate == classicTargetPlate then return 1 end - - -- GUID equality remains as a conservative fallback in case a client build - -- returns a wrapper/alias for the target nameplate rather than the exact - -- frame object tracked by BNP. - local plateGUID = GetVerifiedPlateGUID(plate) - if plateGUID and plateGUID == targetGUID then return 1 end - - -- Once ClassicAPI has positively identified the real target frame, every - -- other visible plate is safely a non-target. Do not bounce it to alpha 1 - -- merely because its projected token was nil for one raid-stack tick. - if BNP.GetNonTargetAlpha then - return BNP:GetNonTargetAlpha() - end - return 1 - end - - -- On target changes and recycled OnShow transitions, the projected token can - -- temporarily expose its previous valid GUID. Keep the plate neutral until - -- Blizzard/SuperWoW have had time to publish the new identity. This is only - -- the legacy path used when exact ClassicAPI nameplate resolution is absent. - if plate.BNPAlphaIdentityHoldUntil and - GetTime() < plate.BNPAlphaIdentityHoldUntil then - return 1 - end - - -- GUID equality is preferred. UnitIsUnit and the displayed Blizzard - -- name/level are conservative fallbacks for the stale-valid-GUID window. - if IsPlateCurrentTarget(plate, targetGUID) then - return 1 - end - - -- Do not dim anything until the selected target itself can be associated - -- with a currently visible plate. Uncertainty must fail to full opacity. - if not HasResolvedVisibleTarget(targetGUID) then - return 1 - end - - -- A recycled/reappearing plate can exist for a moment before SuperWoW has - -- attached a trustworthy GUID again. Never dim an unidentified plate; doing - -- so is what caused the random low-alpha flash when rotating the camera. - local plateGUID = GetVerifiedPlateGUID(plate) - if not plateGUID then return 1 end - - -- Current target always stays fully visible. - if plateGUID == targetGUID then - return 1 - end - - -- Only positively identified non-target plates use the configured alpha. - if BNP.GetNonTargetAlpha then - return BNP:GetNonTargetAlpha() - end - - return 1 -end - -local function SetLevelSafe(frame, level) - if frame and frame.SetFrameLevel and level then - if frame:GetFrameLevel() ~= level then - frame:SetFrameLevel(level) - end - end -end - -local function ApplyTargetFrameLevel(plate) - if not plate or not plate:IsShown() then return end - - -- IMPORTANT: - -- Never change the FrameStrata of projected Vanilla nameplates. - -- Their native client/world layer keeps them behind Blizzard UI frames. - -- Permanent target priority is therefore implemented only with FrameLevel. - - if plate.BNPBaseFrameLevel == nil then - plate.BNPBaseFrameLevel = plate:GetFrameLevel() or 0 - end - - local wrapper = plate.BNPScaleWrapper - if wrapper and plate.BNPBaseWrapperLevel == nil then - plate.BNPBaseWrapperLevel = wrapper:GetFrameLevel() or plate.BNPBaseFrameLevel - end - - local healthbar = plate.healthbar - if healthbar and plate.BNPBaseHealthbarLevel == nil then - plate.BNPBaseHealthbarLevel = healthbar:GetFrameLevel() or 1 - end - - -- ApplyTargetAlpha runs immediately before this function and publishes the - -- exact ClassicAPI/SuperWoW target identity on the plate. - local target = plate.BNPIsCurrentTarget and true or false - - local plateLevel = plate.BNPBaseFrameLevel - local wrapperLevel = plate.BNPBaseWrapperLevel or plateLevel - local healthbarLevel = plate.BNPBaseHealthbarLevel or 1 - - if target then - -- Raise the target only relative to other nameplates while preserving the - -- native projected-nameplate strata beneath the Blizzard interface. - plateLevel = plateLevel + 50 - wrapperLevel = wrapperLevel + 50 - healthbarLevel = healthbarLevel + 50 - end - - SetLevelSafe(plate, plateLevel) - if wrapper then - SetLevelSafe(wrapper, wrapperLevel) - end - if healthbar then - -- The Blizzard border/name are regions of the raised wrapper, but the - -- actual StatusBar is its own frame and must be raised explicitly. - SetLevelSafe(healthbar, healthbarLevel) - end -end - -local function ApplyForeignTagVisual(plate) - if not plate or not plate:IsShown() then return end - - local bar = plate.healthbar - if not bar or not bar.SetStatusBarColor then return end - - local foreign = IsForeignTagged(plate) - - if foreign then - if not plate.BNPForeignTagGrey then - local r, g, b, a = bar:GetStatusBarColor() - plate.BNPForeignTagOldR = r - plate.BNPForeignTagOldG = g - plate.BNPForeignTagOldB = b - plate.BNPForeignTagOldA = a - plate.BNPForeignTagGrey = true - end - - -- Neutral grey: visually distinct from Target Focus transparency. - bar:SetStatusBarColor(0.45, 0.45, 0.45, plate.BNPForeignTagOldA or 1) - elseif plate.BNPForeignTagGrey then - -- Restore the color that BNP/Shagu/class-color logic had before the mob - -- became foreign-tagged. Clear state so future color changes can be learned. - bar:SetStatusBarColor( - plate.BNPForeignTagOldR or 1, - plate.BNPForeignTagOldG or 0, - plate.BNPForeignTagOldB or 0, - plate.BNPForeignTagOldA or 1 - ) - plate.BNPForeignTagGrey = nil - plate.BNPForeignTagOldR = nil - plate.BNPForeignTagOldG = nil - plate.BNPForeignTagOldB = nil - plate.BNPForeignTagOldA = nil - end -end - -local function GetGlowVisualParent(plate) - if not plate then return nil end - - local anchor = plate.healthbar or plate - local visualParent = plate.BNPScaleWrapper - - if not visualParent and anchor and anchor.GetParent then - visualParent = anchor:GetParent() - end - - return visualParent or plate -end - -local function EnsureTargetGlow(plate) - if not plate then return nil end - if plate.BNPTargetGlow then return plate.BNPTargetGlow end - - local anchor = plate.healthbar or plate - if not anchor then return nil end - - -- Keep Blizzard's original healthbar/frame layering completely untouched. - -- The glow belongs to the healthbar itself on BACKGROUND, so it stays behind - -- the statusbar fill without raising/reparenting the Blizzard healthbar. - -- - -- Direct port of ShaguPlates' target glow geometry: - -- nameplate.glow = nameplate:CreateTexture(nil, "BACKGROUND") - -- glow anchored to health CENTER - -- width = health width + 60 - -- height = health height + 30 - -- - -- BNP's only intentional visual change is +3 px on X. - local glow = anchor:CreateTexture(nil, "BACKGROUND") - glow:SetTexture("Interface\\AddOns\\BlizzNameplatesPlus\\media\\shagu_target_glow.tga") - glow:SetPoint("CENTER", anchor, "CENTER", 3, 0) - - local width = anchor.GetWidth and anchor:GetWidth() or 120 - local height = anchor.GetHeight and anchor:GetHeight() or 8 - local glowSize = BNP.GetTargetGlowSize and BNP:GetTargetGlowSize() or 0 - glow:SetWidth(width + 60 + (glowSize * 2)) - glow:SetHeight(height + 30 + (glowSize * 2)) - - local r, g, b = 1, 1, 1 - if BNP.GetTargetGlowColor then - r, g, b = BNP:GetTargetGlowColor() - end - glow:SetVertexColor(r, g, b, 1) - glow:Hide() - - plate.BNPTargetGlow = glow - return glow -end - -local function ApplyTargetGlow(plate) - if not plate or not plate:IsShown() then return end - - local glow = EnsureTargetGlow(plate) - if not glow then return end - - local enabled = BNP.IsTargetFocusEnabled and BNP:IsTargetFocusEnabled() - if not enabled then - glow:Hide() - return - end - - local targetGUID = GetTargetGUID() - local plateGUID = GetPlateGUID(plate) - - if targetGUID and plateGUID and targetGUID == plateGUID then - local anchor = plate.healthbar or plate - local width = anchor.GetWidth and anchor:GetWidth() or 120 - local height = anchor.GetHeight and anchor:GetHeight() or 8 - - - local glowSize = BNP.GetTargetGlowSize and BNP:GetTargetGlowSize() or 0 - glow:ClearAllPoints() - glow:SetPoint("CENTER", anchor, "CENTER", 3, 0) - glow:SetWidth(width + 60 + (glowSize * 2)) - glow:SetHeight(height + 30 + (glowSize * 2)) - - if BNP.GetTargetGlowColor then - local r, g, b = BNP:GetTargetGlowColor() - glow:SetVertexColor(r, g, b, 1) - end - - local glowOpacity = BNP.GetTargetGlowOpacity and BNP:GetTargetGlowOpacity() or 1 - glow:SetAlpha(glowOpacity) - glow:Show() - else - glow:Hide() - end -end - - -local TARGET_ARROW_TEXTURES = { - chevron = { - thin = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\target_arrow_chevron_thin.tga", - thick = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\target_arrow_chevron_thick.tga", - }, - triangle = { - thin = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\target_arrow_triangle_thin.tga", - thick = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\target_arrow_triangle_thick.tga", - }, - slim_triangle = { - thin = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\target_arrow_slim_triangle_thin.tga", - thick = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\target_arrow_slim_triangle_thick.tga", - }, - double_chevron = { - thin = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\target_arrow_double_chevron_thin.tga", - thick = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\target_arrow_double_chevron_thick.tga", - }, - diamond_tip = { - thin = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\target_arrow_diamond_tip_thin.tga", - thick = "Interface\\AddOns\\BlizzNameplatesPlus\\media\\target_arrow_diamond_tip_thick.tga", - }, -} - -local function GetTargetArrowTexture(style, thick) - local textures = TARGET_ARROW_TEXTURES[style] or TARGET_ARROW_TEXTURES.chevron - return thick and textures.thick or textures.thin -end - -local function GetTargetArrowOffsets(style) - local leftGap, rightGap, fallbackGap = -5, 2, 5 - if style == "double_chevron" then - leftGap, rightGap, fallbackGap = -7, 4, 7 - elseif style == "diamond_tip" then - leftGap, rightGap, fallbackGap = -6, 3, 6 - elseif style == "triangle" or style == "slim_triangle" then - leftGap, rightGap, fallbackGap = -5, 3, 5 - end - return leftGap, rightGap, fallbackGap -end - -local function AnchorRightTargetArrow(plate, arrow) - if not plate or not arrow then return end - local anchor = plate.healthbar or plate - local style = BNP.GetTargetArrowStyle and BNP:GetTargetArrowStyle() or "chevron" - local _, rightGap, fallbackGap = GetTargetArrowOffsets(style) - arrow:ClearAllPoints() - - -- Keep the right arrow clearly outside the full right-side cluster so it - -- reads as: > [ HEALTHBAR ] 60 < - -- That means: after the level text for normal plates, and after the custom - -- elite dragon overlay when that overlay is shown. - local side = nil - if plate.BNPDirectDragonTestFrame and plate.BNPDirectDragonTestFrame.IsShown and plate.BNPDirectDragonTestFrame:IsShown() then - side = plate.BNPDirectDragonTestFrame - elseif plate.levelicon and plate.levelicon.IsShown and plate.levelicon:IsShown() then - side = plate.levelicon - elseif plate.level and plate.level.IsShown and plate.level:IsShown() then - side = plate.level - end - - if side then - arrow:SetPoint("LEFT", side, "RIGHT", rightGap, -1) - else - arrow:SetPoint("LEFT", anchor, "RIGHT", fallbackGap, -1) - end -end - -local function EnsureTargetArrows(plate) - if not plate then return nil, nil end - if plate.BNPTargetArrowLeft and plate.BNPTargetArrowRight then - return plate.BNPTargetArrowLeft, plate.BNPTargetArrowRight - end - - local anchor = plate.healthbar or plate - if not anchor or not anchor.CreateTexture then return nil, nil end - - local left = anchor:CreateTexture(nil, "OVERLAY") - left:SetTexture(GetTargetArrowTexture("chevron", false)) - left:SetWidth(14) - left:SetHeight(14) - left:SetTexCoord(0, 1, 0, 1) - left:SetPoint("RIGHT", anchor, "LEFT", -5, 0) - left:Hide() - - local right = anchor:CreateTexture(nil, "OVERLAY") - right:SetTexture(GetTargetArrowTexture("chevron", false)) - right:SetWidth(14) - right:SetHeight(14) - right:SetTexCoord(1, 0, 0, 1) - AnchorRightTargetArrow(plate, right) - right:Hide() - - plate.BNPTargetArrowLeft = left - plate.BNPTargetArrowRight = right - return left, right -end - -local function ApplyTargetArrows(plate) - if not plate or not plate:IsShown() then return end - - local left, right = EnsureTargetArrows(plate) - if not left or not right then return end - - local enabled = BNP.AreTargetArrowsEnabled and BNP:AreTargetArrowsEnabled() - local totemIconOnly = plate.BNPTotemLastKey and - BNP.AreTotemIndicatorsEnabled and BNP:AreTotemIndicatorsEnabled() - if not enabled or totemIconOnly then - left:Hide() - right:Hide() - return - end - - local targetGUID = GetTargetGUID() - local plateGUID = GetPlateGUID(plate) - if targetGUID and plateGUID and targetGUID == plateGUID then - local anchor = plate.healthbar or plate - local arrowSize = BNP.GetTargetArrowSize and BNP:GetTargetArrowSize() or 14 - local thick = BNP.AreTargetArrowsThick and BNP:AreTargetArrowsThick() - local style = BNP.GetTargetArrowStyle and BNP:GetTargetArrowStyle() or "chevron" - local texture = GetTargetArrowTexture(style, thick) - local leftGap = GetTargetArrowOffsets(style) - - left:SetTexture(texture) - right:SetTexture(texture) - left:SetWidth(arrowSize) - left:SetHeight(arrowSize) - right:SetWidth(arrowSize) - right:SetHeight(arrowSize) - left:SetTexCoord(0, 1, 0, 1) - right:SetTexCoord(1, 0, 0, 1) - - left:ClearAllPoints() - left:SetPoint("RIGHT", anchor, "LEFT", leftGap, 0) - AnchorRightTargetArrow(plate, right) - - local r, g, b = 1, 1, 1 - if BNP.GetTargetArrowColor then - r, g, b = BNP:GetTargetArrowColor() - elseif BNP.GetTargetGlowColor then - r, g, b = BNP:GetTargetGlowColor() - end - left:SetVertexColor(r, g, b, 1) - right:SetVertexColor(r, g, b, 1) - left:Show() - right:Show() - else - left:Hide() - right:Hide() - end -end - -local function ApplyTargetAlpha(plate) - if not plate or not plate:IsShown() then return end - local totemIconOnly = plate.BNPTotemLastKey and - BNP.AreTotemIndicatorsEnabled and BNP:AreTotemIndicatorsEnabled() - local wanted = totemIconOnly and 1 or DesiredAlpha(plate) - if plate:GetAlpha() ~= wanted then - plate:SetAlpha(wanted) - end - - -- Publish the same resolved target identity for detached aura containers. - -- They cannot inherit it through their shared WorldFrame parent. - local targetGUID = GetTargetGUID() - local targetPlate = nil - local targetAvailable = false - local isCurrentTarget = false - if targetGUID then - targetPlate, targetAvailable = GetClassicAPITargetPlate(targetGUID) - if targetAvailable and targetPlate then - isCurrentTarget = plate == targetPlate - if not isCurrentTarget then - local plateGUID = GetVerifiedPlateGUID(plate) - isCurrentTarget = plateGUID and plateGUID == targetGUID - end - else - isCurrentTarget = IsPlateCurrentTarget(plate, targetGUID) - end - end - plate.BNPIsCurrentTarget = isCurrentTarget and true or false - - -- ClassicAPI returns the underlying target nameplate frame itself. Force it - -- opaque after every alpha application. Even if SuperWoW momentarily reports - -- the recycled frame's previous valid GUID, both writes happen in the same - -- Lua update and the real target finishes the frame at alpha 1. - if targetGUID then - if targetAvailable and targetPlate and targetPlate.GetAlpha and targetPlate.SetAlpha then - targetPlate.BNPIsCurrentTarget = true - local ok, alpha = pcall(targetPlate.GetAlpha, targetPlate) - if not ok or alpha ~= 1 then - pcall(targetPlate.SetAlpha, targetPlate, 1) - end - end - end -end - -local function InstallAlphaGuard(plate) - plate = plate or this - if not plate or plate.BNPFullAlphaGuard then return end - - plate.BNPAlphaIdentityHoldUntil = GetTime() + ALPHA_IDENTITY_HOLD_TIME - - local oldOnUpdate = plate:GetScript("OnUpdate") - plate:SetScript("OnUpdate", function() - if oldOnUpdate then oldOnUpdate() end - local current = this or plate - - -- Apply the optional Y-offset after Blizzard has had a chance to update - -- the projected nameplate for this frame. This uses the OnUpdate BNP - -- already needs for alpha/glow/target-level handling. - if BNP.MaintainNameplateYOffset then - BNP:MaintainNameplateYOffset(current) - end - - ApplyTargetAlpha(current) - ApplyTargetGlow(current) - ApplyTargetArrows(current) - ApplyForeignTagVisual(current) - ApplyTargetFrameLevel(current) - end) - - plate.BNPFullAlphaGuard = true - ApplyTargetAlpha(plate) -end - -if BNP.libnameplate then - table.insert(BNP.libnameplate.OnInit, function(plate) - InstallAlphaGuard(plate or this) - end) - - table.insert(BNP.libnameplate.OnShow, function(plate) - local current = plate or this - if not current then return end - InvalidateTargetResolution() - current.BNPAlphaIdentityHoldUntil = GetTime() + ALPHA_IDENTITY_HOLD_TIME - if current.GetAlpha and current.SetAlpha and current:GetAlpha() ~= 1 then - current:SetAlpha(1) - end - InstallAlphaGuard(current) - ApplyTargetAlpha(current) - ApplyTargetGlow(current) - ApplyTargetArrows(current) - ApplyForeignTagVisual(current) - ApplyTargetFrameLevel(current) - end) -end - -local targetEvents = CreateFrame("Frame") -targetEvents:RegisterEvent("PLAYER_TARGET_CHANGED") -targetEvents:SetScript("OnEvent", function() - InvalidateTargetResolution() - - local targetGUID = GetTargetGUID() - local _, classicTargetAvailable = GetClassicAPITargetPlate(targetGUID) - local holdUntil = GetTime() + ALPHA_IDENTITY_HOLD_TIME - local plate - for plate in pairs(BNP.plates or {}) do - if classicTargetAvailable then - plate.BNPAlphaIdentityHoldUntil = nil - ApplyTargetAlpha(plate) - else - plate.BNPAlphaIdentityHoldUntil = holdUntil - if plate.GetAlpha and plate.SetAlpha and plate:GetAlpha() ~= 1 then - plate:SetAlpha(1) - end - end - ApplyTargetFrameLevel(plate) - ApplyTargetArrows(plate) - end - - if BNP.RefreshAuraPriorityAlpha then BNP:RefreshAuraPriorityAlpha() end -end) - -function BNP:RefreshTargetFocus() - InvalidateTargetResolution() - local plate - for plate in pairs(self.plates or {}) do - ApplyTargetAlpha(plate) - ApplyTargetGlow(plate) - ApplyTargetArrows(plate) - ApplyForeignTagVisual(plate) - end -end - -function BNP:RefreshNonTargetAlpha() - InvalidateTargetResolution() - local plate - for plate in pairs(self.plates or {}) do - ApplyTargetAlpha(plate) - end -end diff --git a/modules/guiddebug.lua b/modules/guiddebug.lua deleted file mode 100644 index 0eedb2d..0000000 --- a/modules/guiddebug.lua +++ /dev/null @@ -1,73 +0,0 @@ --- SuperWoW GUID probe. --- This module does not alter aura tracking or rendering. It only inspects the --- extended UnitExists return value and the unit token exposed by nameplates. - -local function CleanText(text) - if not text then return nil end - text = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "") - text = string.gsub(text, "|r", "") - return text -end - -local function SafePlateToken(plate) - if not plate or not plate.GetName then return nil end - - -- SuperWoW extends frame:GetName(index). ShaguTweaks already uses - -- plate:GetName(1) for its SuperWoW nameplate castbar implementation. - local ok, token = pcall(function() return plate:GetName(1) end) - if ok then return token end - return nil -end - -local function UnitExistsData(unit) - if not unit then return nil, nil end - local ok, exists, guid = pcall(UnitExists, unit) - if not ok then return nil, nil end - return exists, guid -end - -function BNP:GetSuperWoWGUID(unit) - local exists, guid = UnitExistsData(unit) - if exists and guid then return guid end - return nil -end - -function BNP:GetPlateSuperWoWData(plate) - local token = SafePlateToken(plate) - local exists, guid = UnitExistsData(token) - return token, exists, guid -end - -function BNP:PrintGUIDDiagnostic() - BNP:Print("=== SuperWoW GUID Diagnose ===") - - local targetName = UnitName("target") - local targetExists, targetGUID = UnitExistsData("target") - BNP:Print("Target: " .. tostring(targetName or "kein Ziel")) - BNP:Print("UnitExists(target): " .. tostring(targetExists) .. " | GUID: " .. tostring(targetGUID)) - - local visible = 0 - local sameName = 0 - local guidMatches = 0 - local index = 0 - - for plate in pairs(BNP.plates) do - if plate and plate:IsShown() then - visible = visible + 1 - local plateName = plate.name and plate.name.GetText and CleanText(plate.name:GetText()) or nil - - if not targetName or plateName == targetName then - sameName = sameName + 1 - index = index + 1 - local token, exists, guid = BNP:GetPlateSuperWoWData(plate) - if targetGUID and guid and targetGUID == guid then guidMatches = guidMatches + 1 end - - BNP:Print("Plate " .. index .. ": " .. tostring(plateName)) - BNP:Print(" Token: " .. tostring(token) .. " | Exists: " .. tostring(exists)) - BNP:Print(" GUID: " .. tostring(guid) .. " | Target-Match: " .. tostring(targetGUID and guid == targetGUID or false)) - end - end - end - - BNP:Print("Sichtbar: " .. visible .. " | gleicher Name: " .. sameName .. " | GUID-Matches: " .. guidMatches) -end diff --git a/modules/healthpercent.lua b/modules/healthpercent.lua deleted file mode 100644 index 817e3d7..0000000 --- a/modules/healthpercent.lua +++ /dev/null @@ -1,223 +0,0 @@ -BNP = BNP or {} - -local function Enabled() - return BNP.IsHealthTextEnabled and BNP:IsHealthTextEnabled() -end - -local function CreateHealthText(plate) - if plate.BNPHealthPercent then return plate.BNPHealthPercent end - if not plate or not plate.healthbar then return nil end - - -- Parent the text directly to the Blizzard health StatusBar. This keeps it - -- above the bar texture and automatically follows every frame-level change - -- made for Target Glow / permanent target priority. No separate holder can - -- become stale during target switches or nameplate recycling. - local anchor = plate.healthbar - local text = anchor:CreateFontString(nil, "OVERLAY") - text:SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE") - text:SetPoint("CENTER", anchor, "CENTER", 3, 0) - text:SetTextColor(1, 1, 1) - text:Hide() - - plate.BNPHealthPercent = text - return text -end - -local function GetPercentFromBar(plate) - if not plate or not plate.healthbar then return nil end - - local minValue, maxValue = plate.healthbar:GetMinMaxValues() - local value = plate.healthbar:GetValue() - - if value and minValue and maxValue and maxValue > minValue then - local pct = ((value - minValue) / (maxValue - minValue)) * 100 - return math.floor(pct + 0.5) - end - - return nil -end - -local function GetPercentFromUnit(plate) - if not plate or not plate.GetName then return nil end - - local token = plate:GetName(1) - if not token or not UnitExists(token) then return nil end - if not UnitHealth or not UnitHealthMax then return nil end - - local health = UnitHealth(token) - local maxHealth = UnitHealthMax(token) - - if health and maxHealth and maxHealth > 0 then - return math.floor((health / maxHealth) * 100 + 0.5) - end - - return nil -end - -local function GetHealthFromUnit(plate) - if not plate or not plate.GetName then return nil, nil end - - local token = plate:GetName(1) - if not token or not UnitExists(token) then return nil, nil end - if not UnitHealth or not UnitHealthMax then return nil, nil end - - local health = tonumber(UnitHealth(token)) - local maxHealth = tonumber(UnitHealthMax(token)) - - if health and maxHealth and maxHealth > 0 then - return health, maxHealth - end - - return nil, nil -end - -local function GetHealthFromBar(plate) - if not plate or not plate.healthbar then return nil, nil end - - local minValue, maxValue = plate.healthbar:GetMinMaxValues() - local value = plate.healthbar:GetValue() - - value = tonumber(value) - minValue = tonumber(minValue) - maxValue = tonumber(maxValue) - - if value and minValue and maxValue and maxValue > minValue then - return value - minValue, maxValue - minValue - end - - return nil, nil -end - -local function FormatHealth(value) - value = tonumber(value) - if not value then return nil end - if value < 0 then value = 0 end - - -- Keep boss/player values readable inside the compact Blizzard healthbar. - if value >= 1000000 then - local shown = math.floor((value / 1000000) * 10 + 0.5) / 10 - return tostring(shown) .. "m" - elseif value >= 10000 then - return tostring(math.floor(value / 1000 + 0.5)) .. "k" - elseif value >= 1000 then - local shown = math.floor((value / 1000) * 10 + 0.5) / 10 - return tostring(shown) .. "k" - end - - return tostring(math.floor(value + 0.5)) -end - -local function UpdatePlate(plate) - if not plate or not plate.healthbar then return end - - local text = plate.BNPHealthPercent - - if not Enabled() or not plate:IsShown() then - if text then text:Hide() end - return - end - - text = text or CreateHealthText(plate) - if not text then return end - - local mode = BNP:GetHealthTextMode() - local health, maxHealth = GetHealthFromUnit(plate) - - -- Fallback for clients/units where the SuperWoW unit token is temporarily - -- unavailable. This may be normalized health on stock Vanilla. - if health == nil or maxHealth == nil then - health, maxHealth = GetHealthFromBar(plate) - end - - local pct = nil - if health and maxHealth and maxHealth > 0 then - pct = math.floor((health / maxHealth) * 100 + 0.5) - else - pct = GetPercentFromBar(plate) - if pct == nil then pct = GetPercentFromUnit(plate) end - end - - if pct ~= nil then - if pct < 0 then pct = 0 end - if pct > 100 then pct = 100 end - end - - local hpText = FormatHealth(health) - local display = nil - - if mode == "percent" then - if pct ~= nil then display = tostring(pct) .. "%" end - elseif mode == "hp" then - display = hpText - elseif mode == "both" then - if hpText and pct ~= nil then - display = hpText .. " | " .. tostring(pct) .. "%" - elseif hpText then - display = hpText - elseif pct ~= nil then - display = tostring(pct) .. "%" - end - end - - if not display or display == "" then - text:Hide() - return - end - - if text.BNPLastHealthText ~= display then - text:SetText(display) - text.BNPLastHealthText = display - end - text:Show() -end - --- Intentionally simple fallback: only reads values and updates text. --- No SetScript calls on healthbar, no SetAlpha, no color changes. -local updater = CreateFrame("Frame") -local elapsed = 0 -updater:SetScript("OnUpdate", function() - elapsed = elapsed + arg1 - if elapsed < 0.10 then return end - elapsed = 0 - - local plate - for plate in pairs(BNP.plates or {}) do - if plate:IsShown() then - UpdatePlate(plate) - elseif plate.BNPHealthPercent then - plate.BNPHealthPercent:Hide() - end - end -end) - -function BNP:RefreshHealthPercent() - local plate - for plate in pairs(self.plates or {}) do - if plate and plate:IsShown() then - UpdatePlate(plate) - elseif plate and plate.BNPHealthPercent then - plate.BNPHealthPercent:Hide() - end - end -end - --- Refresh immediately when Blizzard reuses a nameplate instead of waiting for --- the next 0.10 s fallback pass. -if BNP.libnameplate then - table.insert(BNP.libnameplate.OnShow, function(plate) - local current = plate or this - if current then UpdatePlate(current) end - end) -end - --- Target switches can change nameplate frame levels in the same frame. Because --- the text now belongs to the healthbar this is only a value/visibility refresh, --- not a layering workaround. -local healthEvents = CreateFrame("Frame") -healthEvents:RegisterEvent("PLAYER_TARGET_CHANGED") -healthEvents:RegisterEvent("PLAYER_ENTERING_WORLD") -healthEvents:SetScript("OnEvent", function() - if BNP.RefreshHealthPercent then - BNP:RefreshHealthPercent() - end -end) diff --git a/modules/immunities.lua b/modules/immunities.lua deleted file mode 100644 index a4248ad..0000000 --- a/modules/immunities.lua +++ /dev/null @@ -1,969 +0,0 @@ -if not BNP or not BNP.libnameplate then return end - --- PvP immunity / anti-fear indicators (strict whitelist) ------------------- --- Keep this deliberately close to BNP's proven aura/nameplate path: --- SuperWoW's projected nameplate token (plate:GetName(1)) is used directly --- with UnitBuff, just like auras.lua uses it directly with UnitDebuff. --- No GUID -> token reverse conversion and no C_UnitAuras dependency here. - -BNP.guidImmunities = BNP.guidImmunities or {} - -local UI = BNP.UI or {} -local MAX_ICONS = 4 -local ICON_SPACING = UI.ICON_SPACING or 2 -local SIDE_SPACING = 4 -local BOTTOM_SPACING = 4 -local BASE_TOP_OFFSET = UI.ICON_OFFSET_Y or 15 -local COMBO_LAYOUT_OFFSET = 16 -local MISSING_GRACE = 0.20 -local NAME_HIDDEN_COMPACT_SHIFT = 10 - -local _, playerClass = UnitClass("player") - -local IMM_ERR_LAST = {} -local function ReportImmunityError(stage, err) - local now = GetTime and GetTime() or 0 - local last = IMM_ERR_LAST[stage] or -100 - if now - last < 1.5 then return end - IMM_ERR_LAST[stage] = now - local msg = "|cffff4444BNP IMM ERROR [" .. tostring(stage) .. "]|r " .. tostring(err) - if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then - DEFAULT_CHAT_FRAME:AddMessage(msg) - end -end - -local IMMUNITY_DEFS = { - { - key = "divine_shield", - names = { "Divine Shield", "Gottesschild" }, - spellIDs = { 642, 1020 }, - durations = { [642] = 10, [1020] = 12 }, - defaultDuration = 12, - textureMatches = { "spell_holy_divineintervention" }, - selfOnly = true, - }, - { - key = "divine_protection", - names = { "Divine Protection", "Göttlicher Schutz" }, - spellIDs = { 498, 5573 }, - durations = { [498] = 6, [5573] = 8 }, - defaultDuration = 8, - selfOnly = true, - }, - { - key = "blessing_of_protection", - names = { "Blessing of Protection", "Segen des Schutzes" }, - spellIDs = { 1022, 5599, 10278 }, - durations = { [1022] = 6, [5599] = 8, [10278] = 10 }, - defaultDuration = 10, - textureMatches = { "spell_holy_sealofprotection" }, - }, - { - key = "ice_block", - names = { "Ice Block", "Eisblock" }, - spellIDs = { 11958 }, - durations = { [11958] = 10 }, - defaultDuration = 10, - textureMatches = { "spell_frost_frost" }, - selfOnly = true, - }, - { - key = "berserker_rage", - names = { "Berserker Rage", "Berserkerwut" }, - spellIDs = { 18499 }, - durations = { [18499] = 10 }, - defaultDuration = 10, - selfOnly = true, - }, - { - key = "death_wish", - names = { "Death Wish", "Todeswunsch" }, - spellIDs = { 12328 }, - durations = { [12328] = 30 }, - defaultDuration = 30, - selfOnly = true, - }, - { - key = "recklessness", - names = { "Recklessness", "Tollkühnheit" }, - spellIDs = { 1719 }, - durations = { [1719] = 15 }, - defaultDuration = 15, - selfOnly = true, - }, - { - key = "fear_ward", - names = { "Fear Ward", "Furchtzauberschutz" }, - spellIDs = { 6346 }, - durations = { [6346] = 600 }, - defaultDuration = 600, - }, - { - key = "will_of_the_forsaken", - names = { "Will of the Forsaken", "Wille der Verlassenen" }, - spellIDs = { 7744 }, - durations = { [7744] = 5 }, - defaultDuration = 5, - selfOnly = true, - }, -} - -local DEF_BY_ID = {} -local DEF_BY_NAME = {} - -local function Lower(value) - if type(value) ~= "string" or value == "" then return nil end - return string.lower(value) -end - -local function DefDuration(def, spellID) - if def and def.durations and spellID and def.durations[spellID] then - return def.durations[spellID] - end - return def and def.defaultDuration or nil -end - -local function BuildLookup() - local i, j, def, id, name - for i = 1, table.getn(IMMUNITY_DEFS) do - def = IMMUNITY_DEFS[i] - if def.names then - for j = 1, table.getn(def.names) do - name = Lower(def.names[j]) - if name then DEF_BY_NAME[name] = def end - end - end - if def.spellIDs then - for j = 1, table.getn(def.spellIDs) do - id = def.spellIDs[j] - DEF_BY_ID[id] = def - if SpellInfo then - local spellName = SpellInfo(id) - spellName = Lower(spellName) - if spellName then DEF_BY_NAME[spellName] = def end - end - end - end - end -end -BuildLookup() - -local function TextureMatches(def, texture) - local t = Lower(texture) - if not def or not t or not def.textureMatches then return false end - local i - for i = 1, table.getn(def.textureMatches) do - local pattern = Lower(def.textureMatches[i]) - if pattern and string.find(t, pattern, 1, true) then return true end - end - return false -end - -local function FindDef(spellID, name, texture) - -- STRICT WHITELIST: an aura is only an immunity when its spell ID or exact - -- spell name is configured above. Do not guess from a shared icon/texture. - -- Many unrelated server buffs reuse Blizzard icons (e.g. armor/world buffs), - -- which previously allowed false positives such as Forest Armor. - local sid = tonumber(spellID) - if sid then - return DEF_BY_ID[sid] - end - - local n = Lower(name) - if n then - return DEF_BY_NAME[n] - end - - return nil -end - -local function GetIconSize() - if BNP.GetImmunityIconSize then return BNP:GetImmunityIconSize() end - if BNP.GetIconSize then return BNP:GetIconSize() end - return 18 -end - -local function GetPosition() - if BNP.GetImmunityPosition then return BNP:GetImmunityPosition() end - return "top" -end - -local function GetVisualParent(plate) - if plate and plate.GetParent then - local p = plate:GetParent() - if p then return p end - end - return WorldFrame or UIParent -end - -local function GetBaseTopOffset() - local y = BASE_TOP_OFFSET - if (playerClass == "ROGUE" or playerClass == "DRUID") - and BNP.AreComboPointsEnabled and BNP:AreComboPointsEnabled() then - y = y + COMBO_LAYOUT_OFFSET - end - return y -end - -local function IsTopDebuffPosition(position) - return position == "top" or position == "top_left" or position == "top_right" -end - -local function GetTopOffset() - local auraSize = BNP.GetIconSize and BNP:GetIconSize() or 18 - local ccSize = BNP.GetCCIconSize and BNP:GetCCIconSize() or auraSize - local debuffPosition = BNP.GetDebuffPosition and BNP:GetDebuffPosition() or "top" - local ccPosition = BNP.GetCCPosition and BNP:GetCCPosition() or debuffPosition - local debuffs = not BNP.AreDebuffsEnabled or BNP:AreDebuffsEnabled() - local ccs = not BNP.AreCrowdControlEnabled or BNP:AreCrowdControlEnabled() - local separateCC = ccs and debuffs and ccPosition == "top" and IsTopDebuffPosition(debuffPosition) - and BNP.IsSeparateCCRowEnabled and BNP:IsSeparateCCRowEnabled() - local y = GetBaseTopOffset() - - if separateCC then - y = y + auraSize + ICON_SPACING + ccSize + ICON_SPACING - else - local rowHeight = 0 - if debuffs and IsTopDebuffPosition(debuffPosition) then rowHeight = math.max(rowHeight, auraSize) end - if ccs and ccPosition == "top" then rowHeight = math.max(rowHeight, ccSize) end - if rowHeight > 0 then y = y + rowHeight + ICON_SPACING end - end - return y -end - -local function FrameShown(frame) - return frame and frame.IsShown and frame:IsShown() and true or false -end - -local function GetBottomOffset(plate) - local auraSize = BNP.GetIconSize and BNP:GetIconSize() or 18 - local ccSize = BNP.GetCCIconSize and BNP:GetCCIconSize() or auraSize - local debuffPosition = BNP.GetDebuffPosition and BNP:GetDebuffPosition() or "top" - local ccPosition = BNP.GetCCPosition and BNP:GetCCPosition() or debuffPosition - local debuffs = not BNP.AreDebuffsEnabled or BNP:AreDebuffsEnabled() - local ccs = not BNP.AreCrowdControlEnabled or BNP:AreCrowdControlEnabled() - local separateCC = ccs and debuffs and ccPosition == "bottom" and debuffPosition == "bottom" - and BNP.IsSeparateCCRowEnabled and BNP:IsSeparateCCRowEnabled() - - local harmfulDepth = 0 - if separateCC then - -- The CC row is physically the second row, so preserve the first-row - -- offset whenever that second row is visible. - if FrameShown(plate and plate.BNPCCContainer) then - harmfulDepth = auraSize + ICON_SPACING + ccSize - elseif FrameShown(plate and plate.BNPAuraContainer) then - harmfulDepth = auraSize - end - else - if debuffs and debuffPosition == "bottom" and FrameShown(plate and plate.BNPAuraContainer) then - harmfulDepth = math.max(harmfulDepth, auraSize) - end - if ccs and ccPosition == "bottom" then - if ccPosition == debuffPosition then - if FrameShown(plate and plate.BNPAuraContainer) then harmfulDepth = math.max(harmfulDepth, ccSize) end - elseif FrameShown(plate and plate.BNPCCContainer) then - harmfulDepth = math.max(harmfulDepth, ccSize) - end - end - end - - local y = BOTTOM_SPACING - if harmfulDepth > 0 then y = y + harmfulDepth + ICON_SPACING end - return y -end - -local function GetSideOffset(position) - local auraSize = BNP.GetIconSize and BNP:GetIconSize() or 18 - local ccSize = BNP.GetCCIconSize and BNP:GetCCIconSize() or auraSize - local debuffPosition = BNP.GetDebuffPosition and BNP:GetDebuffPosition() or "top" - local ccPosition = BNP.GetCCPosition and BNP:GetCCPosition() or debuffPosition - local debuffs = not BNP.AreDebuffsEnabled or BNP:AreDebuffsEnabled() - local ccs = not BNP.AreCrowdControlEnabled or BNP:AreCrowdControlEnabled() - local rowHeight = 0 - if debuffs and debuffPosition == position then rowHeight = math.max(rowHeight, auraSize) end - if ccs and ccPosition == position then rowHeight = math.max(rowHeight, ccSize) end - if rowHeight > 0 then return (rowHeight / 2) + ICON_SPACING + (GetIconSize() / 2) end - return 0 -end - -local function GetRegionWidth(region) - if not region then return 0 end - if region.IsShown and not region:IsShown() then return 0 end - if region.GetStringWidth then - local w = tonumber(region:GetStringWidth()) or 0 - if w > 0 then return w end - end - if region.GetWidth then return math.max(0, tonumber(region:GetWidth()) or 0) end - return 0 -end - -local function GetRightSpacing(plate) - local extra = GetRegionWidth(plate and plate.level) + GetRegionWidth(plate and plate.levelicon) - if extra > 0 then extra = extra + 6 end - return SIDE_SPACING + extra -end - -local function AnchorContainer(plate, container) - if not plate or not container then return end - container:ClearAllPoints() - local position = GetPosition() - local anchor = plate.healthbar or plate - - if position == "left" then - container:SetPoint("RIGHT", anchor, "LEFT", -SIDE_SPACING, GetSideOffset("left")) - elseif position == "right" then - container:SetPoint("LEFT", anchor, "RIGHT", GetRightSpacing(plate), GetSideOffset("right")) - else - local nameHidden = (BNP.IsPlateNameHidden and BNP:IsPlateNameHidden(plate)) or plate.BNPNameHidden - local compactShift = nameHidden and -NAME_HIDDEN_COMPACT_SHIFT or 0 - if plate.healthbar then - container:SetPoint("BOTTOM", plate.healthbar, "TOP", 0, GetTopOffset() + compactShift) - else - container:SetPoint("BOTTOM", plate, "BOTTOM", 0, 24 + GetTopOffset() + compactShift) - end - end -end - -function BNP:RefreshImmunityLayoutForPlate(plate) - if not plate or not plate.BNPImmunityContainer then return end - AnchorContainer(plate, plate.BNPImmunityContainer) -end - -local function ApplyFrameLevel(plate, container) - if not plate or not container then return end - if plate.GetFrameStrata and container.GetFrameStrata and container.SetFrameStrata then - local strata = plate:GetFrameStrata() - if strata and container:GetFrameStrata() ~= strata then - container:SetFrameStrata(strata) - end - end - if plate.GetFrameLevel and container.GetFrameLevel and container.SetFrameLevel then - local level = math.max(0, (tonumber(plate:GetFrameLevel()) or 0) + 1) - if container:GetFrameLevel() ~= level then container:SetFrameLevel(level) end - local i - for i = 1, table.getn(container.icons or {}) do - local icon = container.icons[i] - if icon and icon.GetFrameLevel and icon.SetFrameLevel and icon:GetFrameLevel() ~= level then - icon:SetFrameLevel(level) - end - end - end -end - -local function CreateIcon(parent, index) - local size = GetIconSize() - local icon = CreateFrame("Frame", nil, parent) - icon:SetWidth(size) - icon:SetHeight(size) - - local texture = icon:CreateTexture(nil, "ARTWORK") - texture:SetAllPoints(icon) - icon.texture = texture - - local timer = icon:CreateFontString(nil, "OVERLAY") - timer:SetFont(UI.TIMER_FONT or "Fonts\\FRIZQT__.TTF", UI.TIMER_SIZE or 8, "OUTLINE") - timer:SetPoint("CENTER", icon, "CENTER", UI.TIMER_OFFSET_X or 0, UI.TIMER_OFFSET_Y or 0) - timer:SetTextColor(1, 1, 1) - icon.timer = timer - - icon.index = index - icon:Hide() - return icon -end - -local function LayoutContainer(plate, container) - if not plate or not container then return end - local size = GetIconSize() - local position = GetPosition() - local i - - if container.BNPLastSize ~= size or container.BNPLastIconPosition ~= position then - container.BNPLastSize = size - container.BNPLastIconPosition = position - container:SetWidth(size * MAX_ICONS + ICON_SPACING * (MAX_ICONS - 1)) - container:SetHeight(size) - for i = 1, table.getn(container.icons or {}) do - local icon = container.icons[i] - icon:SetWidth(size) - icon:SetHeight(size) - icon:ClearAllPoints() - if position == "left" then - icon:SetPoint("RIGHT", container, "RIGHT", -((i - 1) * (size + ICON_SPACING)), 0) - else - icon:SetPoint("LEFT", container, "LEFT", (i - 1) * (size + ICON_SPACING), 0) - end - end - end - - -- Cheap layout key; avoids touching anchors every scan. - local layoutKey = tostring(position) .. ":" .. - tostring(BNP.GetDebuffPosition and BNP:GetDebuffPosition() or "top") .. ":" .. - tostring(BNP.GetCCPosition and BNP:GetCCPosition() or "top") .. ":" .. - tostring(BNP.IsSeparateCCRowEnabled and BNP:IsSeparateCCRowEnabled()) .. ":" .. - tostring(BNP.GetIconSize and BNP:GetIconSize() or 18) .. ":" .. - tostring(BNP.GetCCIconSize and BNP:GetCCIconSize() or 18) .. ":" .. tostring(size) .. ":" .. - tostring(plate.BNPNameHidden and true or false) .. ":" .. - tostring(FrameShown(plate.BNPAuraContainer)) .. ":" .. - tostring(FrameShown(plate.BNPCCContainer)) - - if container.BNPLastLayoutKey ~= layoutKey then - container.BNPLastLayoutKey = layoutKey - AnchorContainer(plate, container) - end - ApplyFrameLevel(plate, container) -end - -local function HideContainer(plate) - local container = plate and plate.BNPImmunityContainer - if not container then return end - local i - for i = 1, table.getn(container.icons or {}) do container.icons[i]:Hide() end - container:Hide() -end - -local function InstallHideGuard(plate) - if not plate or plate.BNPImmunityHideGuard then return end - local oldOnHide = plate:GetScript("OnHide") - plate:SetScript("OnHide", function() - if oldOnHide then oldOnHide() end - HideContainer(this or plate) - end) - plate.BNPImmunityHideGuard = true -end - -local function CreateContainer(plate) - if not plate or plate.BNPImmunityContainer then return end - if BNP.RegisterPlate then BNP:RegisterPlate(plate) end - - local container = CreateFrame("Frame", nil, GetVisualParent(plate)) - container.icons = {} - local i - for i = 1, MAX_ICONS do container.icons[i] = CreateIcon(container, i) end - plate.BNPImmunityContainer = container - LayoutContainer(plate, container) - container:Hide() - InstallHideGuard(plate) -end - -local function GetCache(guid) - if not guid then return nil end - local cache = BNP.guidImmunities[guid] - if not cache then - cache = { _nextOrder = 0 } - BNP.guidImmunities[guid] = cache - end - return cache -end - -local function CommitSeen(guid, def, spellID, texture, duration, expires, now, source) - if not guid or not def then return end - local cache = GetCache(guid) - local entry = cache[def.key] - if not entry then - cache._nextOrder = (cache._nextOrder or 0) + 1 - entry = { order = cache._nextOrder } - cache[def.key] = entry - end - - entry.spellID = tonumber(spellID) or entry.spellID - entry.texture = texture or entry.texture - entry.duration = tonumber(duration) or entry.duration or DefDuration(def, tonumber(spellID)) - entry.lastSeen = now - entry.missingSince = nil - if source == "capi" then - entry.capiConfirmed = true - entry.capiMissingSince = nil - elseif source == "unitbuff" then - entry.unitBuffConfirmed = true - entry.unitBuffMissingSince = nil - end - - expires = tonumber(expires) - if expires and expires > now then - entry.expires = expires - elseif not entry.expires or entry.expires <= now then - local d = entry.duration or DefDuration(def, entry.spellID) - if d then entry.expires = now + d end - end -end - --- These full immunities also cleanse harmful effects in Classic. Keep the --- harmful-debuff module hard-suppressed while the immunity entry is active. -local CLEANSE_ALL = { - divine_shield = true, - ice_block = true, -} - -local function ApplyCleanseSuppression(guid, def, entry, now) - if not guid or not def or not CLEANSE_ALL[def.key] then return end - if not entry then return end - local remaining = entry.expires and (entry.expires - now) or nil - if remaining and remaining <= 0 then return end - if BNP.SuppressHarmfulAurasForImmunity then - -- Renewable short hold: immediate cleanse now, but if Ice Block/Bubble is - -- clicked off early the suppression naturally expires within a fraction - -- of a second instead of hiding new debuffs until the guessed full timer. - BNP:SuppressHarmfulAurasForImmunity(guid, 0.45) - end -end - -local function ParseDurationExpiry(now, a5, a6, a7, a8, a9, a10) - local vals = { a5, a6, a7, a8, a9, a10 } - local i - for i = 1, table.getn(vals) - 1 do - local d = tonumber(vals[i]) - local e = tonumber(vals[i + 1]) - if d and e and d > 0 and d <= 3600 and e > now then return d, e end - end - return nil, nil -end - -local function ReconcileMissingSource(guid, source, seen, now) - local cache = guid and BNP.guidImmunities[guid] or nil - if not cache then return end - - local d, def, entry, confirmedKey, missingKey - if source == "capi" then - confirmedKey = "capiConfirmed" - missingKey = "capiMissingSince" - else - confirmedKey = "unitBuffConfirmed" - missingKey = "unitBuffMissingSince" - end - - for d = 1, table.getn(IMMUNITY_DEFS) do - def = IMMUNITY_DEFS[d] - entry = cache[def.key] - if entry and entry[confirmedKey] then - if seen and seen[def.key] then - entry[missingKey] = nil - else - if not entry[missingKey] then entry[missingKey] = now end - if now - entry[missingKey] >= MISSING_GRACE then - -- Once a live aura source has positively confirmed this exact - -- immunity, that same source becomes authoritative for early - -- removals (Ice Block clicked off, Bubble canceled/removed, dispel). - cache[def.key] = nil - end - end - end - end -end - -local function ScanClassicAPIBuffs(unit, guid, now) - if not unit or not guid then return false end - if type(C_UnitAuras) ~= "table" then return false end - - local seen = {} - - -- Preferred path: ask ClassicAPI directly for the whitelisted spell IDs. - -- Its aura cache is evicted by the client's real OnAuraRemoved hook, so an - -- Ice Block that is clicked off disappears here immediately instead of - -- living until the guessed cast timer ends. - if type(C_UnitAuras.GetUnitAuraBySpellID) == "function" then - local anyCallWorked = false - local d, j, def, sid, ok, aura - for d = 1, table.getn(IMMUNITY_DEFS) do - def = IMMUNITY_DEFS[d] - if def.spellIDs then - for j = 1, table.getn(def.spellIDs) do - sid = def.spellIDs[j] - ok, aura = pcall(C_UnitAuras.GetUnitAuraBySpellID, unit, sid, "HELPFUL") - if ok then - anyCallWorked = true - if type(aura) == "table" then - local auraID = tonumber(aura.spellId or aura.spellID) or sid - local matched = FindDef(auraID, aura.name, aura.icon or aura.texture) - if matched then - seen[matched.key] = true - CommitSeen(guid, matched, auraID, aura.icon or aura.texture, - tonumber(aura.duration), tonumber(aura.expirationTime), now, "capi") - break - end - end - else - -- If this build rejects the exact getter, fall back to index scan - -- below and never use a failed call as negative evidence. - anyCallWorked = false - break - end - end - end - if not anyCallWorked and d == 1 then break end - end - - if anyCallWorked then - ReconcileMissingSource(guid, "capi", seen, now) - return true - end - end - - local getter = nil - local useFilter = false - if type(C_UnitAuras.GetBuffDataByIndex) == "function" then - getter = C_UnitAuras.GetBuffDataByIndex - elseif type(C_UnitAuras.GetAuraDataByIndex) == "function" then - getter = C_UnitAuras.GetAuraDataByIndex - useFilter = true - end - if not getter then return false end - - local completed = true - local i - for i = 1, 48 do - local ok, aura - if useFilter then - ok, aura = pcall(getter, unit, i, "HELPFUL") - else - ok, aura = pcall(getter, unit, i) - end - if not ok then completed = false break end - if not aura then break end - - if type(aura) == "table" then - local spellID = tonumber(aura.spellId or aura.spellID) - local def = FindDef(spellID, aura.name, aura.icon or aura.texture) - if def then - seen[def.key] = true - CommitSeen(guid, def, spellID, aura.icon or aura.texture, - tonumber(aura.duration), tonumber(aura.expirationTime), now, "capi") - end - end - end - - if completed then ReconcileMissingSource(guid, "capi", seen, now) end - return completed -end - -local function ScanUnitBuffs(unit, guid, now) - if not unit or not guid or not UnitBuff then return false end - - local seen = {} - local completed = true - local i - for i = 1, 64 do - local ok, texture, applications, buffID, a4, a5, a6, a7, a8, a9, a10 = pcall(UnitBuff, unit, i) - if not ok then completed = false break end - if not texture then break end - - local spellID = tonumber(buffID) - if not spellID then - if type(a4) == "number" then spellID = a4 - elseif type(a5) == "number" then spellID = a5 end - end - if spellID and spellID < -1 then spellID = spellID + 65536 end - - local auraName = nil - if spellID and SpellInfo then auraName = SpellInfo(spellID) end - local def = FindDef(spellID, auraName, texture) - if def then - seen[def.key] = true - local duration, expires = ParseDurationExpiry(now, a4, a5, a6, a7, a8, a9, a10) - CommitSeen(guid, def, spellID, texture, duration, expires, now, "unitbuff") - end - end - - -- UnitBuff is only allowed to remove an immunity after UnitBuff itself has - -- positively seen that exact whitelist entry at least once. This preserves - -- the cast-event fallback on clients where hostile helpful auras are hidden, - -- while still making clicked-off buffs disappear immediately when visible. - if completed then ReconcileMissingSource(guid, "unitbuff", seen, now) end - return completed -end - -local function ResolveClassicAuraUnit(guid, targetGUID) - if not guid then return nil end - if targetGUID and guid == targetGUID then return "target" end - - if type(UnitTokenFromGUID) == "function" then - local ok, token = pcall(UnitTokenFromGUID, guid) - if ok and token and token ~= "" then return token end - end - return nil -end - -local function FormatTimer(remaining) - if not remaining or remaining <= 0 then return "" end - if remaining >= 3600 then return math.ceil(remaining / 3600) .. "h" end - if remaining >= 60 then return math.ceil(remaining / 60) .. "m" end - return tostring(math.ceil(remaining)) -end - -local function Render(plate, guid, now) - local container = plate and plate.BNPImmunityContainer - if not container then return end - LayoutContainer(plate, container) - - local cache = guid and BNP.guidImmunities[guid] or nil - local active = {} - local d, def, entry, remaining - for d = 1, table.getn(IMMUNITY_DEFS) do - def = IMMUNITY_DEFS[d] - entry = cache and cache[def.key] or nil - remaining = entry and entry.expires and (entry.expires - now) or nil - if entry and (not entry.expires or (remaining and remaining > 0)) then - ApplyCleanseSuppression(guid, def, entry, now) - if table.getn(active) < MAX_ICONS then - table.insert(active, { def = def, entry = entry, remaining = remaining }) - end - elseif cache and entry and entry.expires and remaining and remaining <= 0 then - cache[def.key] = nil - end - end - - if table.getn(active) > 1 then - table.sort(active, function(a, b) return (a.entry.order or 0) < (b.entry.order or 0) end) - end - - local count = table.getn(active) - local size = GetIconSize() - local visibleWidth = size - if count > 1 then visibleWidth = size * count + ICON_SPACING * (count - 1) end - container:SetWidth(visibleWidth) - - local i - for i = 1, MAX_ICONS do - local icon = container.icons[i] - local item = active[i] - if item then - local texture = item.entry.texture - if not texture and item.entry.spellID and SpellInfo then - local _, _, spellTexture = SpellInfo(item.entry.spellID) - texture = spellTexture - end - if texture then icon.texture:SetTexture(texture) end - icon.timer:SetText(FormatTimer(item.remaining)) - icon:Show() - else - icon.timer:SetText("") - icon:Hide() - end - end - - if count > 0 then container:Show() else container:Hide() end -end - -local function GetTargetGUID() - if not UnitExists then return nil end - local exists, guid = UnitExists("target") - if exists and guid then return guid end - return nil -end - -local function GetPlateGUID(plate) - if not plate or not plate.GetName then return nil end - local token = plate:GetName(1) - if token and token ~= "" then return token end - return nil -end - -local function UpdatePlate(plate) - if not plate or not plate:IsShown() then - HideContainer(plate) - return - end - if BNP.ArePvPImmunitiesEnabled and not BNP:ArePvPImmunitiesEnabled() then - HideContainer(plate) - return - end - if not plate.BNPImmunityContainer then CreateContainer(plate) end - - local now = GetTime() - local guid = BNP.GetStablePlateAuraGUID and BNP.GetStablePlateAuraGUID(plate, now) or GetPlateGUID(plate) - if not guid then HideContainer(plate) return end - - local targetGUID = GetTargetGUID() - local unit = nil - if targetGUID and targetGUID == guid then - unit = "target" - else - unit = GetPlateGUID(plate) - end - - -- Scan the two aura surfaces independently. ClassicAPI nameplateN tokens are - -- preferred when available; SuperWoW's raw GUID token remains the legacy - -- UnitBuff fallback. Do not let one resolver's player check block the other. - local classicUnit = ResolveClassicAuraUnit(guid, targetGUID) - local classicCanScan = classicUnit ~= nil - if classicCanScan and UnitIsPlayer then - classicCanScan = UnitIsPlayer(classicUnit) and true or false - end - if classicCanScan then ScanClassicAPIBuffs(classicUnit, guid, now) end - - local legacyCanScan = unit ~= nil - if legacyCanScan and UnitIsPlayer then - legacyCanScan = UnitIsPlayer(unit) and true or false - end - if legacyCanScan then ScanUnitBuffs(unit, guid, now) end - - Render(plate, guid, now) -end - --- UNIT_CASTEVENT is a fast timer seed. Once ClassicAPI/UnitBuff has --- positively confirmed the aura, that live source also removes it early when --- the buff is canceled, dispelled, or otherwise ends before its timer. -local castFrame = CreateFrame("Frame") -castFrame:RegisterEvent("UNIT_CASTEVENT") -local function HandleImmunityCastEvent() - local casterGUID = arg1 - local targetGUID = arg2 - local eventType = arg3 - local spellID = tonumber(arg4) - if spellID and spellID < -1 then spellID = spellID + 65536 end - - -- Keep the last raw event for /bnpimm diagnostics. This is intentionally - -- cheap and lets us verify instant enemy casts without another addon. - BNP._immLastCast = { - casterGUID = casterGUID, - targetGUID = targetGUID, - eventType = eventType, - spellID = spellID, - time = GetTime(), - } - - if not casterGUID or not spellID then return end - if eventType ~= "CAST" and eventType ~= "CHANNEL" then return end - - local texture = nil - local spellName = nil - if SpellInfo then - spellName, _, texture = SpellInfo(spellID) - end - local def = DEF_BY_ID[spellID] or FindDef(spellID, spellName, texture) - if not def then return end - - local guid = def.selfOnly and casterGUID or (targetGUID or casterGUID) - local now = GetTime() - local duration = DefDuration(def, spellID) - if duration then - CommitSeen(guid, def, spellID, texture, duration, now + duration, now) - local cache = BNP.guidImmunities and BNP.guidImmunities[guid] - ApplyCleanseSuppression(guid, def, cache and cache[def.key], now) - end -end - -castFrame:SetScript("OnEvent", function() - local ok, err = pcall(HandleImmunityCastEvent) - if not ok then ReportImmunityError("cast", err) end -end) - -function BNP:RefreshImmunityVisibility() - local plate - for plate in pairs(BNP.plates or {}) do - if plate then - if self:ArePvPImmunitiesEnabled() then - if not plate.BNPImmunityContainer then CreateContainer(plate) end - else - HideContainer(plate) - end - end - end -end - -function BNP:RefreshAllImmunityLayouts() - local plate - for plate in pairs(BNP.plates or {}) do - if plate and plate.BNPImmunityContainer then - plate.BNPImmunityContainer.BNPLastLayoutKey = nil - plate.BNPImmunityContainer.BNPLastSize = nil - plate.BNPImmunityContainer.BNPLastIconPosition = nil - LayoutContainer(plate, plate.BNPImmunityContainer) - end - end -end - --- Small temporary diagnostic for this test build. Target a bubbled player and --- type /bnpimm. It prints the raw UnitBuff slots BNP sees, including spell ID --- when SuperWoW exposes it. This makes server/client differences obvious without --- relying on another addon's truncated error window. -SLASH_BNPIMMUNITYDEBUG1 = "/bnpimm" -SlashCmdList["BNPIMMUNITYDEBUG"] = function() - if not DEFAULT_CHAT_FRAME then return end - local exists, guid = UnitExists("target") - DEFAULT_CHAT_FRAME:AddMessage("|cff33ff99BNP IMM DEBUG|r targetGUID=" .. tostring(guid or "nil") .. " player=" .. tostring(UnitIsPlayer and UnitIsPlayer("target"))) - - local last = BNP._immLastCast - if last then - DEFAULT_CHAT_FRAME:AddMessage(" last CASTEVENT type=" .. tostring(last.eventType) .. " id=" .. tostring(last.spellID) .. " caster=" .. tostring(last.casterGUID) .. " target=" .. tostring(last.targetGUID)) - else - DEFAULT_CHAT_FRAME:AddMessage(" last CASTEVENT: none seen") - end - - if not exists then return end - - local capiFound = 0 - if type(C_UnitAuras) == "table" then - local getter = nil - local useFilter = false - if type(C_UnitAuras.GetBuffDataByIndex) == "function" then - getter = C_UnitAuras.GetBuffDataByIndex - elseif type(C_UnitAuras.GetAuraDataByIndex) == "function" then - getter = C_UnitAuras.GetAuraDataByIndex - useFilter = true - end - DEFAULT_CHAT_FRAME:AddMessage(" ClassicAPI aura getter=" .. tostring(getter ~= nil)) - if getter then - local i - for i = 1, 16 do - local ok, aura - if useFilter then ok, aura = pcall(getter, "target", i, "HELPFUL") - else ok, aura = pcall(getter, "target", i) end - if not ok then - DEFAULT_CHAT_FRAME:AddMessage(" CAPI error slot " .. tostring(i) .. ": " .. tostring(aura)) - break - end - if not aura then break end - if type(aura) == "table" then - local sid = tonumber(aura.spellId or aura.spellID) - local def = FindDef(sid, aura.name, aura.icon or aura.texture) - if def then capiFound = capiFound + 1 end - if def then - DEFAULT_CHAT_FRAME:AddMessage(" CAPI MATCH " .. tostring(i) .. " id=" .. tostring(sid) .. " name=" .. tostring(aura.name) .. " tex=" .. tostring(aura.icon or aura.texture)) - end - else - DEFAULT_CHAT_FRAME:AddMessage(" CAPI slot " .. tostring(i) .. " returned " .. type(aura) .. ": " .. tostring(aura)) - end - end - end - else - DEFAULT_CHAT_FRAME:AddMessage(" ClassicAPI C_UnitAuras: unavailable") - end - - local ubFound = 0 - local i - for i = 1, 32 do - local ok, texture, stacks, buffID, a4, a5, a6 = pcall(UnitBuff, "target", i) - if not ok then - DEFAULT_CHAT_FRAME:AddMessage(" UnitBuff error slot " .. tostring(i) .. ": " .. tostring(texture)) - break - end - if not texture then break end - local spellID = tonumber(buffID) - if not spellID and type(a4) == "number" then spellID = a4 end - if spellID and spellID < -1 then spellID = spellID + 65536 end - local def = FindDef(spellID, spellID and SpellInfo and SpellInfo(spellID) or nil, texture) - if def then ubFound = ubFound + 1 end - if def then - DEFAULT_CHAT_FRAME:AddMessage(" UB MATCH " .. tostring(i) .. " id=" .. tostring(spellID) .. " tex=" .. tostring(texture)) - end - end - if capiFound == 0 and ubFound == 0 then - DEFAULT_CHAT_FRAME:AddMessage(" no configured immunity matched live target buffs") - end -end - --- Sanity-check the whole module callback so one optional PvP feature can never --- stop any other BNP nameplate module. -local function SafeCreateContainer(plate) - local ok, err = pcall(CreateContainer, plate) - if not ok then ReportImmunityError("create", err) end -end - -local function SafeUpdatePlate(plate) - local ok, err = pcall(UpdatePlate, plate) - if not ok then ReportImmunityError("update", err) end -end - -table.insert(BNP.libnameplate.OnInit, SafeCreateContainer) -table.insert(BNP.libnameplate.OnShow, SafeCreateContainer) -table.insert(BNP.libnameplate.OnUpdate, SafeUpdatePlate) diff --git a/modules/names.lua b/modules/names.lua deleted file mode 100644 index 244748a..0000000 --- a/modules/names.lua +++ /dev/null @@ -1,134 +0,0 @@ -BNP = BNP or {} - --- Independent name visibility controls for Blizzard nameplates. --- BNP only hides the name FontString; health bars, level text, raid icons, --- auras, immunities and click behavior remain untouched. - -local function ResolvePlateType(plate) - if not plate or not plate.GetName then return nil end - - local token = plate:GetName(1) - if not token then return nil end - - local exists = UnitExists(token) - if not exists then return nil end - - if UnitIsPlayer and UnitIsPlayer(token) then - return "player" - end - - return "npc" -end - -local function ShouldHide(unitType) - if unitType == "player" then - if BNP.HidePlayerNamesEnabled then return BNP:HidePlayerNamesEnabled() end - return BNP_DB and BNP_DB.hidePlayerNames and true or false - elseif unitType == "npc" then - if BNP.HideNPCNamesEnabled then return BNP:HideNPCNamesEnabled() end - return BNP_DB and BNP_DB.hideNPCNames and true or false - end - return false -end - --- Authoritative query used by aura/immunity layout. Do not rely only on the --- transient BNPNameHidden marker: on recycled/projected Vanilla nameplates the --- name FontString and aura module callbacks can update in a different order. -function BNP:IsPlateNameHidden(plate) - if not plate then return false end - - local unitType = ResolvePlateType(plate) or plate.BNPNameUnitType - if unitType then - return ShouldHide(unitType) - end - - -- If identity is momentarily unresolved, keep the last BNP-owned state so a - -- recycled plate does not jump vertically for a single scan. - return plate.BNPNameHidden and true or false -end - -local function RefreshPlateAuraLayout(plate) - if not plate then return end - if BNP.RefreshAuraLayoutForPlate then BNP:RefreshAuraLayoutForPlate(plate) end - if BNP.RefreshImmunityLayoutForPlate then BNP:RefreshImmunityLayoutForPlate(plate) end -end - -local function RestoreIfBNPHid(plate) - if not plate or not plate.name then return end - if plate.BNPNameHidden then - plate.name:Show() - plate.BNPNameHidden = nil - end -end - -local function UpdatePlateName(plate, resetIdentity) - if not plate or not plate.name then return end - - if resetIdentity then - -- Blizzard reuses nameplate frames. Never carry a hidden name from the - -- previous unit into a newly shown plate while its projected token is not - -- resolved yet. - plate.BNPNameUnitType = nil - RestoreIfBNPHid(plate) - end - - local unitType = ResolvePlateType(plate) - if unitType then - plate.BNPNameUnitType = unitType - else - unitType = plate.BNPNameUnitType - end - - if not unitType then return end - - if ShouldHide(unitType) then - if not plate.BNPNameHidden then - plate.name:Hide() - plate.BNPNameHidden = true - elseif plate.name.IsShown and plate.name:IsShown() then - -- The client may re-show Blizzard regions while updating/recycling a - -- plate. Re-enforce only names BNP explicitly owns as hidden. - plate.name:Hide() - end - else - RestoreIfBNPHid(plate) - end -end - -function BNP:RefreshNameVisibility() - local plate - for plate in pairs(BNP.plates or {}) do - if plate and plate.IsShown and plate:IsShown() then - UpdatePlateName(plate, false) - -- Re-anchor immediately when the option is toggled instead of waiting - -- for the aura module's next identity/update cycle. - RefreshPlateAuraLayout(plate) - elseif plate then - RestoreIfBNPHid(plate) - plate.BNPNameUnitType = nil - end - end -end - -if BNP.libnameplate then - table.insert(BNP.libnameplate.OnInit, function(plate) - local current = plate or this - if not current then return end - UpdatePlateName(current, true) - end) - - table.insert(BNP.libnameplate.OnShow, function(plate) - local current = plate or this - if not current then return end - UpdatePlateName(current, true) - end) - - -- libnameplate updates are already throttled to 0.10s. This is deliberate: - -- if Blizzard re-shows the name FontString, the configured hidden state is - -- restored without adding a per-frame OnUpdate to every nameplate. - table.insert(BNP.libnameplate.OnUpdate, function(plate) - local current = plate or this - if not current or not current:IsShown() then return end - UpdatePlateName(current, false) - end) -end diff --git a/modules/scale.lua b/modules/scale.lua deleted file mode 100644 index ebedae4..0000000 --- a/modules/scale.lua +++ /dev/null @@ -1,353 +0,0 @@ -BNP = BNP or {} - -local MIN_SCALE = 0.50 -local MAX_SCALE = 1.50 -local DEFAULT_SCALE = 1.00 - -local function Clamp(value) - if value < MIN_SCALE then return MIN_SCALE end - if value > MAX_SCALE then return MAX_SCALE end - return value -end - -function BNP:GetNameplateScale() - BNP_DB = BNP_DB or {} - return Clamp(tonumber(BNP_DB.nameplateScale) or DEFAULT_SCALE) -end - -local function EffectiveScale() - local uiScale = 1.0 - if UIParent and UIParent.GetScale then - uiScale = UIParent:GetScale() or 1.0 - end - return uiScale * BNP:GetNameplateScale() -end - -local function CaptureAnchorState(object, plate, wrapper) - if not object or not object.GetPoint then return nil end - - local state = { - object = object, - points = {}, - } - - local count = 1 - if object.GetNumPoints then - count = object:GetNumPoints() or 1 - if count < 1 then count = 1 end - end - - local i - for i = 1, count do - local point, relativeTo, relativePoint, x, y = object:GetPoint(i) - if point then - if relativeTo == plate then - relativeTo = wrapper - end - - state.points[table.getn(state.points) + 1] = { - point = point, - relativeTo = relativeTo or wrapper, - relativePoint = relativePoint or point, - x = x or 0, - y = y or 0, - } - end - end - - return state -end - -local function AnchorStateMatches(state) - if not state or not state.object or not state.object.GetPoint then return false end - - local object = state.object - local expectedCount = table.getn(state.points) - local currentCount = expectedCount - - if object.GetNumPoints then - currentCount = object:GetNumPoints() or 0 - end - - if currentCount ~= expectedCount then - return false - end - - local i - for i = 1, expectedCount do - local expected = state.points[i] - local point, relativeTo, relativePoint, x, y = object:GetPoint(i) - - if point ~= expected.point - or relativeTo ~= expected.relativeTo - or relativePoint ~= expected.relativePoint - or math.abs((x or 0) - expected.x) > 0.01 - or math.abs((y or 0) - expected.y) > 0.01 then - return false - end - end - - return true -end - -local function RestoreAnchorState(state, wrapper) - if not state or not state.object then return end - - local object = state.object - - if object.GetParent and object.SetParent and object:GetParent() ~= wrapper then - object:SetParent(wrapper) - end - - -- Do nothing in the common case. Blizzard normally only rewrites a subset - -- of nameplate anchors while moving, most notably the level FontString. - if AnchorStateMatches(state) then return end - - if not object.ClearAllPoints or not object.SetPoint then return end - - object:ClearAllPoints() - - local i - for i = 1, table.getn(state.points) do - local p = state.points[i] - object:SetPoint( - p.point, - p.relativeTo or wrapper, - p.relativePoint or p.point, - p.x, - p.y - ) - end -end - -local function SetHoverGlow(plate, shown) - if not plate or not plate.glow then return end - - if shown then - if plate.glow.Show then - plate.glow:Show() - end - else - if plate.glow.Hide then - plate.glow:Hide() - end - end -end - -local function SetYOffsetMouseMode(plate, wrapper, offset) - if not plate or not wrapper then return end - - -- Always use the visual wrapper as BNP's interactive frame, including at - -- Y Offset 0. Previously offset 0 used Blizzard's native Button while - -- positive offsets used BNPScaleWrapper. That split mouse path caused the - -- original Blizzard mouseover glow to disappear at offset 0. - -- - -- Keeping clicks, hit geometry and hover feedback on the same frame also - -- guarantees that the clickable area follows the visible plate at every - -- configured Y offset. - if plate.EnableMouse then - plate:EnableMouse(false) - end - - if wrapper.EnableMouse then - wrapper:EnableMouse(true) - end - - if wrapper.RegisterForClicks then - -- Preserve the classic Blizzard nameplate mouse behavior: - -- left click selects the unit, right click selects it and starts auto attack. - wrapper:RegisterForClicks("LeftButtonUp", "RightButtonUp") - end - - if not wrapper.BNPClickForwardInstalled then - wrapper:SetScript("OnClick", function() - -- The wrapper exists only because BNP visually offsets/scales the original - -- Blizzard plate. Keep Blizzard's native nameplate click semantics by - -- forwarding the ACTUAL mouse button to the original Button. - -- - -- parent:Click() without an argument always simulates LeftButton. That was - -- the reason right click turned into a plain target-select while BNP was - -- enabled. Do not call AttackTarget() manually here: it toggles auto attack - -- and is not equivalent to Blizzard's native nameplate right-click path. - local mouseButton = arg1 or "LeftButton" - local current = this or wrapper - local parent = current and current.plate or plate - if parent and parent:IsShown() and parent.Click then - parent:Click(mouseButton) - end - end) - wrapper.BNPClickForwardInstalled = true - end - - if not wrapper.BNPMouseoverFeedbackInstalled then - wrapper:SetScript("OnEnter", function() - local current = this or wrapper - local parent = current and current.plate or plate - SetHoverGlow(parent, true) - end) - - wrapper:SetScript("OnLeave", function() - local current = this or wrapper - local parent = current and current.plate or plate - SetHoverGlow(parent, false) - end) - - wrapper.BNPMouseoverFeedbackInstalled = true - end -end - -local function ApplyWrapperOffset(plate, wrapper) - if not plate or not wrapper then return end - - local offset = 0 - if BNP.GetNameplateYOffset then - offset = BNP:GetNameplateYOffset() - end - - -- Move only the child visual container. The WorldFrame-projected Blizzard - -- nameplate itself keeps its original anchor and can continue to be - -- repositioned by the client every frame. - wrapper:ClearAllPoints() - wrapper:SetPoint("TOPLEFT", plate, "TOPLEFT", 0, offset) - wrapper:SetPoint("BOTTOMRIGHT", plate, "BOTTOMRIGHT", 0, offset) - - SetYOffsetMouseMode(plate, wrapper, offset) -end - -local function EnsureScaleWrapper(plate) - if not plate or plate.BNPScaleWrapper then return end - - local originalWidth = plate:GetWidth() - local originalHeight = plate:GetHeight() - - local wrapper = CreateFrame("Button", nil, plate) - wrapper:SetAllPoints(plate) - wrapper.plate = plate - wrapper.originalWidth = originalWidth - wrapper.originalHeight = originalHeight - wrapper.BNPAnchorStates = {} - wrapper:EnableMouse(false) - - plate.BNPScaleWrapper = wrapper - - -- Capture the ORIGINAL Blizzard anchors before changing parentage. - -- Those stored anchors become BNP's authoritative visual layout whenever - -- Y-offset is enabled. - if plate.healthbar then - local state = CaptureAnchorState(plate.healthbar, plate, wrapper) - if state then - table.insert(wrapper.BNPAnchorStates, state) - end - - plate.healthbar:SetParent(wrapper) - RestoreAnchorState(state, wrapper) - plate.healthbar:SetFrameLevel(1) - end - - local regions = { plate:GetRegions() } - local i, object - for i, object in pairs(regions) do - if object and object.SetParent then - local state = CaptureAnchorState(object, plate, wrapper) - if state then - table.insert(wrapper.BNPAnchorStates, state) - end - - object:SetParent(wrapper) - RestoreAnchorState(state, wrapper) - end - end - - ApplyWrapperOffset(plate, wrapper) -end - -function BNP:ApplyNameplateScale(plate) - if not plate then return end - EnsureScaleWrapper(plate) - - local wrapper = plate.BNPScaleWrapper - if not wrapper then return end - - local scale = EffectiveScale() - wrapper:SetScale(scale) - ApplyWrapperOffset(plate, wrapper) - - -- ShaguTweaks also adjusts the original frame bounds so Blizzard's world - -- positioning remains correct while the child visuals honor UI scale. - plate:SetWidth(wrapper.originalWidth * scale) - plate:SetHeight(wrapper.originalHeight * scale) -end - -function BNP:ApplyNameplateScaleAll() - local plate - for plate in pairs(self.plates) do - self:ApplyNameplateScale(plate) - end -end - -function BNP:ApplyNameplateYOffsetAll() - local plate - for plate in pairs(self.plates or {}) do - if plate then - self:ApplyNameplateScale(plate) - end - end -end - -function BNP:MaintainNameplateYOffset(plate) - if not plate or not plate:IsShown() then return end - if not self.GetNameplateYOffset or self:GetNameplateYOffset() <= 0 then return end - - EnsureScaleWrapper(plate) - - local wrapper = plate.BNPScaleWrapper - if not wrapper then return end - - -- Never rediscover regions through plate:GetRegions() here. Once BNP moves - -- Blizzard regions into the wrapper they are no longer guaranteed to be - -- returned by plate:GetRegions(). Keep and maintain the original region - -- objects captured during wrapper creation instead. - local states = wrapper.BNPAnchorStates or {} - local i - for i = 1, table.getn(states) do - RestoreAnchorState(states[i], wrapper) - end - - ApplyWrapperOffset(plate, wrapper) -end - -function BNP:SetNameplateScale(value) - value = tonumber(value) - if not value then - self:Print("Usage: /bnp scale 0.5-1.5") - return - end - - value = Clamp(value) - BNP_DB = BNP_DB or {} - BNP_DB.nameplateScale = value - self:ApplyNameplateScaleAll() - - self:Print("Nameplate scale multiplier set to " .. tostring(value) .. ".") -end - -function BNP:ResetNameplateScale() - BNP_DB = BNP_DB or {} - BNP_DB.nameplateScale = DEFAULT_SCALE - self:ApplyNameplateScaleAll() - self:Print("Nameplate scale multiplier reset to 1.0 (Shagu/UI scale behavior).") -end - -if BNP.libnameplate then - table.insert(BNP.libnameplate.OnInit, function(plate) - local current = plate or this - if not current then return end - BNP:ApplyNameplateScale(current) - end) - - table.insert(BNP.libnameplate.OnShow, function(plate) - local current = plate or this - if not current then return end - BNP:ApplyNameplateScale(current) - end) -end diff --git a/modules/shagu_darkmode.lua b/modules/shagu_darkmode.lua deleted file mode 100644 index 3b02ebe..0000000 --- a/modules/shagu_darkmode.lua +++ /dev/null @@ -1,67 +0,0 @@ -BNP = BNP or {} - --- ShaguTweaks Darkened UI compatibility ------------------------------------- --- ShaguTweaks darkens a nameplate once and marks it with plate.darkened=true. --- Blizzard can later recycle that same plate and restore its original gold --- textures while the flag remains true. Shagu then skips the recycled plate. --- --- BNP does not implement its own dark mode here. It simply asks ShaguTweaks --- to run its own DarkenFrame() again whenever Blizzard reuses a nameplate. - -local pending = {} - -local function ShaguDarkModeActive() - return ShaguTweaks - and ShaguTweaks.DarkMode - and ShaguTweaks.DarkenFrame -end - -local function ReapplyShaguDarkMode(plate) - if not plate or not plate:IsShown() then return end - if not ShaguDarkModeActive() then return end - - -- Clear Shagu's one-time marker because this Blizzard plate may have been - -- recycled for another unit and had its textures reset. - plate.darkened = nil - - -- Use ShaguTweaks' own implementation/configuration as the single source of - -- truth. Its blacklist already protects StatusBars, spell icons, portraits, - -- etc. from being incorrectly darkened. - ShaguTweaks.DarkenFrame(plate) - plate.darkened = true -end - -local function QueueReapply(plate) - if not plate then return end - - -- Immediate pass handles normal OnShow. - ReapplyShaguDarkMode(plate) - - -- Blizzard may restore some nameplate texture state immediately after - -- OnShow. Do one delayed pass and then stop; no permanent scanner is needed. - pending[plate] = GetTime() + 0.08 -end - -if BNP.libnameplate then - table.insert(BNP.libnameplate.OnInit, function(plate) - QueueReapply(plate or this) - end) - - table.insert(BNP.libnameplate.OnShow, function(plate) - QueueReapply(plate or this) - end) -end - -local updater = CreateFrame("Frame") -updater:SetScript("OnUpdate", function() - if not next(pending) then return end - - local now = GetTime() - local plate, due - for plate, due in pairs(pending) do - if now >= due then - pending[plate] = nil - ReapplyShaguDarkMode(plate) - end - end -end) diff --git a/modules/spellprobe.lua b/modules/spellprobe.lua deleted file mode 100644 index 636dc47..0000000 --- a/modules/spellprobe.lua +++ /dev/null @@ -1,227 +0,0 @@ -BNP = BNP or {} - -local wanted = { - corruption = { "Corruption", "Verderbnis" }, - agony = { "Curse of Agony", "Fluch der Pein" }, - siphon = { "Siphon Life", "Lebensentzug" }, - shadow_word_pain = { "Shadow Word: Pain", "Schattenwort: Schmerz" }, -} - -local function isWanted(name, list) - if not name then return false end - local lower = string.lower(name) - local i - for i = 1, table.getn(list) do - if lower == string.lower(list[i]) then return true end - end - return false -end - -local function normalizeNumber(value) - if not value then return nil end - value = string.gsub(value, ",", ".") - return tonumber(value) -end - --- WoW 1.12 uses Lua 5.0. Use string.find captures instead of string.match. -local function durationFromLine(text) - if not text then return nil end - local lower = string.lower(text) - local _, _, num - - -- German: 14,95 Sek. / 14.95 Sekunden - _, _, num = string.find(lower, "([0-9]+[%,%.]?[0-9]*)%s*sek") - if num then return normalizeNumber(num) end - - -- English: 14.95 sec / seconds - _, _, num = string.find(lower, "([0-9]+[%,%.]?[0-9]*)%s*sec") - if num then return normalizeNumber(num) end - - -- Generic short form: "over 14.95 s" / "14,95 s" - _, _, num = string.find(lower, "over%s+([0-9]+[%,%.]?[0-9]*)%s+s") - if num then return normalizeNumber(num) end - - return nil -end - -local function getTooltip() - local tooltip = getglobal("BNPSpellProbeTooltip") - if tooltip then return tooltip end - - tooltip = CreateFrame("GameTooltip", "BNPSpellProbeTooltip", UIParent, "GameTooltipTemplate") - tooltip:SetOwner(WorldFrame, "ANCHOR_NONE") - return tooltip -end - -local function scanSpell(slot) - local tooltip = getTooltip() - tooltip:ClearLines() - tooltip:SetOwner(WorldFrame, "ANCHOR_NONE") - tooltip:SetSpell(slot, BOOKTYPE_SPELL) - - local lines = {} - local bestDuration = nil - local i - for i = 1, tooltip:NumLines() do - local fs = getglobal("BNPSpellProbeTooltipTextLeft" .. i) - local text = fs and fs:GetText() - if text and text ~= "" then - table.insert(lines, text) - local parsed = durationFromLine(text) - if parsed and parsed > 0 then - bestDuration = parsed - end - end - end - - return bestDuration, lines -end - -local function findSpell(wantedNames) - local slot = 1 - local found = nil - while true do - local name, rank = GetSpellName(slot, BOOKTYPE_SPELL) - if not name then break end - if isWanted(name, wantedNames) then - found = { slot = slot, name = name, rank = rank } - -- keep scanning so the highest learned rank wins - end - slot = slot + 1 - end - return found -end - -local function findSpellByID(def, spellID) - if not def or not spellID or not SpellInfo then return nil end - - local wantedName, wantedRank = SpellInfo(spellID) - if not wantedName then return nil end - - local slot = 1 - local name, rank - while true do - name, rank = GetSpellName(slot, BOOKTYPE_SPELL) - if not name then break end - - if isWanted(name, def.names or { wantedName }) then - -- SuperWoW SpellInfo exposes the localized rank when available. Matching - -- both name and rank lets us scan the exact spellbook rank that was cast - -- instead of always inheriting the highest learned rank's tooltip. - if wantedRank and wantedRank ~= "" and rank == wantedRank then - return { slot = slot, name = name, rank = rank } - end - end - - slot = slot + 1 - end - - return nil -end - - -BNP.spellbookDurations = BNP.spellbookDurations or {} -BNP.spellbookDurationsByID = BNP.spellbookDurationsByID or {} - -function BNP:RefreshSpellbookDurations() - -- Exact-rank tooltip values can change after talent/spellbook updates. - BNP.spellbookDurationsByID = {} - local id - for id, names in pairs(wanted) do - local spell = findSpell(names) - if spell then - local ok, duration = pcall(scanSpell, spell.slot) - if ok and duration and duration > 0 then - BNP.spellbookDurations[id] = duration - BNP.spellbookDurations[string.lower(spell.name)] = duration - end - end - end -end - -function BNP:GetSpellbookDurationForSpellID(def, spellID) - if not def or not spellID then return nil end - BNP.spellbookDurationsByID = BNP.spellbookDurationsByID or {} - - if BNP.spellbookDurationsByID[spellID] then - return BNP.spellbookDurationsByID[spellID] - end - - local spell = findSpellByID(def, spellID) - if spell then - local ok, duration = pcall(scanSpell, spell.slot) - if ok and duration and duration > 0 then - BNP.spellbookDurationsByID[spellID] = duration - return duration - end - end - - return nil -end - -function BNP:GetSpellbookDurationForDef(def) - if not def then return nil end - BNP.spellbookDurations = BNP.spellbookDurations or {} - - if def.key and BNP.spellbookDurations[def.key] then - return BNP.spellbookDurations[def.key] - end - - if def.names then - local i - for i = 1, table.getn(def.names) do - local key = string.lower(def.names[i]) - if BNP.spellbookDurations[key] then - return BNP.spellbookDurations[key] - end - end - - -- Generic on-demand scan: works for every class, not only the original - -- Warlock/Priest probe list. - local spell = findSpell(def.names) - if spell then - local ok, duration = pcall(scanSpell, spell.slot) - if ok and duration and duration > 0 then - if def.key then BNP.spellbookDurations[def.key] = duration end - BNP.spellbookDurations[string.lower(spell.name)] = duration - return duration - end - end - end - - return nil -end - -function BNP:SpellbookProbe(key) - self:RefreshSpellbookDurations() - local keys = {} - if key and key ~= "" and wanted[key] then - table.insert(keys, key) - else - table.insert(keys, "corruption") - table.insert(keys, "agony") - table.insert(keys, "siphon") - table.insert(keys, "shadow_word_pain") - end - - self:Print("=== Spellbook Tooltip Probe ===") - local k - for k = 1, table.getn(keys) do - local id = keys[k] - local spell = findSpell(wanted[id]) - if not spell then - self:Print(id .. ": not found in spellbook") - else - local ok, duration, lines = pcall(scanSpell, spell.slot) - if not ok then - self:Print(spell.name .. ": tooltip scan ERROR: " .. tostring(duration)) - else - self:Print(spell.name .. " " .. tostring(spell.rank or "") .. " | Slot " .. spell.slot .. " | Duration: " .. tostring(duration or "NOT DETECTED")) - local i - for i = 1, table.getn(lines) do - self:Print(" " .. i .. ": " .. lines[i]) - end - end - end - end -end diff --git a/modules/tankmode.lua b/modules/tankmode.lua deleted file mode 100644 index fb12eb7..0000000 --- a/modules/tankmode.lua +++ /dev/null @@ -1,181 +0,0 @@ -BNP = BNP or {} - --- Tank Mode for SuperWoW GUID nameplates. --- GREEN = hostile NPC currently targets the player. --- RED = hostile NPC currently targets another unit. --- Idle hostile NPCs keep their normal Blizzard color. - -local GREEN_R, GREEN_G, GREEN_B = 0.00, 1.00, 0.00 -local RED_R, RED_G, RED_B = 1.00, 0.00, 0.00 - -local playerGUID = nil - -local function GetPlayerGUID() - local exists, guid = UnitExists("player") - if exists and guid then playerGUID = guid end - return playerGUID -end - -local function GetPlateGUID(plate) - if not plate or not plate.GetName then return nil end - local guid = plate:GetName(1) - if guid and guid ~= "" then return guid end - return nil -end - -local function GetTargetGUID(guid) - if not guid then return nil end - local exists, targetGUID = UnitExists(guid .. "target") - if exists then return targetGUID end - return nil -end - -local function GetBar(plate) - return plate and (plate.healthbar or plate.healthBar) or nil -end - -local function IsHostileNPC(guid) - if not guid or not UnitExists(guid) then return false end - if UnitIsPlayer(guid) then return false end - return UnitCanAttack("player", guid) and true or false -end - -local function IsForeignTaggedGUID(guid) - if not guid or not UnitExists(guid) then return false end - if UnitCanAttack and not UnitCanAttack("player", guid) then return false end - if not UnitIsTapped or not UnitIsTapped(guid) then return false end - if UnitIsTappedByPlayer and UnitIsTappedByPlayer(guid) then return false end - return true -end - -local function RestoreNormalColor(plate, bar) - -- Do not restore a cached RGB value. Outside an active Tank Mode state, - -- Blizzard owns the NPC healthbar color completely. - plate.BNPTankActive = nil - plate.BNPTankR = nil - plate.BNPTankG = nil - plate.BNPTankB = nil -end - -local function ClearTankState(plate) - if not plate then return end - plate.BNPTankActive = nil - plate.BNPTankR = nil - plate.BNPTankG = nil - plate.BNPTankB = nil -end - -local function ApplyTankColor(plate, bar, r, g, b) - bar:SetStatusBarColor(r, g, b, 1) - plate.BNPTankActive = true - plate.BNPTankR = r - plate.BNPTankG = g - plate.BNPTankB = b -end - -local function EnforceTankColor(plate) - if not plate or not plate.BNPTankActive then return end - local bar = GetBar(plate) - if not bar or not plate.BNPTankR then return end - - local r, g, b = bar:GetStatusBarColor() - if math.abs(r - plate.BNPTankR) > 0.01 - or math.abs(g - plate.BNPTankG) > 0.01 - or math.abs(b - plate.BNPTankB) > 0.01 then - bar:SetStatusBarColor(plate.BNPTankR, plate.BNPTankG, plate.BNPTankB, 1) - end -end - -function BNP:UpdateTankModePlate(plate) - if not plate or not plate:IsShown() then return end - - local bar = GetBar(plate) - if not bar then return end - - if not self:IsTankModeEnabled() then - RestoreNormalColor(plate, bar) - return - end - - local guid = GetPlateGUID(plate) - if not IsHostileNPC(guid) then - -- NPC reaction colors belong to Blizzard. Never restore a stale cached - -- Tank Mode color onto neutral/friendly/recycled nameplates. - ClearTankState(plate) - return - end - - -- Foreign-tag grey has priority over Tank Mode. Previously Tank Mode wrote - -- RED every 0.10s while fullalpha.lua restored GREY every frame, producing - -- the visible grey/red flicker. Clear any cached tank color and let the - -- foreign-tag renderer own the bar until the mob is ours/untapped again. - if IsForeignTaggedGUID(guid) then - ClearTankState(plate) - return - end - - local myGUID = GetPlayerGUID() - if not myGUID then return end - - local targetGUID = GetTargetGUID(guid) - - if targetGUID and targetGUID == myGUID then - ApplyTankColor(plate, bar, GREEN_R, GREEN_G, GREEN_B) - elseif targetGUID then - ApplyTankColor(plate, bar, RED_R, RED_G, RED_B) - else - RestoreNormalColor(plate, bar) - end -end - -function BNP:RefreshTankMode() - local plate - for plate in pairs(self.plates or {}) do - if plate and plate:IsShown() then - self:UpdateTankModePlate(plate) - end - end -end - -function BNP:UpdateTankMode() - self:RefreshTankMode() -end - -function BNP:InstallTankMode() - if self.BNPTankModeInstalled then return end - if not self.libnameplate or not self.libnameplate.OnUpdate then return end - - GetPlayerGUID() - - table.insert(self.libnameplate.OnInit, function(plate) - local current = plate or this - if not current then return end - - ClearTankState(current) - - local bar = GetBar(current) - if bar and not current.BNPTankColorHooked then - local old = bar:GetScript("OnValueChanged") - bar:SetScript("OnValueChanged", function() - if old then old() end - EnforceTankColor(current) - end) - current.BNPTankColorHooked = true - end - end) - - table.insert(self.libnameplate.OnShow, function(plate) - local current = plate or this - if not current then return end - ClearTankState(current) - end) - - table.insert(self.libnameplate.OnUpdate, function(plate, elapsed) - local current = plate or this - if not current then return end - BNP:UpdateTankModePlate(current) - EnforceTankColor(current) - end) - - self.BNPTankModeInstalled = true -end diff --git a/modules/totems.lua b/modules/totems.lua deleted file mode 100644 index c52fefd..0000000 --- a/modules/totems.lua +++ /dev/null @@ -1,368 +0,0 @@ -if not BNP.libnameplate then return end - --- Lightweight PvP totem indicators ------------------------------------------ --- Totem detection is intentionally nameplate/name based. It never reads or --- modifies the aura cache, combat log timers or CC tracking. - -local TOTEM_DEFS = { - { key = "earthbind", spellID = 2484, names = { "Earthbind Totem", "Totem der Erdbindung" }, texture = "Interface\\Icons\\Spell_Nature_StrengthOfEarthTotem02" }, - { key = "stoneskin", spellID = 8071, names = { "Stoneskin Totem", "Totem der Steinhaut" }, texture = "Interface\\Icons\\Spell_Nature_StoneSkinTotem" }, - { key = "strengthofearth", spellID = 8075, names = { "Strength of Earth Totem", "Totem der Erdstärke" }, texture = "Interface\\Icons\\Spell_Nature_EarthBindTotem" }, - { key = "tremor", spellID = 8143, names = { "Tremor Totem", "Totem des Erdstosses", "Totem des Erdstoßes" }, texture = "Interface\\Icons\\Spell_Nature_TremorTotem" }, - { key = "grounding", spellID = 8177, names = { "Grounding Totem", "Totem der Erdung" }, texture = "Interface\\Icons\\Spell_Nature_GroundingTotem" }, - { key = "stoneclaw", spellID = 5730, names = { "Stoneclaw Totem", "Totem der Steinklaue" }, texture = "Interface\\Icons\\Spell_Nature_StoneClawTotem" }, - - { key = "searing", spellID = 3599, names = { "Searing Totem", "Totem der Verbrennung" }, texture = "Interface\\Icons\\Spell_Fire_SearingTotem" }, - { key = "flametongue", spellID = 8227, names = { "Flametongue Totem", "Totem der Flammenzunge" }, texture = "Interface\\Icons\\Spell_Nature_GuardianWard" }, - { key = "magma", spellID = 8190, names = { "Magma Totem", "Totem des glühenden Magmas" }, texture = "Interface\\Icons\\Spell_Fire_SelfDestruct" }, - { key = "firenova", spellID = 1535, names = { "Fire Nova Totem", "Totem der Feuernova" }, texture = "Interface\\Icons\\Spell_Fire_SealOfFire" }, - { key = "frostres", spellID = 8181, names = { "Frost Resistance Totem", "Totem des Frostwiderstands" }, texture = "Interface\\Icons\\Spell_FrostResistanceTotem_01" }, - { key = "fireres", spellID = 8184, names = { "Fire Resistance Totem", "Totem des Feuerwiderstands" }, texture = "Interface\\Icons\\Spell_FireResistanceTotem_01" }, - - { key = "healingstream", spellID = 5394, names = { "Healing Stream Totem", "Totem des heilenden Flusses" }, texture = "Interface\\Icons\\INV_Spear_04" }, - { key = "manaspring", spellID = 5675, names = { "Mana Spring Totem", "Totem der Manaquelle" }, texture = "Interface\\Icons\\Spell_Nature_ManaRegenTotem" }, - { key = "poisoncleansing", spellID = 8166, names = { "Poison Cleansing Totem", "Totem der Giftreinigung" }, texture = "Interface\\Icons\\Spell_Nature_PoisonCleansingTotem" }, - { key = "diseasecleansing", spellID = 8170, names = { "Disease Cleansing Totem", "Totem der Krankheitsreinigung" }, texture = "Interface\\Icons\\Spell_Nature_DiseaseCleansingTotem" }, - { key = "manatide", spellID = 16190, names = { "Mana Tide Totem", "Totem der Manaflut" }, texture = "Interface\\Icons\\Spell_Frost_SummonWaterElemental" }, - { key = "natureres", spellID = 10595, names = { "Nature Resistance Totem", "Totem des Naturwiderstands" }, texture = "Interface\\Icons\\Spell_Nature_NatureResistanceTotem" }, - - { key = "windfury", spellID = 8512, names = { "Windfury Totem", "Totem des Windzorns" }, texture = "Interface\\Icons\\Spell_Nature_Windfury" }, - { key = "sentry", spellID = 6495, names = { "Sentry Totem", "Totem des Wachens" }, texture = "Interface\\Icons\\Spell_Nature_RemoveCurse" }, - { key = "graceofair", spellID = 8835, names = { "Grace of Air Totem", "Totem der Luftanmut" }, texture = "Interface\\Icons\\Spell_Nature_InvisibilityTotem" }, - { key = "tranquilair", spellID = 25908, names = { "Tranquil Air Totem", "Totem der beruhigenden Winde" }, texture = "Interface\\Icons\\Spell_Nature_Brilliance" }, - { key = "windwall", spellID = 15107, names = { "Windwall Totem", "Totem der Windmauer" }, texture = "Interface\\Icons\\Spell_Nature_EarthBind" }, -} - - -local function CleanName(text) - if not text then return nil end - text = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "") - text = string.gsub(text, "|r", "") - text = string.gsub(text, "^%s+", "") - text = string.gsub(text, "%s+$", "") - if text == "" then return nil end - return text -end - -local function NormalizeTotemName(text) - text = CleanName(text) - if not text then return nil end - - text = string.lower(text) - -- Vanilla totem NPCs often carry their spell rank in the NPC name, e.g. - -- "Searing Totem VI" or "Windfury Totem II". Match the base totem name. - text = string.gsub(text, "%s+rank%s+%d+$", "") - text = string.gsub(text, "%s+%d+$", "") - text = string.gsub(text, "%s+[ivxlcdm]+$", "") - text = string.gsub(text, "%s+$", "") - return text -end - -local function AddName(def, name) - if not name or name == "" then return end - local normalized = NormalizeTotemName(name) - local i - for i = 1, table.getn(def.names) do - if NormalizeTotemName(def.names[i]) == normalized then return end - end - table.insert(def.names, name) -end - -local definitionsResolved = false -local function ResolveDefinitions() - if definitionsResolved then return end - definitionsResolved = true - - if not SpellInfo then return end - local i - for i = 1, table.getn(TOTEM_DEFS) do - local def = TOTEM_DEFS[i] - local ok, name, _, texture = pcall(SpellInfo, def.spellID) - if ok then - AddName(def, name) - if texture and texture ~= "" then def.texture = texture end - end - end -end - -local GENERIC_TOTEM_DEF = { - key = "generic", - names = {}, - texture = "Interface\\Icons\\Spell_Nature_StoneClawTotem", -} - -local function FindTotemDefinition(name) - local normalized = NormalizeTotemName(name) - if not normalized then return nil end - - local i, n - for i = 1, table.getn(TOTEM_DEFS) do - local def = TOTEM_DEFS[i] - for n = 1, table.getn(def.names) do - if normalized == NormalizeTotemName(def.names[n]) then return def end - end - end - - -- Future/custom server totems still get icon-only treatment even when BNP - -- does not yet know their exact spell icon. Standard Classic totems above - -- keep their proper icon; unknown names containing "totem" use a neutral - -- fallback icon instead of leaving the full nameplate visible. - if string.find(normalized, "totem", 1, true) then - return GENERIC_TOTEM_DEF - end - - return nil -end - -local function GetPlateName(plate) - if plate and plate.name and plate.name.GetText then - local name = CleanName(plate.name:GetText()) - if name then return name end - end - - if plate and plate.GetName and UnitName then - local token = plate:GetName(1) - if token then - local ok, name = pcall(UnitName, token) - if ok then return CleanName(name) end - end - end - - return nil -end - -local function PositionIndicator(plate, indicator) - if not plate or not indicator then return end - indicator:ClearAllPoints() - - -- Icon-only mode: replace the visible nameplate at its original position. - if plate.healthbar then - indicator:SetPoint("CENTER", plate.healthbar, "CENTER", 0, 0) - else - indicator:SetPoint("CENTER", plate, "CENTER", 0, 0) - end -end - -local function RememberAndHideVisual(object, state) - if not object or object == state.indicator then return end - if not object.SetAlpha or not object.GetAlpha then return end - - if state.alpha[object] == nil then - state.alpha[object] = object:GetAlpha() - end - object:SetAlpha(0) -end - -local function SuppressTotemPlateVisuals(plate) - if not plate then return end - - local indicator = plate.BNPTotemIndicator - local state = plate.BNPTotemVisualState - if not state then - state = { alpha = {}, indicator = indicator } - plate.BNPTotemVisualState = state - else - state.indicator = indicator - end - - -- Important PvP totems should remain clearly visible even when BNP's - -- non-target alpha option would normally fade the underlying nameplate. - if plate.SetAlpha then plate:SetAlpha(1) end - - local regions = { plate:GetRegions() } - local i - for i = 1, table.getn(regions) do - RememberAndHideVisual(regions[i], state) - end - - local children = { plate:GetChildren() } - for i = 1, table.getn(children) do - RememberAndHideVisual(children[i], state) - end - - -- BNPTotemIndicator is itself a direct child of the plate. - if indicator and indicator.SetAlpha then indicator:SetAlpha(1) end -end - -local function RestoreTotemPlateVisuals(plate) - if not plate then return end - local state = plate.BNPTotemVisualState - if not state then return end - - local object, alpha - for object, alpha in pairs(state.alpha or {}) do - if object and object.SetAlpha then - object:SetAlpha(alpha or 1) - end - end - - plate.BNPTotemVisualState = nil -end - --- Blizzard can restore parts of a projected nameplate every frame. Once a --- plate has been identified as a totem, suppress every normal visual again --- after the plate's native/fullalpha OnUpdate has run. The replacement icon --- remains a child of the plate and is explicitly excluded, so it keeps the --- same proven projection/positioning path as the working detection version. -local function InstallTotemVisualGuard(plate) - if not plate or plate.BNPTotemVisualGuard then return end - - local oldOnUpdate = plate:GetScript("OnUpdate") - plate:SetScript("OnUpdate", function() - if oldOnUpdate then oldOnUpdate() end - local current = this or plate - if not current then return end - - if current.BNPTotemLastKey and - BNP.AreTotemIndicatorsEnabled and BNP:AreTotemIndicatorsEnabled() then - SuppressTotemPlateVisuals(current) - if current.BNPTotemIndicator then - current.BNPTotemIndicator:SetAlpha(1) - current.BNPTotemIndicator:Show() - end - elseif current.BNPTotemVisualState then - RestoreTotemPlateVisuals(current) - end - end) - - plate.BNPTotemVisualGuard = true -end - -local function InstallTotemHideGuard(plate) - if not plate or plate.BNPTotemHideGuard then return end - - local oldOnHide = plate:GetScript("OnHide") - plate:SetScript("OnHide", function() - if oldOnHide then oldOnHide() end - local current = this or plate - if not current then return end - - if current.BNPTotemIndicator then current.BNPTotemIndicator:Hide() end - RestoreTotemPlateVisuals(current) - current.BNPTotemLastName = nil - current.BNPTotemLastKey = nil - end) - - plate.BNPTotemHideGuard = true -end - -local function ApplyIndicatorLayout(plate, force) - local indicator = plate and plate.BNPTotemIndicator - if not indicator then return end - - local size = BNP.GetTotemIconSize and BNP:GetTotemIconSize() or 24 - local level = plate.GetFrameLevel and ((plate:GetFrameLevel() or 0) + 2) or 2 - - if not force and - indicator.BNPLastSize == size and - indicator.BNPLastFrameLevel == level then - return - end - - indicator.BNPLastSize = size - indicator.BNPLastFrameLevel = level - indicator:SetWidth(size) - indicator:SetHeight(size) - if indicator.SetFrameLevel then indicator:SetFrameLevel(level) end - PositionIndicator(plate, indicator) -end - -local function CreateIndicator(plate) - if not plate or plate.BNPTotemIndicator then return end - - local indicator = CreateFrame("Frame", nil, plate) - local size = BNP.GetTotemIconSize and BNP:GetTotemIconSize() or 24 - indicator:SetWidth(size) - indicator:SetHeight(size) - indicator:SetFrameLevel((plate.GetFrameLevel and plate:GetFrameLevel() or 0) + 2) - - local border = indicator:CreateTexture(nil, "BACKGROUND") - border:SetTexture(0, 0, 0, 1) - border:SetPoint("TOPLEFT", indicator, "TOPLEFT", -1, 1) - border:SetPoint("BOTTOMRIGHT", indicator, "BOTTOMRIGHT", 1, -1) - indicator.border = border - - local texture = indicator:CreateTexture(nil, "ARTWORK") - texture:SetAllPoints(indicator) - texture:SetTexCoord(0.07, 0.93, 0.07, 0.93) - indicator.texture = texture - - indicator:Hide() - plate.BNPTotemIndicator = indicator - ApplyIndicatorLayout(plate, true) -end - -local function UpdateIndicator(plate, force) - if not plate then return end - CreateIndicator(plate) - local indicator = plate.BNPTotemIndicator - if not indicator then return end - - ApplyIndicatorLayout(plate, force) - - if not (BNP.AreTotemIndicatorsEnabled and BNP:AreTotemIndicatorsEnabled()) then - indicator:Hide() - RestoreTotemPlateVisuals(plate) - plate.BNPTotemLastName = nil - plate.BNPTotemLastKey = nil - return - end - - ResolveDefinitions() - local name = GetPlateName(plate) - if not force and name == plate.BNPTotemLastName then - if plate.BNPTotemLastKey then - indicator:Show() - SuppressTotemPlateVisuals(plate) - else - indicator:Hide() - RestoreTotemPlateVisuals(plate) - end - return - end - - plate.BNPTotemLastName = name - plate.BNPTotemLastKey = nil - - local def = FindTotemDefinition(name) - if not def then - indicator:Hide() - RestoreTotemPlateVisuals(plate) - return - end - - plate.BNPTotemLastKey = def.key - indicator.texture:SetTexture(def.texture) - indicator:Show() - SuppressTotemPlateVisuals(plate) -end - -function BNP:RefreshTotemIndicators() - ResolveDefinitions() - local plate - for plate in pairs(self.plates or {}) do - if plate then UpdateIndicator(plate, true) end - end -end - --- Public table keeps the totem definitions centralized and easy to extend. -BNP.TotemIndicatorDefinitions = TOTEM_DEFS - -table.insert(BNP.libnameplate.OnInit, function(plate) - CreateIndicator(plate) - InstallTotemVisualGuard(plate) - InstallTotemHideGuard(plate) - UpdateIndicator(plate, true) -end) - -table.insert(BNP.libnameplate.OnShow, function(plate) - InstallTotemVisualGuard(plate) - InstallTotemHideGuard(plate) - plate.BNPTotemLastName = nil - plate.BNPTotemLastKey = nil - UpdateIndicator(plate, true) -end) - -table.insert(BNP.libnameplate.OnUpdate, function(plate) - UpdateIndicator(plate, false) -end) diff --git a/modules/unknown.lua b/modules/unknown.lua deleted file mode 100644 index 15c2e01..0000000 --- a/modules/unknown.lua +++ /dev/null @@ -1,498 +0,0 @@ -BNP = BNP or {} -BNP.unknownDirectEvents = BNP.unknownDirectEvents or {} - --- Beta spell collector ------------------------------------------------------- --- Purpose: help testers report missing Turtle/custom spell IDs without --- editing Lua files. It records unknown own cast IDs and unknown debuff IDs --- that newly appear shortly after one of the player's casts. --- --- It is intentionally diagnostic only: it never changes tracking/rendering. - -BNP.unknownBeta = BNP.unknownBeta or { - entries = {}, - order = {}, - recentCasts = {}, - seenByGUID = {}, - activeUntil = 0, -} - -local _, playerClass = UnitClass("player") - -local CLASS_AURAS = { - WARLOCK = BNP.WarlockAuras, - PRIEST = BNP.PriestAuras, - WARRIOR = BNP.WarriorAuras, - ROGUE = BNP.RogueAuras, - HUNTER = BNP.HunterAuras, - MAGE = BNP.MageAuras, - DRUID = BNP.DruidAuras, - SHAMAN = BNP.ShamanAuras, - PALADIN = BNP.PaladinAuras, -} -local AURA_DEFS = CLASS_AURAS[playerClass] or {} - -local function GetPlayerGUID() - local exists, guid = UnitExists("player") - if exists and guid then return guid end - return nil -end - -local function GetUnitGUID(unit) - local exists, guid = UnitExists(unit) - if exists and guid then return guid end - return nil -end - -local function IsExplicitKnownID(spellID) - if not spellID then return false end - - -- Dark Harvest is handled by dedicated Warlock logic. - if spellID == 52552 then return true end - - local i, j, def - for i = 1, table.getn(AURA_DEFS) do - def = AURA_DEFS[i] - - if def.spellIDs then - for j = 1, table.getn(def.spellIDs) do - if def.spellIDs[j] == spellID then return true end - end - end - - if def.durations and def.durations[spellID] then - return true - end - end - - return false -end - -local function SpellName(spellID) - if not spellID or not SpellInfo then return "?" end - local name = SpellInfo(spellID) - return name or "?" -end - -local function AddEntry(kind, spellID, spellName, guid, trigger, confidence) - if not spellID then return end - - local key = tostring(kind) .. ":" .. tostring(spellID) - local data = BNP.unknownBeta - local old = data.entries[key] - - if old then - old.count = (old.count or 1) + 1 - old.lastSeen = GetTime() - if confidence == "TARGET" then old.confidence = confidence end - if trigger and not old.trigger then old.trigger = trigger end - return - end - - local entry = { - kind = kind, - spellID = spellID, - name = spellName or SpellName(spellID), - class = playerClass or "?", - guid = guid, - trigger = trigger, - confidence = confidence or "?", - count = 1, - firstSeen = GetTime(), - lastSeen = GetTime(), - } - - data.entries[key] = entry - table.insert(data.order, key) -end - -local function RememberCast(spellID, targetGUID, eventType) - local data = BNP.unknownBeta - local now = GetTime() - data.activeUntil = now + 8.0 - local cast = { - time = now, - spellID = spellID, - name = SpellName(spellID), - targetGUID = targetGUID, - eventType = eventType, - } - - table.insert(data.recentCasts, cast) - while table.getn(data.recentCasts) > 20 do - table.remove(data.recentCasts, 1) - end - - -- Unknown own CAST/CHANNEL IDs are useful too. Some trap/proc systems expose - -- the effect as a separate cast event before the aura is seen. - if spellID and not IsExplicitKnownID(spellID) then - AddEntry("CAST", spellID, cast.name, targetGUID, nil, "OWN") - end -end - -local function FindRecentTrigger(guid, now) - local casts = BNP.unknownBeta.recentCasts - local i - for i = table.getn(casts), 1, -1 do - local cast = casts[i] - local age = now - cast.time - - if age > 6 then - break - end - - if cast.targetGUID and guid and cast.targetGUID == guid then - return cast, "TARGET" - end - - -- Short global fallback catches effects such as traps where the placement - -- cast may not carry the eventual victim GUID. - if age <= 1.5 then - return cast, "RECENT" - end - end - - return nil, nil -end - -local function ScanUnit(unit, guid, now) - if not unit or not guid then return end - - local seen = BNP.unknownBeta.seenByGUID[guid] - if not seen then - seen = {} - BNP.unknownBeta.seenByGUID[guid] = seen - end - - local current = {} - local i - for i = 1, 32 do - local texture, stacks, dtype, auraSpellID = UnitDebuff(unit, i) - if not texture then break end - - if auraSpellID then - current[auraSpellID] = true - - if not seen[auraSpellID] and not IsExplicitKnownID(auraSpellID) then - local trigger, confidence = FindRecentTrigger(guid, now) - if trigger then - AddEntry( - "AURA", - auraSpellID, - SpellName(auraSpellID), - guid, - tostring(trigger.name) .. " [" .. tostring(trigger.spellID) .. "]", - confidence - ) - end - end - end - end - - -- Keep only currently present aura IDs so a later re-application can be - -- detected again without growing this table forever. - BNP.unknownBeta.seenByGUID[guid] = current -end - - --- Indirect Effect Audit: traps/procs may never expose a UnitDebuff ID. -BNP.unknownBeta.combat = BNP.unknownBeta.combat or {} -local EFFECT_WINDOW = 8.0 - - -local function IsUsefulEffectLine(raw) - if not raw or raw == "" then return false end - local lower = string.lower(raw) - - -- Ignore obvious player-action/noise lines. - if string.find(lower, "you cast ", 1, true) then return false end - if string.find(lower, "auto shot", 1, true) then return false end - if string.find(lower, "login", 1, true) then return false end - - -- Keep lines that actually describe a harmful aura/effect or its periodic damage. - if string.find(lower, "afflicted by", 1, true) then return true end - if string.find(lower, "is afflicted", 1, true) then return true end - if string.find(lower, "is affected by", 1, true) then return true end - if string.find(lower, "suffers", 1, true) then return true end - if string.find(lower, "periodic", 1, true) then return true end - - return false -end - -local function AddCombatEffect(raw) - if not IsUsefulEffectLine(raw) then return end - local now=GetTime() - local casts=BNP.unknownBeta.recentCasts - local trigger=nil - local i - for i=table.getn(casts),1,-1 do - local c=casts[i] - if now-c.time > EFFECT_WINDOW then break end - if c.spellID ~= 75 then trigger=c break end - end - if not trigger then return end - if string.find(string.lower(raw),"auto shot",1,true) then return end - - local key=tostring(trigger.spellID)..":"..raw - local old=BNP.unknownBeta.combat[key] - if old then old.count=old.count+1 return end - BNP.unknownBeta.combat[key]={ - raw=raw, triggerID=trigger.spellID, triggerName=trigger.name, count=1 - } -end - -local combatFrame=CreateFrame("Frame") -local combatEvents={ - "CHAT_MSG_COMBAT_SELF_HITS", - "CHAT_MSG_SPELL_SELF_DAMAGE", - "CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE", - "CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE", -} -local cei -for cei=1,table.getn(combatEvents) do combatFrame:RegisterEvent(combatEvents[cei]) end -combatFrame:SetScript("OnEvent",function() AddCombatEffect(arg1) end) - -local castFrame = CreateFrame("Frame") -castFrame:RegisterEvent("UNIT_CASTEVENT") -castFrame:SetScript("OnEvent", function() - local playerGUID = GetPlayerGUID() - if not playerGUID or arg1 ~= playerGUID then return end - - local eventType = arg3 - if eventType ~= "CAST" and eventType ~= "CHANNEL" then return end - - RememberCast(arg4, arg2, eventType) -end) - -local scanner = CreateFrame("Frame") -local elapsed = 0 -scanner:SetScript("OnUpdate", function() - elapsed = elapsed + arg1 - if elapsed < 0.35 then return end - elapsed = 0 - - local now = GetTime() - if not BNP.unknownBeta.activeUntil or now > BNP.unknownBeta.activeUntil then - return - end - - local targetGUID = GetUnitGUID("target") - if targetGUID then - ScanUnit("target", targetGUID, now) - end - - local plate - for plate in pairs(BNP.plates) do - if plate:IsShown() and plate.GetName then - local token = plate:GetName(1) - if token then - local guid = GetUnitGUID(token) - if guid then - ScanUnit(token, guid, now) - end - end - end - end -end) - -function BNP:PrintUnknownBeta(verbose) - local data = self.unknownBeta - self:Print("=== BNP Beta: Unknown Auras (" .. tostring(playerClass or "?") .. ") ===") - - local auraCount = 0 - local i - for i = 1, table.getn(data.order) do - local entry = data.entries[data.order[i]] - if entry and entry.kind == "AURA" then - auraCount = auraCount + 1 - local line = - tostring(auraCount) .. - " | AURA | ID " .. tostring(entry.spellID) .. - " | " .. tostring(entry.name) .. - " | x" .. tostring(entry.count or 1) - - if entry.confidence then - line = line .. " | " .. tostring(entry.confidence) - end - - self:Print(line) - - if entry.trigger then - self:Print(" Trigger: " .. tostring(entry.trigger)) - end - end - end - - local combatCount = 0 - local ck - for ck in pairs(data.combat or {}) do combatCount = combatCount + 1 end - - if combatCount > 0 then - self:Print("--- Indirect Effects ---") - local key, e - for key, e in pairs(data.combat) do - self:Print("EFFECT | Trigger " .. tostring(e.triggerName) .. " [" .. tostring(e.triggerID) .. "] | x" .. tostring(e.count)) - self:Print(" " .. tostring(e.raw)) - end - end - - if auraCount == 0 and combatCount == 0 then - self:Print("No unknown negative auras/effects found since login.") - end - - if verbose then - self:Print("--- Verbose: unknown player cast IDs ---") - local castCount = 0 - for i = 1, table.getn(data.order) do - local entry = data.entries[data.order[i]] - if entry and entry.kind == "CAST" then - castCount = castCount + 1 - self:Print( - tostring(castCount) .. - " | CAST | ID " .. tostring(entry.spellID) .. - " | " .. tostring(entry.name) .. - " | x" .. tostring(entry.count or 1) - ) - end - end - if castCount == 0 then - self:Print("No unknown cast IDs.") - end - else - self:Print("Open Missing Spell / Aura in the settings to copy a complete report.") - end -end - -function BNP:ResetUnknownBeta() - self.unknownBeta.entries = {} - self.unknownBeta.order = {} - self.unknownBeta.recentCasts = {} - self.unknownBeta.seenByGUID = {} - self.unknownBeta.combat = {} - self.unknownBeta.activeUntil = 0 - self:Print("Unknown spell/effect list cleared.") -end - - - --- Target Aura Probe ---------------------------------------------------------- --- Diagnostic helper for proc/talent auras that the normal unknown collector --- may miss because they were already present in its seen-state. -function BNP:PrintTargetAuraProbe() - local exists, guid = UnitExists("target") - if not exists or not guid then - self:Print("Target Aura Probe: no target selected.") - return - end - - self:Print("=== BNP Target Negative Auras ===") - self:Print("Target: " .. tostring(UnitName("target") or "?") .. " | GUID " .. tostring(guid)) - - local count = 0 - local i - for i = 1, 32 do - local texture, stacks, dtype, auraSpellID = UnitDebuff("target", i) - if not texture then break end - - count = count + 1 - local name = auraSpellID and SpellName(auraSpellID) or "?" - self:Print( - tostring(count) .. - " | ID " .. tostring(auraSpellID or "?") .. - " | " .. tostring(name) .. - " | stacks " .. tostring(stacks or 0) .. - " | type " .. tostring(dtype or "?") - ) - end - - if count == 0 then - self:Print("No negative target auras found.") - end -end - --- Direct player spell collector -------------------------------------------- --- Captures successful player spell events that do not create a persistent --- aura and therefore never appear in the normal unknown-aura collector. - -local function GetLocalPlayerGUID() - local exists, guid = UnitExists("player") - if exists and guid then return guid end - return nil -end - -local function RememberDirectEvent(eventType, spellID, targetGUID, extra) - if not spellID then return end - - local name = nil - if SpellInfo then name = SpellInfo(spellID) end - name = name or ("Spell " .. tostring(spellID)) - - local key = tostring(eventType) .. ":" .. tostring(spellID) - local entry = BNP.unknownDirectEvents[key] - - if not entry then - entry = { - eventType = eventType, - spellID = spellID, - name = name, - targetGUID = targetGUID, - count = 0, - extra = extra, - last = GetTime(), - } - BNP.unknownDirectEvents[key] = entry - end - - entry.count = (entry.count or 0) + 1 - entry.targetGUID = targetGUID or entry.targetGUID - entry.extra = extra or entry.extra - entry.last = GetTime() -end - -local directFrame = CreateFrame("Frame") -directFrame:RegisterEvent("UNIT_CASTEVENT") -directFrame:SetScript("OnEvent", function() - local casterGUID = arg1 - local targetGUID = arg2 - local eventType = arg3 - local spellID = arg4 - local playerGUID = GetLocalPlayerGUID() - - if not playerGUID or casterGUID ~= playerGUID then return end - if not spellID then return end - - if eventType == "START" or eventType == "CAST" or eventType == "CHANNEL" then - RememberDirectEvent(eventType, spellID, targetGUID, arg5) - end -end) - -function BNP:PrintUnknownDirectEvents() - local list = {} - local key, entry - - for key, entry in pairs(self.unknownDirectEvents or {}) do - table.insert(list, entry) - end - - table.sort(list, function(a, b) - return (a.last or 0) > (b.last or 0) - end) - - if table.getn(list) == 0 then - self:Print("--- Direct Player Events: none captured ---") - return - end - - self:Print("--- Direct Player Events ---") - - local i, e - for i = 1, table.getn(list) do - e = list[i] - self:Print( - tostring(i) .. " | " .. - tostring(e.eventType) .. " | ID " .. - tostring(e.spellID) .. " | " .. - tostring(e.name) .. " | x" .. - tostring(e.count or 1) - ) - end -end