Go to most recent revision | Compare with Previous | Blame | View Log
-- -- NightWatch -- -- A custom spell watcher/reporter by Nightyniight of Frostmourne-US. -- -- 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") -- 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 type = type local wipe = wipe local SendChatMessage = SendChatMessage local UnitInRaid = UnitInRaid local UnitInParty = UnitInParty local GetNumRaidMembers = GetNumRaidMembers local GetNumPartyMembers = GetNumPartyMembers local InCombatLockdown = InCombatLockdown local addonVersion = GetAddOnMetadata("NightWatch", "Version") local dbVers = 8 local playerName = UnitName("player") local firstEnable = true local db local clipboard = {} local sink = {} local i for i = 1, 4 do sink[i] = addon:NewModule("Sink" .. i , "LibSink-2.0") end 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 = {}, sinkStorage = { [1] = { sink20OutputSink = "ChatFrame", }, [2] = { sink20OutputSink = "None", }, [3] = { sink20OutputSink = "None", }, [4] = { 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, output3 = false, output4 = false, combatOnly = false, anyFlag = true, flags = addon:flagDefaults(), msgFormat = addon:defaultMsgFormat(), } end local wantIconLinks = { ["ChatFrame"] = true, ["Default"] = true, ["Blizzard Error Frame"] = true, ["Raid Warning"] = true, } 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 " .. addonVersion, 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", }, 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", }, }, } for i = 1,4 do opts.args.spell.args.general.args["output" .. i] = { name = "Output " .. i, desc = "Send output here.", order = 70 + 1, type = "toggle", } end 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() return addon:chatTest() 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() return addon:whisperTest() end, order = 60, type = "description", }, -- head1 = { -- name = '', -- order = 70, -- type = "header", -- }, setDefaultFmt = { name = "Set As Default", desc = "Make these formats the default for new spells.", order = 100, -- width = "half", type = "execute", func = function() if not db.selSpell then return end 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 = function() addon:copyFmt(db.selSpell) end, }, pasteFmt = { name = "Paste", desc = "Paste formats.", order = 120, width = "half", type = "execute", func = function() addon:pasteFmt(db.selSpell) end, }, setFmtToAll = { name = "Set All", desc = "Sets these formats for all spells", order = 130, width = "half", type = "execute", func = function() addon:setFmtToAll(db.selSpell) end, }, resetFmtToDefault = { name = "Reset", desc = "Resets these formats to program defaults.", order = 140, width = "half", type = "execute", func = function() addon:resetFmtToDefault(db.selSpell) end, }, 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 = "Outputs", desc = "Configure output options.", type = "group", order = 30, cmdHidden = true, args = {} } local sinkOpts = {} for i = 1, #sink do sinkOpts[i] = sink[i]:GetSinkAce3OptionsDataTable() sinkOpts[i].name = "Output " .. i sinkOpts[i].desc = "Where to send this output." sinkOpts[i].order = 10 + i -- sinkOpts[i].inline = true opts.args.output.args["output" .. i] = sinkOpts[i] end 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", "profileChanged" ) -- OnProfileChanged seems to cover new profiles too. addon.db.RegisterCallback( self, "OnProfileReset", "profileChanged" ) addon.db.RegisterCallback( self, "OnProfileChanged", "profileChanged" ) addon.db.RegisterCallback( self, "OnProfileCopied", "profileChanged" ) LibStub("AceConfig-3.0"):RegisterOptionsTable("NightWatch", addon:makeOptions()) LibStub("AceConfigDialog-3.0"):AddToBlizOptions("NightWatch", "NightWatch") addon:RegisterChatCommand("nw", "chatCommand") addon:RegisterChatCommand("nightwatch", "chatCommand") addon:profileChanged() 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) for i = 1, #sink do if ws["output" .. i] then -- Send to output if wantIconLinks[db.sinkStorage[i].sink20OutputSink] then msg = addon:mkIconLinks(msg) end addon:pour(sink[i], msg) end end end ------------------------------------------------------------------------------- function addon:profileChanged() db = addon.db.profile for i = 1, #sink do sink[i]:SetSinkStorage(db.sinkStorage[i]) end addon:dbUpgrade() addon:resetSelected() end function addon:dbUpgrade() if db.dbVers and db.dbVers > 7 then return end -- DB Already upgraded -- Upgrade data format from previous addon versions (=<0.6) -- (Renamed db.spell to db.spells just to torture myself. -- Also moved away from AceDB's magic ['*'] feature since it was -- auto-creating spells everty time I checked the spell was in the db. -- Sigh.) if db.spell then Print("Upgrading database") db.spells = addon:deepcopy(db.spell) -- Quick and dirty ... db.spell = nil db.selected = nil Print("Upgrade complete") end -- Remove obsolete sink data from previous addon versions (=<0.7) for i = 1, 4 do if db["sinkStorage" .. i] then db.sinkStorage[i] = db["sinkStorage" .. i] db["sinkStorage" .. i] = nil end end db.dbVers = dbVers 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(value) if not value then return end clipboard.fmt = addon:deepcopy(db.spells[value].msgFormat) end function addon:pasteFmt(value) if not value then return end db.spells[value].msgFormat = addon:deepcopy(clipboard.fmt) end function addon:setFmtToAll(value) if not value then return end addon:copyFmt(value) for k,_ in pairs(db.spells) do addon:pasteFmt(k) end end function addon:resetFmtToDefault(value) if not value then return end db.spells[value].msgFormat = addon:defaultMsgFormat() 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