Use Nampower more fully; fix timer frame leak and persist SS cooldown
- Add SPELL_GO_OTHER (spellId-filtered) to record soulstones cast by group members from exact caster/target GUIDs, silently, without changing the SSTracker addon-message protocol. - Classify curses/soulstone by spell name via GetSpellNameAndRankForId when available (covers all ranks); hard-coded IDs remain as fallback. - Register AURA_CAST_* events only while /ss diag aura is on. - Replace per-event CreateFrame debounce timers with reusable frames. - Persist own soulstone cooldown ready-time across reload/relog. - Add .gitattributes (LF). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019W3rwwdDZZ4CD4oJihtY5e
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
+132
-57
@@ -63,6 +63,28 @@ m.banish = {} -- { warlockName = raidIconIndex }
|
||||
m.shardCounts = {} -- { warlockName = count }
|
||||
m.ssCooldowns = {} -- { warlockName = readyAtEpochSeconds (0/nil = ready now) }
|
||||
m.history = {} -- { {text=..., time=...}, ... } session-only log
|
||||
|
||||
-- Reusable debounce: one persistent frame per key (frames are never freed in
|
||||
-- 1.12, so creating one per event leaked). Calling again with the same key
|
||||
-- restarts the delay.
|
||||
do
|
||||
local frames = {}
|
||||
function m.debounce(key, delay, fn)
|
||||
local f = frames[key]
|
||||
if not f then
|
||||
f = CreateFrame("Frame")
|
||||
frames[key] = f
|
||||
end
|
||||
local e = 0
|
||||
f:SetScript("OnUpdate", function()
|
||||
e = e + arg1
|
||||
if e >= delay then
|
||||
f:SetScript("OnUpdate", nil)
|
||||
fn()
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
m.frame = nil
|
||||
m.settings = nil -- settings panel frame
|
||||
m.locked = false
|
||||
@@ -596,6 +618,10 @@ local lastBroadcastReadyAt = nil
|
||||
function m.startOwnSoulstoneCooldown()
|
||||
local readyAt = time() + SOULSTONE_DURATION
|
||||
m.ssCooldowns[UnitName("player")] = readyAt
|
||||
if SoulstoneTrackerDB then
|
||||
SoulstoneTrackerDB.ssReadyAt = SoulstoneTrackerDB.ssReadyAt or {}
|
||||
SoulstoneTrackerDB.ssReadyAt[UnitName("player")] = readyAt
|
||||
end
|
||||
if not lastBroadcastReadyAt or math.abs((lastBroadcastReadyAt or 0) - readyAt) > 5 then
|
||||
lastBroadcastReadyAt = readyAt
|
||||
local channel = GetNumRaidMembers() > 0 and "RAID" or (GetNumPartyMembers() > 0 and "PARTY" or nil)
|
||||
@@ -1849,6 +1875,46 @@ local CURSE_SPELL_IDS = {
|
||||
[11717] = "CoR", -- Curse of Recklessness
|
||||
}
|
||||
|
||||
-- Classify a spellId as "SS" (any rank of Soulstone Resurrection), a curse
|
||||
-- short name (CoE/CoS/CoR/CoW), or nil. Uses nampower's spell name lookup
|
||||
-- when available so every rank is covered; falls back to the hard-coded IDs.
|
||||
do
|
||||
local cache = {}
|
||||
function m.classifySpell(id)
|
||||
if not id then return nil end
|
||||
local v = cache[id]
|
||||
if v == nil then
|
||||
v = false
|
||||
if id == SOULSTONE_SPELL_ID then
|
||||
v = "SS"
|
||||
elseif CURSE_SPELL_IDS[id] then
|
||||
v = CURSE_SPELL_IDS[id]
|
||||
elseif GetSpellNameAndRankForId then
|
||||
local name = GetSpellNameAndRankForId(id)
|
||||
if name == "Soulstone Resurrection" then
|
||||
v = "SS"
|
||||
elseif name and SHORT_NAMES[name] then
|
||||
v = SHORT_NAMES[name]
|
||||
end
|
||||
end
|
||||
cache[id] = v
|
||||
end
|
||||
return v or nil
|
||||
end
|
||||
end
|
||||
|
||||
function m.isInGroup(name)
|
||||
if not name then return false end
|
||||
if name == UnitName("player") then return true end
|
||||
for i = 1, GetNumRaidMembers() do
|
||||
if UnitName("raid"..i) == name then return true end
|
||||
end
|
||||
for i = 1, GetNumPartyMembers() do
|
||||
if UnitName("party"..i) == name then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function m.detectCurse(msg, caster)
|
||||
for fullName, shortName in pairs(SHORT_NAMES) do
|
||||
if string.find(msg, fullName) then
|
||||
@@ -1907,16 +1973,10 @@ function m.detectCurse(msg, caster)
|
||||
-- fixable in code) -- send a redundant follow-up after a
|
||||
-- short delay so a single lost packet doesn't leave
|
||||
-- other clients out of sync. Idempotent to repeat.
|
||||
local resendTimer = CreateFrame("Frame")
|
||||
local elapsed = 0
|
||||
resendTimer:SetScript("OnUpdate", function()
|
||||
elapsed = elapsed + arg1
|
||||
if elapsed >= 1.5 then
|
||||
resendTimer:SetScript("OnUpdate", nil)
|
||||
local ch = GetNumRaidMembers() > 0 and "RAID" or (GetNumPartyMembers() > 0 and "PARTY" or nil)
|
||||
if ch and m.curses[shortName] and m.curses[shortName].caster == casterName then
|
||||
SendAddonMessage(ADDON_MSG_PREFIX, "CURSE:"..shortName..":"..casterName, ch)
|
||||
end
|
||||
m.debounce("curseResend_"..shortName, 1.5, function()
|
||||
local ch = GetNumRaidMembers() > 0 and "RAID" or (GetNumPartyMembers() > 0 and "PARTY" or nil)
|
||||
if ch and m.curses[shortName] and m.curses[shortName].caster == casterName then
|
||||
SendAddonMessage(ADDON_MSG_PREFIX, "CURSE:"..shortName..":"..casterName, ch)
|
||||
end
|
||||
end)
|
||||
end
|
||||
@@ -2428,11 +2488,24 @@ eFrame:RegisterEvent("CHAT_MSG_ADDON")
|
||||
local hasNampower = (GetNampowerVersion ~= nil)
|
||||
if hasNampower then
|
||||
eFrame:RegisterEvent("SPELL_GO_SELF")
|
||||
eFrame:RegisterEvent("AURA_CAST_ON_OTHER")
|
||||
eFrame:RegisterEvent("AURA_CAST_ON_SELF")
|
||||
-- Unlike SPELL_GO_SELF, aura cast events default to OFF and must be
|
||||
-- explicitly enabled via this CVar.
|
||||
SetCVar("NP_EnableAuraCastEvents", 1)
|
||||
-- Others' casts: gives exact caster/target GUIDs for soulstones cast by
|
||||
-- group members who don't run this addon. Filtered by spellId first.
|
||||
eFrame:RegisterEvent("SPELL_GO_OTHER")
|
||||
-- AURA_CAST_* only matter for /ss diag aura, and (nampower 4.5+) merely
|
||||
-- registering an event enables its emission, so they are registered only
|
||||
-- while the diagnostic is on -- see m.setAuraDiag.
|
||||
function m.setAuraDiag(on)
|
||||
m.diagAura = on and true or false
|
||||
if on then
|
||||
eFrame:RegisterEvent("AURA_CAST_ON_OTHER")
|
||||
eFrame:RegisterEvent("AURA_CAST_ON_SELF")
|
||||
SetCVar("NP_EnableAuraCastEvents", 1)
|
||||
else
|
||||
eFrame:UnregisterEvent("AURA_CAST_ON_OTHER")
|
||||
eFrame:UnregisterEvent("AURA_CAST_ON_SELF")
|
||||
SetCVar("NP_EnableAuraCastEvents", 0)
|
||||
end
|
||||
end
|
||||
-- Fallback for auras without a spell effect component (which
|
||||
-- AURA_CAST_ON_OTHER/SELF won't catch) -- Soulstone Resurrection may be
|
||||
-- one of these, per nampower's own docs.
|
||||
@@ -2473,7 +2546,17 @@ eFrame:SetScript("OnEvent", function()
|
||||
-- session. If you're actually still on cooldown from before this
|
||||
-- reload, it'll incorrectly show ready until corrected by your next
|
||||
-- real cast -- an accepted limitation given no API can tell us.
|
||||
if playerIsWarlock() then m.ssCooldowns[UnitName("player")] = 0 end
|
||||
-- The ready-at time of our own last cast is persisted, so a reload
|
||||
-- or relog mid-cooldown now restores it (item cooldowns tick in
|
||||
-- real time, including while offline).
|
||||
if playerIsWarlock() then
|
||||
local saved = SoulstoneTrackerDB.ssReadyAt and SoulstoneTrackerDB.ssReadyAt[UnitName("player")]
|
||||
if saved and saved > time() then
|
||||
m.ssCooldowns[UnitName("player")] = saved
|
||||
else
|
||||
m.ssCooldowns[UnitName("player")] = 0
|
||||
end
|
||||
end
|
||||
if SoulstoneTrackerDB.hidden then m.frame:Hide() else m.frame:Show() end
|
||||
if SoulstoneTrackerDB.locked then m.locked = true m.frame.updateLock() end
|
||||
m.updateGroupVisibility()
|
||||
@@ -2507,16 +2590,8 @@ eFrame:SetScript("OnEvent", function()
|
||||
-- Silent unless it actually finds something, and coalesces rapid
|
||||
-- roster churn (e.g. several people joining a raid in quick
|
||||
-- succession) into a single scan instead of one per event.
|
||||
if m.__rosterScanTimer then
|
||||
m.__rosterScanTimer:SetScript("OnUpdate", nil)
|
||||
end
|
||||
local t = CreateFrame("Frame")
|
||||
m.__rosterScanTimer = t
|
||||
local e = 0
|
||||
t:SetScript("OnUpdate", function()
|
||||
e = e + arg1
|
||||
if e >= 2 then
|
||||
t:SetScript("OnUpdate", nil)
|
||||
m.debounce("rosterScan", 2, function()
|
||||
do
|
||||
m.scanForSoulstones(true)
|
||||
-- Ask others with the addon to resend their accurate data --
|
||||
-- this is what lets a scanned "Unknown"/estimated stone get
|
||||
@@ -2536,44 +2611,20 @@ eFrame:SetScript("OnEvent", function()
|
||||
-- from warlocks who don't have the addon (best-effort, no caster
|
||||
-- attribution possible). Debounced since target changes can fire
|
||||
-- rapidly while tab-targeting.
|
||||
if m.__targetScanTimer then
|
||||
m.__targetScanTimer:SetScript("OnUpdate", nil)
|
||||
end
|
||||
local t2 = CreateFrame("Frame")
|
||||
m.__targetScanTimer = t2
|
||||
local e2 = 0
|
||||
t2:SetScript("OnUpdate", function()
|
||||
e2 = e2 + arg1
|
||||
if e2 >= 0.5 then
|
||||
t2:SetScript("OnUpdate", nil)
|
||||
m.scanForCurses(true)
|
||||
end
|
||||
m.debounce("targetScan", 0.5, function()
|
||||
m.scanForCurses(true)
|
||||
end)
|
||||
|
||||
elseif event == "BAG_UPDATE" then
|
||||
-- Debounced since bag updates fire once per slot touched, and a
|
||||
-- single bag operation (looting, splitting stacks) can touch many.
|
||||
if m.__shardScanTimer then
|
||||
m.__shardScanTimer:SetScript("OnUpdate", nil)
|
||||
end
|
||||
local t3 = CreateFrame("Frame")
|
||||
m.__shardScanTimer = t3
|
||||
local e3 = 0
|
||||
t3:SetScript("OnUpdate", function()
|
||||
e3 = e3 + arg1
|
||||
if e3 >= 1 then
|
||||
t3:SetScript("OnUpdate", nil)
|
||||
m.updateAndBroadcastShards()
|
||||
end
|
||||
m.debounce("shardScan", 1, function()
|
||||
m.updateAndBroadcastShards()
|
||||
end)
|
||||
|
||||
elseif event == "PLAYER_ENTERING_WORLD" then
|
||||
if m.frame then
|
||||
local t = CreateFrame("Frame") local e = 0
|
||||
t:SetScript("OnUpdate", function()
|
||||
e = e + arg1
|
||||
if e >= 3 then t:SetScript("OnUpdate", nil) m.scanForSoulstones() end
|
||||
end)
|
||||
m.debounce("enterWorldScan", 3, function() m.scanForSoulstones() end)
|
||||
end
|
||||
|
||||
elseif event == "SPELLCAST_START" then
|
||||
@@ -2641,7 +2692,8 @@ eFrame:SetScript("OnEvent", function()
|
||||
elseif event == "SPELL_GO_SELF" then
|
||||
-- itemId, spellID, casterGuid, targetGuid, castFlags, numTargetsHit, numTargetsMissed
|
||||
local spellID = arg2
|
||||
local shortName = spellID and CURSE_SPELL_IDS[spellID]
|
||||
local shortName = m.classifySpell(spellID)
|
||||
if shortName == "SS" then shortName = nil end
|
||||
if shortName then
|
||||
-- Guaranteed self-scoped by nampower, so always safe to
|
||||
-- attribute directly to the local player.
|
||||
@@ -2649,6 +2701,25 @@ eFrame:SetScript("OnEvent", function()
|
||||
end
|
||||
m.updateSoulstoneCooldown()
|
||||
|
||||
elseif event == "SPELL_GO_OTHER" then
|
||||
-- itemId, spellId, casterGuid, targetGuid, castFlags, ...
|
||||
-- Cheap early filter: this fires for every spell cast near us.
|
||||
if m.classifySpell(arg2) == "SS" then
|
||||
local casterName = UnitName(arg3)
|
||||
local targetName = UnitName(arg4)
|
||||
if m.diagAura then
|
||||
say("|cff00ff00[DIAG]|r SPELL_GO_OTHER soulstone: spellId="..tostring(arg2).." caster="..tostring(casterName).." target="..tostring(targetName))
|
||||
end
|
||||
-- Only group members' stones; silent (the caster's own client
|
||||
-- is the one that broadcasts/announces, as before).
|
||||
if casterName and targetName and m.isInGroup(casterName) then
|
||||
local ex = m.stones[targetName]
|
||||
if not (ex and ex.caster == casterName and (time() - (ex.castTime or 0)) < 5) then
|
||||
m.addStone(targetName, casterName, time() + SOULSTONE_DURATION, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
elseif event == "AURA_CAST_ON_OTHER" or event == "AURA_CAST_ON_SELF" then
|
||||
-- spellId, casterGuid, targetGuid, effect, effectAuraName, effectAmplitude, effectMiscValue, durationMs, auraCapStatus
|
||||
if m.diagAura then
|
||||
@@ -2967,7 +3038,11 @@ SlashCmdList["SOULSTONETRACKER"] = function(args)
|
||||
local spacePos = string.find(cmd, " ", 1, true)
|
||||
local sub = spacePos and string.sub(cmd, spacePos + 1)
|
||||
if sub == "aura" then
|
||||
m.diagAura = not m.diagAura
|
||||
if m.setAuraDiag then
|
||||
m.setAuraDiag(not m.diagAura)
|
||||
else
|
||||
m.diagAura = not m.diagAura
|
||||
end
|
||||
say("Aura-cast diagnostic "..(m.diagAura and "|cff00ff00ENABLED|r" or "|cffff0000DISABLED|r")..". Cast/receive anything to see spellId/target/durationMs in chat.")
|
||||
else
|
||||
-- On-demand diagnostic: reveal exact string contents (including
|
||||
|
||||
Reference in New Issue
Block a user