WoWInterface SVN NightWatch

Compare Revisions

  • This comparison shows the changes necessary to convert path
    /trunk
    from Rev 4 to Rev 5
    Reverse comparison

Rev 4 → Rev 5

NightWatch.toc New file
0,0 → 1,26
## Interface: 30300
## Title: NightWatch
## Notes: Sends chat alerts for certain spells. "/nw" for config.
## Author: Nightyniight @ Frostmourne - US
## Version: 0.7
## DefaultState: Enabled
## LoadOnDemand: 0
## SavedVariables: NightWatchDB
## OptionalDeps: Ace3, LibSink
## X-Embeds: Ace3, LibSink
 
#@no-lib-strip@
Libs\LibStub\LibStub.lua
Libs\CallbackHandler-1.0\CallbackHandler-1.0.xml
Libs\AceAddon-3.0\AceAddon-3.0.xml
Libs\AceGUI-3.0\AceGUI-3.0.xml
Libs\AceConfig-3.0\AceConfig-3.0.xml
Libs\AceConsole-3.0\AceConsole-3.0.xml
Libs\AceDB-3.0\AceDB-3.0.xml
Libs\AceDBOptions-3.0\AceDBOptions-3.0.xml
Libs\AceEvent-3.0\AceEvent-3.0.xml
libs\LibSink-2.0\LibSink-2.0.lua
#@end-no-lib-strip@
 
 
NightWatch.lua
NightWatch.lua New file
0,0 → 1,788
--
-- 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