From 5a48b4fba15a7085d3ce991de5eeea3c30c10d55 Mon Sep 17 00:00:00 2001 From: Wurmschwanz Date: Sun, 4 Oct 2026 13:11:22 +0200 Subject: [PATCH] Add files via upload --- BlizzNameplatesPlus.toc | 2 +- core/config.lua | 26 +++++++ core/core.lua | 2 +- core/options.lua | 83 +++++++++++++++------ modules/aurarecorder.lua | 4 + modules/auras.lua | 98 ++++++++++++++++++------- modules/castbar.lua | 12 +++ modules/combopoints.lua | 8 +- modules/debug.lua | 18 ++++- modules/fullalpha.lua | 60 +++++++++------ modules/immunities.lua | 23 +++++- modules/raidmarks.lua | 6 ++ modules/tankmode.lua | 154 +++++++++++++++++++++++++++++++++++++-- modules/unknown.lua | 45 ++++++++++-- 14 files changed, 450 insertions(+), 91 deletions(-) diff --git a/BlizzNameplatesPlus.toc b/BlizzNameplatesPlus.toc index 4963c5e..82fc6f8 100644 --- a/BlizzNameplatesPlus.toc +++ b/BlizzNameplatesPlus.toc @@ -3,7 +3,7 @@ ## Notes: Lightweight personal debuffs and class colors for Blizzard nameplates. Requires SuperWoW and ClassicAPI. ## Author: Wurmschwanz; nameplate discovery inspired by Shagu's Vanilla projects ## OptionalDeps: ShaguTweaks -## Version: 2.0.0 +## Version: 2.0.1 ## SavedVariablesPerCharacter: BNP_DB core\core.lua diff --git a/core/config.lua b/core/config.lua index b9d37b3..0e0b135 100644 --- a/core/config.lua +++ b/core/config.lua @@ -18,6 +18,8 @@ BNP.defaults = BNP.defaults or { debuffPosition = "top", ccPosition = "top", tankMode = false, + tankNoTarget = false, + tankNoTargetColor = { r = 0.00, g = 0.45, b = 1.00 }, invertTankColors = false, invertTankAggroColor = { r = 1.00, g = 0.00, b = 0.00 }, invertTankNoAggroColor = { r = 0.00, g = 1.00, b = 0.00 }, @@ -124,6 +126,7 @@ function BNP:InitConfig() if BNP_DB.ccYOffset == nil then BNP_DB.ccYOffset = self.defaults.ccYOffset end if BNP_DB.debuffPosition == nil then BNP_DB.debuffPosition = self.defaults.debuffPosition end if BNP_DB.ccPosition == nil then BNP_DB.ccPosition = BNP_DB.debuffPosition or self.defaults.ccPosition end + if BNP_DB.tankNoTarget == nil then BNP_DB.tankNoTarget = self.defaults.tankNoTarget end if BNP_DB.tankMode == nil then BNP_DB.tankMode = self.defaults.tankMode end if BNP_DB.invertTankColors == nil then BNP_DB.invertTankColors = self.defaults.invertTankColors end @@ -137,6 +140,7 @@ function BNP:InitConfig() if color.g == nil then color.g = fallback.g end if color.b == nil then color.b = fallback.b end end + InitTankColor("tankNoTargetColor", self.defaults.tankNoTargetColor) InitTankColor("invertTankAggroColor", self.defaults.invertTankAggroColor) InitTankColor("invertTankNoAggroColor", self.defaults.invertTankNoAggroColor) @@ -390,6 +394,28 @@ local function ClampTankColor(value, fallback) return value end +function BNP:IsTankNoTargetEnabled() + return BNP_DB and BNP_DB.tankNoTarget and true or false +end + +function BNP:GetTankNoTargetColor() + local fallback = self.defaults.tankNoTargetColor + local color = BNP_DB and BNP_DB.tankNoTargetColor or fallback + return ClampTankColor(color and color.r, fallback.r), + ClampTankColor(color and color.g, fallback.g), + ClampTankColor(color and color.b, fallback.b) +end + +function BNP:SetTankNoTargetColor(r, g, b) + if not BNP_DB then return end + BNP_DB.tankNoTargetColor = { + r = ClampTankColor(r, 0), + g = ClampTankColor(g, 0.45), + b = ClampTankColor(b, 1), + } + if self.UpdateTankMode then self:UpdateTankMode() end +end + function BNP:GetInvertTankAggroColor() local fallback = self.defaults.invertTankAggroColor or { r = 1, g = 0, b = 0 } local color = BNP_DB and BNP_DB.invertTankAggroColor or fallback diff --git a/core/core.lua b/core/core.lua index e45f13f..8c3710a 100644 --- a/core/core.lua +++ b/core/core.lua @@ -1,5 +1,5 @@ BNP = BNP or {} -BNP.version = "2.0.0" +BNP.version = "2.0.1" BNP.plates = BNP.plates or {} BNP.detected = 0 BNP.debugEnabled = false diff --git a/core/options.lua b/core/options.lua index b97ee80..94f4ede 100644 --- a/core/options.lua +++ b/core/options.lua @@ -1145,7 +1145,7 @@ function BNP:CreateOptions() end frame.pages = { - nameplates = CreatePage(510), + nameplates = CreatePage(542), personal = CreatePage(345), auras = CreatePage(520), totems = CreatePage(610), @@ -1454,22 +1454,59 @@ function BNP:CreateOptions() 0.00, 1.00, 0.00) frame.invertNoAggroColorSwatch = invertNoAggroColor - -- NAMES & LEVEL --------------------------------------------------------- - CreateSection(nameplatesPage, "Names & Level", -202) + -- Separate scope keeps the large options builder below Lua 5.0's local limit. + do + local noTarget = CreateCheck(nameplatesPage, "Inactive Combat", -198, function() + BNP_DB.tankNoTarget = this:GetChecked() and true or false + if BNP.UpdateTankMode then BNP:UpdateTankMode() end + if frame.UpdateDependentControls then frame:UpdateDependentControls() end + end, 22) + noTarget:SetScript("OnEnter", function() + GameTooltip:SetOwner(this, "ANCHOR_RIGHT") + GameTooltip:SetText("Inactive Combat Color", 1, 0.82, 0) + GameTooltip:AddLine("Uses a third color only for enemies in combat that are temporarily unable to attack (fear, sheep, stuns or fleeing). Idle enemies are excluded.", 1, 1, 1, true) + GameTooltip:AddLine("Reuses tracked CCs, even with a retained target. Direct fleeing status is used when available; otherwise no target in combat is the fallback. Independent of Invert Tank Colors.", 0.8, 0.8, 0.8, true) + GameTooltip:Show() + end) + noTarget:SetScript("OnLeave", function() GameTooltip:Hide() end) + frame.tankNoTargetCheck = noTarget + frame.tankNoTargetColorSwatch = CreateColorSwatch(nameplatesPage, "Inactive Color", -198, 214, + function() return BNP:GetTankNoTargetColor() end, + function(r, g, b) BNP:SetTankNoTargetColor(r, g, b) end, + 0.00, 0.45, 1.00, + "Choose the color for temporarily inactive enemies in combat.", + "Restores the inactive combat color to blue.") + end - local hidePlayerNames = CreateCheck(nameplatesPage, "Hide Player Names", -226, function() + -- Fixed label columns keep all Tank Mode color boxes aligned, regardless + -- of label length. Scope avoids adding long-lived options-builder locals. + do + local _, swatch + for _, swatch in ipairs({ frame.invertAggroColorSwatch, + frame.invertNoAggroColorSwatch, frame.tankNoTargetColorSwatch }) do + if swatch and swatch.BNPLabel then + swatch.BNPLabel:SetWidth(110) + swatch.BNPLabel:SetJustifyH("LEFT") + end + end + end + + -- NAMES & LEVEL --------------------------------------------------------- + CreateSection(nameplatesPage, "Names & Level", -234) + + local hidePlayerNames = CreateCheck(nameplatesPage, "Hide Player Names", -258, function() BNP_DB.hidePlayerNames = this:GetChecked() and true or false if BNP.RefreshNameVisibility then BNP:RefreshNameVisibility() end end, 22) frame.hidePlayerNamesCheck = hidePlayerNames - local hideNPCNames = CreateCheck(nameplatesPage, "Hide NPC Names", -226, function() + local hideNPCNames = CreateCheck(nameplatesPage, "Hide NPC Names", -258, function() BNP_DB.hideNPCNames = this:GetChecked() and true or false if BNP.RefreshNameVisibility then BNP:RefreshNameVisibility() end end, 214) frame.hideNPCNamesCheck = hideNPCNames - local hideNameplateLevel = CreateCheck(nameplatesPage, "Hide Level", -250, function() + local hideNameplateLevel = CreateCheck(nameplatesPage, "Hide Level", -282, function() BNP_DB.hideNameplateLevel = this:GetChecked() and true or false if BNP.RefreshNameplateLevelVisibility then BNP:RefreshNameplateLevelVisibility() end if BNP.RefreshCastbarLayout then BNP:RefreshCastbarLayout() end @@ -1483,7 +1520,7 @@ function BNP:CreateOptions() hideNameplateLevel:SetScript("OnLeave", function() GameTooltip:Hide() end) frame.hideNameplateLevelCheck = hideNameplateLevel - local customNameColor = CreateCheck(nameplatesPage, "Custom Name Color", -250, function() + local customNameColor = CreateCheck(nameplatesPage, "Custom Name Color", -282, function() BNP_DB.customNameColor = this:GetChecked() and true or false if BNP.RefreshNameAppearance then BNP:RefreshNameAppearance() end if frame.UpdateDependentControls then frame:UpdateDependentControls() end @@ -1497,7 +1534,7 @@ function BNP:CreateOptions() customNameColor:SetScript("OnLeave", function() GameTooltip:Hide() end) frame.customNameColorCheck = customNameColor - local nameColorSwatch, nameColorLabel = CreateColorSwatch(nameplatesPage, "Name Color", -276, 214, + local nameColorSwatch, nameColorLabel = CreateColorSwatch(nameplatesPage, "Name Color", -308, 214, function() return BNP:GetNameColor() end, function(r, g, b) BNP:SetNameColor(r, g, b) end, 1.00, 1.00, 1.00, @@ -1514,7 +1551,7 @@ function BNP:CreateOptions() -- Name text controls are independent from Nameplate Scale. The size setting -- only touches the Blizzard name FontString; the Y offset only moves it. - local nameFontSize = CreateSlider(nameplatesPage, "Name Font Size", 8, 24, 1, -306, 28, 150) + local nameFontSize = CreateSlider(nameplatesPage, "Name Font Size", 8, 24, 1, -338, 28, 150) nameFontSize:SetScript("OnValueChanged", function() if not BNP_DB or frame.BNPSyncingNameControls then return end local value = math.floor(this:GetValue() + 0.5) @@ -1524,7 +1561,7 @@ function BNP:CreateOptions() end) frame.nameFontSizeSlider = nameFontSize - local nameFontYOffset = CreateSlider(nameplatesPage, "Name Y Offset", -50, 50, 1, -306, 220, 150) + local nameFontYOffset = CreateSlider(nameplatesPage, "Name Y Offset", -50, 50, 1, -338, 220, 150) nameFontYOffset:SetScript("OnValueChanged", function() if not BNP_DB or frame.BNPSyncingNameControls then return end local value = RoundSignedInteger(this:GetValue()) @@ -1535,15 +1572,15 @@ function BNP:CreateOptions() frame.nameFontYOffsetSlider = nameFontYOffset -- HEALTH BAR & TEXT ----------------------------------------------------- - CreateSection(nameplatesPage, "Health Bar & Text", -336) + CreateSection(nameplatesPage, "Health Bar & Text", -368) local healthTextLabel = nameplatesPage:CreateFontString(nil, "OVERLAY", "GameFontNormal") - healthTextLabel:SetPoint("TOPLEFT", nameplatesPage, "TOPLEFT", 28, -358) + healthTextLabel:SetPoint("TOPLEFT", nameplatesPage, "TOPLEFT", 28, -390) healthTextLabel:SetText("Display") healthTextLabel:SetTextColor(1.00, 0.82, 0.00) local healthTextDropdown = CreateFrame("Frame", "BNPHealthTextDropdown", nameplatesPage, "UIDropDownMenuTemplate") - healthTextDropdown:SetPoint("TOPLEFT", nameplatesPage, "TOPLEFT", 10, -370) + healthTextDropdown:SetPoint("TOPLEFT", nameplatesPage, "TOPLEFT", 10, -402) UIDropDownMenu_SetWidth(150, healthTextDropdown) local healthModeLabels = { @@ -1579,12 +1616,12 @@ function BNP:CreateOptions() frame.SetHealthTextMode = SetHealthTextMode local healthOutlineLabel = nameplatesPage:CreateFontString(nil, "OVERLAY", "GameFontNormal") - healthOutlineLabel:SetPoint("TOPLEFT", nameplatesPage, "TOPLEFT", 220, -358) + healthOutlineLabel:SetPoint("TOPLEFT", nameplatesPage, "TOPLEFT", 220, -390) healthOutlineLabel:SetText("Outline") healthOutlineLabel:SetTextColor(1.00, 0.82, 0.00) local healthOutlineDropdown = CreateFrame("Frame", "BNPHealthOutlineDropdown", nameplatesPage, "UIDropDownMenuTemplate") - healthOutlineDropdown:SetPoint("TOPLEFT", nameplatesPage, "TOPLEFT", 200, -370) + healthOutlineDropdown:SetPoint("TOPLEFT", nameplatesPage, "TOPLEFT", 200, -402) UIDropDownMenu_SetWidth(150, healthOutlineDropdown) local healthOutlineLabels = { @@ -1617,7 +1654,7 @@ function BNP:CreateOptions() frame.healthTextOutlineDropdown = healthOutlineDropdown frame.SetHealthTextOutline = SetHealthTextOutline - local healthFontSize = CreateSlider(nameplatesPage, "Health Font Size", 8, 20, 1, -416, 28, 150) + local healthFontSize = CreateSlider(nameplatesPage, "Health Font Size", 8, 20, 1, -448, 28, 150) healthFontSize:SetScript("OnValueChanged", function() if not BNP_DB or frame.BNPSyncingHealthTextControls then return end local value = math.floor(this:GetValue() + 0.5) @@ -1627,7 +1664,7 @@ function BNP:CreateOptions() end) frame.healthTextFontSizeSlider = healthFontSize - local blackHealthbarBackground = CreateCheck(nameplatesPage, "Black Health Background", -412, function() + local blackHealthbarBackground = CreateCheck(nameplatesPage, "Black Health Background", -444, function() BNP_DB.blackHealthbarBackground = this:GetChecked() and true or false if BNP.RefreshHealthbarBackground then BNP:RefreshHealthbarBackground() end end, 214) @@ -1642,9 +1679,9 @@ function BNP:CreateOptions() -- COMBO POINTS (Rogue / Druid only) ------------------------------------ if comboOptionsClass then - CreateSection(nameplatesPage, "Combo Points", -442) + CreateSection(nameplatesPage, "Combo Points", -474) - local comboPoints = CreateCheck(nameplatesPage, "Combo Points", -466, function() + local comboPoints = CreateCheck(nameplatesPage, "Combo Points", -498, function() BNP_DB.comboPoints = this:GetChecked() and true or false if BNP.RefreshComboPoints then BNP:RefreshComboPoints() end if BNP.RefreshAllAuraLayouts then BNP:RefreshAllAuraLayouts() end @@ -1653,7 +1690,7 @@ function BNP:CreateOptions() end, 22) frame.comboPointsCheck = comboPoints - local darkComboPointBorder = CreateCheck(nameplatesPage, "Dark CP Border", -466, function() + local darkComboPointBorder = CreateCheck(nameplatesPage, "Dark CP Border", -498, function() BNP_DB.darkComboPointBorder = this:GetChecked() and true or false if BNP.RefreshComboPointBorderStyle then BNP:RefreshComboPointBorderStyle() end if BNP.RefreshComboPoints then BNP:RefreshComboPoints() end @@ -1668,7 +1705,7 @@ function BNP:CreateOptions() darkComboPointBorder:SetScript("OnLeave", function() GameTooltip:Hide() end) frame.darkComboPointBorderCheck = darkComboPointBorder - local comboYOffset = CreateSlider(nameplatesPage, "Combo Point Y Offset", -50, 50, 1, -466, 278, 105) + local comboYOffset = CreateSlider(nameplatesPage, "Combo Point Y Offset", -50, 50, 1, -498, 278, 105) comboYOffset:SetScript("OnValueChanged", function() if not BNP_DB then return end local value = RoundSignedInteger(this:GetValue()) @@ -2783,6 +2820,8 @@ function BNP:CreateOptions() swatch.BNPLabel:SetTextColor(enabled and 1 or 0.5, enabled and 0.82 or 0.5, enabled and 0 or 0.5) end end + SetCheckEnabled(self.tankNoTargetCheck, BNP:IsTankModeEnabled()) + SetTankSwatchEnabled(self.tankNoTargetColorSwatch, BNP:IsTankModeEnabled() and BNP:IsTankNoTargetEnabled()) SetTankSwatchEnabled(self.invertAggroColorSwatch, invertTankEnabled) SetTankSwatchEnabled(self.invertNoAggroColorSwatch, invertTankEnabled) @@ -3117,7 +3156,9 @@ function BNP:SyncOptions() frame.castbarTestCheck:SetChecked(self.IsCastbarTestMode and self:IsCastbarTestMode() or false) end frame.tankCheck:SetChecked(self:IsTankModeEnabled()) + if frame.tankNoTargetCheck then frame.tankNoTargetCheck:SetChecked(self:IsTankNoTargetEnabled()) end if frame.invertTankColorsCheck then frame.invertTankColorsCheck:SetChecked(self:AreTankModeColorsInverted()) end + if frame.tankNoTargetColorSwatch and frame.tankNoTargetColorSwatch.RefreshColor then frame.tankNoTargetColorSwatch:RefreshColor() end if frame.invertAggroColorSwatch and frame.invertAggroColorSwatch.RefreshColor then frame.invertAggroColorSwatch:RefreshColor() end if frame.invertNoAggroColorSwatch and frame.invertNoAggroColorSwatch.RefreshColor then frame.invertNoAggroColorSwatch:RefreshColor() end if frame.comboPointsCheck and comboOptionsClass then diff --git a/modules/aurarecorder.lua b/modules/aurarecorder.lua index 4a9a1d9..d59cd9b 100644 --- a/modules/aurarecorder.lua +++ b/modules/aurarecorder.lua @@ -860,9 +860,11 @@ function BNP:StartAuraRecorder() recorder.active = false UnregisterRecorderEvents() updateFrame:SetScript("OnUpdate", nil) + if self.SetUnknownDiagnosticsActive then self:SetUnknownDiagnosticsActive(false) end end UnregisterRecorderEvents() + if self.ResetUnknownBeta then self:ResetUnknownBeta(true) end recorder.lines = {} recorder.charCount = 0 recorder.limitHit = false @@ -879,6 +881,7 @@ function BNP:StartAuraRecorder() recorder.traceGUID = nil recorder.traceName = nil recorder.active = true + if self.SetUnknownDiagnosticsActive then self:SetUnknownDiagnosticsActive(true) end local registeredCount = 0 local i @@ -959,6 +962,7 @@ function BNP:StopAuraRecorder(reason) recorder.stoppedAt = GetTime() UnregisterRecorderEvents() updateFrame:SetScript("OnUpdate", nil) + if self.SetUnknownDiagnosticsActive then self:SetUnknownDiagnosticsActive(false) end DumpInternalDiagnostics() AppendLine("=== END REPORT | reason=" .. CleanValue(reason or "user") .. " | elapsed=" .. string.format("%.3f", recorder.stoppedAt - (recorder.startedAt or recorder.stoppedAt)) .. "s ===", true) diff --git a/modules/auras.lua b/modules/auras.lua index 8b6427d..cbf932b 100644 --- a/modules/auras.lua +++ b/modules/auras.lua @@ -2892,18 +2892,19 @@ local function BuildGlobalCCDefs() end BuildGlobalCCDefs() -local function ResolveGlobalCCDef(spellID, texture) - -- Prefer reliable aura identity over icon matching. Several unrelated auras - -- reuse the same Blizzard icon (for example Resurrection Sickness and - -- Howl of Terror), so texture matching must never override a known spell. +local function ResolveGlobalCCDef(spellID, auraName) + -- Foreign CCs must be identified by exact spell identity, never by icon. + -- Blizzard reuses many spell textures across unrelated buffs/debuffs, so a + -- texture fallback can turn harmless effects into ghost CC icons. ClassicAPI + -- supplies either the aura spell ID or the exact aura name on supported paths. 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 + if (not auraName or auraName == "") and spellID and SpellInfo then + auraName = SpellInfo(spellID) + end - -- A known spell name is authoritative. If it is not one of our CCs, cache - -- the negative result and do not reinterpret it from a shared icon texture. + local _, ccDef if auraName and auraName ~= "" then for _, ccDef in ipairs(GLOBAL_CC_DEFS) do if NameMatches(ccDef, auraName) then @@ -2911,31 +2912,63 @@ local function ResolveGlobalCCDef(spellID, texture) return ccDef end end - - if spellID then GLOBAL_CC_NEGATIVE[spellID] = true end - return nil - end - - -- Texture is only a compatibility fallback for aura scans where ClassicAPI - -- cannot provide a usable spell ID/name. - if texture then - local lowerTexture = string.lower(texture) - for _, ccDef in ipairs(GLOBAL_CC_DEFS) do - if ccDef.textureMatch and string.find(lowerTexture, ccDef.textureMatch) then - return ccDef - end - end end if spellID then GLOBAL_CC_NEGATIVE[spellID] = true end return nil end +-- Reuse the already-read UNIT_AURA snapshot for Tank Mode. This does not +-- depend on aura display settings and never triggers an additional API scan. +function BNP:CacheTankControlSnapshot(plate, guid, snapshot) + if not self.IsTankNoTargetEnabled or not self:IsTankNoTargetEnabled() + or not self:IsTankModeEnabled() then + plate.BNPTankControlGUID = nil + plate.BNPTankControlled = nil + return + end + local controlled = false + local i + for i = 1, snapshot.count do + local entry = snapshot.entries[i] + local def = ResolveGlobalCCDef(entry[4], entry[10]) + if def and self.TankDisablingCC and self.TankDisablingCC[def.key] then + controlled = true + break + end + end + plate.BNPTankControlGUID = guid + plate.BNPTankControlled = controlled +end + +-- One initial read when enabling the option on an already visible plate. +-- Subsequent updates come from the existing UNIT_AURA watcher only. +function BNP:PrimeTankControlSnapshot(plate, guid, now) + local token = plate.BNPClassicUnitToken + if not token or not self._ClassicGetNamePlateForUnit or not self._ClassicUnitGUID then return end + if self._ClassicGetNamePlateForUnit(token) ~= plate or self._ClassicUnitGUID(token) ~= guid then return end + self:CacheTankControlSnapshot(plate, guid, CaptureLiveDebuffSnapshot(token, now)) +end + local function WantsForeignCCs() return BNP.AreCrowdControlEnabled and BNP:AreCrowdControlEnabled() and BNP.ShowOtherPlayersCCs and BNP:ShowOtherPlayersCCs() end +-- "Show CCs from Other Players" must only show a foreign CC when ClassicAPI +-- gives us a caster we can positively identify as player-controlled. Treating +-- missing/unresolved source metadata as "probably a player" can turn unrelated +-- NPC auras into ghost CC icons (for example a Paladin-looking seal icon with no +-- Paladin present). Own player/pet CCs are handled by the normal own-aura path. +function BNP:_IsAllowedForeignCCSource(source) + if not source or source == "" then return false end + if self:_IsOwnClassicAuraSource(source) then return false end + + if not UnitExists or not UnitExists(source) then return false end + if not UnitPlayerControlled then return false end + return UnitPlayerControlled(source) and true or false +end + local FOREIGN_CC_SEEN = {} local foreignCCSeenGeneration = 0 @@ -2952,17 +2985,23 @@ local function SyncForeignCCs(unit, guid, now, snapshot, eventAuthoritative) local i local maxIndex = snapshot and snapshot.count or 64 for i = 1, maxIndex do - local texture, stacks, dtype, auraSpellID + local texture, stacks, dtype, auraSpellID, liveDuration, liveExpires, source, auraName 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] + texture, stacks, dtype, auraSpellID, source, auraName = + entry[1], entry[2], entry[3], entry[4], entry[7], entry[10] else - texture, stacks, dtype, auraSpellID = BNP:_ReadClassicDebuff(unit, i) + local isStealable, nameplateShowPersonal + texture, stacks, dtype, auraSpellID, liveDuration, liveExpires, source, + isStealable, nameplateShowPersonal, auraName = BNP:_ReadClassicDebuff(unit, i) if not texture then break end end - local ccDef = ResolveGlobalCCDef(auraSpellID, texture) + local ccDef = nil + if BNP:_IsAllowedForeignCCSource(source) then + ccDef = ResolveGlobalCCDef(auraSpellID, auraName) + end if ccDef then FOREIGN_CC_SEEN[ccDef.key] = seenGeneration if not live then @@ -3798,6 +3837,7 @@ function BNP:_HandleClassicUnitAura(token, plate) end local snapshot = CaptureLiveDebuffSnapshot(token, now) + self:CacheTankControlSnapshot(plate, guid, snapshot) -- Pending own casts and AoE applications consume the same snapshot instead -- of re-walking the aura descriptor list. @@ -3822,6 +3862,12 @@ function BNP:_HandleClassicUnitAura(token, plate) plate.BNPClassicAuraLastEvent = now UpdatePlate(plate, now) + -- Reflect confirmed CC appearance/removal in this event, without waiting + -- for the 0.10s nameplate poller. The snapshot above is already cached. + if self.UpdateTankModePlate and self.IsTankModeEnabled and self:IsTankModeEnabled() + and self:IsTankNoTargetEnabled() then + self:UpdateTankModePlate(plate) + end -- Tiny diagnostics only; no per-event tables are created. if not self.classicAuraStats then self.classicAuraStats = { events = 0, scans = 0 } end diff --git a/modules/castbar.lua b/modules/castbar.lua index f4ec54e..d7618f5 100644 --- a/modules/castbar.lua +++ b/modules/castbar.lua @@ -539,6 +539,18 @@ discovery:SetScript("OnUpdate", function() return end + -- No active timed cast and no short success/fail feedback means there is + -- nothing to discover. UNIT_CASTEVENT wakes this path automatically by + -- creating an entry in casts/castFeedback, so idle gameplay no longer walks + -- every visible nameplate 20 times per second. + if not castbarTestMode and not next(casts) and not next(castFeedback) then + local plate + for plate in pairs(activeCastPlates) do + MarkCastPlateInactive(plate) + end + return + end + local now = GetTime() -- Drop expired feedback even if its nameplate disappeared before the short diff --git a/modules/combopoints.lua b/modules/combopoints.lua index e5498e3..72a7821 100644 --- a/modules/combopoints.lua +++ b/modules/combopoints.lua @@ -441,9 +441,11 @@ table.insert(BNP.libnameplate.OnShow, function(plate) end end) -table.insert(BNP.libnameplate.OnUpdate, function(plate) - if COMBO_CLASS then UpdatePlate(plate) end -end) +if COMBO_CLASS then + table.insert(BNP.libnameplate.OnUpdate, function(plate) + UpdatePlate(plate) + end) +end if COMBO_CLASS then local events = CreateFrame("Frame") diff --git a/modules/debug.lua b/modules/debug.lua index 7cd623a..7f2318b 100644 --- a/modules/debug.lua +++ b/modules/debug.lua @@ -4,6 +4,16 @@ local function CreateMarker(plate) if not plate then return end BNP:RegisterPlate(plate) + -- Debug is disabled for normal gameplay. Do not allocate an extra Frame, + -- Texture and FontString on every nameplate unless the user actually enables + -- the marker. Existing markers are simply hidden when debug is off. + if not BNP.debugEnabled then + if plate.BNPMarker and plate.BNPMarker.IsShown and plate.BNPMarker:IsShown() then + plate.BNPMarker:Hide() + end + return + end + if not plate.BNPMarker then local marker = CreateFrame("Frame", nil, plate) marker:SetWidth(14) @@ -23,7 +33,7 @@ local function CreateMarker(plate) plate.BNPMarker = marker end - if BNP.debugEnabled then plate.BNPMarker:Show() else plate.BNPMarker:Hide() end + plate.BNPMarker:Show() end table.insert(BNP.libnameplate.OnInit, CreateMarker) @@ -35,8 +45,10 @@ function BNP:SetDebug(enabled) 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 + if enabled then + CreateMarker(plate) + elseif plate.BNPMarker then + plate.BNPMarker:Hide() end end BNP:Print("Debug-Marker " .. (enabled and "aktiviert" or "deaktiviert") .. ".") diff --git a/modules/fullalpha.lua b/modules/fullalpha.lua index 8229c88..7554a98 100644 --- a/modules/fullalpha.lua +++ b/modules/fullalpha.lua @@ -865,6 +865,13 @@ local function ApplyTargetArrows(plate) end +local function SetDetachedObjectAlpha(object, wantedAlpha) + if object and object.SetAlpha and + (not object.GetAlpha or object:GetAlpha() ~= wantedAlpha) then + object:SetAlpha(wantedAlpha) + end +end + local function ApplyTargetOnlyPresentation(plate, alpha, targetOnly) if not plate then return end @@ -872,15 +879,9 @@ local function ApplyTargetOnlyPresentation(plate, alpha, targetOnly) -- projected plate alpha. Keep them in lockstep with Target Only and restore -- them to full opacity immediately when the option is disabled. local detachedAlpha = targetOnly and alpha or 1 - local function SetObjectAlpha(object) - if object and object.SetAlpha and - (not object.GetAlpha or object:GetAlpha() ~= detachedAlpha) then - object:SetAlpha(detachedAlpha) - end - end - SetObjectAlpha(plate.BNPAuraContainer) - SetObjectAlpha(plate.BNPCCContainer) - SetObjectAlpha(plate.BNPImmunityContainer) + SetDetachedObjectAlpha(plate.BNPAuraContainer, detachedAlpha) + SetDetachedObjectAlpha(plate.BNPCCContainer, detachedAlpha) + SetDetachedObjectAlpha(plate.BNPImmunityContainer, detachedAlpha) -- BNP routes nameplate clicks through BNPScaleWrapper. Alpha-0 frames can -- still receive mouse input in the WoW UI, so disable the wrapper hit area @@ -888,7 +889,10 @@ local function ApplyTargetOnlyPresentation(plate, alpha, targetOnly) local wrapper = plate.BNPScaleWrapper if wrapper and wrapper.EnableMouse then local shouldEnableMouse = (not targetOnly) or alpha > 0 - wrapper:EnableMouse(shouldEnableMouse) + if wrapper.BNPTargetOnlyMouseEnabled ~= shouldEnableMouse then + wrapper:EnableMouse(shouldEnableMouse) + wrapper.BNPTargetOnlyMouseEnabled = shouldEnableMouse + end if not shouldEnableMouse and BNP.BNPMouseoverOwner == wrapper then BNP.BNPMouseoverOwner = nil @@ -950,19 +954,33 @@ local function InstallAlphaGuard(plate) BNP:MaintainNameplateYOffset(current) end - -- Blizzard and the scale/Y-offset wrapper may restore the native raid icon - -- anchor while the plate is being projected. Reapply the optional custom - -- Raid Mark position afterwards so it stays stable without touching its - -- texture, visibility or size. - if BNP.MaintainRaidMarkPosition then BNP:MaintainRaidMarkPosition(current) end - + -- Alpha / Target Only remains per-frame because projected Vanilla plates + -- can be recycled between normal events and Blizzard may rewrite alpha. + -- The heavier visual guards below do not need render-rate polling: target + -- changes refresh the old/new target immediately through PLAYER_TARGET_CHANGED, + -- while a 20 Hz safety pass is more than enough to repair occasional client + -- rewrites without doing the same work 60-165 times per second per plate. ApplyTargetAlpha(current) - if BNP.MaintainTargetScale then BNP:MaintainTargetScale(current) end - ApplyTargetGlow(current) - ApplyTargetArrows(current) - ApplyTargetBorderColor(current) + -- Foreign-tag grey is deliberately still enforced every frame. Blizzard can + -- rewrite healthbar colors between events and even a short throttle can + -- reintroduce a visible grey/red flash on tagged mobs. The expensive owner + -- query inside ApplyForeignTagVisual is already cached to 10 Hz. ApplyForeignTagVisual(current) - ApplyTargetFrameLevel(current) + + current.BNPVisualGuardElapsed = (current.BNPVisualGuardElapsed or 0) + (arg1 or 0) + if current.BNPVisualGuardElapsed >= 0.05 then + current.BNPVisualGuardElapsed = 0 + + -- Blizzard and the scale/Y-offset wrapper may restore the native raid icon + -- anchor while the plate is being projected. Reapply the optional custom + -- Raid Mark position on the throttled safety pass. + if BNP.MaintainRaidMarkPosition then BNP:MaintainRaidMarkPosition(current) end + if BNP.MaintainTargetScale then BNP:MaintainTargetScale(current) end + ApplyTargetGlow(current) + ApplyTargetArrows(current) + ApplyTargetBorderColor(current) + ApplyTargetFrameLevel(current) + end end) plate.BNPFullAlphaGuard = true diff --git a/modules/immunities.lua b/modules/immunities.lua index 550cf9a..e3f103a 100644 --- a/modules/immunities.lua +++ b/modules/immunities.lua @@ -817,6 +817,17 @@ local function UpdatePlate(plate) local guid = BNP.GetStablePlateAuraGUID and BNP.GetStablePlateAuraGUID(plate, now) or GetPlateGUID(plate) if not guid then HideContainer(plate) return end + local cache = BNP.guidImmunities and BNP.guidImmunities[guid] + + -- PvE fast path: once a stable GUID has been positively resolved as an + -- existing non-player unit, do not repeat ClassicAPI/UnitBuff player checks + -- every 0.10s for that same NPC. A recycled plate gets a different GUID and + -- automatically falls back to normal detection. Cast-event cache always wins. + if plate.BNPImmunityNonPlayerGUID == guid and not cache then + HideContainer(plate) + return + end + local targetGUID = GetTargetGUID() local unit = nil if targetGUID and targetGUID == guid then @@ -840,12 +851,20 @@ local function UpdatePlate(plate) -- Lazy creation: NPC nameplates with no immunity state never need a -- container or its four icon frames. Existing cast-event state still gets - -- rendered even during a short token-resolution gap. - local cache = BNP.guidImmunities and BNP.guidImmunities[guid] + -- rendered even during a short token-resolution gap. Cache a negative player + -- result only when UnitExists positively confirms that the projected unit is + -- valid; transient/unresolved tokens therefore never get stuck as NPCs. if not classicCanScan and not legacyCanScan and not cache then + local unitExists = false + if unit and UnitExists then + local ok, exists = pcall(UnitExists, unit) + unitExists = ok and exists and true or false + end + if unitExists then plate.BNPImmunityNonPlayerGUID = guid end HideContainer(plate) return end + plate.BNPImmunityNonPlayerGUID = nil if not plate.BNPImmunityContainer then CreateContainer(plate) end if classicCanScan then ScanClassicAPIBuffs(classicUnit, guid, now) end diff --git a/modules/raidmarks.lua b/modules/raidmarks.lua index 99a5f2c..d9ba00f 100644 --- a/modules/raidmarks.lua +++ b/modules/raidmarks.lua @@ -58,6 +58,12 @@ end function BNP:MaintainRaidMarkPosition(plate) if not plate or not plate.IsShown or not plate:IsShown() then return end + local icon = plate.raidicon + if not icon then return end + -- This path runs from the per-frame plate guard. Most plates have no active + -- raid mark, so avoid GetPoint/anchor comparisons until Blizzard actually + -- shows the icon. The first visible frame will still be corrected instantly. + if icon.IsShown and not icon:IsShown() then return end self:ApplyRaidMarkPosition(plate) end diff --git a/modules/tankmode.lua b/modules/tankmode.lua index 49a5613..df18ad0 100644 --- a/modules/tankmode.lua +++ b/modules/tankmode.lua @@ -4,11 +4,75 @@ BNP = BNP or {} -- Default: GREEN = hostile NPC currently targets the player, RED = another unit. -- Normal Tank Mode is fixed: GREEN = aggro, RED = no aggro. -- Invert Tank Colors uses two independently user-selectable colors. --- Idle hostile NPCs keep their normal Blizzard color. +-- Optional third color for in-combat NPCs temporarily unable to attack. +-- The existing two-color behavior is preserved while this option is disabled. 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 +-- Only effects that prevent attacking; roots, snares, silence and disarm +-- alone do not qualify. Shared with the existing aura snapshot reader. +BNP.TankDisablingCC = { + fear=true, howl_of_terror=true, banish=true, death_coil=true, + freezing_trap_effect=true, scatter_shot=true, wyvern_sting=true, + intimidation=true, scare_beast=true, improved_concussive_shot=true, + polymorph=true, impact=true, psychic_scream=true, blackout=true, + shackle_undead=true, hammer_of_justice=true, repentance=true, turn_undead=true, + gouge=true, kidney_shot=true, cheap_shot=true, sap=true, blind=true, + bash=true, pounce=true, hibernate=true, intimidating_shout=true, + charge_stun=true, intercept_stun=true, concussion_blow=true, revenge_stun=true, +} + +local function HasFlag(flags, mask) + -- Lua 5.0 compatible, without requiring a bit library. + return math.mod(math.floor(flags / mask), 2) == 1 +end + +local function HasCachedControl(plate, guid, now) + -- Authoritative UNIT_AURA snapshot, independent of CC icon visibility. + if plate.BNPClassicAuraEventDriven and plate.BNPTankControlGUID ~= guid + and BNP.PrimeTankControlSnapshot then + BNP:PrimeTankControlSnapshot(plate, guid, now) + end + if plate.BNPTankControlGUID == guid and plate.BNPClassicAuraEventDriven then + return plate.BNPTankControlled and true or false + end + local cache = BNP.guidAuras and BNP.guidAuras[guid] + local key, aura + if cache then + for key, aura in pairs(cache) do + if BNP.TankDisablingCC[key] and aura.expires and aura.expires > now then return true end + end + end + cache = BNP.guidLiveCCs and BNP.guidLiveCCs[guid] + if cache then + for key, aura in pairs(cache) do + if BNP.TankDisablingCC[key] and (aura.eventDriven or now - (aura.lastSeen or 0) <= 0.60) then + return true + end + end + end + return false +end + +local function IsTemporarilyInactive(plate, guid, now, noTarget) + -- One optional descriptor read covers combat, stun, confusion and fleeing, + -- including low-health fleeing with a retained target. No tooltip/aura scan. + local flags + if GetUnitField then + local ok, value = pcall(GetUnitField, guid, "flags") + if ok and type(value) == "number" then flags = value end + end + local inCombat + if flags then inCombat = HasFlag(flags, 524288) + else inCombat = UnitAffectingCombat and UnitAffectingCombat(guid) end + if not inCombat then return false end + if flags and (HasFlag(flags, 262144) or HasFlag(flags, 4194304) or HasFlag(flags, 8388608)) then + return true + end + return HasCachedControl(plate, guid, now) or noTarget +end + local playerGUID = nil local function GetPlayerGUID() @@ -19,6 +83,14 @@ end local function GetPlateGUID(plate) if not plate or not plate.GetName then return nil end + -- The ClassicAPI token is stable across camera projection changes. Only + -- use it while the API maps it back to this exact frame. + local token = plate.BNPClassicUnitToken + if token and BNP._ClassicUnitGUID and BNP._ClassicGetNamePlateForUnit + and BNP._ClassicGetNamePlateForUnit(token) == plate then + local stableGUID = BNP._ClassicUnitGUID(token) + if stableGUID then return stableGUID end + end local guid = plate:GetName(1) if guid and guid ~= "" then return guid end return nil @@ -27,8 +99,8 @@ 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 + if exists then return targetGUID, false end + return nil, true end local function GetBar(plate) @@ -55,6 +127,22 @@ 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. + -- Only undo the new third color for this same live NPC. Never replay a + -- cached reaction color onto a recycled, player-controlled or tagged plate. + local guid = plate.BNPTankNoTargetActive and bar and GetPlateGUID(plate) + if guid and guid == plate.BNPTankUnitGUID + and IsHostileNPC(guid) and not IsForeignTaggedGUID(guid) then + local reaction = UnitReaction and UnitReaction(guid, "player") + if reaction then + if reaction > 4 then bar:SetStatusBarColor(0, 1, 0, 1) + elseif reaction == 4 then bar:SetStatusBarColor(1, 1, 0, 1) + else bar:SetStatusBarColor(1, 0, 0, 1) end + end + end + plate.BNPTankNoTargetActive = nil + plate.BNPTankNoTargetSince = nil + plate.BNPTankUnitGUID = nil + plate.BNPTankColorGUID = nil plate.BNPTankActive = nil plate.BNPTankR = nil plate.BNPTankG = nil @@ -63,6 +151,10 @@ end local function ClearTankState(plate) if not plate then return end + plate.BNPTankNoTargetActive = nil + plate.BNPTankNoTargetSince = nil + plate.BNPTankUnitGUID = nil + plate.BNPTankColorGUID = nil plate.BNPTankActive = nil plate.BNPTankR = nil plate.BNPTankG = nil @@ -71,6 +163,7 @@ end local function ApplyTankColor(plate, bar, r, g, b) bar:SetStatusBarColor(r, g, b, 1) + plate.BNPTankColorGUID = GetPlateGUID(plate) plate.BNPTankActive = true plate.BNPTankR = r plate.BNPTankG = g @@ -79,6 +172,11 @@ end local function EnforceTankColor(plate) if not plate or not plate.BNPTankActive then return end + -- Never paint a previous unit's cached color onto a recycled plate. + if not BNP:IsTankModeEnabled() or plate.BNPTankColorGUID ~= GetPlateGUID(plate) then + ClearTankState(plate) + return + end local bar = GetBar(plate) if not bar or not plate.BNPTankR then return end @@ -118,10 +216,36 @@ function BNP:UpdateTankModePlate(plate) return end + local thirdColor = self:IsTankNoTargetEnabled() + if thirdColor then + if plate.BNPTankUnitGUID ~= guid then ClearTankState(plate) end + plate.BNPTankUnitGUID = guid + if UnitIsDead and UnitIsDead(guid) then + RestoreNormalColor(plate, bar) + return + end + elseif plate.BNPTankNoTargetSince or plate.BNPTankNoTargetActive then + RestoreNormalColor(plate, bar) + end + local myGUID = GetPlayerGUID() if not myGUID then return end - local targetGUID = GetTargetGUID(guid) + local targetGUID, noTarget = GetTargetGUID(guid) + local now = thirdColor and GetTime() + if thirdColor and IsTemporarilyInactive(plate, guid, now, noTarget) then + local r, g, b = self:GetTankNoTargetColor() + ApplyTankColor(plate, bar, r, g, b) + plate.BNPTankNoTargetActive = true + return + end + plate.BNPTankNoTargetSince = nil + if thirdColor and not targetGUID then + -- No active control and no readable target: relinquish the third color. + RestoreNormalColor(plate, bar) + return + end + plate.BNPTankNoTargetActive = nil local inverted = self:AreTankModeColorsInverted() if targetGUID and targetGUID == myGUID then @@ -153,6 +277,14 @@ function BNP:RefreshTankMode() end function BNP:UpdateTankMode() + local enabled = self:IsTankModeEnabled() and self:IsTankNoTargetEnabled() + if not enabled then + local plate + for plate in pairs(self.plates or {}) do + plate.BNPTankControlGUID = nil + plate.BNPTankControlled = nil + end + end self:RefreshTankMode() end @@ -175,6 +307,13 @@ function BNP:InstallTankMode() if old then old() end EnforceTankColor(current) end) + -- Blizzard may repaint on camera movement without changing HP. Keep + -- only the cached color in sync here; combat/aura reads stay throttled. + local oldUpdate = bar:GetScript("OnUpdate") + bar:SetScript("OnUpdate", function() + if oldUpdate then oldUpdate() end + EnforceTankColor(current) + end) current.BNPTankColorHooked = true end end) @@ -183,6 +322,9 @@ function BNP:InstallTankMode() local current = plate or this if not current then return end ClearTankState(current) + current.BNPTankControlGUID = nil + current.BNPTankControlled = nil + BNP:UpdateTankModePlate(current) end) table.insert(self.libnameplate.OnUpdate, function(plate, elapsed) @@ -192,7 +334,9 @@ function BNP:InstallTankMode() -- Tank Mode is optional. Avoid all GUID/target/tap queries while it is off. -- Only clean an old BNP-owned state if one is still present. if not BNP:IsTankModeEnabled() then - if current.BNPTankActive then ClearTankState(current) end + if current.BNPTankActive or current.BNPTankNoTargetSince then + RestoreNormalColor(current, GetBar(current)) + end return end diff --git a/modules/unknown.lua b/modules/unknown.lua index 15c2e01..30a1dcd 100644 --- a/modules/unknown.lua +++ b/modules/unknown.lua @@ -247,12 +247,9 @@ local combatEvents={ "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 @@ -265,7 +262,7 @@ end) local scanner = CreateFrame("Frame") local elapsed = 0 -scanner:SetScript("OnUpdate", function() +local function UnknownScannerOnUpdate() elapsed = elapsed + arg1 if elapsed < 0.35 then return end elapsed = 0 @@ -292,7 +289,7 @@ scanner:SetScript("OnUpdate", function() end end end -end) +end function BNP:PrintUnknownBeta(verbose) local data = self.unknownBeta @@ -362,14 +359,17 @@ function BNP:PrintUnknownBeta(verbose) end end -function BNP:ResetUnknownBeta() +function BNP:ResetUnknownBeta(silent) 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.") + self.unknownDirectEvents = {} + if not silent then + self:Print("Unknown spell/effect list cleared.") + end end @@ -449,7 +449,6 @@ local function RememberDirectEvent(eventType, spellID, targetGUID, extra) end local directFrame = CreateFrame("Frame") -directFrame:RegisterEvent("UNIT_CASTEVENT") directFrame:SetScript("OnEvent", function() local casterGUID = arg1 local targetGUID = arg2 @@ -465,6 +464,36 @@ directFrame:SetScript("OnEvent", function() end end) +-- Keep the legacy unknown-spell collector completely dormant during normal +-- gameplay. It is diagnostic-only and can otherwise turn into a permanent +-- 0.35s aura scan while the player is continuously casting in combat. The +-- Missing Spell / Aura Recorder enables it only for the duration of an active +-- recording, where the extra diagnostics are actually useful. +local unknownDiagnosticsActive = false +function BNP:SetUnknownDiagnosticsActive(enabled) + enabled = enabled and true or false + if unknownDiagnosticsActive == enabled then return end + unknownDiagnosticsActive = enabled + + if enabled then + local i + for i = 1, table.getn(combatEvents) do + combatFrame:RegisterEvent(combatEvents[i]) + end + castFrame:RegisterEvent("UNIT_CASTEVENT") + directFrame:RegisterEvent("UNIT_CASTEVENT") + elapsed = 0 + scanner:SetScript("OnUpdate", UnknownScannerOnUpdate) + else + combatFrame:UnregisterAllEvents() + castFrame:UnregisterAllEvents() + directFrame:UnregisterAllEvents() + scanner:SetScript("OnUpdate", nil) + elapsed = 0 + BNP.unknownBeta.activeUntil = 0 + end +end + function BNP:PrintUnknownDirectEvents() local list = {} local key, entry