WoWInterface SVN RaidWatch2

[/] [trunk/] [RaidWatch_Core/] [BasePlugins/] [CombatScanner.lua] - Rev 22

Compare with Previous | Blame | View Log

--[[ 
Combat Scanner
Handles:
- Check for boss engage
- Combat log events
- Yells, emotes etc.
]]--

------------------------------------
-- Create plugin

local RW = LibStub("AceAddon-3.0"):GetAddon("Raid Watch")
local plugin = RW:NewModule("CombatScanner", "AceEvent-3.0", "AceTimer-3.0")
if not plugin then return end

RW.Combat = plugin

local pName, pClass, party
local modEndTime = {}
local lastWarned = 0
local lastCombatlogEvent = 0
local UnitTracking = {}
local BossDebugging = false

function plugin:OnInitialize()
        self.Running = {}
        self.CombatWatcher = {}
        
        party = RW.Party
        pName = UnitName("player")
        pClass = select(2, UnitClass("player"))
        
        -- Start tracking incase you in the zone at login in which case the boss mid sends the message before the plguin enables for some reason
        RW.RegisterCallback(self, "CombatTrackingRegistered")
end

function plugin:OnEnable()
        RW.RegisterCallback(self, "TestEvent")
        RW.RegisterCallback(self, "BossEngaged")
        RW.RegisterCallback(self, "BossDisengaged")
        
        if not RWDEBUG then
                RW.RegisterCallback(self, "EnteringEnabledZone")
                RW.RegisterCallback(self, "LeavingEnabledZone")
        else
                self:EnteringEnabledZone()
        end
        
        -- Call functions once in case logging in in an 
        -- enabled zone or if in combat allready when logging in
        self:PLAYER_REGEN_DISABLED()
end

function plugin:OnDisable()
        wipe(self.Running)
end

-- Scanning function to run while in combat to detect if a boss gets engaged
-- Returns the bossmod and the cID of the boss/creature that initated bossfight
local function scanEngage(self)
        for bossName, boss in RW:IterateBosses() do
                if boss.DB and boss.DB.enabled then
                        if boss.mobTriggers then
                                for _, player in pairs(party.group) do
                                        for cID, _ in pairs(boss.mobTriggers) do
                                                if cID == boss:CUIDFromUID(player.id.."target") and UnitAffectingCombat(player.id.."target") then
                                                        
                                                        if boss.OnPreEngage and not boss:OnPreEngage() then
                                                                return
                                                        else
                                                                return boss, cID
                                                        end
                                                end
                                        end
                                end
                        end
                end
        end
end

-- Scanning function while in combat with a boss to check if its a wipe
-- Triggers when all in the raid is dead or when released to ghost.
-- Wont trigger if the WinTrigger of the boss is not combat based, ie. a yell or so
local function scanWipe(self)
        local wipe = true
        for name, player in pairs(party.group) do
                if UnitAffectingCombat(player.id) and not UnitIsDeadOrGhost(player.id) then                     
                        wipe = false
                        break
                end
        end     
        
        if not wipe then
                self:ScheduleTimer(scanWipe, 3, self)
        else
                for boss in pairs(self.Running) do
                        if boss.OnPreEnd and not boss:OnPreEnd() then
                                self:ScheduleTimer(scanWipe, 3, self)
                                return
                        end
                        if GetTime() - boss.engageTime > (boss.endDelay or 0) and not boss.hold then
                                RW.Callbacks:Fire("BossDisengaged", boss, boss.winTrigger ~= "combat" and true, "combat")
                        else
                                self:ScheduleTimer(scanWipe, 3, self)
                        end
                end
        end
        
        local runningCount = 0
        for boss in pairs(self.Running) do
                runningCount = runningCount + 1
        end
        
        -- Warn for broken combatlog
        -- How Often does the combat log really break? Some fights have a long period of time where you get no combat events.
        -- Shuld most likely add a hook to the bossmods, to indicate 'dead time' where you are waiting for some RP to end of phases to complete
        -- Halion comes to mind, where if your outside in P2 you see nothing till P3.
        local currentTime = GetTime()
        if (currentTime - lastCombatlogEvent > 20) and UnitAffectingCombat("player")  and currentTime - lastWarned > 60 and runningCount > 0 then
                lastWarned = currentTime
                RW:Msg("You have not received any combatlog event for 20s, Switching to tracking UINT_AURA instead, some warnings and timers may be missing. It is recommended to restart your client after the fight")
                CombatLogClearEntries()
                self:RegisterEvent("UNIT_AURA")
                plugin.TrackingUnitAura = true
        end
