Add files via upload

This commit is contained in:
Wurmschwanz
2026-10-04 13:11:22 +02:00
committed by GitHub
parent 3a6591ee3a
commit 5a48b4fba1
14 changed files with 450 additions and 91 deletions
+1 -1
View File
@@ -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
+26
View File
@@ -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
+1 -1
View File
@@ -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
+62 -21
View File
@@ -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
+4
View File
@@ -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)
+72 -26
View File
@@ -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
+12
View File
@@ -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
+5 -3
View File
@@ -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")
+15 -3
View File
@@ -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") .. ".")
+39 -21
View File
@@ -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
+21 -2
View File
@@ -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
+6
View File
@@ -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
+149 -5
View File
@@ -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
+37 -8
View File
@@ -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