WoWInterface SVN LibResInfo

Compare Revisions

  • This comparison shows the changes necessary to convert path
    /trunk
    from Rev 59 to Rev 60
    Reverse comparison

Rev 59 → Rev 60

LibResInfo-1.0/LibResInfo-1.0.toc
1,5 → 1,5
## Interface: 50200
## Version: v10
## Interface: 50400
## Version: 13
 
## Title: Lib: ResInfo-1.0
## Notes: Library to provide information about resurrections.
LibResInfo-1.0/LibResInfo-1.0/LibResInfo-1.0.lua
3,25 → 3,17
Library to provide information about resurrections in your group.
Copyright (c) 2012-2013 A. Kinley <addons@phanx.net>. All rights reserved.
See the accompanying README and LICENSE files for more information.
 
Things to do:
* Refactor messy and redundant sections.
* Fire ResCastStarted for units who die while Mass Res is casting.
* Fire ResCastStopped for units who res while Mass Res is casting.
 
Things that can't be done:
* Detect when a pending res is declined manually.
* Detect resurrections cast by or on players outside the group.
* Detect pending resurrections on units who were not in the group
when the res spell was cast.
------------------------------------------------------------------------
TODO:
* Handle Reincarnation
----------------------------------------------------------------------]]
 
local DEBUG_LEVEL = GetAddOnMetadata("LibResInfo-1.0", "Version") and 1 or 0
local DEBUG_FRAME = ChatFrame1
local DEBUG_FRAME = ChatFrame3
 
------------------------------------------------------------------------
 
local MAJOR, MINOR = "LibResInfo-1.0", 12
local MAJOR, MINOR = "LibResInfo-1.0", 13
assert(LibStub, MAJOR.." requires LibStub")
assert(LibStub("CallbackHandler-1.0"), MAJOR.." requires CallbackHandler-1.0")
local lib, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
29,40 → 21,43
 
------------------------------------------------------------------------
 
lib.eventFrame = lib.eventFrame or CreateFrame("Frame")
lib.callbacks = lib.callbacks or LibStub("CallbackHandler-1.0"):New(lib)
lib.guidFromUnit = lib.guidFromUnit or {} -- t[unit] = guid (faster than calling UnitGUID again)
lib.nameFromGUID = lib.nameFromGUID or {} -- t[guid] = name
lib.unitFromGUID = lib.unitFromGUID or {} -- t[guid] = unit
lib.castStart = lib.castStart or {} -- t[casterGUID] = startTime
lib.castEnd = lib.castEnd or {} -- t[casterGUID] = endTime
lib.castTarget = lib.castTarget or {} -- t[casterGUID] = targetGUID
lib.castMass = lib.castMass or {} -- t[casterGUID] = endTime
lib.resCasting = lib.resCasting or {} -- t[targetGUID] = numCasting
lib.resPending = lib.resPending or {} -- t[targetGUID] = expiryTime
lib.total = lib.total or {} -- casting = numCasting, pending = numPending
lib.ghost = lib.ghost or {} -- t[targetGUID] = true
local callbacks = lib.callbacks or LibStub("CallbackHandler-1.0"):New(lib)
local eventFrame = lib.eventFrame or CreateFrame("Frame")
 
local guidFromUnit = lib.guidFromUnit or {} -- t[unit] = guid -- table lookup is faster than calling UnitGUID
local nameFromGUID = lib.nameFromGUID or {} -- t[guid] = name
local unitFromGUID = lib.unitFromGUID or {} -- t[guid] = unit
 
local castingSingle = lib.castingSingle or {} -- t[casterGUID] = { startTime = <number>, endTime = <number>, target = <guid> }
local castingMass = lib.castingMass or {} -- t[casterGUID] = endTime
local hasPending = lib.hasPending or {} -- t[targetGUID] = endTime
 
local hasSoulstone = lib.hasSoulstone or {} -- t[targetGUID] = <boolean>
local isDead = lib.isDead or {} -- t[targetGUID] = <boolean>
local isGhost = lib.isGhost or {} -- t[targetGUID] = <boolean>
 
------------------------------------------------------------------------
 
local f, callbacks = lib.eventFrame, lib.callbacks
local guidFromUnit, nameFromGUID, unitFromGUID = lib.guidFromUnit, lib.nameFromGUID, lib.unitFromGUID
local castStart, castEnd, castTarget = lib.castStart, lib.castEnd, lib.castTarget
local castMass = lib.castMass
local resCasting, resPending = lib.resCasting, lib.resPending
local total, ghost = lib.total, lib.ghost
lib.callbacks = callbacks
lib.eventFrame = eventFrame
 
total.casting = total.casting or 0 -- # res spells being cast
total.pending = total.pending or 0 -- # resses available to take
lib.guidFromUnit = guidFromUnit
lib.nameFromGUID = nameFromGUID
lib.unitFromGUID = unitFromGUID
 
if DEBUG_LEVEL > 0 then
LibResInfo = lib
end
lib.castingSingle = castingSingle
lib.castingMass = castingMass
lib.hasPending = hasPending
 
lib.hasSoulstone = hasSoulstone
lib.isDead = isDead
lib.isGhost = isGhost
 
------------------------------------------------------------------------
 
local RESURRECT_PENDING_TIME = 60
local RECENTLY_MASS_RESURRECTED = GetSpellInfo(95223)
local SOULSTONE = GetSpellInfo(20707)
 
local resSpells = {
[2008] = GetSpellInfo(2008), -- Ancestral Spirit (shaman)
79,24 → 74,11
[50769] = GetSpellInfo(50769), -- Revive (druid)
[982] = GetSpellInfo(982), -- Revive Pet (hunter)
[20707] = GetSpellInfo(20707), -- Soulstone (warlock)
-- [83968] = GetSpellInfo(83968), -- Mass Resurrection
[83968] = GetSpellInfo(83968), -- Mass Resurrection
}
 