end
 
-- Check if in combat with any boss, else schedule to check again 
function plugin:PLAYER_REGEN_DISABLED()
        local boss, trigger = scanEngage(self)
        if boss and not self.Running[boss] then
                if (modEndTime[boss] and (GetTime() - modEndTime[boss]) > 5 ) or modEndTime[boss] == nill then
                        RW.Callbacks:Fire("BossEngaged", boss, trigger)
                elseif UnitAffectingCombat("player") then 
                        self:ScheduleTimer("PLAYER_REGEN_DISABLED", 2)
                end
        else
                if UnitAffectingCombat("player") then 
                        self:ScheduleTimer("PLAYER_REGEN_DISABLED", 2)
                end
        end
end

function plugin:PLAYER_REGEN_ENABLED()
        -- run a clear of the combat log when we exit combat
        --CombatLogClearEntries()
end

function plugin:MonsterCom(event, msg, name)    
        -- Trigger bossmods that are started by yells   
        for bossName, boss in RW:IterateBosses() do
                if boss.DB.enabled then
                        if not self.Running[boss] then
                                if boss.yellTriggers then
                                        for yell in pairs(boss.yellTriggers) do
                                                if yell == msg  or msg:find(yell) then
                                                        self:ScheduleTimer(scanWipe, 3, self)
                                                        RW.Callbacks:Fire("BossEngaged", boss, msg)
                                                end
                                        end
                                end
                        end
                end
        end
        
        -- For all running bossmods
        for boss in pairs(self.Running) do      
                -- Trigger wins
                if boss.yellWins then
                        for yell in pairs(boss.yellWins) do
                                if yell == msg or msg:find(yell) then
                                        RW.Callbacks:Fire("BossDisengaged", boss, false, msg)                                   
                                end
                        end
                end             
                
                -- Trigger tracked yell functions
                if boss.trackedYells and boss.trackedYells[msg] then
                        local func = boss.trackedYells[msg]
                        if type(func) == "string" then
                                boss[func](boss, event, msg, name)
                        else
                                func(boss, event, msg, name)
                        end
                elseif boss.trackedYells then
                        for yell, func in pairs(boss.trackedYells) do
                                if msg:find(yell) then
                                        boss[func](boss, event, msg, name)
                                end
                        end
                end     
                
                -- Trigger general yell function
                if boss[event] and type(boss[event]) == "function" then
                        boss[event](boss, event, msg, name)
                end
        end
end


function plugin:UNIT_DIED(gUID)
        local cID = gUID
        if type(gUID) == "string" then
                if select(4, GetBuildInfo()) < 30400 then
                        cID = tonumber(gUID:sub(9, 12), 16)
                else
                        cID = tonumber(gUID:sub(7, 10), 16)
                end
        end
        for boss in pairs(self.Running) do
                if boss.mobWins then
                        for winMob, state in pairs(boss.mobWins) do                                     
                                if cID == winMob then
                                        boss.mobWins[winMob] = false
                                end
                                
                                local win = true                                
                                for mob, state in pairs(boss.mobWins) do
                                        if state then
                                                win = false
                                                break
                                        end
                                end                             
                                
                                if win then
                                        RW.Callbacks:Fire("BossDisengaged", boss, false, "Mobs")                                        
                                end
                        end
                end
                
                if boss.trackedDeaths and boss.trackedDeaths[cID] then
                        local func = boss.trackedDeaths[cID]
                        if type(func) == "string" then
                                boss[func](boss, cID, gUID)
                        else
                                func(boss, cID, gUID)
                        end
                end
                
                if boss["UNIT_DIED"] and type(boss["UNIT_DIED"]) == "function" then
                        boss["UNIT_DIED"](boss, cID, gUID)
                end
        end
