-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathAlerts.lua
More file actions
476 lines (436 loc) · 13.3 KB
/
Alerts.lua
File metadata and controls
476 lines (436 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
--[[
AdiCCMonitor - Crowd-control monitor.
Copyright 2011-2012 Adirelle (adirelle@gmail.com)
All rights reserved.
--]]
-- Copy globals in local scope to easily spot global leaks with "luac -l | grep GLOBAL"
local _G = _G
local floor = _G.floor
local format = _G.format
local GetNumGroupMembers = _G.GetNumGroupMembers
local GetRaidRosterInfo = _G.GetRaidRosterInfo
local GetTime = _G.GetTime
local ICON_LIST = _G.ICON_LIST
local IsAddonMessagePrefixRegistered = _G.IsAddonMessagePrefixRegistered
local IsInInstance = _G.IsInInstance
local pairs = _G.pairs
local print = _G.print
local RegisterAddonMessagePrefix = _G.RegisterAddonMessagePrefix
local select = _G.select
local SendAddonMessage = _G.SendAddonMessage
local strsplit = _G.strsplit
local UnitGroupRolesAssigned = _G.UnitGroupRolesAssigned
local UnitInParty = _G.UnitInParty
local UnitInRaid = _G.UnitInRaid
local UnitName = _G.UnitName
local wipe = _G.wipe
local addonName, addon = ...
local L = addon.L
local mod = addon:NewModule('Alerts', 'AceEvent-3.0', 'LibSink-2.0')
local prefs
local DEFAULT_SETTINGS = {
profile = {
inInstances = {
['*'] = false,
party = true,
},
messages = {
['*'] = true,
applied = false,
removed = false,
},
ignoreTank = true,
delay = 5,
}
}
local COMM_PREFIX = "ACCMAlerts"
function mod:OnInitialize()
self.db = addon.db:RegisterNamespace(self.moduleName, DEFAULT_SETTINGS)
end
function mod:OnEnable()
prefs = self.db.profile
self:SetSinkStorage(prefs)
self:RegisterEvent('PLAYER_ENTERING_WORLD', 'UpdateListeners')
self.partySize = 0
self.announcer = nil
self:RegisterEvent('GROUP_ROSTER_UPDATE')
self:RegisterEvent('CHAT_MSG_ADDON')
if not IsAddonMessagePrefixRegistered(COMM_PREFIX) then
RegisterAddonMessagePrefix(COMM_PREFIX)
end
self:GROUP_ROSTER_UPDATE('OnEnable')
self:RegisterMessage('AdiCCMonitor_TestFlagChanged', 'UpdateListeners')
self:UpdateListeners()
end
function mod:OnDisable()
addon.UnregisterAllCombatLogEvents(self)
self:CancelAllTimers()
self:UpdateChattiness()
end
function mod:OnConfigChanged(key, ...)
self:UpdateListeners()
if key == "sink20OutputSink" then
self:ScheduleTimer("UpdateChattiness", 1)
end
end
function mod:UpdateListeners()
local listening = addon.testing
if not listening then
local _, instanceType = IsInInstance()
listening = prefs.inInstances[instanceType or "none"]
end
if listening then
if not self.listening then
self:RegisterMessage('AdiCCMonitor_SpellAdded')
self:RegisterMessage('AdiCCMonitor_SpellUpdated', 'PlanNextUpdate')
self:RegisterMessage('AdiCCMonitor_SpellRemoved')
self:RegisterMessage('AdiCCMonitor_SpellBroken')
self:RegisterMessage('AdiCCMonitor_WipeTarget', 'PlanNextUpdate')
self.listening = true
self:Debug('Started listening')
self:ScheduleTimer("UpdateChattiness", 1)
end
if prefs.messages.failure then
addon.RegisterCombatLogEvent(self, 'SPELL_CAST_FAILED')
addon.RegisterCombatLogEvent(self, 'SPELL_MISSED')
else
addon.UnregisterCombatLogEvent(self, 'SPELL_CAST_FAILED')
addon.UnregisterCombatLogEvent(self, 'SPELL_MISSED')
end
self:PlanNextUpdate()
elseif self.listening then
self:UnregisterMessage('AdiCCMonitor_SpellAdded')
self:UnregisterMessage('AdiCCMonitor_SpellRemoved')
self:UnregisterMessage('AdiCCMonitor_SpellBroken')
self:UnregisterMessage('AdiCCMonitor_SpellUpdated')
self:UnregisterMessage('AdiCCMonitor_WipeTarget')
addon.UnregisterAllCombatLogEvents(self)
self:CancelAllTimers()
self.listening = nil
self:Debug('Stopped listening')
self:ScheduleTimer("UpdateChattiness", 1)
end
end
-- Light facade to AceTimer that allows easy rescheduling
do
local AceTimer = LibStub('AceTimer-3.0')
local timers = {}
local function ExecTimer(name)
timers[name] = nil
mod:Debug('Timer:', name)
return mod[name](mod)
end
function mod:ScheduleTimer(name, delay)
if timers[name] then
AceTimer.CancelTimer(self, timers[name], true)
end
self:Debug('Scheduling', name, 'in', delay, 'secs')
timers[name] = AceTimer.ScheduleTimer(self, ExecTimer, delay, name)
end
function mod:CancelTimer(name)
if timers[name] then
self:Debug('Canceling', name)
AceTimer.CancelTimer(self, timers[name], true)
timers[name] = nil
end
end
function mod:CancelAllTimers()
AceTimer.CancelAllTimers(self)
wipe(timers)
end
end
local playerName = UnitName("player")
function mod:SendMessage(message)
local channel = (select(2, IsInInstance()) == "pvp" or IsInLFGDungeon()) and "INSTANCE_CHAT" or "RAID"
if self.partySize == 0 then
self:CHAT_MSG_ADDON("SendMessage", COMM_PREFIX, message, channel, playerName)
else
self:Debug('Sending to', channel, ':', message)
SendAddonMessage(COMM_PREFIX, message, channel)
end
end
function mod:SendQuery()
self:SendMessage("QUERY")
end
function mod:UpdateChattiness()
self:CancelTimer("SendQuery")
local chatty = self:IsEnabled() and self.listening and prefs.sink20OutputSink == "Channel" or false
if chatty ~= self.chatty then
self.chatty = chatty
self:SendQuery()
end
end
function mod:SendReply()
if self.chatty then
self:SendMessage("REPLY")
end
end
function mod:CHAT_MSG_ADDON(event, prefix, message, channel, sender)
if prefix == COMM_PREFIX and sender then
self:Debug("Message from", sender, ":", message)
sender = strsplit('-', sender)
if message == "QUERY" then
self.announcer = playerName
self:CancelTimer("SendQuery")
self:ScheduleTimer("SendReply", 1)
elseif message == "REPLY" then
if not self.announcer or sender < self.announcer then
self.announcer = sender
self:Debug('New announcer:', self.announcer)
end
end
end
end
function mod:GROUP_ROSTER_UPDATE()
local partySize = GetNumGroupMembers()
if partySize ~= self.partySize then
if self.partySize == 0 or (self.announcer and not UnitInParty(self.announcer) and not UnitInRaid(self.announcer)) then
self:ScheduleTimer("SendQuery", 2)
elseif partySize == 0 then
self.announcer = playerName
self:CancelTimer("SendQuery")
self:CancelTimer("SendReply")
end
self.partySize = partySize
end
end
local ignoredFailures = {
[_G.SPELL_FAILED_INTERRUPTED] = true,
[_G.SPELL_FAILED_INTERRUPTED_COMBAT] = true,
[_G.SPELL_FAILED_NOT_READY] = true,
[_G.SPELL_FAILED_TARGETS_DEAD] = true,
[_G.ERR_GENERIC_NO_TARGET] = true,
}
function mod:SPELL_CAST_FAILED(event, _, sourceName, _, _, _, _, _, spellName, _, reason)
if not ignoredFailures[reason] then
sourceName = strsplit('-', sourceName)
self:Alert('failure', sourceName, spellName, reason)
end
end
function mod:SPELL_MISSED(event, _, sourceName, _, _, _, _, _, spellName, _, missType)
sourceName = strsplit('-', sourceName)
self:Alert('failure', sourceName, spellName, _G[missType] or missType)
end
local function HasOtherSpells(guid, ignoreSpellID)
for spellID, spell in addon:IterateTargetSpells(guid) do
if ignoreSpellID ~= spellID then
return true
end
end
end
function mod:PlanNextUpdate()
self:CancelTimer('PlanNextUpdate')
if not prefs.messages.warning then return end
self:Debug('PlanNextUpdate')
local delay = prefs.delay
local nextTime
local now = GetTime()
for guid, data in addon:IterateTargets() do
local maxTimeLeft, longestSpell
for spellId, spell in addon:IterateTargetSpells(guid) do
local alertTime = spell.expires - delay
if alertTime > now and (not nextTime or alertTime < nextTime) then
nextTime = alertTime
end
local timeLeft = spell.expires - now
if timeLeft > 0 and (not maxTimeLeft or timeLeft > maxTimeLeft) then
maxTimeLeft, longestSpell = timeLeft, spell
end
end
if maxTimeLeft and maxTimeLeft < delay then
if not data.warningAlert then
data.warningAlert = true
self:Alert('warning', nil, longestSpell.target, longestSpell.symbol, longestSpell.expires)
end
else
data.warningAlert = nil
data.removedAlert = nil
end
end
if nextTime then
self:Debug('Next update in', nextTime - now)
self:ScheduleTimer('PlanNextUpdate', nextTime - now)
end
end
function mod:AdiCCMonitor_SpellAdded(event, guid, spellID, spell)
self:PlanNextUpdate()
for otherSpellID, otherSpell in addon:IterateTargetSpells(guid) do
if otherSpellID ~= spellID and otherSpell.expires > spell.expires + 1 then
return
end
end
self:Alert('applied', spell.caster, spell.target, spell.symbol, spell.expires, spell.name)
end
function mod:AdiCCMonitor_SpellRemoved(event, guid, spellID, spell)
self:PlanNextUpdate()
if HasOtherSpells(guid, spellID) then return end
local data = addon:GetGUIDData(guid)
if not data.removedAlert then
data.removedAlert = true
self:Alert("removed", spell.caster, spell.target, spell.symbol, spell.expires)
end
end
function mod:AdiCCMonitor_SpellBroken(event, guid, spellID, spell, brokenByName, brokenBySpell)
self:PlanNextUpdate()
if HasOtherSpells(guid, spellID) then return end
local data = addon:GetGUIDData(guid)
if data.removedAlert then return end
data.removedAlert = true
if prefs.ignoreTank and brokenByName then
local role = UnitGroupRolesAssigned(brokenByName)
if not role then
local raidID = UnitInRaid(brokenByName)
role = raidID and select(10, GetRaidRosterInfo(raidID))
end
if role == "TANK" then
return
end
end
if prefs.messages.early then
if not data.warningAlert then
self:Alert("early", spell.caster, spell.target, spell.symbol, spell.expires, brokenByName, brokenBySpell)
end
else
self:Alert("removed", spell.caster, spell.target, spell.symbol, spell.expires)
end
end
local SYMBOLS = {}
for i = 1, 8 do SYMBOLS[i] = '{'.._G["RAID_TARGET_"..i]..'}' end
function mod:Alert(messageID, caster, ...)
if not prefs.messages[messageID] then
self:Debug(messageID, 'alerts are disabled')
return
elseif not addon.testing and caster ~= playerName and self.chatty and self.announcer ~= playerName then
self:Debug('Ignored alert for', caster, 'since we are not the group announcer')
return
end
self:Debug('Alert', messageID, caster, ...)
local message
if messageID == 'failure' then
local spell, reason = ...
message = format("%s (%s): %s", spell, caster, reason)
else
local target, symbol, expires, moreArg, moreArg2 = ...
local targetName = target
if symbol then
if prefs.sink20OutputSink ~= "Channel" or addon.testing then
targetName = ICON_LIST[symbol].."0|t"
else
targetName = SYMBOLS[symbol]
end
end
local timeLeft = expires and floor(expires - GetTime() + 0.5)
if messageID == 'applied' then
message = format(L['%s is affected by %s, lasting %d seconds.'], targetName, moreArg, timeLeft)
elseif messageID == 'warning' then
message = format(L['%s will break free in %d seconds.'], targetName, timeLeft)
elseif messageID == 'removed' then
message = format(L['%s is free !'], targetName)
elseif messageID == 'early' then
if moreArg then
local name = strsplit('-', moreArg)
message = format(L['%s has been freed by %s !'], targetName, name)
if moreArg2 then
message = format('%s (%s)', message, moreArg2)
end
else
message = format(L['%s has broken free!'], targetName)
end
end
end
if message then
message = '<< '..message..' >>'
if addon.testing then
print("|cff44ffaa"..format(L["%s would send this alert:"], addon.name).."|r\n"..message)
else
self:Pour(message, 1, 1, 1)
end
--@debug@
else
self:Debug('No message to send (!)')
--@end-debug@
end
end
function mod:GetOptions()
-- Dynamic instance type list
local allInstanceList = {
raid = L['Raid instances'],
party = L['5-man instances'],
arena = L['Arenas'],
pvp = L['Battlegrounds'],
none = L['Open world'],
scenario = L['Scenarios']
}
local instanceList = {}
local function GetInstanceList()
for key, label in pairs(allInstanceList) do
instanceList[key] = addon.db.profile.inInstances[key] and label or nil
end
return instanceList
end
-- Fetch LibSink options
local sinkOpts = self:GetSinkAce3OptionsDataTable()
sinkOpts.order = 800
sinkOpts.inline = true
local sinkOptsSet = sinkOpts.set
sinkOpts.set = function(info, ...)
sinkOptsSet(info, ...)
local key = info[#info]
if key ~= "ScrollArea" and key ~= "Sticky" then
return self:OnConfigChanged("sink20OutputSink")
end
end
-- Finally our options
return {
name = L['Alerts'],
type = 'group',
handler = addon:GetOptionHandler(self),
set = 'Set',
get = 'Get',
disabled = 'IsDisabled',
args = {
inInstances = {
name = L['Enabled in ...'],
desc = L['AdiCCMonitor will keep quiet in unchecked zones.'],
order = 5,
type = 'multiselect',
values = GetInstanceList,
},
messages = {
name = L['Events to announce'],
width = 'full',
type = 'multiselect',
values = {
applied = L['Beginning'],
removed = L['End'],
warning = L['About to end'],
failure = L['Failures'],
early = L['Broken early'],
},
order = 10,
},
_earlyNote = {
type = 'description',
name = format(L["Note: '%s' ignores players flagged as tanks."], L['Broken early']),
order = 11,
},
delay = {
name = L['Warning threshold (sec)'],
desc = L['A warning message is sent when the time left for a spell runs below this value.'],
type = 'range',
min = 2,
max = 15,
step = 1,
disabled = function(info) return info.handler:IsDisabled(info) or not prefs.messages.warning end,
order = 20,
},
ignoreTank = {
name = L['Ignore tanks'],
desc = L['Keep quiet when a character flagged as a tank breaks a spell.'],
type = 'toggle',
order = 30,
},
output = sinkOpts,
},
}
end