------------------------------------------------------------------------
 
f:SetScript("OnEvent", function(self, event, ...)
return self[event] and self[event](self, event, ...)
end)
 
f:RegisterEvent("GROUP_ROSTER_UPDATE")
f:RegisterEvent("INCOMING_RESURRECT_CHANGED")
f:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
f:RegisterEvent("UNIT_SPELLCAST_START")
f:RegisterEvent("UNIT_SPELLCAST_STOP")
f:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
 
------------------------------------------------------------------------
 
local function debug(level, text, ...)
if level <= DEBUG_LEVEL then
if select("#", ...) > 0 then
112,574 → 94,558
end
end
 
local newTable, remTable
do
local pool = {}
function newTable()
local t = next(pool)
if t then
pool[t] = nil
return t
end
return {}
end
function remTable(t)
pool[wipe(t)] = true
return nil
end
end
 
------------------------------------------------------------------------
 
lib.callbacksInUse = lib.callbacksInUse or {}
 
eventFrame:SetScript("OnEvent", function(self, event, ...)
return self[event] and self[event](self, event, ...)
end)
--[[
function callbacks:OnUsed(lib, callback)
if not next(lib.callbacksInUse) then
debug(1, "Used")]]
eventFrame:RegisterEvent("GROUP_ROSTER_UPDATE")
eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
eventFrame:RegisterEvent("INCOMING_RESURRECT_CHANGED")
eventFrame:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
eventFrame:RegisterEvent("UNIT_SPELLCAST_START")
eventFrame:RegisterEvent("UNIT_SPELLCAST_STOP")
eventFrame:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
-- Test for Soulstone support:
eventFrame:RegisterEvent("UNIT_AURA")
eventFrame:RegisterEvent("UNIT_CONNECTION")
eventFrame:RegisterEvent("UNIT_HEALTH")--[[
end
lib.callbacksInUse[callback] = true
end
 
function callbacks:OnUnused(lib, callback)
lib.callbacksInUse[callback] = nil
if not next(lib.callbacksInUse) then
debug(1, "Unused")
eventFrame:UnregisterAllEvents()
eventFrame:Hide()
wipe(guidFromUnit)
wipe(nameFromGUID)
wipe(unitFromGUID)
wipe(castingMass)
wipe(hasPending)
wipe(isGhost)
for caster, data in pairs(castingSingle) do
castingSingle[caster] = remTable(data)
end
end
end]]
 
------------------------------------------------------------------------
 
function lib.RegisterAllCallbacks(handler, method)
lib.RegisterCallback(handler, "LibResInfo_ResCastStarted", method)
lib.RegisterCallback(handler, "LibResInfo_ResCastCancelled", method)
lib.RegisterCallback(handler, "LibResInfo_ResCastFinished", method)
 
lib.RegisterCallback(handler, "LibResInfo_MassResStarted", method)
lib.RegisterCallback(handler, "LibResInfo_MassResCancelled", method)
lib.RegisterCallback(handler, "LibResInfo_MassResFinished", method)
 
lib.RegisterCallback(handler, "LibResInfo_ResPending", method)
lib.RegisterCallback(handler, "LibResInfo_ResUsed", method)
lib.RegisterCallback(handler, "LibResInfo_ResExpired", method)
end
 
------------------------------------------------------------------------
-- Returns information about the res being cast on the specified unit.
-- Arguments: unit (unitID or GUID)
-- Returns: resType (string), endTime (number), caster (unitID), casterGUID
-- * All returns are nil if no res is being cast on the unit.
-- * resType is one of:
-- - SELFRES if the unit has a Soulstone or other self-res ability available,
-- - PENDING if the unit already has a res available to accept, or
-- - CASTING if a res is being cast on the unit.
-- * caster and casterGUID are nil if the unit is being Mass Ressed.
 
function lib:UnitHasIncomingRes(unit)
if type(unit) == "string" then
local guid
if strmatch(unit, "^0x") then
guid = unit
unit = unitFromGUID[unit]
else
guid = UnitGUID(unit)
unit = unitFromGUID[guid]
if type(unit) ~= "string" then return end
local guid
if strmatch(unit, "^0x") then
guid = unit
unit = unitFromGUID[guid]
else
guid = UnitGUID(unit)
unit = unitFromGUID[guid]
end
if not guid or not unit or not UnitIsDeadOrGhost(unit) or not UnitIsConnected(unit) then
return
end
if hasPending[guid] then
local state = hasSoulstone[guid] and "SELFRES" or "PENDING"
debug(2, "UnitHasIncomingRes", nameFromGUID[guid], state)
return state, hasPending[guid]
end
 
