WoWInterface SVN NightWatch

[/] [NightWatch.lua] - Rev 2

Go to most recent revision | Compare with Previous | Blame | View Log

--
-- NightWatch
--
-- A custom spell watcher/reporter.
--
-- Inspired by "tricks message" by Intermission.
--
--

local addon = LibStub("AceAddon-3.0"):NewAddon("NightWatch", "AceConsole-3.0", "AceEvent-3.0")
local ACD = LibStub("AceConfigDialog-3.0")

local sink1 = addon:NewModule("NightWatchSink1", "LibSink-2.0")
local sink2 = addon:NewModule("NightWatchSink2", "LibSink-2.0")

-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded.
-- Listed here for Mikk's FindGlobals script.
-- GLOBALS: LibStub

local pairs = pairs
local print = print
local next = next
local format = string.format
local string_upper = string.upper
local string_gsub = string.gsub
local wipe = wipe
local SendChatMessage = SendChatMessage
local UnitInRaid = UnitInRaid
local UnitInParty = UnitInParty
local GetNumRaidMembers = GetNumRaidMembers
local GetNumPartyMembers = GetNumPartyMembers
local InCombatLockdown = InCombatLockdown

local version = GetAddOnMetadata("NightWatch", "Version") 
local playerName = UnitName("player")
local firstEnable = true

local db
local clipboard = {}

local function Print(msg)
        print(format("|cffffff7fnw|r: %s",msg))
end
                
function addon:defaultMsgFormat()
        return {
                chat =                  "<%C's> %s%t: %f",
                chatTarget =    " on <<%t>>",
                whisper =               "<%C's> %s%t: %f",
                whisperTarget = " on YOU",
        }
end

local Default_Profile = {
        profile = {
                enabled = true,
                spells = {},
                sinkStorage1 = { sink20OutputSink = "ChatFrame", },
                sinkStorage2 = { sink20OutputSink = "None",     },
                customMsgFormat = addon:defaultMsgFormat(),
                
                -- flagMap is used as a translation lookup and to help generate the default flag options for each watched spell.
                flagMap = {
                        SPELL_CAST_START = "started",
                        SPELL_CAST_SUCCESS = "success",
                        SPELL_CAST_FAILED = "failed",
                        SPELL_RESURRECT = "resurrected",
                },
        },
}

function addon:flagDefaults()
        local result = {}
        for k,_ in pairs(db.flagMap) do
                result[k] = false
        end
        return result
end
                
function addon:wsDefaults()
        return {
                watched = true,
                selfOnly = false,
                groupOnly = false,
                whisperTarget = false,
                output1 = true,
                output2 = false,
                combatOnly = false,
                anyFlag = true,
                flags = addon:flagDefaults(),
                msgFormat = addon:defaultMsgFormat(),
        }
end

local icons = {
        star = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_1:0|t",
        circle = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_2:0|t",
        diamond = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_3:0|t",
        triangle = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_4:0|t",
        moon = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_5:0|t",
        square = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_6:0|t",
        cross = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_7:0|t",
        skull = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_8:0|t",
}