end

-------------------------
-- AceMessage handling --
-------------------------

function plugin:EnteringEnabledZone()
        self:RegisterEvent("CHAT_MSG_MONSTER_YELL", "MonsterCom")
        self:RegisterEvent("CHAT_MSG_MONSTER_EMOTE", "MonsterCom")
        self:RegisterEvent("CHAT_MSG_RAID_BOSS_EMOTE", "MonsterCom")
        self:RegisterEvent("CHAT_MSG_MONSTER_SAY", "MonsterCom")
        self:RegisterEvent("PLAYER_REGEN_DISABLED")
        if self.combatMonitor then
                self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
        end
end

function plugin:LeavingEnabledZone()
        self:UnregisterEvent("CHAT_MSG_MONSTER_YELL")
        self:UnregisterEvent("PLAYER_REGEN_DISABLED")
        self:UnregisterEvent("CHAT_MSG_MONSTER_EMOTE")
        self:UnregisterEvent("CHAT_MSG_RAID_BOSS_EMOTE")
        self:UnregisterEvent("CHAT_MSG_MONSTER_SAY")
        self:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
end

function plugin:CombatTrackingRegistered(event, boss)
        self.combatMonitor = true
        self.CombatWatcher[boss] = true
        self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
end

function plugin:BossEngaged(event, boss, trigger)
        if self.Running[boss] then return end
        self.Running[boss] = true       
        if not self.combatMonitor then
                self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
        end
        if trigger and trigger ~= "Debug" then
                self:ScheduleTimer(scanWipe, 3, self)
        end
        boss:Start(trigger)
end

function plugin:BossDisengaged(event, boss, wipe, trigger)
        if boss.mobWins then
                for mob in pairs(boss.mobWins) do
                        boss.mobWins[mob] = true
                end
        end
        self.Running[boss] = nil
        if not self.combatMonitor then
                self:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
                self:UnregisterEvent("UNIT_AURA")
        end
        boss:End(wipe, trigger)
        modEndTime[boss] = GetTime()
end