local state, firstCaster, firstEnd
for caster, data in pairs(castingSingle) do
if data.target == guid then
if not firstEnd or data.endTime < firstEnd then
state, firstCaster, firstEnd = "CASTING", caster, data.endTime
end
end
if guid and unit and UnitIsDeadOrGhost(unit) and UnitIsConnected(unit) then
if resPending[guid] then
debug(2, "UnitHasIncomingRes", nameFromGUID[guid], "PENDING")
return "PENDING", resPending[guid]
else
local firstCaster, firstEnd
for casterGUID, targetGUID in pairs(castTarget) do
if targetGUID == guid then
local endTime = castEnd[casterGUID]
if endTime and (not firstEnd or endTime < firstEnd) then
firstCaster, firstEnd = casterGUID, endTime
end
end
end
if not UnitDebuff(unit, RECENTLY_MASS_RESURRECTED) then
for casterGUID in pairs(castMass) do
local endTime = castEnd[casterGUID]
if endTime and (not firstEnd or endTime < firstEnd) then
firstCaster, firstEnd = casterGUID, endTime
end
end
end
if firstCaster and firstEnd then
debug(2, "UnitHasIncomingRes", nameFromGUID[guid], "CASTING", nameFromGUID[firstCaster])
return "CASTING", firstEnd, unitFromGUID[firstCaster], firstCaster
end
end
if not UnitDebuff(unit, RECENTLY_MASS_RESURRECTED) then
for caster, endTime in pairs(castingMass) do
if not firstEnd or endTime < firstEnd then
state, firstCaster, firstEnd = "MASSRES", caster, endTime
end
end
debug(3, "UnitHasIncomingRes", nameFromGUID[guid], "nil")
end
if state and firstCaster and firstEnd then
debug(2, "UnitHasIncomingRes", nameFromGUID[guid], state, nameFromGUID[firstCaster])
return state, firstEnd, unitFromGUID[firstCaster], firstCaster
end
debug(3, "UnitHasIncomingRes", nameFromGUID[guid], "nil")
end
 
------------------------------------------------------------------------
-- Return information about the res being cast by the specified unit.
-- Arguments: unit (unitID or GUID)
-- Returns: endTime (number), target (unitID), targetGUID (guid), isFirst (boolean)
-- * all returns are nil if the unit is not casting a res
-- * target and targetGUID are nil if the unit is casting Mass Res
 
function lib:UnitIsCastingRes(unit)
if type(unit) == "string" then
local guid
if strmatch(unit, "^0x") then
guid = unit
unit = unitFromGUID[unit]
else
guid = UnitGUID(unit)
unit = unitFromGUID[guid]
if type(unit) ~= "string" then return end
local guid
if strmatch(unit, "^0x") then
guid = unit
unit = unitFromGUID[guid]
else
guid = UnitGUID(unit)
unit = unitFromGUID[guid]
end
if not guid or not unit then
return
end
 
local casting = castingSingle[guid]
if casting then
local endTime, target, isFirst = casting.endTime, casting.target, true
-- TODO: Handle edge case where this function is called in between the cast start and the target identification?
for caster, data in pairs(castingSingle) do
if data.target == target and data.endTime < endTime then
isFirst = false
break
end
end
if guid and unit then
debug(4, unit, guid, nameFromGUID[guid])
local isFirst, targetGUID, endTime = true, castTarget[guid], castEnd[guid]
if targetGUID then
if resPending[targetGUID] then
isFirst = nil
else
for k, v in pairs(castTarget) do
if k ~= guid and v == targetGUID and castEnd[k] < endTime then
isFirst = nil
break
end
end
for k in pairs(castMass) do
if k ~= guid and castEnd[k] < endTime then
isFirst = nil
break
end
end
end
debug(2, "UnitIsCastingRes", nameFromGUID[guid], nameFromGUID[targetGUID], isFirst)
return endTime, unitFromGUID[targetGUID], targetGUID, isFirst
debug(2, "UnitIsCastingRes", nameFromGUID[guid], "casting on", nameFromGUID[casting.target], isFirst and "(first)" or "(duplicate)")
return endTime, unitFromGUID[casting.target], casting.target, isFirst
end
 
elseif castMass[guid] then
for k in pairs(castMass) do
if not castEnd[k] then
castMass[k], castStart[k] = nil, nil
elseif k ~= guid and castEnd[k] < endTime then
isFirst = nil
break
end
end
debug(2, "UnitIsCastingRes", nameFromGUID[guid], "MASS", isFirst)
return endTime, nil, nil, isFirst
casting = castingMass[guid]
if casting then
local endTime, isFirst = casting, true
for caster, endTime2 in pairs(castingMass) do
if endTime2 < endTime then
isFirst = false
break
end
return debug(3, "UnitIsCastingRes", nameFromGUID[guid], "NIL")
end
return debug(3, "UnitIsCastingRes", unit, "INVALID")
debug(2, "UnitIsCastingRes", nameFromGUID[guid], "casting Mass Res", isFirst and "(first)" or "(duplicate)")
return endTime, nil, nil, isFirst
end
debug(3, "UnitIsCastingRes", tostring(unit), "INVALID", type(unit))
 
debug(3, "UnitIsCastingRes", nameFromGUID[guid], "nil")
end
 
------------------------------------------------------------------------
-- Handle group changes:
 
local unitFromGUID_old, guidFromUnit_old = {}, {}
local function AddUnit(unit)
local guid = UnitGUID(unit)
if not guid then return end
guidFromUnit[unit] = guid
nameFromGUID[guid] = UnitName(unit)
unitFromGUID[guid] = unit
-- Check for soulstones:
eventFrame:UNIT_AURA("AddUnit", unit)
end
 
function f:GROUP_ROSTER_UPDATE()
debug(3, "GROUP_ROSTER_UPDATE")
function eventFrame:GROUP_ROSTER_UPDATE(event)
debug(3, event)
 
wipe(guidFromUnit_old)
wipe(unitFromGUID_old)
for guid, unit in pairs(unitFromGUID) do
unitFromGUID_old[guid] = unit
unitFromGUID[guid] = nil
end
for unit, guid in pairs(guidFromUnit) do
guidFromUnit_old[unit] = guid
guidFromUnit[unit] = nil
end
 
