Add files via upload

This commit is contained in:
Wurmschwanz
2026-09-04 20:52:41 +02:00
committed by GitHub
parent ec365b7e93
commit a3dfb369a7
37 changed files with 12583 additions and 0 deletions
+969
View File
@@ -0,0 +1,969 @@
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)