function addon:makeOptions()
        local opts = {
                name = "NightWatch " .. version,
                type = "group",
                handler = addon,
                -- inline = false,
                childGroups = "tab",
                args = {
                        test = {
                                name = "test",
                                order = 0, guiHidden = true, cmdHidden = true,
                                type = "execute",
                                func = "test",
                        },
                        enable = {
                                name = "Enable addon",
                                order = 10, guiHidden = true,
                                type = "execute",
                                func = function(info) addon:Enable() end,
                        },
                        enabled = {
                                name = "Enabled",
                                order = 20, cmdHidden = true,
                                type = "toggle",
                                get = function(info) return db[info[#info]] end,
                                set = function(info, value) 
                                        if value then 
                                                addon:Enable() 
                                        else 
                                                addon:Disable() 
                                        end
                                end,
                        },
                        reset = {
                                name = "Reset Profile",
                                desc = "Reset *all* profile settings to default values.",
                                order = 30, -- width = "half",
                                type = "execute",
                                func = "resetSettings",
                                confirm = true,
                                confirmText = "This action is permanent.  Are you sure?",
                        },
                        disable = {
                                name = "Disable addon",
                                order = 40, guiHidden = true,
                                type = "execute",
                                func = function(info) addon:Disable() end,
                        },
                        config = {
                                name = "Toggle config",
                                order = 50, guiHidden = true,
                                type = "execute",
                                func = "openConfig"
                        },
                },
        }
        
        opts.args.spell = {
                name = "Spell Options",
                order = 10,
                type = "group",
                handler = addon,
                cmdHidden = true,
                childGroups = "tab",
                args = {
                        selectedSpell = {
                                name = "Selected Spell",
                                order = 10,
                                type = "select",
                                values = function() return addon:convertLookup(db.spells) end,
                                get = function() return db.selSpell end,
                                set = function(_, value) db.selSpell = value end,
                        },
                        deleteSpell = {
                                name = "Delete",
                                desc = "Delete selected spell.",
                                order = 20, width = "half",  cmdHidden = true,
                                type = "execute",
                                func =  "deleteSpell",
                                confirm = true,
                                confirmText = "This action is permanent.  Are you sure?",
                        },
                        addSpell = {
                                name = "Add New Spell",
                                desc = "Add a new spell.",
                                order = 30, cmdHidden = true, -- width = "half",
                                type = "input",
                                get = function() return nil end,
                                set = "addSpell",
                                validate = function(info, value)
                                        if value:len() < 3 then
                                                return "Must be at least 3 characters long."
                                        end
                                        return true
                                end,
                        },
                },
        }
        
        opts.args.spell.args.general = {
                name = "General",
                order = 10,
                type = "group",
                cmdHidden = true, width = "half",
                handler = addon,
                get = "getterFunc",
                set = "setterFunc",
                disabled = function() return not db.selSpell end,
                args = {
                        watched = {
                                name = "Watch",
                                desc = "Watch this spell.",
                                order = 10,
                                type = "toggle",
                        },
                        head1 = {
                                name = "",
                                order = 20,
                                type = "header",
                        },
                        selfOnly = {
                                name = "Self Only",
                                desc = "Only announce when YOU cast this spell.",
                                order = 30,
                                type = "toggle",
                        },
                        groupOnly = {
                                name = "Group Only",
                                desc = "Only announce spells cast by persons in your raid/party.",
                                order = 40,
                                type = "toggle",
                        },
                        combatOnly = {
                                name = "Combat Only",
                                desc = "Only announce when you are in combat.",
                                order = 50,
                                type = "toggle",
                        },
                        whisperTarget = {
                                name = "Whisper Target",
                                desc = "Whisper the target of spell, if applicable.",
                                order = 60,
                                type = "toggle",
                        },
                        output1 = {
                                name = "Output 1",
                                desc = "Send output here.",
                                order = 70,
                                type = "toggle",
                        },
                        output2 = {
                                name = "Output 2",
                                desc = "Send output here.",
                                order = 80,
                                type = "toggle",
                        },
                        flags = {
                                name = "Combatlog Flags",
                                order = 90,
                                type = "multiselect",
                                values = function() return addon:convertLookup(db.flagMap) end,
                                get = "getFlag",
                                set = "setFlag",
                                disabled = function() return (not db.selSpell) or addon:isAnyFlag() end,
                        },
                        all = {
                                name = "All",
                                desc = "Set all flags.",
                                order = 100, width = "half",
                                type = "execute",
                                func =  function(info,key) addon:setAllFlagsTo(info, true) end,
                                disabled = "isAnyFlag",
                        },
                        none = {
                                name = "None",
                                desc = "Clear all flags.",
                                order = 110, width = "half",
                                type = "execute",
                                func =  function(info,key) addon:setAllFlagsTo(info, false) end,
                                disabled = "isAnyFlag",
                        },
                        anyFlag = {
                                name = "Any flag",
                                desc = "Announce on ANY flag (not just the ones listed).",
                                order = 120,
                                type = "toggle",
                        },
                },
        }

        opts.args.spell.args.msgFormat = {
                name = "Message Format",
                type = "group",
                order = 20,
                cmdHidden = true,
                handler = addon,
                get = "mfGetterFunc",
                set = "mfSetterFunc",
                disabled = function() return not db.selSpell end,
                args = {
                        chat = {
                                name = "Chat Format",
                                desc = "Define chat format.",
                                order = 10, width = "double",
                                type = "input",
                        },
                        chatTarget = {
                                name = "Chat Target Format",
                                desc = "If set, this will replace %t if there is a target.",
                                order = 20,
                                type = "input",
                        },
                        chatTest = {
                                name = function(info) return addon:chatTest(info) end,
                                order = 25,
                                type = "description",
                        },
                        whisper = {
                                name = "Whisper Format",
                                desc = "Define whisper format.",
                                order = 30, width = "double",
                                type = "input",
                        },
                        whisperTarget = {
                                name = "Whisper Target Format",
                                desc = "If set, this will replace %t if there is a target.",
                                order = 40,
                                type = "input",
                        },                      
                        whisperTest = {
                                name = function(info) return addon:whisperTest(info) end,
                                order = 60,
                                type = "description",
                        },
                        -- head1 = {
                                -- name = '',
                                -- order = 70,
                                -- type = "header",
                        -- },
                        setDefaultFmt = {
                                name = "Set Default",
                                desc = "Make these formats the default for new spells.",
                                order = 100, -- width = "half",
                                type = "execute",
                                func = function() 
                                        db.customMsgFormat.chat = db.spells[db.selSpell].msgFormat.chat
                                        db.customMsgFormat.chatTarget = db.spells[db.selSpell].msgFormat.chatTarget
                                end,
                        },
                        copyFmt = {
                                name = "Copy*",
                                desc = "Copy these formats.",
                                order = 110, width = "half",
                                type = "execute",
                                func = "copyFmt",
                        },
                        pasteFmt = {
                                name = "Paste*",
                                desc = "Paste formats.",
                                order = 120, width = "half",
                                type = "execute",
                                func = "pastFmt",
                        },
                        copyFmtToAll = {
                                name = "Copy To All*",
                                desc = "Copies these formats to all spells.",
                                order = 130, -- width = "half",
                                type = "execute",
                                func = "CopyFmtToAll",
                        },
                        formatHelp = {
                                name =  "\n** Format Syntax **\n" ..
                                                "%c = Caster\n" ..
                                                "%s = Spell\n" ..
                                                "%t = Target\n" ..
                                                "%f = Flags\n" ..
                                                "For UPPERCASE text, use an uppercase placeholder.",
                                order = 150,
                                type = "description",
                        },
                },
        }

        opts.args.output = {
                name = "Output",
                desc = "Configure output options.",
                type = "group",
                order = 30,
                cmdHidden = true,
                args = {}
        }

        local sink1 = sink1:GetSinkAce3OptionsDataTable()
        sink1.name = "Output 1"
        sink1.desc = "Where to send this addon's primary output."
        sink1.order = 10
        -- sink1.cmdHidden = true
        sink1.inline = true

        local sink2 = sink2:GetSinkAce3OptionsDataTable()
        sink2.name = "Output 2"
        sink2.desc = "Where to send this addon's secondary output."
        sink2.order = 20
        -- sink2.cmdHidden = true
        sink2.inline = true
        
        opts.args.output.args.output1 = sink1
        opts.args.output.args.output2 = sink2

        
        opts.args.flagOptions = {
                name = "Flag Options",
                desc = "Configure flag options.",
                type = "group",
                order = 700,
                cmdHidden = true,
                args = {
                        selectedFlag = {
                                name = "Selected Flag",
                                order = 10,
                                type = "select",
                                values = function() return addon:convertLookup(db.flagMap) end,
                                get = function() return db.selFlag end,
                                set = function(_, value) db.selFlag = value end,
                        },
                        translation = {
                                name = "Translation",
                                desc = "Friendly text for this flag.",
                                order = 20,
                                type = "input",
                                get = function() return db.flagMap[db.selFlag] or '' end,
                                set = function(_, value) db.flagMap[db.selFlag] = value end,
                        },
                        head1 = {
                                name = "",
                                order = 40,
                                type = "header",
                        },
                        deleteFlag = {
                                name = "Delete Flag",
                                desc = "Delete selected flag.",
                                order = 75, -- width = "half",
                                type = "execute",
                                func =  function()
                                        db.flagMap[db.selFlag] = nil
                                        LibStub("AceConfigRegistry-3.0"):NotifyChange("NightWatch")
                                        end,
                                confirm = true,
                                confirmText = "This action is permanent.  Are you sure?",
                        },
                        addFlag = {
                                name = "Add New Flag",
                                desc = "Add a new flag.",
                                order = 80,
                                type = "input",
                                get = function() return nil end,
                                set = function(_, value) 
                                        db.flagMap[value] = value
                                        LibStub("AceConfigRegistry-3.0"):NotifyChange("NightWatch")
                                end,
                                validate = function(info, value)
                                        if value:len() < 3 then
                                                return "Must be at least 3 characters long."
                                        end
                                        return true
                                end,
                        },
                },
        }

        
        opts.args.profile = LibStub("AceDBOptions-3.0"):GetOptionsTable(addon.db)
        opts.args.profile.order = -10

        return opts
end

function addon:OnInitialize()
    addon.db = LibStub("AceDB-3.0"):New("NightWatchDB", Default_Profile, "Default")
        db = addon.db.profile

        addon.db.RegisterCallback( self, "OnNewProfile", "handleProfileChanges" )
        addon.db.RegisterCallback( self, "OnProfileReset", "handleProfileChanges" )
        addon.db.RegisterCallback( self, "OnProfileChanged", "handleProfileChanges" )
        addon.db.RegisterCallback( self, "OnProfileCopied", "handleProfileChanges" )

        LibStub("AceConfig-3.0"):RegisterOptionsTable("NightWatch", addon:makeOptions())
        LibStub("AceConfigDialog-3.0"):AddToBlizOptions("NightWatch", "NightWatch")
    addon:RegisterChatCommand("nw", "chatCommand")
    addon:RegisterChatCommand("nightwatch", "chatCommand")
        sink1:SetSinkStorage(db.sinkStorage1)
        sink2:SetSinkStorage(db.sinkStorage2)

        addon:upgrade()
        
        addon:resetSelected()
end


function addon:OnEnable()
        addon:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED", "combatLogEvent")
        db.enabled = true
        if not firstEnable then
                Print("enabled")
        else
                firstEnable = false
        end
end

function addon:OnDisable()
        addon:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
        db.enabled = false
        Print("disabled")
end

function addon:chatCommand(input)
    if not input or input:trim() == "" then
        addon:openConfig()
                return
        end
    if input == "help" then
                input = ""
        end
        LibStub("AceConfigCmd-3.0").HandleCommand(addon, "nw", "NightWatch", input)
end

-------------------------------------------------------------------------------
-- 0 1          2      3        4        5         6        7        8         9        10         11           12
-- _,timestamp, event, srcGUID, srcName, srcFlags, dstGUID, dstName, dstFlags, spellID, spellName, spellSchool, missType, ...)
--
--                              0  1  2     3  4       5  6  7        8  9  10      
local msg
local whisperTarget
function addon:combatLogEvent(_, _, flag, _, source, _, _, target, _, _, spell, ...)

        if not db.spells[spell] then return end                                                 -- Spell not in database, quit
        
        local ws = db.spells[spell]

        if not ws.watched then return end                                                                                       -- Not being watched, quit
        
        if not (ws.anyFlag or ws.flags[flag]) then return end                                   -- Doesn't match flags, quit

        if ws.selfOnly and not (source == playerName) then return end                   -- Check for self-cast only

        if ws.combatOnly and not InCombatLockdown() then return end                             -- Check for self in combat
        
        if ws.groupOnly and not (UnitInRaid(source) or UnitInParty(source)) then return end     -- Check for source in raid/party
        
        if db.flagMap[flag] then                                                                                                        -- Convert flag to more friendly version, if available.
                flag = db.flagMap[flag]
        end
        
        if target then
                if ws.whisperTarget then                                                                                                -- Whisper target
                        if ws.msgFormat.whisperTarget then
                                whisperTarget = addon:formatMsg(nil, nil, target, nil, ws.msgFormat.whisperTarget)
                        end
                        SendChatMessage(addon:formatMsg(source, spell, whisperTarget, flag, ws.msgFormat.whisper), "WHISPER", nil, target)
                end
                target = addon:formatMsg(nil, nil, target, nil, ws.msgFormat.chatTarget)
        end

        msg = addon:formatMsg(source, spell, target, flag, ws.msgFormat.chat)
        
        if ws.output1 then                                                                                                                      -- Send to output
                if db.sinkStorage1.sink20OutputSink == "ChatFrame" then
                        msg = addon:mkIconLinks(msg)
                end
                addon:pour(sink1, msg)
        end

        if ws.output2 then
                if db.sinkStorage2.sink20OutputSink == "ChatFrame" then
                        msg = addon:mkIconLinks(msg)
                end
                addon:pour(sink2, msg)
        end
end

-------------------------------------------------------------------------------

function addon:handleProfileChanges()
        db = addon.db.profile
        addon:upgrade()
        addon:resetSelected()
end

function addon:upgrade()
        -- Upgrade database from previous version
        if not db.spell then return end
        Print("Upgrading database")
        local keys = { chat = true, chatTarget = true, whisper = true, whisperTarget = true }
        db.spells = addon:deepcopy(db.spell)
        for k,_ in pairs(db.spell) do
                Print(" Checking: " .. k)
                for key,_ in pairs(keys) do
                        if not db.spell[k].msgFormat[key] then
                                db.spells[k].msgFormat[key] = addon:deepcopy(db.customMsgFormat[key])
                        end
                end
        end
        db.spell = nil
        db.selected = nil
end

function addon:resetSelected()
        if not db.selSpell or not db.spells[db.selSpell] or db.spells[db.selSpell] == "" then
                db.selSpell = (next(db.spells) or nil)
        end
end

function addon:openConfig()
        ACD[ACD.OpenFrames.NightWatch and "Close" or "Open"](ACD,"NightWatch")
end

function addon:resetSettings() 
        addon.db:ResetProfile()
        Print("All settings in this profile have been reset to default values.")
end

function addon:getterFunc(info)
        if not db.selSpell then return nil end
        return db.spells[db.selSpell][info[#info]]
end

function addon:setterFunc(info, value)
        if not db.selSpell then return end
        db.spells[db.selSpell][info[#info]] = value
end

function addon:test()
        Print("Adding test spells.")
        addon:addSpell(nil, "Hearthstone")
        addon:addSpell(nil, "Using Direbrew's Remote")
        addon:addSpell(nil, "Toy Train Set")
        addon:addSpell(nil, "Rebirth")
        addon:addSpell(nil, "Tricks of the Trade")
        addon:addSpell(nil, "Misdirection")
        LibStub("AceConfigRegistry-3.0"):NotifyChange("NightWatch")
end

function addon:addSpell(info, value) 
        db.spells[value] = addon:wsDefaults()
        -- db.spells[value].watched = true
        db.spells[value].msgFormat = addon:deepcopy(db.customMsgFormat)
        db.selSpell = value
end

function addon:deleteSpell()
        if not db.selSpell then return end
        wipe(db.spells[db.selSpell])
        db.spells[db.selSpell] = nil
        addon:resetSelected()
end

function addon:getFlag(info, key)
        if not db.selSpell then return nil end
        return db.spells[db.selSpell].flags[key]
end

function addon:setFlag(info, key, value)
        if not db.selSpell then return nil end
        db.spells[db.selSpell].flags[key] = value
end

function addon:isAnyFlag()
        if not db.selSpell then return nil end
        return db.spells[db.selSpell].anyFlag
end

function addon:setAllFlagsTo(info, value)
        if not db.selSpell then return end
        for k in pairs(db.spells[db.selSpell].flags) do
                db.spells[db.selSpell].flags[k] = value
        end
end

function addon:mfGetterFunc(info)
        if not db.selSpell then return nil end
        return db.spells[db.selSpell][info[#info-1]][info[#info]]
end

function addon:mfSetterFunc(info, value)
        if not db.selSpell then return nil end
        db.spells[db.selSpell][info[#info-1]][info[#info]] = value
end

function addon:chatTest()
        if not db.selSpell then return nil end
        return addon:testMsg(
                        db.spells[db.selSpell].msgFormat.chat, 
                        db.spells[db.selSpell].msgFormat.chatTarget
                ) 
end

function addon:whisperTest()
        if not db.selSpell then return nil end
        return addon:testMsg(
                        db.spells[db.selSpell].msgFormat.whisper, 
                        db.spells[db.selSpell].msgFormat.whisperTarget
                ) 
end
 
 function addon:testMsg(formatString, targetFormatString)
        local source, spell, target, flag = "Caster", db.selSpell or "Spell", "Target", "Flag"
        if not (targetFormatString == "" or targetFormatString == nil) then
                target = addon:formatMsg(nil, nil, target, nil, targetFormatString)
        end
        return addon:mkIconLinks(addon:formatMsg(source, spell, target, flag, formatString))
end

function addon:formatMsg(caster, spell, target, flag, formatString)
        if not formatString then return "" end
        local msg = formatString
        if not caster then caster = '' end
        if not spell then spell = '' end
        if not target then target = '' end
        if not flag then flag = '' end
        msg = string_gsub(msg, "%%c", caster)
        msg = string_gsub(msg, "%%C", string_upper(caster))
        msg = string_gsub(msg, "%%t", target)
        msg = string_gsub(msg, "%%T", string_upper(target))
        msg = string_gsub(msg, "%%s", spell)
        msg = string_gsub(msg, "%%S", string_upper(spell))
        msg = string_gsub(msg, "%%f", flag)
        msg = string_gsub(msg, "%%F", string_upper(flag))
        return msg
end

function addon:mkIconLinks(msg)
        if not msg then return "" end
        for k,v in pairs(icons) do
                msg = string_gsub(msg, "{" .. k .."}", v)
        end
        return msg
end

function addon:copyFmt()
        if not db.selSpell then return end
end

function addon:pasteFmt()
        if not db.selSpell then return end
end

function addon:copyFmtToAll()
        if not db.selSpell then return end
end     

-- From Recount
function addon:deepcopy(object)
        local lookup_table = {}
        local function _copy(object)
                if type(object) ~= "table" then
                        return object
                elseif lookup_table[object] then
                        return lookup_table[object]
                end
                local new_table = {}
                lookup_table[object] = new_table
                for index, value in pairs(object) do
                        new_table[_copy(index)] = _copy(value)
                end
                return setmetatable(new_table, getmetatable(object))
        end
        return _copy(object)
end

function addon:pour(dest, text, color, icon)
        text = format("|cffffff7fnw|r: %s", text)
        color = color or {r=1,g=1,b=1}
        icon = icon or nil
        dest:Pour(text,color.r,color.g,color.b,nil,nil,nil,nil,nil,icon)
end

function addon:convertLookup(data)
        local result = {}
        for k,_ in pairs(data) do
                result[k] = k
        end
        return result
end

Go to most recent revision | Compare with Previous | Blame