-- Update guid <==> unit mappings:
wipe(guidFromUnit)
wipe(unitFromGUID)
if IsInRaid() then
debug(4, "raid")
for i = 1, GetNumGroupMembers() do
local unit = "raid"..i
local guid = UnitGUID(unit)
if guid then
guidFromUnit[unit] = guid
nameFromGUID[guid] = UnitName(unit)
unitFromGUID[guid] = unit
end
unit = "raidpet"..i
guid = UnitGUID(unit)
if guid then
guidFromUnit[unit] = guid
nameFromGUID[guid] = UnitName(unit)
unitFromGUID[guid] = unit
end
AddUnit("raid"..i)
AddUnit("raidpet"..i)
end
else
local unit, guid = "player", UnitGUID("player")
guidFromUnit[unit] = guid
nameFromGUID[guid] = UnitName(unit)
unitFromGUID[guid] = unit
 
unit, guid = "pet", UnitGUID("pet")
if guid then
guidFromUnit[unit] = guid
nameFromGUID[guid] = UnitName(unit)
unitFromGUID[guid] = unit
end
 
AddUnit("player")
AddUnit("pet")
if IsInGroup() then
debug(4, "party")
for i = 1, GetNumGroupMembers() - 1 do
unit = "party"..i
guid = UnitGUID(unit)
if guid then
guidFromUnit[unit] = guid
nameFromGUID[guid] = UnitName(unit)
unitFromGUID[guid] = unit
end
unit = "partypet"..i
guid = UnitGUID(unit)
if guid then
guidFromUnit[unit] = guid
nameFromGUID[guid] = UnitName(unit)
unitFromGUID[guid] = unit
end
AddUnit("party"..i)
AddUnit("partypet"..i)
end
else
debug(4, "solo")
end
end
 
-- Someone left the group while casting a res.
-- Find who they were casting on and cancel it.
for caster in pairs(castEnd) do
-- Remove data for single casters no longer in the group:
for caster, data in pairs(castingSingle) do
if not unitFromGUID[caster] then
debug(4, nameFromGUID[caster], "left while casting on", nameFromGUID[target])
local startTime, endTime = castStart[caster] or 0, castEnd[caster] or 0
castStart[caster], castEnd[caster] = nil, nil
local target = castTarget[caster]
if target then
if resCasting[target] and resCasting[target] > 1 then
resCasting[target] = resCasting[target] - 1
else
resCasting[target] = nil
end
castTarget[caster] = nil
debug(1, ">> ResCastCancelled", "on", nameFromGUID[target], "by", nameFromGUID[caster], resCasting[target])
callbacks:Fire("LibResInfo_ResCastCancelled", unitFromGUID[target], target, unitFromGUID_old[caster], caster)
elseif castMass[caster] then
castMass[caster] = nil
for guid, unit in pairs(guidFromUnit) do
if UnitIsDeadOrGhost(unit) and UnitIsConnected(unit) and not UnitDebuff(unit, RECENTLY_MASS_RESURRECTED) then
if resCasting[guid] and resCasting[guid] > 1 then
resCasting[guid] = resCasting[guid] - 1
else
resCasting[guid] = nil
end
debug(1, ">> ResCastCancelled", "on", nameFromGUID[caster], "by", nameFromGUID[guid])
callbacks:Fire("LibResInfo_ResCastCancelled", unitFromGUID[target], target, unitFromGUID_old[caster], caster)
end
end
end
local target = data.target
castingSingle[caster] = remTable(data)
debug(1, ">> ResCastCancelled on", nameFromGUID[target], "by", nameFromGUID[caster], "(caster left group)")
callbacks:Fire("LibResInfo_ResCastCancelled", unitFromGUID[target], target, nil, caster)
end
end
 
-- Someone left the group while a res was being cast on them.
-- Find the cast and cancel it.
for target, n in pairs(resCasting) do
-- Remove data for mass casters no longer in the group:
for caster in pairs(castingMass) do
if not unitFromGUID[caster] then
castingMass[caster] = nil
debug(1, ">> MassResCancelled by", nameFromGUID[caster], "(left group)")
callbacks:Fire("LibResInfo_MassResCancelled", nil, caster)
end
end
 
-- Remove data for targets no longer in the group:
for caster, data in pairs(castingSingle) do
local target = data.target
if not unitFromGUID[target] then
debug(4, nameFromGUID[target], "left while incoming.")
for caster, castertarget in pairs(castTarget) do
if target == castertarget then
resCasting[target] = nil
castTarget[caster], castStart[caster], castEnd[caster] = nil, nil, nil
debug(1, ">> ResCastCancelled", "on", nameFromGUID[target], "by", nameFromGUID[caster])
callbacks:Fire("LibResInfo_ResCastCancelled", unitFromGUID[target], target, unitFromGUID_old[caster], caster)
end
end
for caster in pairs(castMass) do
debug(1, ">> ResCastCancelled", "on", nameFromGUID[target], "by", nameFromGUID[caster])
callbacks:Fire("LibResInfo_ResCastCancelled", unitFromGUID[target], target, unitFromGUID_old[caster], caster)
end
castingSingle[caster] = remTable(data)
-- TODO: Is this callback needed, or will the cast cancel on its own?
debug(1, ">> ResCastCancelled on", nameFromGUID[target], "by", nameFromGUID[caster], "(target left group)")
callbacks:Fire("LibResInfo_ResCastCancelled", nil, target, unitFromGUID[caster], caster)
end
end
 
-- Someone left the group when they had a res available.
-- Find the res and cancel it.
for target in pairs(resPending) do
-- Remove data for waiters no longer in the group:
for target in pairs(hasPending) do
if not unitFromGUID[target] then
debug(3, nameFromGUID[target], "left while pending.")
resPending[target] = nil
total.pending = total.pending - 1
debug(1, ">> ResExpired", "on", nameFromGUID[target])
callbacks:Fire("LibResInfo_ResExpired", unitFromGUID_old[target], target)
hasPending[target] = nil
debug(1, ">> ResExpired on", nameFromGUID[target], "(left group)")
callbacks:Fire("LibResInfo_ResExpired", nil, target)
end
end
 
