-- |
-- 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 |