------------------------
-- Combatlog handling --
------------------------
do
        local argsPrototype = {}
        local mt = {__index = argsPrototype}
        local args = setmetatable({}, mt)
        function argsPrototype:IsPlayer() if self.dName == pName then return true end end
        function argsPrototype:IsHostile() return bit.band(self.sFlags, COMBATLOG_OBJECT_CONTROL_NPC) ~= 0 end
        
        unitArgsPrototype = {}
        local unitMT = {__index = unitArgsPrototype}
        local unitArgs = setmetatable({sName = "", sGUID = "", dGUID = "", sFlags = "", dFlags = "", amount = 0, absorbed = 0}, unitMT)
        function unitArgsPrototype:IsPlayer() if self.dName == pName then return true end end
        function unitArgsPrototype:IsHostile() return false end
        
        local function prepareArgs(time, event, sGUID, sName, sFlags, dGUID, dName, dFlags, ...)
                wipe(args)
                args.event = event
                args.sGUID = sGUID
                args.sName = sName
                args.sFlags = sFlags
                args.dGUID = dGUID
                args.dName = dName
                args.dFlags = dFlags
                
                if event:sub(1, 5) == "SPELL" then
                        args.spellID, args.spellName, args.spellSchool = ...
                        if event == "SPELL_DAMAGE" then
                                args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = select(4, ...)
                                args.amount = args.amount - args.overkill
                        elseif event == "SPELL_MISSED" then
                                args.missType, args.amountMissed = select(4, ...)
                        elseif event == "SPELL_MISSED" then 
                                args.missType, args.amountMissed = select(4, ...)
                        elseif event == "SPELL_HEAL" or event == "SPELL_BUILDING_HEAL" then 
                                args.amount, args.overhealing, args.absorbed, args.critical = select(4, ...)
                                args.amount = args.amount - args.overhealing
                        elseif event == "SPELL_ENERGIZE" then
                                args.amount, args.powerType = select(4, ...)
                        elseif event:sub(1, 14) == "SPELL_PERIODIC" then
                                if event == "SPELL_PERIODIC_MISSED" then
                                        args.missType, args.amount = select(4, ...)
                                elseif event == "SPELL_PERIODIC_DAMAGE" then
                                        args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = select(4, ...);
                                        args.amount = args.amount - args.overkill
                                elseif event == "SPELL_PERIODIC_HEAL" then
                                        args.amount, args.overhealing, args.absorbed, args.critical = select(4, ...)
                                        args.amount = args.amount - args.overhealing
                                elseif event == "SPELL_PERIODIC_DRAIN" then
                                        args.amount, args.powerType, args.extraAmount = select(4, ...)
                                elseif event == "SPELL_PERIODIC_LEECH" then
                                        args.amount, args.powerType, args.extraAmount = select(4, ...)
                                elseif event == "SPELL_PERIODIC_ENERGIZE" then
                                        args.amount, args.powerType = select(4, ...)
                                end
                        elseif event == "SPELL_CAST_FAILED" then 
                                args.missType = select(4, ...)
                        elseif event == "SPELL_DRAIN" then
                                args.amount, args.powerType, args.extraAmount = select(4, ...)
                        elseif event == "SPELL_LEECH" then
                                args.amount, args.powerType, args.extraAmount = select(4, ...)
                        elseif event == "SPELL_INTERRUPT" then
                                args.extraSpellId, args.extraSpellName, args.extraSpellSchool = args.spellID, args.spellName, args.spellSchool
                                args.spellID, args.spellName, args.spellSchool = select(4, ...)
                        elseif event == "SPELL_EXTRA_ATTACKS" then
                                args.amount = select(4, ...)
                        elseif event == "SPELL_DISPEL_FAILED" then
                                args.extraSpellId, args.extraSpellName, args.extraSpellSchool = select(4, ...)
                        elseif event == "SPELL_DISPEL" or event == "SPELL_STOLEN" then
                                args.extraSpellId, args.extraSpellName, args.extraSpellSchool, args.auraType = select(4, ...)
                        elseif event == "SPELL_AURA_BROKEN" then
                                args.auraType = select(4, ...)
                        elseif event == "SPELL_AURA_BROKEN_SPELL" then
                                args.extraSpellId, args.extraSpellName, args.extraSpellSchool, args.auraType = select(4, ...)
                        elseif event == "SPELL_AURA_APPLIED" or event == "SPELL_AURA_REMOVED" or event == "SPELL_AURA_REFRESH"then
                                args.auraType = select(4, ...)
                        elseif ( event == "SPELL_AURA_APPLIED_DOSE" or event == "SPELL_AURA_REMOVED_DOSE" ) then
                                args.auraType, args.amount = select(4, ...)
                        end
                elseif event:sub(1, 5) == "SWING" then
                        args.spellName = ACTION_SWING
                        if event == "SWING_DAMAGE" then
                                args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = ...
                                args.amount = args.amount - args.overkill
                        elseif event == "SWING_MISSED" then 
                                args.spellName = ACTION_SWING;
                                args.missType, args.amountMissed = ...
                        end
                elseif event:sub(1, 5) == "RANGE" then
                        args.spellId, args.spellName, args.spellSchool = ...
                        if event == "RANGE_DAMAGE" then 
                                args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = select(4, ...)
                                args.amount = args.amount - args.overkill
                        elseif event == "RANGE_MISSED" then 
                                args.missType, args.amountMissed = select(4, ...)
                        end
                elseif event == "DAMAGE_SHIELD" then
                        args.spellId, args.spellName, args.spellSchool, args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = ...
                        args.amount = args.amount - args.overkill
                elseif event == "DAMAGE_SHIELD_MISSED" then
                        args.spellId, args.spellName, args.spellSchool, args.missType = ...
                elseif event == "UNIT_DIED" or event == "UNIT_DESTROYED" or event == "UNIT_DISSIPATES" then
                        args.sourceName, args.sourceGUID, args.sourceFlags = args.destName, args.destGUID, args.destFlags
                elseif event == "ENVIRONMENTAL_DAMAGE" then
                        environmentalType, args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = ...
                        args.spellName = _G["ACTION_ENVIRONMENTAL_DAMAGE_"..environmentalType]
                        args.amount = args.amount - args.overkill
                elseif event == "DAMAGE_SPLIT" then
                        args.spellId, args.spellName, args.spellSchool, args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = ...;
                        args.amount = args.amount - args.overkill
                end
        end
        
        local startBossHandle = nil
        local triggerSpell = 0
        local function startBoss(boss)
                RW.Callbacks:Fire("BossEngaged", boss, trigger)
                startBossHandle = nil
        end     
        
        function plugin:COMBAT_LOG_EVENT_UNFILTERED(_, time, event, sGUID, sName, sFlags, dGUID, dName, dFlags, ...)
                lastCombatlogEvent = GetTime()
                if plugin.TrackingUnitAura then plugin:UnregisterEvent("UNIT_AURA") plugin.TrackingUnitAura = nil end
                if event == "UNIT_DIED" then
                        self:UNIT_DIED(dGUID)
                end
                local spell
                if event == "SPELL_INTERRUPT" then
                        spell = select(4, ...)
                else
                        spell = select(1, ...)
                end
                for boss in pairs(self.Running) do
                        if boss.trackedSpells[spell] then
                                prepareArgs(time, event, sGUID, sName, sFlags, dGUID, dName, dFlags, ...)
                                local func = boss.trackedSpells[spell]
                                if type(func) == "string" then
                                        boss[func](boss, event, args)
                                else
                                        func(boss, event, args)
                                end
                        end
                        if boss[event] and type(boss[event]) == "function" then
                                prepareArgs(time, event, sGUID, sName, sFlags, dGUID, dName, dFlags, ...)
                                boss[event](boss, event, args)
                        end
                end
                for boss in pairs(self.CombatWatcher) do
                        if boss.DB.enabled then
                                if not self.Running[boss] then
                                        if boss.combatTriggers and boss.combatTriggers[event] then
                                                prepareArgs(time, event, sGUID, sName, sFlags, dGUID, dName, dFlags, ...)
                                                local mobId = boss:CUIDFromGUID(sGUID)
                                                if boss.combatTriggers[event].uid and boss.combatTriggers[event].uid ==  mobId then
                                                        for _,spell in ipairs(boss.combatTriggers[event].spells) do
                                                                -- here we only care about the spellname or spellid, since Environmental or unit died wont have a source and we are asking for a specifc mob as the sourcs
                                                                if args.spellID == spell or args.spellName == spell then
                                                                        if startBossHandle == nil then
                                                                                triggerSpell = spell
                                                                                startBossHandler = self:ScheduleTimer(startBoss,0.1,boss)
                                                                                break
                                                                        end
                                                                end
                                                        end
                                                        
                                                else
                                                        for _,spell in ipairs(boss.combatTriggers[event].spells) do
                                                                -- here we dont have a mob source so check spellid, spellname, target for DIED/Destroyed, and spell school for environ damage
                                                                if args.spellID == spell or args.spellName == spell or spell == boss:CUIDFromGUID(args.destGUID) or spell == args.school then
                                                                        if startBossHandle == nil then
                                                                                triggerSpell = spell
                                                                                startBossHandler = self:ScheduleTimer(startBoss,0.1,boss)
                                                                                break
                                                                        end
                                                                end
                                                        end                                                                                                             
                                                end
                                        end
                                end
                        end
                end
        end
        
        local function prepareUnitArgs(unit, spellID, count)
                wipe(unitArgs)
                local spellData = UnitTracking[unit][spellID] or {}
                spellData.valid = true
                spellData.count = count
                UnitTracking[unit][spellID] = spellData
                unitArgs.dName = UnitName(unit)
                unitArgs.spellID = spellID
        end
        
        function plugin:UNIT_AURA(event, unit)
                if UnitTracking[unit] then
                        for spell, data in pairs(UnitTracking[unit]) do
                                data.valid = false
                        end
                else
                        UnitTracking[unit] = {}
                end
                
                local name, index = "", 1
                while name do
                        name, _, _, count, debuffType, _, _, _, _, _, spellID  = UnitAura(unit, index, "HARMFUL")
                        for boss in pairs(self.Running) do                      
                                if boss.trackedSpells[spellID] then                             
                                        -- SPELL_AURA_APPLIED
                                        if not UnitTracking[unit][spellID] then
                                                prepareUnitArgs(unit, spellID, count)
                                                local func = boss.trackedSpells[spellID]
                                                local event = count == 0 and "SPELL_AURA_APPLIED" or "SPELL_AURA_APPLIED_DOSE"
                                                
                                                if type(func) == "string" then
                                                        boss[func](boss, event, unitArgs)
                                                else
                                                        func(boss, event, unitArgs)
                                                end
                                                
                                        -- SPELL_AURA_APPLIED_DOSE
                                        elseif UnitTracking[unit][spellID].count < count then
                                                prepareUnitArgs(unit, spellID, count)
                                                if type(func) == "string" then
                                                        boss[func](boss, "SPELL_AURA_APPLIED_DOSE", unitArgs)
                                                else
                                                        func(boss, "SPELL_AURA_APPLIED_DOSE", unitArgs)
                                                end
                                        elseif UnitTracking[unit][spellID] then 
                                                UnitTracking[unit][spellID].valid = true                                                
                                        end
                                end
                        end
                        
                        index = index + 1
                end             
                
                
                for spell, data in pairs(UnitTracking[unit]) do
                        for boss in pairs(self.Running) do
                                if boss.trackedSpells[spell] then                       
                                        -- SPELL_AURA_REMOVED
                                        if not data.valid then                                  
                                                prepareUnitArgs(unit, spell, 0)
                                                local func = boss.trackedSpells[spell]
                                                if type(func) == "string" then
                                                        boss[func](boss, "SPELL_AURA_REMOVED", unitArgs)
                                                else
                                                        func(boss, "SPELL_AURA_REMOVED", unitArgs)
                                                end
                                                UnitTracking[unit][spell] = nil
                                        end
                                end
                        end
                end
                
        end
        
        function plugin:COMBAT_LOG_TEST(event, args)
                lastCombatlogEvent = GetTime()
                if event == "UNIT_DIED" then
                        self:UNIT_DIED(args.dGUID)
                end
                for boss in pairs(self.Running) do                      
                        if boss.trackedSpells[args.spellID] then
                                local func = boss.trackedSpells[args.spellID]
                                if type(func) == "string" then
                                        boss[func](boss, event, args)
                                else
                                        func(boss, event, args)
                                end
                        end
                        if boss[event] and type(boss[event]) == "function" then
                                boss[event](boss, event, args)
                        end
                end             
        end     
        
        function plugin:TestEvent(event, event, sargs)
                args.spellID = sargs.spellID
                args.dName = sargs.dName
                args.sName = sargs.sName
                args.amount = sargs.amount
                args.spellName = sargs.spellName
                args.dGUID = sargs.dguid
                self:COMBAT_LOG_TEST(event, args)
        end
end

Compare with Previous | Blame