-- Check events
debug(3, "# pending:", total.pending)
if total.pending == 0 then
self:UnregisterEvent("UNIT_HEALTH")
-- Unregister unit events and stop the timer if there are no waiters:
if not next(hasPending) then
--debug(3, "Nobody pending, unregistering unit state events")
--self:UnregisterEvent("UNIT_AURA")
--self:UnregisterEvent("UNIT_CONNECTION")
--self:UnregisterEvent("UNIT_HEALTH")
self:Hide()
end
 
local most = 0
for _, n in pairs(resCasting) do
most = max(n, most)
end
debug(3, "highest # casting:", most)
if most < 2 then
-- Unregister CLEU if there are no casts:
if not next(castingSingle) and not next(castingMass) then
debug(3, "Nobody casting, unregistering CLEU")
self:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
end
 
-- Remove names no longer in the group:
for guid, name in pairs(nameFromGUID) do
if not unitFromGUID[guid] then
debug(4, name, "is no longer in the group")
nameFromGUID[guid] = nil
end
end
end
 
eventFrame.PLAYER_ENTERING_WORLD = eventFrame.GROUP_ROSTER_UPDATE
 
------------------------------------------------------------------------
 
function f:INCOMING_RESURRECT_CHANGED(event, unit)
if guidFromUnit[unit] then
local guid = UnitGUID(unit)
local hasRes = UnitHasIncomingResurrection(unit)
debug(3, "INCOMING_RESURRECT_CHANGED", nameFromGUID[guid], hasRes)
function eventFrame:INCOMING_RESURRECT_CHANGED(event, unit)
local guid = guidFromUnit[unit]
if not guid then return end
 
if hasRes then
local now, found = GetTime()
for casterGUID, startTime in pairs(castStart) do
if startTime - now < 10 and not castTarget[casterGUID] and not castMass[casterGUID] then -- time in ms between cast start and res gain
if not castMass[casterGUID] then
castTarget[casterGUID] = guid
end
resCasting[guid] = 1 + (resCasting[guid] or 0)
local casterUnit = unitFromGUID[casterGUID]
debug(1, ">> ResCastStarted", "on", nameFromGUID[guid], "by", nameFromGUID[casterGUID], "DIFF", floor(now - startTime * 100) / 100, "#", resCasting[guid])
callbacks:Fire("LibResInfo_ResCastStarted", unit, guid, casterUnit, casterGUID, castEnd[casterGUID])
found = true
end
local hasRes = UnitHasIncomingResurrection(unit)
debug(3, event, nameFromGUID[guid], hasRes)
 
if hasRes then
-- Unit has a res incoming. Match it to a spell.
local now = GetTime()
for caster, data in pairs(castingSingle) do
if not data.target and data.startTime - now < 10 then
-- Found it!
data.target = guid
debug(1, ">> ResCastStarted on", nameFromGUID[guid], "by", nameFromGUID[caster], "in", event)
callbacks:Fire("LibResInfo_ResCastStarted", unit, guid, caster, unitFromGUID[caster], data.endTime)
break
end
if not found then
debug(4, "No new cast found.")
end
for casterGUID, endTime in pairs(castEnd) do
if endTime - now < 10 and not castStart[casterGUID] and not castMass[casterGUID] then -- time in ms between cast end and res loss
local targetGUID = castTarget[casterGUID]
if targetGUID == guid then
local casterUnit = unitFromGUID[casterGUID]
castTarget[casterGUID], castEnd[casterGUID] = nil, nil
debug(1, ">> ResCastFinished", "on",nameFromGUID[guid], "by", nameFromGUID[casterGUID], "#", resCasting[guid], "hasRes")
callbacks:Fire("LibResInfo_ResCastFinished", unit, guid, casterUnit, casterGUID)
end
total.casting = total.casting + 1
debug(4, n, "casting, waiting for CLEU.")
end
-- TODO: Why was I searching for finished casts here???
else
-- Check if unit previously had any resses.
for caster, data in pairs(castingSingle) do
if data.target == guid then
debug(4, nameFromGUID[caster], "was casting...")
if data.startTime then
debug(4, "...and stopped.")
castingSingle[caster] = remTable(data)
debug(1, ">> ResCastCancelled", "on", nameFromGUID[guid], "by", nameFromGUID[casterGUID], "in", event)
callbacks:Fire("LibResInfo_ResCastCancelled", unit, guid, unitFromGUID[casterGUID], casterGUID)
else
debug(4, "...and finished.")
castingSingle[caster] = remTable(data)
hasPending[guid] = nil
debug(1, ">> ResCastFinished", "on", nameFromGUID[guid], "by", nameFromGUID[casterGUID], "in", event)
callbacks:Fire("LibResInfo_ResCastFinished", unit, guid, unitFromGUID[casterGUID], casterGUID)
self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
end
end
 
elseif resCasting[guid] then
-- had a res, doesn't anymore
local finished, stopped
for casterGUID, targetGUID in pairs(castTarget) do
if targetGUID == guid then
debug(4, nameFromGUID[casterGUID], "was casting...")
if castStart[casterGUID] then
debug(4, "...and stopped.")
castStart[casterGUID], castEnd[casterGUID], castTarget[casterGUID] = nil, nil, nil
 
debug(1, ">> ResCastCancelled", "on", nameFromGUID[guid], "by", nameFromGUID[casterGUID], "#", resCasting[guid] - 1)
resCasting[guid] = nil
callbacks:Fire("LibResInfo_ResCastCancelled", unit, guid, unitFromGUID[casterGUID], casterGUID)
else
debug(4, "...and finished.")
castStart[casterGUID], castEnd[casterGUID], castTarget[casterGUID] = nil, nil, nil
 
