463 lines
12 KiB
Lua
463 lines
12 KiB
Lua
BNP = BNP or {}
|
|
BNP.unknownDirectEvents = BNP.unknownDirectEvents or {}
|
|
|
|
-- Beta spell collector -------------------------------------------------------
|
|
-- Purpose: help testers report missing Turtle/custom spell IDs without
|
|
-- editing Lua files. It records unknown own cast IDs and unknown debuff IDs
|
|
-- that newly appear shortly after one of the player's casts.
|
|
--
|
|
-- It is intentionally diagnostic only: it never changes tracking/rendering.
|
|
|
|
BNP.unknownBeta = BNP.unknownBeta or {
|
|
entries = {},
|
|
order = {},
|
|
recentCasts = {},
|
|
seenByGUID = {},
|
|
activeUntil = 0,
|
|
}
|
|
|
|
local _, playerClass = UnitClass("player")
|
|
|
|
local CLASS_AURAS = {
|
|
WARLOCK = BNP.WarlockAuras,
|
|
PRIEST = BNP.PriestAuras,
|
|
WARRIOR = BNP.WarriorAuras,
|
|
ROGUE = BNP.RogueAuras,
|
|
HUNTER = BNP.HunterAuras,
|
|
MAGE = BNP.MageAuras,
|
|
DRUID = BNP.DruidAuras,
|
|
SHAMAN = BNP.ShamanAuras,
|
|
PALADIN = BNP.PaladinAuras,
|
|
}
|
|
local AURA_DEFS = CLASS_AURAS[playerClass] or {}
|
|
|
|
local function GetPlayerGUID()
|
|
local exists, guid = UnitExists("player")
|
|
if exists and guid then return guid end
|
|
return nil
|
|
end
|
|
|
|
local function GetUnitGUID(unit)
|
|
local exists, guid = UnitExists(unit)
|
|
if exists and guid then return guid end
|
|
return nil
|
|
end
|
|
|
|
local function IsExplicitKnownID(spellID)
|
|
if not spellID then return false end
|
|
|
|
-- Dark Harvest is handled by dedicated Warlock logic.
|
|
if spellID == 52552 then return true end
|
|
|
|
local i, j, def
|
|
for i = 1, table.getn(AURA_DEFS) do
|
|
def = AURA_DEFS[i]
|
|
|
|
if def.spellIDs then
|
|
for j = 1, table.getn(def.spellIDs) do
|
|
if def.spellIDs[j] == spellID then return true end
|
|
end
|
|
end
|
|
|
|
if def.durations and def.durations[spellID] then
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
local function SpellName(spellID)
|
|
if not spellID or not SpellInfo then return "?" end
|
|
local name = SpellInfo(spellID)
|
|
return name or "?"
|
|
end
|
|
|
|
local function AddEntry(kind, spellID, spellName, guid, trigger, confidence)
|
|
if not spellID then return end
|
|
|
|
local key = tostring(kind) .. ":" .. tostring(spellID)
|
|
local data = BNP.unknownBeta
|
|
local old = data.entries[key]
|
|
|
|
if old then
|
|
old.count = (old.count or 1) + 1
|
|
old.lastSeen = GetTime()
|
|
if confidence == "TARGET" then old.confidence = confidence end
|
|
if trigger and not old.trigger then old.trigger = trigger end
|
|
return
|
|
end
|
|
|
|
local entry = {
|
|
kind = kind,
|
|
spellID = spellID,
|
|
name = spellName or SpellName(spellID),
|
|
class = playerClass or "?",
|
|
guid = guid,
|
|
trigger = trigger,
|
|
confidence = confidence or "?",
|
|
count = 1,
|
|
firstSeen = GetTime(),
|
|
lastSeen = GetTime(),
|
|
}
|
|
|
|
data.entries[key] = entry
|
|
table.insert(data.order, key)
|
|
end
|
|
|
|
local function RememberCast(spellID, targetGUID, eventType)
|
|
local data = BNP.unknownBeta
|
|
local now = GetTime()
|
|
data.activeUntil = now + 8.0
|
|
local cast = {
|
|
time = now,
|
|
spellID = spellID,
|
|
name = SpellName(spellID),
|
|
targetGUID = targetGUID,
|
|
eventType = eventType,
|
|
}
|
|
|
|
table.insert(data.recentCasts, cast)
|
|
while table.getn(data.recentCasts) > 20 do
|
|
table.remove(data.recentCasts, 1)
|
|
end
|
|
|
|
-- Unknown own CAST/CHANNEL IDs are useful too. Some trap/proc systems expose
|
|
-- the effect as a separate cast event before the aura is seen.
|
|
if spellID and not IsExplicitKnownID(spellID) then
|
|
AddEntry("CAST", spellID, cast.name, targetGUID, nil, "OWN")
|
|
end
|
|
end
|
|
|
|
local function FindRecentTrigger(guid, now)
|
|
local casts = BNP.unknownBeta.recentCasts
|
|
local i
|
|
for i = table.getn(casts), 1, -1 do
|
|
local cast = casts[i]
|
|
local age = now - cast.time
|
|
|
|
if age > 6 then
|
|
break
|
|
end
|
|
|
|
if cast.targetGUID and guid and cast.targetGUID == guid then
|
|
return cast, "TARGET"
|
|
end
|
|
|
|
-- Short global fallback catches effects such as traps where the placement
|
|
-- cast may not carry the eventual victim GUID.
|
|
if age <= 1.5 then
|
|
return cast, "RECENT"
|
|
end
|
|
end
|
|
|
|
return nil, nil
|
|
end
|
|
|
|
local function ScanUnit(unit, guid, now)
|
|
if not unit or not guid then return end
|
|
|
|
local seen = BNP.unknownBeta.seenByGUID[guid]
|
|
if not seen then
|
|
seen = {}
|
|
BNP.unknownBeta.seenByGUID[guid] = seen
|
|
end
|
|
|
|
local current = {}
|
|
local i
|
|
for i = 1, 32 do
|
|
local texture, stacks, dtype, auraSpellID = UnitDebuff(unit, i)
|
|
if not texture then break end
|
|
|
|
if auraSpellID then
|
|
current[auraSpellID] = true
|
|
|
|
if not seen[auraSpellID] and not IsExplicitKnownID(auraSpellID) then
|
|
local trigger, confidence = FindRecentTrigger(guid, now)
|
|
if trigger then
|
|
AddEntry(
|
|
"AURA",
|
|
auraSpellID,
|
|
SpellName(auraSpellID),
|
|
guid,
|
|
tostring(trigger.name) .. " [" .. tostring(trigger.spellID) .. "]",
|
|
confidence
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Keep only currently present aura IDs so a later re-application can be
|
|
-- detected again without growing this table forever.
|
|
BNP.unknownBeta.seenByGUID[guid] = current
|
|
end
|
|
|
|
|
|
-- Indirect Effect Audit: traps/procs may never expose a UnitDebuff ID.
|
|
BNP.unknownBeta.combat = BNP.unknownBeta.combat or {}
|
|
local EFFECT_WINDOW = 8.0
|
|
|
|
|
|
local function IsUsefulEffectLine(raw)
|
|
if not raw or raw == "" then return false end
|
|
local lower = string.lower(raw)
|
|
|
|
-- Ignore obvious player-action/noise lines.
|
|
if string.find(lower, "you cast ", 1, true) then return false end
|
|
if string.find(lower, "auto shot", 1, true) then return false end
|
|
if string.find(lower, "login", 1, true) then return false end
|
|
|
|
-- Keep lines that actually describe a harmful aura/effect or its periodic damage.
|
|
if string.find(lower, "afflicted by", 1, true) then return true end
|
|
if string.find(lower, "is afflicted", 1, true) then return true end
|
|
if string.find(lower, "is affected by", 1, true) then return true end
|
|
if string.find(lower, "suffers", 1, true) then return true end
|
|
if string.find(lower, "periodic", 1, true) then return true end
|
|
|
|
return false
|
|
end
|
|
|
|
local function AddCombatEffect(raw)
|
|
if not IsUsefulEffectLine(raw) then return end
|
|
local now=GetTime()
|
|
local casts=BNP.unknownBeta.recentCasts
|
|
local trigger=nil
|
|
local i
|
|
for i=table.getn(casts),1,-1 do
|
|
local c=casts[i]
|
|
if now-c.time > EFFECT_WINDOW then break end
|
|
if c.spellID ~= 75 then trigger=c break end
|
|
end
|
|
if not trigger then return end
|
|
if string.find(string.lower(raw),"auto shot",1,true) then return end
|
|
|
|
local key=tostring(trigger.spellID)..":"..raw
|
|
local old=BNP.unknownBeta.combat[key]
|
|
if old then old.count=old.count+1 return end
|
|
BNP.unknownBeta.combat[key]={
|
|
raw=raw, triggerID=trigger.spellID, triggerName=trigger.name, count=1
|
|
}
|
|
end
|
|
|
|
local combatFrame=CreateFrame("Frame")
|
|
local combatEvents={
|
|
"CHAT_MSG_COMBAT_SELF_HITS",
|
|
"CHAT_MSG_SPELL_SELF_DAMAGE",
|
|
"CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE",
|
|
"CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE",
|
|
}
|
|
local cei
|
|
for cei=1,table.getn(combatEvents) do combatFrame:RegisterEvent(combatEvents[cei]) end
|
|
combatFrame:SetScript("OnEvent",function() AddCombatEffect(arg1) end)
|
|
|
|
local castFrame = CreateFrame("Frame")
|
|
castFrame:RegisterEvent("UNIT_CASTEVENT")
|
|
castFrame:SetScript("OnEvent", function()
|
|
local playerGUID = GetPlayerGUID()
|
|
if not playerGUID or arg1 ~= playerGUID then return end
|
|
|
|
local eventType = arg3
|
|
if eventType ~= "CAST" and eventType ~= "CHANNEL" then return end
|
|
|
|
RememberCast(arg4, arg2, eventType)
|
|
end)
|
|
|
|
local scanner = CreateFrame("Frame")
|
|
local elapsed = 0
|
|
scanner:SetScript("OnUpdate", function()
|
|
elapsed = elapsed + arg1
|
|
if elapsed < 0.35 then return end
|
|
elapsed = 0
|
|
|
|
local now = GetTime()
|
|
if not BNP.unknownBeta.activeUntil or now > BNP.unknownBeta.activeUntil then
|
|
return
|
|
end
|
|
|
|
local targetGUID = GetUnitGUID("target")
|
|
if targetGUID then
|
|
ScanUnit("target", targetGUID, now)
|
|
end
|
|
|
|
local plate
|
|
for plate in pairs(BNP.plates) do
|
|
if plate:IsShown() and plate.GetName then
|
|
local token = plate:GetName(1)
|
|
if token then
|
|
local guid = GetUnitGUID(token)
|
|
if guid then
|
|
ScanUnit(token, guid, now)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
|
|
function BNP:PrintUnknownBeta(verbose)
|
|
local data = self.unknownBeta
|
|
self:Print("=== BNP Beta: Unknown Auras (" .. tostring(playerClass or "?") .. ") ===")
|
|
|
|
local auraCount = 0
|
|
local i
|
|
for i = 1, table.getn(data.order) do
|
|
local entry = data.entries[data.order[i]]
|
|
if entry and entry.kind == "AURA" then
|
|
auraCount = auraCount + 1
|
|
local line =
|
|
tostring(auraCount) ..
|
|
" | AURA | ID " .. tostring(entry.spellID) ..
|
|
" | " .. tostring(entry.name) ..
|
|
" | x" .. tostring(entry.count or 1)
|
|
|
|
if entry.confidence then
|
|
line = line .. " | " .. tostring(entry.confidence)
|
|
end
|
|
|
|
self:Print(line)
|
|
|
|
if entry.trigger then
|
|
self:Print(" Trigger: " .. tostring(entry.trigger))
|
|
end
|
|
end
|
|
end
|
|
|
|
local combatCount = 0
|
|
local ck
|
|
for ck in pairs(data.combat or {}) do combatCount = combatCount + 1 end
|
|
|
|
if combatCount > 0 then
|
|
self:Print("--- Indirect Effects ---")
|
|
local key, e
|
|
for key, e in pairs(data.combat) do
|
|
self:Print("EFFECT | Trigger " .. tostring(e.triggerName) .. " [" .. tostring(e.triggerID) .. "] | x" .. tostring(e.count))
|
|
self:Print(" " .. tostring(e.raw))
|
|
end
|
|
end
|
|
|
|
if auraCount == 0 and combatCount == 0 then
|
|
self:Print("No unknown negative auras/effects found since login.")
|
|
end
|
|
|
|
if verbose then
|
|
self:Print("--- Verbose: unknown player cast IDs ---")
|
|
local castCount = 0
|
|
for i = 1, table.getn(data.order) do
|
|
local entry = data.entries[data.order[i]]
|
|
if entry and entry.kind == "CAST" then
|
|
castCount = castCount + 1
|
|
self:Print(
|
|
tostring(castCount) ..
|
|
" | CAST | ID " .. tostring(entry.spellID) ..
|
|
" | " .. tostring(entry.name) ..
|
|
" | x" .. tostring(entry.count or 1)
|
|
)
|
|
end
|
|
end
|
|
if castCount == 0 then
|
|
self:Print("No unknown cast IDs.")
|
|
end
|
|
else
|
|
self:Print("A screenshot of this output is enough. Raw data: /bnp unknown verbose")
|
|
end
|
|
end
|
|
|
|
function BNP:ResetUnknownBeta()
|
|
self.unknownBeta.entries = {}
|
|
self.unknownBeta.order = {}
|
|
self.unknownBeta.recentCasts = {}
|
|
self.unknownBeta.seenByGUID = {}
|
|
self.unknownBeta.combat = {}
|
|
self.unknownBeta.activeUntil = 0
|
|
self:Print("Unknown spell/effect list cleared.")
|
|
end
|
|
|
|
|
|
-- Direct player spell collector --------------------------------------------
|
|
-- Captures successful player spell events that do not create a persistent
|
|
-- aura and therefore never appear in the normal unknown-aura collector.
|
|
|
|
local function GetLocalPlayerGUID()
|
|
local exists, guid = UnitExists("player")
|
|
if exists and guid then return guid end
|
|
return nil
|
|
end
|
|
|
|
local function RememberDirectEvent(eventType, spellID, targetGUID, extra)
|
|
if not spellID then return end
|
|
|
|
local name = nil
|
|
if SpellInfo then name = SpellInfo(spellID) end
|
|
name = name or ("Spell " .. tostring(spellID))
|
|
|
|
local key = tostring(eventType) .. ":" .. tostring(spellID)
|
|
local entry = BNP.unknownDirectEvents[key]
|
|
|
|
if not entry then
|
|
entry = {
|
|
eventType = eventType,
|
|
spellID = spellID,
|
|
name = name,
|
|
targetGUID = targetGUID,
|
|
count = 0,
|
|
extra = extra,
|
|
last = GetTime(),
|
|
}
|
|
BNP.unknownDirectEvents[key] = entry
|
|
end
|
|
|
|
entry.count = (entry.count or 0) + 1
|
|
entry.targetGUID = targetGUID or entry.targetGUID
|
|
entry.extra = extra or entry.extra
|
|
entry.last = GetTime()
|
|
end
|
|
|
|
local directFrame = CreateFrame("Frame")
|
|
directFrame:RegisterEvent("UNIT_CASTEVENT")
|
|
directFrame:SetScript("OnEvent", function()
|
|
local casterGUID = arg1
|
|
local targetGUID = arg2
|
|
local eventType = arg3
|
|
local spellID = arg4
|
|
local playerGUID = GetLocalPlayerGUID()
|
|
|
|
if not playerGUID or casterGUID ~= playerGUID then return end
|
|
if not spellID then return end
|
|
|
|
if eventType == "START" or eventType == "CAST" or eventType == "CHANNEL" then
|
|
RememberDirectEvent(eventType, spellID, targetGUID, arg5)
|
|
end
|
|
end)
|
|
|
|
function BNP:PrintUnknownDirectEvents()
|
|
local list = {}
|
|
local key, entry
|
|
|
|
for key, entry in pairs(self.unknownDirectEvents or {}) do
|
|
table.insert(list, entry)
|
|
end
|
|
|
|
table.sort(list, function(a, b)
|
|
return (a.last or 0) > (b.last or 0)
|
|
end)
|
|
|
|
if table.getn(list) == 0 then
|
|
self:Print("--- Direct Player Events: none captured ---")
|
|
return
|
|
end
|
|
|
|
self:Print("--- Direct Player Events ---")
|
|
|
|
local i, e
|
|
for i = 1, table.getn(list) do
|
|
e = list[i]
|
|
self:Print(
|
|
tostring(i) .. " | " ..
|
|
tostring(e.eventType) .. " | ID " ..
|
|
tostring(e.spellID) .. " | " ..
|
|
tostring(e.name) .. " | x" ..
|
|
tostring(e.count or 1)
|
|
)
|
|
end
|
|
end
|