debug(1, ">> ResCastFinished", "on", nameFromGUID[guid], "by", nameFromGUID[casterGUID], "#", resCasting[guid] - 1, "resCasting")
callbacks:Fire("LibResInfo_ResCastFinished", unit, guid, unitFromGUID[casterGUID], casterGUID)
 
total.casting = total.casting + 1
debug(3, n, "casting, waiting for CLEU")
self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
end
break
end
end
end
end
end
 
------------------------------------------------------------------------
 
function f:UNIT_SPELLCAST_START(event, unit, spellName, _, _, spellID)
function eventFrame:UNIT_SPELLCAST_START(event, unit, spellName, _, _, spellID)
if not resSpells[spellID] then return end
local guid = guidFromUnit[unit]
if not guid then return end
debug(3, event, nameFromGUID[guid], "casting", spellName)
 
if spellID == 83968 then -- Mass Resurrection
local _, _, _, _, startTime, endTime = UnitCastingInfo(unit)
castMass[guid] = endTime
debug(1, ">> MassResStarted", "by", nameFromGUID[guid])
callbacks:Fire("LibResInfo_MassResStarted", nil, nil, unit, guid, endTime / 1000)
local _, _, _, _, startTime, endTime = UnitCastingInfo(unit)
 
elseif resSpells[spellID] then
local _, _, _, _, startTime, endTime = UnitCastingInfo(unit)
debug(3, event, "=>", nameFromGUID[guid], "=>", spellName)
castStart[guid] = startTime / 1000
castEnd[guid] = endTime / 1000
if spellID == 83968 then -- Mass Resurrection
castingMass[guid] = endTime / 1000
debug(1, ">> MassResStarted", nameFromGUID[guid])
callbacks:Fire("LibResInfo_MassResStarted", unit, guid)
return
end
 
local data = newTable()
data.startTime = startTime / 1000
data.endTime = endTime / 1000
castingSingle[guid] = data
end
 
function f:UNIT_SPELLCAST_SUCCEEDED(event, unit, spellName, _, _, spellID)
function eventFrame:UNIT_SPELLCAST_SUCCEEDED(event, unit, spellName, _, _, spellID)
if not resSpells[spellID] then return end
local guid = guidFromUnit[unit]
if not guid then return end
 
if castMass[guid] then -- Mass Resurrection
debug(3, event, nameFromGUID[guid], "finished", spellName)
 
for targetGUID, targetUnit in pairs(unitFromGUID) do
if UnitIsDeadOrGhost(targetUnit) and UnitIsConnected(targetUnit) and not UnitDebuff(targetUnit, RECENTLY_MASS_RESURRECTED) then
debug(1, ">> ResCastFinished", "on", nameFromGUID[targetGUID], "by", nameFromGUID[guid])
callbacks:Fire("LibResInfo_ResCastFinished", targetUnit, targetGUID, unit, guid)
n = n + 1
end
end
if spellID == 83968 then -- Mass Resurrection
castingMass[guid] = nil
debug(1, ">> MassResFinished", nameFromGUID[guid])
callbacks:Fire("LibResInfo_MassResFinished", unit, guid)
self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
return
end
 
if n > 0 then
debug(4, n, "casting, waiting for CLEU")
self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
local data = castingSingle[guid]
if data then -- No START event for instant cast spells.
local target = data.target
castingSingle[guid] = remTable(data) -- TODO: Why was I removing only startTime here and waiting for IRC?
if not target then
-- Probably Soulstone precast on a live target.
return
end
debug(1, ">> ResCastFinished", "on", nameFromGUID[target], "by", nameFromGUID[guid], "in", event)
callbacks:Fire("LibResInfo_ResCastFinished", unitFromGUID[target], target, unit, guid)
end
if guidFromUnit[unit] and resSpells[spellID] then
local guid = UnitGUID(unit)
if castStart[guid] then
debug(3, event, "=>", nameFromGUID[guid], "=>", spellName)
castStart[guid] = nil
else
-- instant cast
resCasting[guid] = 1 + (resCasting[guid] or 1)
debug(3, event, "=>", nameFromGUID[guid], "=>", spellName)
debug(3, "instant cast, waiting for CLEU")
self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
end
end
 
debug(3, "Registering CLEU")
self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
end
 
function f:UNIT_SPELLCAST_STOP(event, unit, spellName, _, _, spellID)
if guidFromUnit[unit] and resSpells[spellID] then
local guid = UnitGUID(unit)
if castStart[guid] then
debug(3, event, "=>", nameFromGUID[guid], "=>", spellName)
local targetGUID = castTarget[guid]
if targetGUID then
local n = resCasting[targetGUID]
if n and n > 1 then
-- someone else is still casting, send cancellation here
local targetUnit = unitFromGUID[targetGUID]
castStart[guid], castEnd[guid], castTarget[guid] = nil, nil
resCasting[targetGUID] = n - 1
debug(1, ">> ResCastCancelled", "on", nameFromGUID[targetGUID], "by", nameFromGUID[guid])
callbacks:Fire("LibResInfo_ResCastCancelled", targetUnit, targetGUID, unit, guid)
else
debug(4, "Waiting for IRC.")
end
function eventFrame:UNIT_SPELLCAST_STOP(event, unit, spellName, _, _, spellID)
if not resSpells[spellID] then return end
local guid = guidFromUnit[unit]
if not guid then return end
 
elseif castMass[guid] then -- Mass Resurrection
castStart[guid], castEnd[guid], castMass[guid] = nil, nil, nil
for targetGUID, targetUnit in pairs(unitFromGUID) do
if UnitIsDeadOrGhost(targetUnit) and UnitIsConnected(targetUnit) and not UnitDebuff(targetUnit, RECENTLY_MASS_RESURRECTED) then
local n = resCasting[targetGUID]
if n and n > 1 then
resCasting[targetGUID] = n - 1
else
resCasting[targetGUID] = nil
end
debug(1, ">> ResCastCancelled", "on", nameFromGUID[targetGUID], "by", nameFromGUID[guid])
callbacks:Fire("LibResInfo_ResCastCancelled", targetUnit, targetGUID, unit, guid)
end
end
debug(3, event, nameFromGUID[guid], "stopped", spellName)
 
if spellID == 83968 and castingMass[guid] then -- Mass Resurrection
castingMass[guid] = nil
debug(1, ">> MassResCancelled", nameFromGUID[guid])
callbacks:Fire("LibResInfo_MassResCancelled", unit, guid)
else
local data = castingSingle[guid]
if data then
local target = data.target
castingSingle[guid] = remTable(data)
if not target then
-- Probably Soulstone precast on a live target.
return
end
debug(1, ">> ResCastCancelled", "on", nameFromGUID[target], "by", nameFromGUID[guid])
callbacks:Fire("LibResInfo_ResCastCancelled", unitFromGUID[target], target, unit, guid)
end
end
 
-- Unregister CLEU if there are no casts:
if not next(castingSingle) and not next(castingMass) then
debug(3, "Nobody casting, unregistering CLEU")
self:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
end
end
 
f.UNIT_SPELLCAST_INTERRUPTED = f.UNIT_SPELLCAST_STOP
eventFrame.UNIT_SPELLCAST_INTERRUPTED = eventFrame.UNIT_SPELLCAST_STOP
 
------------------------------------------------------------------------
 
function f:COMBAT_LOG_EVENT_UNFILTERED(event, timestamp, combatEvent, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, spellID, spellName, spellSchool)
if combatEvent == "SPELL_RESURRECT" then
local destUnit = unitFromGUID[destGUID]
if destUnit then
local now = GetTime()
debug(3, combatEvent, "=>", sourceName, "=>", spellName, "=>", destName)
function eventFrame:COMBAT_LOG_EVENT_UNFILTERED(event, timestamp, combatEvent, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, spellID, spellName, spellSchool)
if event ~= "SPELL_RESURRECT" then return end
 
local casting = resCasting[destGUID]
if casting or castMass[sourceGUID] then
debug(4, "yes")
local destUnit = unitFromGUID[destGUID]
if not destUnit then return end
debug(3, combatEvent, "on", destName, "by", sourceName)
 
if casting and casting > 1 then
resCasting[destGUID] = casting - 1
else
resCasting[destGUID] = nil
end
local now = GetTime()
local endTime = now + RESURRECT_PENDING_TIME
 
local new = not resPending[destGUID]
resPending[destGUID] = now + RESURRECT_PENDING_TIME
hasPending[guid] = endTime
--isGhost[guid] = UnitIsGhost(destUnit)
 
total.casting = total.casting - 1
if total.casting == 0 then
debug(4, "0 casting, unregister CLEU")
self:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
end
--debug(3, "Registering unit state events")
--self:RegisterEvent("UNIT_AURA")
--self:RegisterEvent("UNIT_CONNECTION")
--self:RegisterEvent("UNIT_HEALTH")
self:Show()
 
if castMass[sourceGUID] then
local n = 0
for guid, unit in pairs(unitFromGUID) do
if guid ~= destGUID and (not resPending[guid] or resPending[guid] - now > RESURRECT_PENDING_TIME) and UnitIsDeadOrGhost(unit) and UnitIsConnected(unit) and not UnitDebuff(unit, RECENTLY_MASS_RESURRECTED) then
n = n + 1
end
end
if n == 0 then
castMass[sourceGUID] = nil
end
debug(4, n, "mass targets left")
end
debug(1, ">> ResPending", "on", strmatch(destName, "[^%-]+"), "by", strmatch(sourceName, "[^%-]+"))
callbacks:Fire("LibResInfo_ResPending", destUnit, destGUID, endTime)
 
ghost[destGUID] = UnitIsGhost(destUnit)
 
if new then
total.pending = total.pending + 1
debug(4, total.pending, "pending, start timer, register UNIT_HEALTH/AURA")
self:RegisterEvent("UNIT_HEALTH")
self:RegisterEvent("UNIT_AURA")
self:Show()
else
debug(4, total.pending, "pending, timer already running")
end
 
debug(1, ">> ResPending", "on", strmatch(destName, "[^%-]+"), "by", strmatch(sourceName, "[^%-]+"))
callbacks:Fire("LibResInfo_ResPending", destUnit, destGUID, now + RESURRECT_PENDING_TIME)
end
end
-- Unregister CLEU if there are no casts:
if not next(castingSingle) and not next(castingMass) then
-- TODO: Keep track of number of instant casts?
-- Seems unlikely that multiple casts would end so close together that this would be an issue.
debug(3, "Nobody casting, unregistering CLEU")
self:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
end
end
 
------------------------------------------------------------------------
 
function f:UNIT_HEALTH(event, unit)
function eventFrame:UNIT_AURA(event, unit)
local guid = guidFromUnit[unit]
if guidFromUnit[unit] then
if resPending[guid] then
debug(3, event, nameFromGUID[guid], "Dead", UnitIsDead(unit), "Ghost", UnitIsGhost(unit), "Offline", not UnitIsConnected(unit))
if not guid then return end
--debug(5, event, unit)
 
local callback
if not isDead[guid] then
local stoned = UnitAura(unit, SOULSTONE)
if stoned ~= hasSoulstone[guid] then
hasSoulstone[guid] = stoned
debug(2, nameFromGUID[guid], stoned and "gained" or "lost", SOULSTONE)
end
return
end
 
if not UnitIsConnected(unit) then
callback = "LibResInfo_ResExpired"
if UnitIsGhost(unit) and not isGhost[guid] then
isGhost[guid] = true
if hasPending[guid] then
hasPending[guid] = nil
debug(1, ">> ResExpired", nameFromGUID[guid], "(released)")
callbacks:Fire("LibResInfo_ResExpired", unit, guid)
end
end
--[[
if not next(hasPending) then
debug(4, "Unregistering unit state events")
--self:UnregisterEvent("UNIT_AURA")
--self:UnregisterEvent("UNIT_CONNECTION")
--self:UnregisterEvent("UNIT_HEALTH")
end]]
end
 
elseif UnitIsGhost(unit) and not ghost[guid] then
ghost[guid] = true
callback = "LibResInfo_ResExpired"
function eventFrame:UNIT_CONNECTION(unit)
local guid = guidFromUnit[unit]
if not guid then return end
debug(4, event, unit)
 
elseif not UnitIsDead(unit) then
ghost[guid] = nil
callback = "LibResInfo_ResUsed"
end
if hasPending[unit] and not UnitIsConnected(unit) then
hasPending[guid] = nil
debug(1, ">> ResExpired", nameFromGUID[guid], "(offline)")
callbacks:Fire("LibResInfo_ResExpired", unit, guid)
end
end
 
if callback then
resPending[guid] = nil
debug(1, ">>", callback, nameFromGUID[guid])
callbacks:Fire(callback, unit, guid)
function eventFrame:UNIT_HEALTH(unit)
local guid = guidFromUnit[unit]
if not guid then return end
--debug(5, event, unit)
 
total.pending = total.pending - 1
if total.pending == 0 then
debug(4, "0 pending, unregister UNIT_HEALTH/AURA")
self:UnregisterEvent("UNIT_AURA")
self:UnregisterEvent("UNIT_HEALTH")
end
end
local dead = UnitIsDead(unit)
if dead and not isDead[guid] then
isDead[guid] = true
if hasSoulstone[guid] then
local endTime = GetTime() + RESURRECT_PENDING_TIME
hasPending[guid] = endTime
debug(1, ">> ResPending", nameFromGUID[guid], SOULSTONE)
callbacks:Fire("LibResInfo_ResPending", unit, guid, endTime, true)
end
 
elseif not dead then
isDead[guid] = nil
if hasPending[guid] then
isGhost[guid] = nil
hasPending[guid] = nil
debug(1, ">> ResUsed", nameFromGUID[guid])
callbacks:Fire("LibResInfo_ResUsed", unit, guid)
end
end
end
 
f.UNIT_AURA = f.UNIT_HEALTH
 
------------------------------------------------------------------------
 
f:Hide()
eventFrame:Hide()
 
local timer = 0
local INTERVAL = 0.5
f:SetScript("OnUpdate", function(self, elapsed)
local timer, INTERVAL = 0, 0.5
eventFrame:SetScript("OnUpdate", function(self, elapsed)
timer = timer + elapsed
if timer >= INTERVAL then
debug(6, "timer update")
debug(6, "Timer update")
if not next(hasPending) then
--debug(4, "Nobody pending, unregistering unit state events")
--self:UnregisterEvent("UNIT_AURA")
--self:UnregisterEvent("UNIT_CONNECTION")
--self:UnregisterEvent("UNIT_HEALTH")
return self:Hide()
end
local now = GetTime()
for guid, expiry in pairs(resPending) do
if expiry - now < INTERVAL then -- will expire before next update
for guid, endTime in pairs(hasPending) do
if endTime - now < INTERVAL then -- It will expire before the next update.
local unit = unitFromGUID[guid]
resPending[guid] = nil
hasPending[guid] = nil
debug(1, ">> ResExpired", nameFromGUID[guid])
callbacks:Fire("LibResInfo_ResExpired", unit, guid, true)
 
total.pending = total.pending - 1
if total.pending == 0 then
self:UnregisterEvent("UNIT_HEALTH")
self:Hide()
end
end
end
timer = 0
end
end)
 
f:SetScript("OnShow", function()
debug(4, "timer start")
eventFrame:SetScript("OnShow", function()
debug(4, "Timer start")
end)
 
f:SetScript("OnHide", function()
debug(4, "timer stop")
eventFrame:SetScript("OnHide", function()
debug(4, "Timer stop")
timer = 0
end)
 
688,23 → 654,22
SLASH_LIBRESINFO1 = "/lri"
SlashCmdList.LIBRESINFO = function(input)
input = gsub(input, "[^A-Za-z0-9]", "")
if strlen(input) > 0 then
if strmatch(input, "%D") then
local f = _G[input]
if type(f) == "table" and type(f.AddMessage) == "function" then
DEBUG_FRAME = f
debug(0, "Debug frame set to", input)
else
debug(0, input, "is not a valid debug output frame!")
end
if strlen(input) < 1 then return end
if strmatch(input, "%D") then
local f = _G[input]
if type(f) == "table" and type(f.AddMessage) == "function" then
DEBUG_FRAME = f
debug(0, "Debug frame set to", input)
else
local v = tonumber(input)
if v and v >= 0 then
DEBUG_LEVEL = v
debug(0, "Debug level set to", input)
else
debug(0, input, "is not a valid debug level!")
end
debug(0, input, "is not a valid debug output frame!")
end
else
local v = tonumber(input)
if v and v >= 0 then
DEBUG_LEVEL = v
debug(0, "Debug level set to", input)
else
debug(0, input, "is not a valid debug level!")
end
end
end
\ No newline at end of file