· 9 years ago · Jan 26, 2017, 10:30 PM
1-- *********************************************************
2-- ** Deadly Boss Mods - Core **
3-- ** http://www.deadlybossmods.com **
4-- ** https://www.patreon.com/deadlybossmods **
5-- *********************************************************
6--
7-- This addon is written and copyrighted by:
8-- * Paul Emmerich (Tandanu @ EU-Aegwynn) (DBM-Core)
9-- * Martin Verges (Nitram @ EU-Azshara) (DBM-GUI)
10-- * Adam Williams (Omegal @ US-Whisperwind) (Primary boss mod author & DBM maintainer)
11--
12-- The localizations are written by:
13-- * enGB/enUS: Omegal Twitter @MysticalOS
14-- * deDE: Ebmor http://www.deadlybossmods.com/forum/memberlist.php?mode=viewprofile&u=79
15-- * ruRU: TOM_RUS http://www.curseforge.com/profiles/TOM_RUS/
16-- * zhTW: Whyv ultrashining@gmail.com
17-- * koKR: nBlueWiz everfinale@gmail.com
18-- * zhCN: Mini Dragon projecteurs@gmail.com
19--
20--
21-- Special thanks to:
22-- * Arta
23-- * Tennberg (a lot of fixes in the enGB/enUS localization)
24-- * nBlueWiz (a lot of fixes in the koKR localization as well as boss mod work) Contact: everfinale@gmail.com
25--
26--
27-- The code of this addon is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 License. (see license.txt)
28-- All included textures and sounds are copyrighted by their respective owners, license information for these media files can be found in the modules that make use of them.
29--
30--
31-- You are free:
32-- * to Share - to copy, distribute, display, and perform the work
33-- * to Remix - to make derivative works
34-- Under the following conditions:
35-- * Attribution. You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work). (A link to http://www.deadlybossmods.com is sufficient)
36-- * Noncommercial. You may not use this work for commercial purposes.
37-- * Share Alike. If you alter, transform, or build upon this work, you may distribute the resulting work only under the same or similar license to this one.
38--
39
40-------------------------------
41-- Globals/Default Options --
42-------------------------------
43DBM = {
44 Revision = tonumber(("$Revision: 15744 $"):sub(12, -3)),
45 DisplayVersion = "7.1.12 alpha", -- the string that is shown as version
46 ReleaseRevision = 15725 -- the revision of the latest stable version that is available
47}
48DBM.HighestRelease = DBM.ReleaseRevision --Updated if newer version is detected, used by update nags to reflect critical fixes user is missing on boss pulls
49
50-- support for git svn which doesn't support svn keyword expansion
51-- just use the latest release revision
52if not DBM.Revision then
53 DBM.Revision = DBM.ReleaseRevision
54end
55
56local testBuild = false
57if IsTestBuild() then
58 testBuild = true
59end
60
61-- dual profile setup
62local _, class = UnitClass("player")
63DBM_UseDualProfile = true
64if class == "MAGE" or class == "WARLOCK" and class == "ROGUE" then
65 DBM_UseDualProfile = false
66end
67DBM_CharSavedRevision = 2
68
69--Hard code STANDARD_TEXT_FONT since skinning mods like to taint it (or worse, set it to nil, wtf?)
70local standardFont = STANDARD_TEXT_FONT
71if (LOCALE_koKR) then
72 standardFont = "Fonts\\2002.TTF"
73elseif (LOCALE_zhCN) then
74 standardFont = "Fonts\\ARKai_T.ttf"
75elseif (LOCALE_zhTW) then
76 standardFont = "Fonts\\blei00d.TTF"
77elseif (LOCALE_ruRU) then
78 standardFont = "Fonts\\FRIZQT___CYR.TTF"
79else
80 standardFont = "Fonts\\FRIZQT__.TTF"
81end
82
83DBM.DefaultOptions = {
84 WarningColors = {
85 {r = 0.41, g = 0.80, b = 0.94}, -- Color 1 - #69CCF0 - Turqoise
86 {r = 0.95, g = 0.95, b = 0.00}, -- Color 2 - #F2F200 - Yellow
87 {r = 1.00, g = 0.50, b = 0.00}, -- Color 3 - #FF8000 - Orange
88 {r = 1.00, g = 0.10, b = 0.10}, -- Color 4 - #FF1A1A - Red
89 },
90 RaidWarningSound = "Sound\\Doodad\\BellTollNightElf.ogg",
91 SpecialWarningSound = "Sound\\Spells\\PVPFlagTaken.ogg",
92 SpecialWarningSound2 = "Sound\\Creature\\AlgalonTheObserver\\UR_Algalon_BHole01.ogg",
93 SpecialWarningSound3 = "Sound\\Creature\\KilJaeden\\KILJAEDEN02.ogg",
94 SpecialWarningSound4 = "Sound\\Creature\\HoodWolf\\HoodWolfTransformPlayer01.ogg",
95 SpecialWarningSound5 = "Sound\\Creature\\Loathstare\\Loa_Naxx_Aggro02.ogg",
96 ModelSoundValue = "Short",
97 CountdownVoice = "Corsica",
98 CountdownVoice2 = "Kolt",
99 CountdownVoice3v2 = "HoTS_R",
100 ChosenVoicePack = "None",
101 VoiceOverSpecW2 = "DefaultOnly",
102 AlwaysPlayVoice = false,
103 ShowCountdownText = false,
104 Enabled = true,
105 ShowWarningsInChat = true,
106 ShowSWarningsInChat = true,
107 WarningIconLeft = true,
108 WarningIconRight = true,
109 WarningIconChat = true,
110 WarningAlphabetical = true,
111 StripServerName = true,
112 ShowAllVersions = true,
113 ShowPizzaMessage = true,
114 ShowEngageMessage = true,
115 ShowDefeatMessage = true,
116 ShowGuildMessages = true,
117 AutoRespond = true,
118 StatusEnabled = true,
119 WhisperStats = false,
120 DisableStatusWhisper = false,
121 HideBossEmoteFrame = true,
122 SpamBlockBossWhispers = true,
123 ShowMinimapButton = false,
124 ShowFlashFrame = true,
125 SWarningAlphabetical = true,
126 SWarnNameInNote = true,
127 CustomSounds = 0,
128 AlwaysShowHealthFrame = false,
129 ShowBigBrotherOnCombatStart = false,
130 FilterTankSpec = true,
131 FilterInterrupt = true,
132 FilterInterruptNoteName = false,
133 FilterDispel = true,
134 FilterSelfHud = true,
135 AutologBosses = false,
136 AdvancedAutologBosses = false,
137 LogOnlyRaidBosses = false,
138 UseSoundChannel = "Master",
139 LFDEnhance = true,
140 WorldBossNearAlert = false,
141 RLReadyCheckSound = true,
142 AFKHealthWarning = false,
143 AutoReplySound = true,
144 HideObjectivesFrame = true,
145 HideGarrisonToasts = true,
146 HideGuildChallengeUpdates = true,
147 HideQuestTooltips = true,
148 HideTooltips = false,
149 DisableSFX = false,
150 EnableModels = true,
151 RangeFrameFrames = "radar",
152 RangeFrameUpdates = "Average",
153 RangeFramePoint = "CENTER",
154 RangeFrameX = 50,
155 RangeFrameY = -50,
156 RangeFrameSound1 = "none",
157 RangeFrameSound2 = "none",
158 RangeFrameLocked = false,
159 RangeFrameRadarPoint = "CENTER",
160 RangeFrameRadarX = 100,
161 RangeFrameRadarY = -100,
162 InfoFramePoint = "CENTER",
163 InfoFrameX = 75,
164 InfoFrameY = -75,
165 InfoFrameShowSelf = false,
166 InfoFrameLines = 0,
167 HPFramePoint = "CENTER",
168 HPFrameX = -50,
169 HPFrameY = 50,
170 HPFrameMaxEntries = 5,
171 WarningDuration = 4,
172 WarningPoint = "CENTER",
173 WarningX = 0,
174 WarningY = 260,
175 WarningFont = standardFont,
176 WarningFontSize = 20,
177 WarningFontStyle = "None",
178 WarningFontShadow = true,
179 SpecialWarningDuration = 4,
180 SpecialWarningPoint = "CENTER",
181 SpecialWarningX = 0,
182 SpecialWarningY = 75,
183 SpecialWarningFont = standardFont,
184 SpecialWarningFontSize2 = 40,
185 SpecialWarningFontStyle = "THICKOUTLINE",
186 SpecialWarningFontShadow = false,
187 SpecialWarningIcon = true,
188 SpecialWarningFontCol = {1.0, 0.7, 0.0},--Yellow, with a tint of orange
189 SpecialWarningFlashCol1 = {1.0, 1.0, 0.0},--Yellow
190 SpecialWarningFlashCol2 = {1.0, 0.5, 0.0},--Orange
191 SpecialWarningFlashCol3 = {1.0, 0.0, 0.0},--Red
192 SpecialWarningFlashCol4 = {1.0, 0.0, 1.0},--Purple
193 SpecialWarningFlashCol5 = {0.2, 1.0, 1.0},--Tealish
194 SpecialWarningFlashDura1 = 0.4,
195 SpecialWarningFlashDura2 = 0.4,
196 SpecialWarningFlashDura3 = 1,
197 SpecialWarningFlashDura4 = 0.7,
198 SpecialWarningFlashDura5 = 1,
199 SpecialWarningFlashAlph1 = 0.3,
200 SpecialWarningFlashAlph2 = 0.3,
201 SpecialWarningFlashAlph3 = 0.4,
202 SpecialWarningFlashAlph4 = 0.4,
203 SpecialWarningFlashAlph5 = 0.5,
204 SpecialWarningFlashRepeat1 = false,
205 SpecialWarningFlashRepeat2 = false,
206 SpecialWarningFlashRepeat3 = true,
207 SpecialWarningFlashRepeat4 = false,
208 SpecialWarningFlashRepeat5 = true,
209 SpecialWarningFlashRepeatAmount = 2,--Repeat 2 times, mean 3 flashes (first plus 2 repeat)
210 SWarnClassColor = true,
211 HUDColorOverride = false,
212 HUDSizeOverride = false,
213 HUDAlphaOverride = false,
214 HUDTextureOverride = false,
215 HUDSize1 = 5,
216 HUDSize2 = 5,
217 HUDSize3 = 5,
218 HUDSize4 = 5,
219 HUDAlpha1 = 0.5,
220 HUDAlpha2 = 0.5,
221 HUDAlpha3 = 0.5,
222 HUDAlpha4 = 0.5,
223 HUDColor1 = {1.0, 1.0, 0.0},--Yellow
224 HUDColor2 = {1.0, 0.0, 0.0},--Red
225 HUDColor3 = {1.0, 0.5, 0.0},--Orange
226 HUDColor4 = {0.0, 1.0, 0.0},--Green
227 HUDTexture1 = "highlight",
228 HUDTexture2 = "highlight",
229 HUDTexture3 = "highlight",
230 HUDTexture4 = "highlight",
231 HealthFrameGrowUp = false,
232 HealthFrameLocked = false,
233 HealthFrameWidth = 200,
234 ArrowPosX = 0,
235 ArrowPosY = -150,
236 ArrowPoint = "TOP",
237 -- global boss mod settings (overrides mod-specific settings for some options)
238 DontShowBossAnnounces = false,
239 DontShowSpecialWarnings = false,
240 DontShowBossTimers = false,
241 DontShowUserTimers = false,
242 DontShowFarWarnings = true,
243 DontSetIcons = false,
244 DontRestoreIcons = false,
245 DontShowRangeFrame = false,
246 DontRestoreRange = false,
247 DontShowInfoFrame = false,
248 DontShowHudMap2 = false,
249 DontShowHealthFrame = false,
250 DontPlayCountdowns = false,
251 DontSendYells = false,
252 BlockNoteShare = false,
253 DontShowPT2 = false,
254 DontShowPTCountdownText = false,
255 DontPlayPTCountdown = false,
256 DontShowPTText = false,
257 DontShowPTNoID = false,
258 DontShowCTCount = false,
259 DontShowFlexMessage = false,
260 PTCountThreshold = 5,
261 LatencyThreshold = 250,
262 BigBrotherAnnounceToRaid = false,
263 SettingsMessageShown = false,
264 ForumsMessageShown = false,
265 PGMessageShown = false,
266 MCMessageShown = false,
267 BCTWMessageShown = false,
268 WOTLKTWMessageShown = false,
269 CATATWMessageShown = false,
270 AlwaysShowSpeedKillTimer = true,
271 CRT_Enabled = false,
272 ShowRespawn = true,
273 ShowQueuePop = true,
274 HelpMessageVersion = 3,
275 NewsMessageShown = 4,
276 MoviesSeen = {},
277 MovieFilter = "AfterFirst",
278 LastRevision = 0,
279 FilterSayAndYell = false,
280 DebugMode = false,
281 DebugLevel = 1,
282 RoleSpecAlert = true,
283 CheckGear = true,
284 WorldBossAlert = false,
285 AutoAcceptFriendInvite = false,
286 AutoAcceptGuildInvite = false,
287 FakeBWVersion = false,
288 AITimer = true,
289 AutoCorrectTimer = false,
290 ShortTimerText = true,
291 ChatFrame = "DEFAULT_CHAT_FRAME",
292}
293
294DBM.Bars = DBT:New()
295DBM.Mods = {}
296DBM.ModLists = {}
297DBM.Counts = {
298 { text = "Moshne (Male)", value = "Mosh", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Mosh\\", max = 5},
299 { text = "Corsica (Female)",value = "Corsica", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Corsica\\", max = 10},
300 { text = "Koltrane (Male)",value = "Kolt", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Kolt\\", max = 10},
301 { text = "Pewsey (Male)",value = "Pewsey", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Pewsey\\", max = 10},
302 { text = "Bear (Male Child)",value = "Bear", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Bear\\", max = 10},
303 { text = "Overwatch: Announcer", value = "Overwatch", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Announcer\\", max = 10},
304 { text = "Overwatch: Bastion", value = "Bastion", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Bastion\\", max = 5},
305 { text = "Overwatch: D.Va", value = "DVa", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\DVa\\", max = 5},
306 { text = "Overwatch: D.Va (Korean)", value = "DVakr", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\DVa\\kr\\", max = 5},
307 { text = "Overwatch: Genji", value = "Genji", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Genji\\", max = 5},
308 { text = "Overwatch: Genji (Japanese)", value = "Genjijp", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Genji\\jp\\", max = 5},
309 { text = "Overwatch: Hanzo", value = "Hanzo", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Hanzo\\", max = 5},
310 { text = "Overwatch: Junkrat", value = "Junkrat", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Junkrat\\", max = 5},
311 { text = "Overwatch: Lucio", value = "Lucio", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Lucio\\", max = 5},
312 { text = "Overwatch: Mccree", value = "Mccree", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Mccree\\", max = 5},
313 { text = "Overwatch: Mei", value = "Mei", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Mei\\", max = 5},
314 { text = "Overwatch: Mei (Chinese)", value = "Meicn", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Mei\\cn\\", max = 5},
315 { text = "Overwatch: Mercy", value = "Mercy", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Mercy\\", max = 5},
316 { text = "Overwatch: Pharah", value = "Pharah", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Pharah\\", max = 5},
317 { text = "Overwatch: Reaper", value = "Reaper", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Reaper\\", max = 5},
318 { text = "Overwatch: Reinhardt", value = "Reinhardt", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Reinhardt\\", max = 5},
319 { text = "Overwatch: Roadhog", value = "Roadhog", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Roadhog\\", max = 5},
320 { text = "Overwatch: Soldier", value = "Soldier", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Soldier\\", max = 5},
321 { text = "Overwatch: Symmetra", value = "Symmetra", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Symmetra\\", max = 5},
322 { text = "Overwatch: Torbjorn", value = "Torbjorn", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Torbjorn\\", max = 5},
323 { text = "Overwatch: Tracer", value = "Tracer", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Tracer\\", max = 5},
324 { text = "Overwatch: Widowmaker", value = "Widowmaker", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Widowmaker\\", max = 5},
325 { text = "Overwatch: Winston", value = "Winston", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Winston\\", max = 5},
326 { text = "Overwatch: Zarya", value = "Zarya", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Zarya\\", max = 5},
327 { text = "Overwatch: Zenyatta", value = "Zenyatta", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Zenyatta\\", max = 5},
328 { text = "HoTS: Default", value = "HoTS_D", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Heroes\\Default\\", max = 5},
329 { text = "HoTS: Blackheart", value = "HoTS_B", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Heroes\\Blackheart\\", max = 5},
330 { text = "HoTS: Gardens", value = "HoTS_G", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Heroes\\Gardens\\", max = 5},
331 { text = "HoTS: Lady of Thorns", value = "HoTS_T", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Heroes\\LadyOfThorns\\", max = 5},
332 { text = "HoTS: Necromancer", value = "HoTS_N", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Heroes\\Necromancer\\", max = 5},
333 { text = "HoTS: Ravenlord", value = "HoTS_R", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Heroes\\RavenLord\\", max = 5},
334 { text = "HoTS: Snake", value = "HoTS_S", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Heroes\\Snake\\", max = 5},
335 { text = "Anshlun (ptBR Male)",value = "Anshlun", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Anshlun\\", max = 10},
336 { text = "Neryssa (ptBR Female)",value = "Neryssa", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Neryssa\\", max = 10},
337}
338
339------------------------
340-- Global Identifiers --
341------------------------
342DBM_DISABLE_ZONE_DETECTION = newproxy(false)
343DBM_OPTION_SPACER = newproxy(false)
344
345--------------
346-- Locals --
347--------------
348local bossModPrototype = {}
349local usedProfile = "Default"
350local dbmIsEnabled = true
351local lastCombatStarted = GetTime()
352local loadcIds = {}
353local inCombat = {}
354local combatInfo = {}
355local bossIds = {}
356local updateFunctions = {}
357local raid = {}
358local modSyncSpam = {}
359local autoRespondSpam = {}
360local chatPrefix = "<Deadly Boss Mods> "
361local chatPrefixShort = "<DBM> "
362local ver = ("%s (r%d)"):format(DBM.DisplayVersion, DBM.Revision)
363local mainFrame = CreateFrame("Frame", "DBMMainFrame")
364local newerVersionPerson = {}
365local newerRevisionPerson = {}
366local combatInitialized = false
367local healthCombatInitialized = false
368local pformat
369local schedulerFrame = CreateFrame("Frame", "DBMScheduler")
370schedulerFrame:Hide()
371local startScheduler
372local schedule
373local unschedule
374local unscheduleAll
375local loadOptions
376local checkWipe
377local checkBossHealth
378local checkCustomBossHealth
379local loopCRTimer
380local fireEvent
381local playerName = UnitName("player")
382local playerLevel = UnitLevel("player")
383local playerRealm = GetRealmName()
384local connectedServers = GetAutoCompleteRealms()
385local LastInstanceMapID = -1
386local LastGroupSize = 0
387local LastInstanceType = nil
388local queuedBattlefield = {}
389local loadDelay = nil
390local loadDelay2 = nil
391local noDelay = true
392local watchFrameRestore = false
393local bossHealth = {}
394local bossHealthuIdCache = {}
395local bossuIdCache = {}
396local savedDifficulty, difficultyText, difficultyIndex
397local lastBossEngage = {}
398local lastBossDefeat = {}
399local bossuIdFound = false
400local timerRequestInProgress = false
401local updateNotificationDisplayed = 0
402local showConstantReminder = 0
403local tooltipsHidden = false
404local SWFilterDisabed = 3
405local currentSpecGroup = GetSpecialization() or 1
406local currentSpecID, currentSpecName
407local cSyncSender = {}
408local cSyncReceived = 0
409local eeSyncSender = {}
410local eeSyncReceived = 0
411local canSetIcons = {}
412local iconSetRevision = {}
413local iconSetPerson = {}
414local addsGUIDs = {}
415local targetEventsRegistered = false
416local targetMonitor = nil
417local statusWhisperDisabled = false
418local wowVersionString, _, _, wowTOC = GetBuildInfo()
419local dbmToc = 0
420local UpdateChestTimer
421local breakTimerStart
422
423local fakeBWVersion, fakeBWHash = 39, "7aa089f"
424local versionQueryString, versionResponseString = "Q^%d^%s", "V^%d^%s"
425
426local enableIcons = true -- set to false when a raid leader or a promoted player has a newer version of DBM
427local guiRequested = false
428
429local bannedMods = { -- a list of "banned" (meaning they are replaced by another mod like DBM-Battlegrounds (replaced by DBM-PvP)) boss mods, these mods will not be loaded by DBM (and they wont show up in the GUI)
430 "DBM-Battlegrounds", --replaced by DBM-PvP
431 -- ZG and ZA are now part of the party mods for Cataclysm
432 "DBM-ZulAman",
433 "DBM-ZG",
434 "DBM-SiegeOfOrgrimmar",--Block legacy version. New version is "DBM-SiegeOfOrgrimmarV2"
435 "DBM-HighMail",
436 "DBM-ProvingGrounds-MoP",--Renamed to DBM-ProvingGrounds in 6.0 version since blizzard updated content for WoD
437 "DBM-VPKiwiBeta",--Renamed to DBM-VPKiwi in final version.
438 "DBM-Suramar",--Renamed to DBM-Nighthold
439}
440
441
442-----------------
443-- Libraries --
444-----------------
445local LL
446if LibStub("LibLatency", true) then
447 LL = LibStub("LibLatency")
448end
449
450
451--------------------------------------------------------
452-- Cache frequently used global variables in locals --
453--------------------------------------------------------
454local DBM = DBM
455-- these global functions are accessed all the time by the event handler
456-- so caching them is worth the effort
457local ipairs, pairs, next = ipairs, pairs, next
458local tinsert, tremove, twipe, tsort, tconcat = table.insert, table.remove, table.wipe, table.sort, table.concat
459local type, select = type, select
460local GetTime = GetTime
461local bband = bit.band
462local floor, mhuge, mmin, mmax = math.floor, math.huge, math.min, math.max
463local GetNumGroupMembers, GetRaidRosterInfo = GetNumGroupMembers, GetRaidRosterInfo
464local UnitName, GetUnitName = UnitName, GetUnitName
465local IsInRaid, IsInGroup, IsInInstance = IsInRaid, IsInGroup, IsInInstance
466local UnitAffectingCombat, InCombatLockdown, IsEncounterInProgress, UnitPlayerOrPetInRaid, UnitPlayerOrPetInParty = UnitAffectingCombat, InCombatLockdown, IsEncounterInProgress, UnitPlayerOrPetInRaid, UnitPlayerOrPetInParty
467local UnitGUID, UnitHealth, UnitHealthMax, UnitBuff = UnitGUID, UnitHealth, UnitHealthMax, UnitBuff
468local UnitExists, UnitIsDead, UnitIsFriend, UnitIsUnit, UnitIsAFK = UnitExists, UnitIsDead, UnitIsFriend, UnitIsUnit, UnitIsAFK
469local GetSpellInfo, EJ_GetSectionInfo, GetSpellTexture, GetSpellCooldown = GetSpellInfo, EJ_GetSectionInfo, GetSpellTexture, GetSpellCooldown
470local EJ_GetEncounterInfo, EJ_GetCreatureInfo, GetDungeonInfo = EJ_GetEncounterInfo, EJ_GetCreatureInfo, GetDungeonInfo
471local GetInstanceInfo = GetInstanceInfo
472local UnitPosition, GetCurrentMapDungeonLevel, GetMapInfo, GetCurrentMapZone, SetMapToCurrentZone, GetPlayerMapAreaID = UnitPosition, GetCurrentMapDungeonLevel, GetMapInfo, GetCurrentMapZone, SetMapToCurrentZone, GetPlayerMapAreaID
473local GetSpecialization, GetSpecializationInfo, GetSpecializationInfoByID = GetSpecialization, GetSpecializationInfo, GetSpecializationInfoByID
474local UnitDetailedThreatSituation = UnitDetailedThreatSituation
475local GetPartyAssignment, UnitGroupRolesAssigned, UnitIsGroupLeader, UnitIsGroupAssistant, UnitRealmRelationship = GetPartyAssignment, UnitGroupRolesAssigned, UnitIsGroupLeader, UnitIsGroupAssistant, UnitRealmRelationship
476local LoadAddOn, GetAddOnInfo, GetAddOnEnableState, GetAddOnMetadata, GetNumAddOns = LoadAddOn, GetAddOnInfo, GetAddOnEnableState, GetAddOnMetadata, GetNumAddOns
477local PlaySoundFile, PlaySound = PlaySoundFile, PlaySound
478local Ambiguate = Ambiguate
479local C_TimerNewTicker, C_TimerAfter = C_Timer.NewTicker, C_Timer.After
480local BNGetGameAccountInfo = BNGetGameAccountInfo
481
482-- for Phanx' Class Colors
483local RAID_CLASS_COLORS = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
484
485---------------------------------
486-- General (local) functions --
487---------------------------------
488-- checks if a given value is in an array
489-- returns true if it finds the value, false otherwise
490local function checkEntry(t, val)
491 for i, v in ipairs(t) do
492 if v == val then
493 return true
494 end
495 end
496 return false
497end
498
499local function findEntry(t, val)
500 for i, v in ipairs(t) do
501 if v and val and val:find(v) then
502 return true
503 end
504 end
505 return false
506end
507
508-- removes all occurrences of a value in an array
509-- returns true if at least one occurrence was remove, false otherwise
510local function removeEntry(t, val)
511 local existed = false
512 for i = #t, 1, -1 do
513 if t[i] == val then
514 tremove(t, i)
515 existed = true
516 end
517 end
518 return existed
519end
520
521-- automatically sends an addon message to the appropriate channel (INSTANCE_CHAT, RAID or PARTY)
522local function sendSync(prefix, msg)
523 msg = msg or ""
524 if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and IsInInstance() and not C_Garrison:IsOnGarrisonMap() then--For BGs, LFR and LFG (we also check IsInInstance() so if you're in queue but fighting something outside like a world boss, it'll sync in "RAID" instead)
525 SendAddonMessage("D4", prefix .. "\t" .. msg, "INSTANCE_CHAT")
526 else
527 if IsInRaid() then
528 SendAddonMessage("D4", prefix .. "\t" .. msg, "RAID")
529 elseif IsInGroup(LE_PARTY_CATEGORY_HOME) then
530 SendAddonMessage("D4", prefix .. "\t" .. msg, "PARTY")
531 else--for solo raid
532 SendAddonMessage("D4", prefix .. "\t" .. msg, "WHISPER", playerName)
533 end
534 end
535end
536
537local function strFromTime(time)
538 if type(time) ~= "number" then time = 0 end
539 time = floor(time*100)/100
540 if time < 60 then
541 return DBM_CORE_TIMER_FORMAT_SECS:format(time)
542 elseif time % 60 == 0 then
543 return DBM_CORE_TIMER_FORMAT_MINS:format(time/60)
544 else
545 return DBM_CORE_TIMER_FORMAT:format(time/60, time % 60)
546 end
547end
548
549do
550 -- fail-safe format, replaces missing arguments with unknown
551 -- note: doesn't handle cases like %%%s correctly at the moment (should become %unknown, but becomes %%s)
552 -- also, the end of the format directive is not detected in all cases, but handles everything that occurs in our boss mods ;)
553 --> not suitable for general-purpose use, just for our warnings and timers (where an argument like a spell-target might be nil due to missing target information from unreliable detection methods)
554 local function replace(cap1, cap2)
555 return cap1 == "%" and DBM_CORE_UNKNOWN
556 end
557
558 function pformat(fstr, ...)
559 local ok, str = pcall(format, fstr, ...)
560 return ok and str or fstr:gsub("(%%+)([^%%%s<]+)", replace):gsub("%%%%", "%%")
561 end
562end
563
564-- sends a whisper to a player by his or her character name or BNet presence id
565-- returns true if the message was sent, nil otherwise
566local function sendWhisper(target, msg)
567 if type(target) == "number" then
568 if not BNIsSelf(target) then -- never send BNet whispers to ourselves
569 BNSendWhisper(target, msg)
570 return true
571 end
572 elseif type(target) == "string" then
573 -- whispering to ourselves here is okay and somewhat useful for whisper-warnings
574 SendChatMessage(msg, "WHISPER", nil, target)
575 return true
576 end
577end
578local BNSendWhisper = sendWhisper
579
580local function stripServerName(cap)
581 cap = cap:sub(2, -2)
582 if DBM.Options.StripServerName then
583 cap = Ambiguate(cap, "short")
584 end
585 return cap
586end
587
588local function countDownTextDelay(timer)
589 TimerTracker_OnEvent(TimerTracker, "START_TIMER", 2, timer+1, timer+1)
590end
591
592--------------
593-- Events --
594--------------
595do
596 local registeredEvents = {}
597 local registeredSpellIds = {}
598 local unfilteredCLEUEvents = {}
599 local registeredUnitEventIds = {}
600 local argsMT = {__index = {}}
601 local args = setmetatable({}, argsMT)
602
603 function argsMT.__index:IsSpellID(a1, a2, a3, a4, a5)
604 local v = self.spellId
605 return v == a1 or v == a2 or v == a3 or v == a4 or v == a5
606 end
607
608 function argsMT.__index:IsPlayer()
609 return bband(args.destFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) ~= 0 and bband(args.destFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
610 end
611
612 function argsMT.__index:IsPlayerSource()
613 return bband(args.sourceFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) ~= 0 and bband(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
614 end
615
616 function argsMT.__index:IsNPC()
617 return bband(args.destFlags, COMBATLOG_OBJECT_TYPE_NPC) ~= 0
618 end
619
620 function argsMT.__index:IsPet()
621 return bband(args.destFlags, COMBATLOG_OBJECT_TYPE_PET) ~= 0
622 end
623
624 function argsMT.__index:IsPetSource()
625 return bband(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PET) ~= 0
626 end
627
628 function argsMT.__index:IsSrcTypePlayer()
629 return bband(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
630 end
631
632 function argsMT.__index:IsDestTypePlayer()
633 return bband(args.destFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
634 end
635
636 function argsMT.__index:IsSrcTypeHostile()
637 return bband(args.sourceFlags, COMBATLOG_OBJECT_REACTION_HOSTILE) ~= 0
638 end
639
640 function argsMT.__index:IsDestTypeHostile()
641 return bband(args.destFlags, COMBATLOG_OBJECT_REACTION_HOSTILE) ~= 0
642 end
643
644 function argsMT.__index:GetSrcCreatureID()
645 return DBM:GetCIDFromGUID(self.sourceGUID)
646 end
647
648 function argsMT.__index:GetDestCreatureID()
649 return DBM:GetCIDFromGUID(self.destGUID)
650 end
651
652 local function handleEvent(self, event, ...)
653 local isUnitEvent = event:sub(0, 5) == "UNIT_" and event ~= "UNIT_DIED" and event ~= "UNIT_DESTROYED"
654 if self == mainFrame and isUnitEvent then
655 -- UNIT_* events that come from mainFrame are _UNFILTERED variants and need their suffix
656 event = event .. "_UNFILTERED"
657 isUnitEvent = false -- not actually a real unit id for this function...
658 end
659 if not registeredEvents[event] or not dbmIsEnabled then return end
660 for i, v in ipairs(registeredEvents[event]) do
661 local zones = v.zones
662 local handler = v[event]
663 local modEvents = v.registeredUnitEvents
664 if handler and (not isUnitEvent or not modEvents or modEvents[event .. ...]) and (not zones or zones[LastInstanceMapID]) and not (v.isTrashMod and #inCombat > 0) then
665 handler(v, ...)
666 end
667 end
668 end
669
670 local registerUnitEvent, unregisterUnitEvent, registerSpellId, unregisterSpellId, registerCLEUEvent, unregisterCLEUEvent
671 do
672 local frames = {} -- frames that are being used for unit events, one frame per unit id (this could be optimized, as it currently creates a new frame even for a different event, but that's not worth the effort as 90% of all calls are just boss1 anyways)
673
674 function registerUnitEvent(mod, event, ...)
675 mod.registeredUnitEvents = mod.registeredUnitEvents or {}
676 for i = 1, select("#", ...) do
677 local uId = select(i, ...)
678 if not uId then break end
679 local frame = frames[uId]
680 if not frame then
681 frame = CreateFrame("Frame")
682 if uId == "mouseover" then
683 -- work-around for mouse-over events (broken!)
684 frame:SetScript("OnEvent", function(self, event, uId, ...)
685 -- we registered mouseover events, so we only want mouseover events, thanks.
686 handleEvent(self, event, "mouseover", ...)
687 end)
688 else
689 frame:SetScript("OnEvent", handleEvent)
690 end
691 frames[uId] = frame
692 end
693 registeredUnitEventIds[event .. uId] = (registeredUnitEventIds[event .. uId] or 0) + 1
694 mod.registeredUnitEvents[event .. uId] = true
695 frame:RegisterUnitEvent(event, uId)
696 end
697 end
698
699 function unregisterUnitEvent(mod, event, ...)
700 for i = 1, select("#", ...) do
701 local uId = select(i, ...)
702 if not uId then break end
703 local frame = frames[uId]
704 local refs = (registeredUnitEventIds[event .. uId] or 1) - 1
705 registeredUnitEventIds[event .. uId] = refs
706 if refs <= 0 then
707 registeredUnitEventIds[event .. uId] = nil
708 if frame then
709 frame:UnregisterEvent(event)
710 end
711 end
712 if mod.registeredUnitEvents and mod.registeredUnitEvents[event .. uId] then
713 mod.registeredUnitEvents[event .. uId] = nil
714 end
715 end
716 for i = #registeredEvents[event], 1, -1 do
717 if registeredEvents[event][i] == mod then
718 tremove(registeredEvents[event], i)
719 end
720 end
721 if #registeredEvents[event] == 0 then
722 registeredEvents[event] = nil
723 end
724 end
725
726 function registerSpellId(event, spellId)
727 if type(spellId) == "string" then--Something is screwed up, like SPELL_AURA_APPLIED DOSE
728 DBM:AddMsg("DBM RegisterEvents Error: "..spellId.." is not a number!")
729 end
730 if spellId and not GetSpellInfo(spellId) then
731 DBM:AddMsg("DBM RegisterEvents Error: "..spellId.." spell id does not exist!")
732 return
733 end
734 if not registeredSpellIds[event] then
735 registeredSpellIds[event] = {}
736 end
737 registeredSpellIds[event][spellId] = (registeredSpellIds[event][spellId] or 0) + 1
738 end
739
740 function unregisterSpellId(event, spellId)
741 if not registeredSpellIds[event] then return end
742 local refs = (registeredSpellIds[event][spellId] or 1) - 1
743 registeredSpellIds[event][spellId] = refs
744 if refs <= 0 then
745 registeredSpellIds[event][spellId] = nil
746 end
747 end
748
749 --There are 2 tables. unfilteredCLEUEvents and registeredSpellIds table.
750 --unfilteredCLEUEvents saves UNFILTERED cleu event count. this is count table to prevent bad unregister.
751 --registeredSpellIds tables filtered table. this saves event and spell ids. works smiliar with unfilteredCLEUEvents table.
752 function registerCLEUEvent(mod, event)
753 local argTable = {strsplit(" ", event)}
754 -- filtered cleu event. save information in registeredSpellIds table.
755 if #argTable > 1 then
756 event = argTable[1]
757 for i = 2, #argTable do
758 registerSpellId(event, tonumber(argTable[i]))
759 end
760 -- no args. works as unfiltered. save information in unfilteredCLEUEvents table.
761 else
762 unfilteredCLEUEvents[event] = (unfilteredCLEUEvents[event] or 0) + 1
763 end
764 registeredEvents[event] = registeredEvents[event] or {}
765 tinsert(registeredEvents[event], mod)
766 end
767
768 function unregisterCLEUEvent(mod, event)
769 local argTable = {strsplit(" ", event)}
770 local eventCleared = false
771 -- filtered cleu event. save information in registeredSpellIds table.
772 if #argTable > 1 then
773 event = argTable[1]
774 for i = 2, #argTable do
775 unregisterSpellId(event, tonumber(argTable[i]))
776 end
777 local remainingSpellIdCount = 0
778 if registeredSpellIds[event] then
779 for i, v in pairs(registeredSpellIds[event]) do
780 remainingSpellIdCount = remainingSpellIdCount + 1
781 end
782 end
783 if remainingSpellIdCount == 0 then
784 registeredSpellIds[event] = nil
785 -- if unfilteredCLEUEvents and registeredSpellIds do not exists, clear registeredEvents.
786 if not unfilteredCLEUEvents[event] then
787 eventCleared = true
788 end
789 end
790 -- no args. works as unfiltered. save information in unfilteredCLEUEvents table.
791 else
792 local refs = (unfilteredCLEUEvents[event] or 1) - 1
793 unfilteredCLEUEvents[event] = refs
794 if refs <= 0 then
795 unfilteredCLEUEvents[event] = nil
796 -- if unfilteredCLEUEvents and registeredSpellIds do not exists, clear registeredEvents.
797 if not registeredSpellIds[event] then
798 eventCleared = true
799 end
800 end
801 end
802 for i = #registeredEvents[event], 1, -1 do
803 if registeredEvents[event][i] == mod then
804 registeredEvents[event][i] = {}
805 break
806 end
807 end
808 if eventCleared then
809 registeredEvents[event] = nil
810 end
811 end
812 end
813
814 -- UNIT_* events are special: they can take 'parameters' like this: "UNIT_HEALTH boss1 boss2" which only trigger the event for the given unit ids
815 function DBM:RegisterEvents(...)
816 for i = 1, select("#", ...) do
817 local event = select(i, ...)
818 -- spell events with special care.
819 if event:sub(0, 6) == "SPELL_" or event:sub(0, 6) == "RANGE_" then
820 registerCLEUEvent(self, event)
821 else
822 local eventWithArgs = event
823 -- unit events need special care
824 if event:sub(0, 5) == "UNIT_" and event ~= "UNIT_DIED" and event ~= "UNIT_DESTROYED" then
825 -- unit events are limited to 8 "parameters", as there is no good reason to ever use more than 5 (it's just that the code old code supported 8 (boss1-5, target, focus))
826 local arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8
827 event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 = strsplit(" ", event)
828 if not arg1 and event:sub(event:len() - 10) ~= "_UNFILTERED" then -- no arguments given, support for legacy mods
829 eventWithArgs = event .. " boss1 boss2 boss3 boss4 boss5 target focus"
830 event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 = strsplit(" ", eventWithArgs)
831 end
832 if event:sub(event:len() - 10) == "_UNFILTERED" then
833 -- we really want *all* unit ids
834 mainFrame:RegisterEvent(event:sub(0, -12))
835 else
836 registerUnitEvent(self, event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)
837 end
838 -- spell events with filter
839 else
840 -- normal events
841 mainFrame:RegisterEvent(event)
842 end
843 registeredEvents[eventWithArgs] = registeredEvents[eventWithArgs] or {}
844 tinsert(registeredEvents[eventWithArgs], self)
845 if event ~= eventWithArgs then
846 registeredEvents[event] = registeredEvents[event] or {}
847 tinsert(registeredEvents[event], self)
848 end
849 end
850 end
851 end
852
853 local function unregisterUEvent(mod, event)
854 if event:sub(0, 5) == "UNIT_" and event ~= "UNIT_DIED" and event ~= "UNIT_DESTROYED" then
855 local event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 = strsplit(" ", event)
856 if event:sub(event:len() - 10) == "_UNFILTERED" then
857 mainFrame:UnregisterEvent(event:sub(0, -12))
858 else
859 unregisterUnitEvent(mod, event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)
860 end
861 end
862 end
863
864 local function findRealEvent(t, val)
865 for i, v in ipairs(t) do
866 local event = strsplit(" ", v)
867 if event == val then
868 return v
869 end
870 end
871 end
872
873 function DBM:UnregisterInCombatEvents(srmOnly)
874 for event, mods in pairs(registeredEvents) do
875 if srmOnly then
876 local i = 1
877 while mods[i] do
878 if mods[i] == self and event == "SPELL_AURA_REMOVED" then
879 local findEvent = findRealEvent(self.inCombatOnlyEvents, "SPELL_AURA_REMOVED")
880 if findEvent then
881 unregisterCLEUEvent(self, findEvent)
882 break
883 end
884 end
885 i = i +1
886 end
887 elseif (event:sub(0, 6) == "SPELL_" or event:sub(0, 6) == "RANGE_") then
888 local i = 1
889 while mods[i] do
890 if mods[i] == self and event ~= "SPELL_AURA_REMOVED" then
891 local findEvent = findRealEvent(self.inCombatOnlyEvents, event)
892 if findEvent then
893 unregisterCLEUEvent(self, findEvent)
894 break
895 end
896 end
897 i = i +1
898 end
899 else
900 local match = false
901 for i = #mods, 1, -1 do
902 if mods[i] == self and checkEntry(self.inCombatOnlyEvents, event) then
903 tremove(mods, i)
904 match = true
905 end
906 end
907 if #mods == 0 or (match and event:sub(0, 5) == "UNIT_" and event:sub(0, -10) ~= "_UNFILTERED" and event ~= "UNIT_DIED" and event ~= "UNIT_DESTROYED") then -- unit events have their own reference count
908 unregisterUEvent(self, event)
909 end
910 if #mods == 0 then
911 registeredEvents[event] = nil
912 end
913 end
914 end
915 end
916
917 function DBM:RegisterShortTermEvents(...)
918 if self.shortTermEventsRegistered then
919 return
920 end
921 self.shortTermRegisterEvents = {...}
922 for k, v in pairs(self.shortTermRegisterEvents) do
923 if v:sub(0, 5) == "UNIT_" and v:sub(v:len() - 10) ~= "_UNFILTERED" and not v:find(" ") and v ~= "UNIT_DIED" and v ~= "UNIT_DESTROYED" then
924 -- legacy event, oh noes
925 self.shortTermRegisterEvents[k] = v .. " boss1 boss2 boss3 boss4 boss5 target focus"
926 end
927 end
928 self.shortTermEventsRegistered = 1
929 self:RegisterEvents(unpack(self.shortTermRegisterEvents))
930 end
931
932 function DBM:UnregisterShortTermEvents()
933 if self.shortTermRegisterEvents then
934 for event, mods in pairs(registeredEvents) do
935 if event:sub(0, 6) == "SPELL_" or event:sub(0, 6) == "RANGE_" then
936 local i = 1
937 while mods[i] do
938 if mods[i] == self then
939 local findEvent = findRealEvent(self.shortTermRegisterEvents, event)
940 if findEvent then
941 unregisterCLEUEvent(self, findEvent)
942 break
943 end
944 end
945 i = i +1
946 end
947 else
948 local match = false
949 for i = #mods, 1, -1 do
950 if mods[i] == self and checkEntry(self.shortTermRegisterEvents, event) then
951 tremove(mods, i)
952 match = true
953 end
954 end
955 if #mods == 0 or (match and event:sub(0, 5) == "UNIT_" and event:sub(0, -10) ~= "_UNFILTERED" and event ~= "UNIT_DIED" and event ~= "UNIT_DESTROYED") then
956 unregisterUEvent(self, event)
957 end
958 if #mods == 0 then
959 registeredEvents[event] = nil
960 end
961 end
962 end
963 self.shortTermEventsRegistered = nil
964 self.shortTermRegisterEvents = nil
965 end
966 end
967
968 DBM:RegisterEvents("ADDON_LOADED")
969
970 function DBM:FilterRaidBossEmote(msg, ...)
971 return handleEvent(nil, "CHAT_MSG_RAID_BOSS_EMOTE_FILTERED", msg:gsub("\124c%x+(.-)\124r", "%1"), ...)
972 end
973
974 local noArgTableEvents = {
975 SWING_DAMAGE = true,
976 SWING_MISSED = true,
977 RANGE_DAMAGE = true,
978 RANGE_MISSED = true,
979 SPELL_DAMAGE = true,
980 SPELL_BUILDING_DAMAGE = true,
981 SPELL_MISSED = true,
982 SPELL_ABSORBED = true,
983 SPELL_HEAL = true,
984 SPELL_ENERGIZE = true,
985 SPELL_PERIODIC_ENERGIZE = true,
986 SPELL_PERIODIC_MISSED = true,
987 SPELL_PERIODIC_DAMAGE = true,
988 SPELL_PERIODIC_DRAIN = true,
989 SPELL_PERIODIC_LEECH = true,
990 SPELL_PERIODIC_ENERGIZE = true,
991 SPELL_DRAIN = true,
992 SPELL_LEECH = true,
993 SPELL_CAST_FAILED = true
994 }
995 function DBM:COMBAT_LOG_EVENT_UNFILTERED(timestamp, event, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, ...)
996 if not registeredEvents[event] then return end
997 local eventSub6 = event:sub(0, 6)
998 if (eventSub6 == "SPELL_" or eventSub6 == "RANGE_") and not unfilteredCLEUEvents[event] then
999 local spellId = ...
1000 if not registeredSpellIds[event][spellId] then return end
1001 end
1002 -- process some high volume events without building the whole table which is somewhat faster
1003 -- this prevents work-around with mods that used to have their own event handler to prevent this overhead
1004 if noArgTableEvents[event] then
1005 return handleEvent(nil, event, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, ...)
1006 else
1007 twipe(args)
1008 args.timestamp = timestamp
1009 args.event = event
1010 args.sourceGUID = sourceGUID
1011 args.sourceName = sourceName
1012 args.sourceFlags = sourceFlags
1013 args.sourceRaidFlags = sourceRaidFlags
1014 args.destGUID = destGUID
1015 args.destName = destName
1016 args.destFlags = destFlags
1017 args.destRaidFlags = destRaidFlags
1018 if eventSub6 == "SPELL_" then
1019 args.spellId, args.spellName = ...
1020 if event == "SPELL_AURA_APPLIED" or event == "SPELL_AURA_REFRESH" or event == "SPELL_AURA_REMOVED" then
1021 if not args.sourceName then
1022 args.sourceName = args.destName
1023 args.sourceGUID = args.destGUID
1024 args.sourceFlags = args.destFlags
1025 end
1026 elseif event == "SPELL_AURA_APPLIED_DOSE" or event == "SPELL_AURA_REMOVED_DOSE" then
1027 local _
1028 _, _, _, _, args.amount = ...
1029 if not args.sourceName then
1030 args.sourceName = args.destName
1031 args.sourceGUID = args.destGUID
1032 args.sourceFlags = args.destFlags
1033 end
1034 elseif event == "SPELL_INTERRUPT" or event == "SPELL_DISPEL" or event == "SPELL_DISPEL_FAILED" or event == "SPELL_AURA_STOLEN" then
1035 local _
1036 _, _, _, args.extraSpellId, args.extraSpellName = ...
1037 end
1038 elseif event == "UNIT_DIED" or event == "UNIT_DESTROYED" then
1039 args.sourceName = args.destName
1040 args.sourceGUID = args.destGUID
1041 args.sourceFlags = args.destFlags
1042 elseif event == "ENVIRONMENTAL_DAMAGE" then
1043 args.environmentalType, args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = ...
1044 end
1045 return handleEvent(nil, event, args)
1046 end
1047 end
1048 mainFrame:SetScript("OnEvent", handleEvent)
1049end
1050
1051--------------
1052-- OnLoad --
1053--------------
1054do
1055 local isLoaded = false
1056 local onLoadCallbacks = {}
1057
1058 local function runDelayedFunctions(self)
1059 noDelay = false
1060 --Check if voice pack missing
1061 local activeVP = self.Options.ChosenVoicePack
1062 if activeVP ~= "None" then
1063 if not self.VoiceVersions[activeVP] or (self.VoiceVersions[activeVP] and self.VoiceVersions[activeVP] == 0) then--A voice pack is selected that does not belong
1064 self.Options.ChosenVoicePack = "None"--Set ChosenVoicePack back to None
1065 self:AddMsg(DBM_CORE_VOICE_MISSING)
1066 end
1067 else
1068 if #self.Voices > 1 then
1069 --At least one voice pack installed but activeVP set to "None"
1070 self:AddMsg(DBM_CORE_VOICE_DISABLED)
1071 end
1072 end
1073 --Check if any of countdown sounds are using missing voice pack
1074 local voice1 = self.Options.CountdownVoice
1075 local voice2 = self.Options.CountdownVoice2
1076 local voice3 = self.Options.CountdownVoice3v2
1077 if voice1 == "None" then--Migrate to new setting
1078 self.Options.CountdownVoice = self.DefaultOptions.CountdownVoice
1079 self.Options.DontPlayCountdowns = true
1080 end
1081 local found1, found2, found3 = false, false, false
1082 for i = 1, #self.Counts do
1083 local voice = self.Counts[i].value
1084 if voice == self.Options.CountdownVoice then
1085 found1 = true
1086 end
1087 if voice == self.Options.CountdownVoice2 then
1088 found2 = true
1089 end
1090 if voice == self.Options.CountdownVoice3v2 then
1091 found3 = true
1092 end
1093 end
1094 if not found1 then
1095 self:AddMsg(DBM_CORE_VOICE_COUNT_MISSING:format(1))
1096 self.Options.CountdownVoice = self.DefaultOptions.CountdownVoice
1097 end
1098 if not found2 then
1099 self:AddMsg(DBM_CORE_VOICE_COUNT_MISSING:format(2))
1100 self.Options.CountdownVoice2 = self.DefaultOptions.CountdownVoice2
1101 end
1102 if not found3 then
1103 self:AddMsg(DBM_CORE_VOICE_COUNT_MISSING:format(3))
1104 self.Options.CountdownVoice3v2 = self.DefaultOptions.CountdownVoice3v2
1105 end
1106 self:BuildVoiceCountdownCache()
1107 --Break timer recovery
1108 --Try local settings
1109 if self.Options.tempBreak2 then
1110 local timer, startTime = string.split("/", self.Options.tempBreak2)
1111 local elapsed = time() - tonumber(startTime)
1112 local remaining = timer - elapsed
1113 if remaining > 0 then
1114 breakTimerStart(DBM, remaining, playerName)
1115 else--It must have ended while we were offline, kill variable.
1116 self.Options.tempBreak2 = nil
1117 end
1118 end
1119 end
1120
1121 -- register a callback that will be executed once the addon is fully loaded (ADDON_LOADED fired, saved vars are available)
1122 function DBM:RegisterOnLoadCallback(cb)
1123 if isLoaded then
1124 cb()
1125 else
1126 onLoadCallbacks[#onLoadCallbacks + 1] = cb
1127 end
1128 end
1129
1130 function DBM:ADDON_LOADED(modname)
1131 if modname == "DBM-Core" and not isLoaded then
1132 dbmToc = tonumber(GetAddOnMetadata("DBM-Core", "X-Min-Interface"))
1133 isLoaded = true
1134 for i, v in ipairs(onLoadCallbacks) do
1135 xpcall(v, geterrorhandler())
1136 end
1137 onLoadCallbacks = nil
1138 loadOptions(self)
1139 if GetAddOnEnableState(playerName, "VEM-Core") >= 1 then
1140 self:Disable(true)
1141 C_TimerAfter(10, function() self:AddMsg(DBM_CORE_VEM) end)
1142 C_TimerAfter(20, function() self:AddMsg(DBM_CORE_VEM) end)
1143 return
1144 end
1145 if GetAddOnEnableState(playerName, "DBM-Profiles") >= 1 then
1146 self:Disable(true)
1147 C_TimerAfter(10, function() self:AddMsg(DBM_CORE_3RDPROFILES) end)
1148 C_TimerAfter(20, function() self:AddMsg(DBM_CORE_3RDPROFILES) end)
1149 return
1150 end
1151 if GetAddOnEnableState(playerName, "DPMCore") >= 1 then
1152 self:Disable(true)
1153 C_TimerAfter(10, function() self:AddMsg(DBM_CORE_DPMCORE) end)
1154 C_TimerAfter(20, function() self:AddMsg(DBM_CORE_DPMCORE) end)
1155 return
1156 end
1157 self.Bars:LoadOptions("DBM")
1158 self.Arrow:LoadPosition()
1159 if not self.Options.ShowMinimapButton then self:HideMinimapButton() end
1160 local soundChannels = tonumber(GetCVar("Sound_NumChannels")) or 24--if set to 24, may return nil, Defaults usually do
1161 if soundChannels < 64 then
1162 SetCVar("Sound_NumChannels", 64)
1163 end
1164 self.AddOns = {}
1165 self.Voices = { {text = "None",value = "None"}, }--Create voice table, with default "None" value
1166 self.VoiceVersions = {}
1167 for i = 1, GetNumAddOns() do
1168 local addonName = GetAddOnInfo(i)
1169 local enabled = GetAddOnEnableState(playerName, i)
1170 local minToc = tonumber(GetAddOnMetadata(i, "X-Min-Interface"))
1171 if GetAddOnMetadata(i, "X-DBM-Mod") and enabled ~= 0 then
1172 if checkEntry(bannedMods, addonName) then
1173 self:AddMsg("The mod " .. addonName .. " is deprecated and will not be available. Please remove the folder " .. addonName .. " from your Interface" .. (IsWindowsClient() and "\\" or "/") .. "AddOns folder to get rid of this message. Check for an updated version of " .. addonName .. " that is compatible with your game version.")
1174 elseif not testBuild and minToc and minToc > wowTOC then
1175 self:Debug(i.." not loaded because mod requires minimum toc of "..minToc)
1176 else
1177 local mapIdTable = {strsplit(",", GetAddOnMetadata(i, "X-DBM-Mod-MapID") or "")}
1178 tinsert(self.AddOns, {
1179 sort = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Sort") or mhuge) or mhuge,
1180 type = GetAddOnMetadata(i, "X-DBM-Mod-Type") or "OTHER",
1181 category = GetAddOnMetadata(i, "X-DBM-Mod-Category") or "Other",
1182 name = GetAddOnMetadata(i, "X-DBM-Mod-Name") or GetRealZoneText(tonumber(mapIdTable[1])) or DBM_CORE_UNKNOWN,
1183 mapId = mapIdTable,
1184 subTabs = GetAddOnMetadata(i, "X-DBM-Mod-SubCategoriesID") and {strsplit(",", GetAddOnMetadata(i, "X-DBM-Mod-SubCategoriesID"))} or GetAddOnMetadata(i, "X-DBM-Mod-SubCategories") and {strsplit(",", GetAddOnMetadata(i, "X-DBM-Mod-SubCategories"))},
1185 oneFormat = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-Single-Format") or 0) == 1,
1186 hasLFR = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-LFR") or 0) == 1,
1187 hasChallenge = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-Challenge") or 0) == 1,
1188 noHeroic = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-No-Heroic") or 0) == 1,
1189 noStatistics = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-No-Statistics") or 0) == 1,
1190 hasMythic = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-Mythic") or 0) == 1,
1191 hasTimeWalker = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-TimeWalker") or 0) == 1,
1192 isWorldBoss = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-World-Boss") or 0) == 1,
1193 minRevision = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-MinCoreRevision") or 0),
1194 modId = addonName,
1195 })
1196 for i = #self.AddOns[#self.AddOns].mapId, 1, -1 do
1197 local id = tonumber(self.AddOns[#self.AddOns].mapId[i])
1198 if id then
1199 self.AddOns[#self.AddOns].mapId[i] = id
1200 else
1201 tremove(self.AddOns[#self.AddOns].mapId, i)
1202 end
1203 end
1204 if self.AddOns[#self.AddOns].subTabs then
1205 for k, v in ipairs(self.AddOns[#self.AddOns].subTabs) do
1206 local id = tonumber(self.AddOns[#self.AddOns].subTabs[k])
1207 if id then
1208 self.AddOns[#self.AddOns].subTabs[k] = GetRealZoneText(id):trim() or id
1209 else
1210 self.AddOns[#self.AddOns].subTabs[k] = (self.AddOns[#self.AddOns].subTabs[k]):trim()
1211 end
1212 end
1213 end
1214 if GetAddOnMetadata(i, "X-DBM-Mod-LoadCID") then
1215 local idTable = {strsplit(",", GetAddOnMetadata(i, "X-DBM-Mod-LoadCID"))}
1216 for i = 1, #idTable do
1217 loadcIds[tonumber(idTable[i]) or ""] = addonName
1218 end
1219 end
1220 end
1221 end
1222 if GetAddOnMetadata(i, "X-DBM-Voice") and enabled ~= 0 then
1223 if checkEntry(bannedMods, addonName) then
1224 self:AddMsg("The mod " .. addonName .. " is deprecated and will not be available. Please remove the folder " .. addonName .. " from your Interface" .. (IsWindowsClient() and "\\" or "/") .. "AddOns folder to get rid of this message. Check for an updated version of " .. addonName .. " that is compatible with your game version.")
1225 else
1226 local voiceValue = GetAddOnMetadata(i, "X-DBM-Voice-ShortName")
1227 local voiceVersion = tonumber(GetAddOnMetadata(i, "X-DBM-Voice-Version") or 0)
1228 if voiceVersion > 0 then--Do not insert voice version 0 into THIS table. 0 should be used by voice packs that insert only countdown
1229 tinsert(self.Voices, { text = GetAddOnMetadata(i, "X-DBM-Voice-Name"), value = voiceValue })
1230 end
1231 self.VoiceVersions[voiceValue] = voiceVersion
1232 self:Schedule(10, self.CheckVoicePackVersion, self, voiceValue)--Still at 1 since the count sounds won't break any mods or affect filter. V2 if support countsound path
1233 if GetAddOnMetadata(i, "X-DBM-Voice-HasCount") then--Supports adding countdown options, insert new countdown into table
1234 tinsert(self.Counts, { text = GetAddOnMetadata(i, "X-DBM-Voice-Name"), value = "VP:"..voiceValue, path = "Interface\\AddOns\\DBM-VP"..voiceValue.."\\count\\", max = 10})
1235 end
1236 end
1237 end
1238 end
1239 tsort(self.AddOns, function(v1, v2) return v1.sort < v2.sort end)
1240 self:RegisterEvents(
1241 "COMBAT_LOG_EVENT_UNFILTERED",
1242 "GROUP_ROSTER_UPDATE",
1243 "INSTANCE_GROUP_SIZE_CHANGED",
1244 "CHAT_MSG_ADDON",
1245 "BN_CHAT_MSG_ADDON",
1246 "PLAYER_REGEN_DISABLED",
1247 "PLAYER_REGEN_ENABLED",
1248 "INSTANCE_ENCOUNTER_ENGAGE_UNIT",
1249 "UNIT_TARGETABLE_CHANGED",
1250 "UNIT_SPELLCAST_SUCCEEDED boss1 boss2 boss3 boss4 boss5",
1251 "UNIT_TARGET_UNFILTERED",
1252 "ENCOUNTER_START",
1253 "ENCOUNTER_END",
1254 "BOSS_KILL",
1255 "UNIT_DIED",
1256 "UNIT_DESTROYED",
1257 "UNIT_HEALTH mouseover target focus player",
1258 "CHAT_MSG_WHISPER",
1259 "CHAT_MSG_BN_WHISPER",
1260 "CHAT_MSG_MONSTER_YELL",
1261 "CHAT_MSG_MONSTER_EMOTE",
1262 "CHAT_MSG_MONSTER_SAY",
1263 "CHAT_MSG_RAID_BOSS_EMOTE",
1264 "RAID_BOSS_EMOTE",
1265 "RAID_BOSS_WHISPER",
1266 "PLAYER_ENTERING_WORLD",
1267 "LFG_ROLE_CHECK_SHOW",
1268 "LFG_PROPOSAL_SHOW",
1269 "LFG_PROPOSAL_FAILED",
1270 "LFG_PROPOSAL_SUCCEEDED",
1271 "READY_CHECK",
1272 "UPDATE_BATTLEFIELD_STATUS",
1273 "CINEMATIC_START",
1274 "PLAYER_LEVEL_UP",
1275 "PLAYER_SPECIALIZATION_CHANGED",
1276 "PARTY_INVITE_REQUEST",
1277 "LOADING_SCREEN_DISABLED",
1278 "SCENARIO_CRITERIA_UPDATE"
1279 )
1280 RolePollPopup:UnregisterEvent("ROLE_POLL_BEGIN")
1281 self:GROUP_ROSTER_UPDATE()
1282 C_TimerAfter(1.5, function()
1283 combatInitialized = true
1284 end)
1285 C_TimerAfter(20, function()--Delay UNIT_HEALTH combat start for 20 sec. (not to break Timer Recovery stuff)
1286 healthCombatInitialized = true
1287 end)
1288 self:Schedule(10, runDelayedFunctions, self)
1289 end
1290 end
1291end
1292
1293
1294-----------------
1295-- Callbacks --
1296-----------------
1297do
1298 local callbacks = {}
1299
1300 function fireEvent(event, ...)
1301 if not callbacks[event] then return end
1302 for i, v in ipairs(callbacks[event]) do
1303 local ok, err = pcall(v, event, ...)
1304 if not ok then DBM:AddMsg(("Error while executing callback %s for event %s: %s"):format(tostring(v), tostring(event), err)) end
1305 end
1306 end
1307
1308 function DBM:IsCallbackRegistered(event, f)
1309 if not event or type(f) ~= "function" then
1310 error("Usage: IsCallbackRegistered(event, callbackFunc)", 2)
1311 end
1312 if not callbacks[event] then return end
1313 for i = 1, #callbacks[event] do
1314 if callbacks[event][i] == f then return true end
1315 end
1316 return false
1317 end
1318
1319 function DBM:RegisterCallback(event, f)
1320 if not event or type(f) ~= "function" then
1321 error("Usage: DBM:RegisterCallback(event, callbackFunc)", 2)
1322 end
1323 callbacks[event] = callbacks[event] or {}
1324 tinsert(callbacks[event], f)
1325 return #callbacks[event]
1326 end
1327
1328 function DBM:UnregisterCallback(event, f)
1329 if not event or not callbacks[event] then return end
1330 if f then
1331 if type(f) ~= "function" then
1332 error("Usage: UnregisterCallback(event, callbackFunc)", 2)
1333 end
1334 --> checking from the end to start and not stoping after found one result in case of a func being twice registered.
1335 for i = #callbacks[event], 1, -1 do
1336 if callbacks[event][i] == f then tremove (callbacks[event], i) end
1337 end
1338 else
1339 callbacks[event] = nil
1340 end
1341 end
1342end
1343
1344
1345--------------------------
1346-- OnUpdate/Scheduler --
1347--------------------------
1348do
1349 -- stack that stores a few tables (up to 8) which will be recycled
1350 local popCachedTable, pushCachedTable
1351 local numChachedTables = 0
1352 do
1353 local tableCache = nil
1354
1355 -- gets a table from the stack, it will then be recycled.
1356 function popCachedTable()
1357 local t = tableCache
1358 if t then
1359 tableCache = t.next
1360 numChachedTables = numChachedTables - 1
1361 end
1362 return t
1363 end
1364
1365 -- tries to push a table on the stack
1366 -- only tables with <= 4 array entries are accepted as cached tables are only used for tasks with few arguments as we don't want to have big arrays wasting our precious memory space doing nothing...
1367 -- also, the maximum number of cached tables is limited to 8 as DBM rarely has more than eight scheduled tasks with less than 4 arguments at the same time
1368 -- this is just to re-use all the tables of the small tasks that are scheduled all the time (like the wipe detection)
1369 -- note that the cache does not use weak references anywhere for performance reasons, so a cached table will never be deleted by the garbage collector
1370 function pushCachedTable(t)
1371 if numChachedTables < 8 and #t <= 4 then
1372 twipe(t)
1373 t.next = tableCache
1374 tableCache = t
1375 numChachedTables = numChachedTables + 1
1376 end
1377 end
1378 end
1379
1380 -- priority queue (min-heap) that stores all scheduled tasks.
1381 -- insert: O(log n)
1382 -- deleteMin: O(log n)
1383 -- getMin: O(1)
1384 -- removeAllMatching: O(n)
1385 local insert, removeAllMatching, getMin, deleteMin
1386 do
1387 local heap = {}
1388 local firstFree = 1
1389
1390 -- gets the next task
1391 function getMin()
1392 return heap[1]
1393 end
1394
1395 -- restores the heap invariant by moving an item up
1396 local function siftUp(n)
1397 local parent = floor(n / 2)
1398 while n > 1 and heap[parent].time > heap[n].time do -- move the element up until the heap invariant is restored, meaning the element is at the top or the element's parent is <= the element
1399 heap[n], heap[parent] = heap[parent], heap[n] -- swap the element with its parent
1400 n = parent
1401 parent = floor(n / 2)
1402 end
1403 end
1404
1405 -- restores the heap invariant by moving an item down
1406 local function siftDown(n)
1407 local m -- position of the smaller child
1408 while 2 * n < firstFree do -- #children >= 1
1409 -- swap the element with its smaller child
1410 if 2 * n + 1 == firstFree then -- n does not have a right child --> it only has a left child as #children >= 1
1411 m = 2 * n -- left child
1412 elseif heap[2 * n].time < heap[2 * n + 1].time then -- #children = 2 and left child < right child
1413 m = 2 * n -- left child
1414 else -- #children = 2 and right child is smaller than the left one
1415 m = 2 * n + 1 -- right
1416 end
1417 if heap[n].time <= heap[m].time then -- n is <= its smallest child --> heap invariant restored
1418 return
1419 end
1420 heap[n], heap[m] = heap[m], heap[n]
1421 n = m
1422 end
1423 end
1424
1425 -- inserts a new element into the heap
1426 function insert(ele)
1427 heap[firstFree] = ele
1428 siftUp(firstFree)
1429 firstFree = firstFree + 1
1430 end
1431
1432 -- deletes the min element
1433 function deleteMin()
1434 local min = heap[1]
1435 firstFree = firstFree - 1
1436 heap[1] = heap[firstFree]
1437 heap[firstFree] = nil
1438 siftDown(1)
1439 return min
1440 end
1441
1442 -- removes multiple scheduled tasks from the heap
1443 -- note that this function is comparatively slow by design as it has to check all tasks and allows partial matches
1444 function removeAllMatching(f, mod, ...)
1445 -- remove all elements that match the signature, this destroyes the heap and leaves a normal array
1446 local v, match
1447 local foundMatch = false
1448 for i = #heap, 1, -1 do -- iterate backwards over the array to allow usage of table.remove
1449 v = heap[i]
1450 if (not f or v.func == f) and (not mod or v.mod == mod) then
1451 match = true
1452 for i = 1, select("#", ...) do
1453 if select(i, ...) ~= v[i] then
1454 match = false
1455 break
1456 end
1457 end
1458 if match then
1459 tremove(heap, i)
1460 firstFree = firstFree - 1
1461 foundMatch = true
1462 end
1463 end
1464 end
1465 -- rebuild the heap from the array in O(n)
1466 if foundMatch then
1467 for i = floor((firstFree - 1) / 2), 1, -1 do
1468 siftDown(i)
1469 end
1470 end
1471 end
1472 end
1473
1474
1475 local wrappers = {}
1476 local function range(max, cur, ...)
1477 cur = cur or 1
1478 if cur > max then
1479 return ...
1480 end
1481 return cur, range(max, cur + 1, select(2, ...))
1482 end
1483 local function getWrapper(n)
1484 wrappers[n] = wrappers[n] or loadstring(([[
1485 return function(func, tbl)
1486 return func(]] .. ("tbl[%s], "):rep(n):sub(0, -3) .. [[)
1487 end
1488 ]]):format(range(n)))()
1489 return wrappers[n]
1490 end
1491
1492 local nextModSyncSpamUpdate = 0
1493 --mainFrame:SetScript("OnUpdate", function(self, elapsed)
1494 local function onUpdate(self, elapsed)
1495 local time = GetTime()
1496
1497 -- execute scheduled tasks
1498 local nextTask = getMin()
1499 while nextTask and nextTask.func and nextTask.time <= time do
1500 deleteMin()
1501 local n = nextTask.n
1502 if n == #nextTask then
1503 nextTask.func(unpack(nextTask))
1504 else
1505 -- too many nil values (or a trailing nil)
1506 -- this is bad because unpack will not work properly
1507 -- TODO: is there a better solution?
1508 getWrapper(n)(nextTask.func, nextTask)
1509 end
1510 pushCachedTable(nextTask)
1511 nextTask = getMin()
1512 end
1513
1514 -- execute OnUpdate handlers of all modules
1515 local foundModFunctions = 0
1516 for i, v in pairs(updateFunctions) do
1517 foundModFunctions = foundModFunctions + 1
1518 if i.Options.Enabled and (not i.zones or i.zones[LastInstanceMapID]) then
1519 i.elapsed = (i.elapsed or 0) + elapsed
1520 if i.elapsed >= (i.updateInterval or 0) then
1521 v(i, i.elapsed)
1522 i.elapsed = 0
1523 end
1524 end
1525 end
1526
1527 -- clean up sync spam timers and auto respond spam blockers
1528 if time > nextModSyncSpamUpdate then
1529 nextModSyncSpamUpdate = time + 20
1530 -- TODO: optimize this; using next(t, k) all the time on nearly empty hash tables is not a good idea...doesn't really matter here as modSyncSpam only very rarely contains more than 4 entries...
1531 -- we now do this just every 20 seconds since the earlier assumption about modSyncSpam isn't true any longer
1532 -- note that not removing entries at all would be just a small memory leak and not a problem (the sync functions themselves check the timestamp)
1533 local k, v = next(modSyncSpam, nil)
1534 if v and (time - v > 8) then
1535 modSyncSpam[k] = nil
1536 end
1537 end
1538 if not nextTask and foundModFunctions == 0 then--Nothing left, stop scheduler
1539 schedulerFrame:SetScript("OnUpdate", nil)
1540 schedulerFrame:Hide()
1541 end
1542 end
1543
1544 function startScheduler()
1545 if not schedulerFrame:IsShown() then
1546 schedulerFrame:Show()
1547 schedulerFrame:SetScript("OnUpdate", onUpdate)
1548 end
1549 end
1550
1551 function schedule(t, f, mod, ...)
1552 if type(f) ~= "function" then
1553 error("usage: schedule(time, func, [mod, args...])", 2)
1554 end
1555 startScheduler()
1556 local v
1557 if numChachedTables > 0 and select("#", ...) <= 4 then -- a cached table is available and all arguments fit into an array with four slots
1558 v = popCachedTable()
1559 v.time = GetTime() + t
1560 v.func = f
1561 v.mod = mod
1562 v.n = select("#", ...)
1563 for i = 1, v.n do
1564 v[i] = select(i, ...)
1565 end
1566 -- clear slots if necessary
1567 for i = v.n + 1, 4 do
1568 v[i] = nil
1569 end
1570 else -- create a new table
1571 v = {time = GetTime() + t, func = f, mod = mod, n = select("#", ...), ...}
1572 end
1573 insert(v)
1574 end
1575
1576 function unschedule(f, mod, ...)
1577 if not f and not mod then
1578 -- you really want to kill the complete scheduler? call unscheduleAll
1579 error("cannot unschedule everything, pass a function and/or a mod")
1580 end
1581 return removeAllMatching(f, mod, ...)
1582 end
1583
1584 function unscheduleAll()
1585 return removeAllMatching()
1586 end
1587end
1588
1589function DBM:Schedule(t, f, ...)
1590 if type(f) ~= "function" then
1591 error("usage: DBM:Schedule(time, func, [args...])", 2)
1592 end
1593 return schedule(t, f, nil, ...)
1594end
1595
1596function DBM:Unschedule(f, ...)
1597 return unschedule(f, nil, ...)
1598end
1599
1600---------------
1601-- Profile --
1602---------------
1603function DBM:CreateProfile(name)
1604 if not name or name == "" or name:find(" ") then
1605 self:AddMsg(DBM_CORE_PROFILE_CREATE_ERROR)
1606 return
1607 end
1608 if DBM_AllSavedOptions[name] then
1609 self:AddMsg(DBM_CORE_PROFILE_CREATE_ERROR_D:format(name))
1610 return
1611 end
1612 -- create profile
1613 usedProfile = name
1614 DBM_UsedProfile = usedProfile
1615 DBM_AllSavedOptions[usedProfile] = DBM_AllSavedOptions[usedProfile] or {}
1616 self:AddDefaultOptions(DBM_AllSavedOptions[usedProfile], self.DefaultOptions)
1617 self.Options = DBM_AllSavedOptions[usedProfile]
1618 -- rearrange position
1619 self.Bars:CreateProfile("DBM")
1620 self:RepositionFrames()
1621 self:AddMsg(DBM_CORE_PROFILE_CREATED:format(name))
1622end
1623
1624function DBM:ApplyProfile(name)
1625 if not name or not DBM_AllSavedOptions[name] then
1626 self:AddMsg(DBM_CORE_PROFILE_APPLY_ERROR:format(name or DBM_CORE_UNKNOWN))
1627 return
1628 end
1629 usedProfile = name
1630 DBM_UsedProfile = usedProfile
1631 self:AddDefaultOptions(DBM_AllSavedOptions[usedProfile], self.DefaultOptions)
1632 self.Options = DBM_AllSavedOptions[usedProfile]
1633 -- rearrange position
1634 self.Bars:ApplyProfile("DBM")
1635 self:RepositionFrames()
1636 self:AddMsg(DBM_CORE_PROFILE_APPLIED:format(name))
1637end
1638
1639function DBM:CopyProfile(name)
1640 if not name or not DBM_AllSavedOptions[name] then
1641 self:AddMsg(DBM_CORE_PROFILE_COPY_ERROR:format(name or DBM_CORE_UNKNOWN))
1642 return
1643 elseif name == usedProfile then
1644 self:AddMsg(DBM_CORE_PROFILE_COPY_ERROR_SELF)
1645 return
1646 end
1647 DBM_AllSavedOptions[usedProfile] = DBM_AllSavedOptions[name]
1648 self:AddDefaultOptions(DBM_AllSavedOptions[usedProfile], self.DefaultOptions)
1649 self.Options = DBM_AllSavedOptions[usedProfile]
1650 -- rearrange position
1651 self.Bars:CopyProfile(name, "DBM")
1652 self:RepositionFrames()
1653 self:AddMsg(DBM_CORE_PROFILE_COPIED:format(name))
1654end
1655
1656function DBM:DeleteProfile(name)
1657 if not name or not DBM_AllSavedOptions[name] then
1658 self:AddMsg(DBM_CORE_PROFILE_DELETE_ERROR:format(name or DBM_CORE_UNKNOWN))
1659 return
1660 elseif name == "Default" then-- Default profile cannot be deleted.
1661 self:AddMsg(DBM_CORE_PROFILE_CANNOT_DELETE)
1662 return
1663 end
1664 --Delete
1665 DBM_AllSavedOptions[name] = nil
1666 usedProfile = "Default"--Restore to default
1667 DBM_UsedProfile = usedProfile
1668 self.Options = DBM_AllSavedOptions[usedProfile]
1669 if not self.Options then
1670 -- the default profile got lost somehow (maybe WoW crashed and the saved variables file got corrupted)
1671 self:CreateProfile("Default")
1672 end
1673 -- rearrange position
1674 self.Bars:DeleteProfile(name, "DBM")
1675 self:RepositionFrames()
1676 self:AddMsg(DBM_CORE_PROFILE_DELETED:format(name))
1677end
1678
1679function DBM:RepositionFrames()
1680 -- rearrange position
1681 self:UpdateWarningOptions()
1682 self:UpdateSpecialWarningOptions()
1683 self.Arrow:LoadPosition()
1684 if DBMBossHealth then
1685 DBMBossHealth:ClearAllPoints()
1686 DBMBossHealth:SetPoint(self.Options.HPFramePoint, UIParent, self.Options.HPFramePoint, self.Options.HPFrameX, self.Options.HPFrameY)
1687 end
1688 if DBMRangeCheck then
1689 DBMRangeCheck:ClearAllPoints()
1690 DBMRangeCheck:SetPoint(self.Options.RangeFramePoint, UIParent, self.Options.RangeFramePoint, self.Options.RangeFrameX, self.Options.RangeFrameY)
1691 end
1692 if DBMRangeCheckRadar then
1693 DBMRangeCheckRadar:ClearAllPoints()
1694 DBMRangeCheckRadar:SetPoint(self.Options.RangeFrameRadarPoint, UIParent, self.Options.RangeFrameRadarPoint, self.Options.RangeFrameRadarX, self.Options.RangeFrameRadarY)
1695 end
1696 if DBMInfoFrame then
1697 DBMInfoFrame:ClearAllPoints()
1698 DBMInfoFrame:SetPoint(self.Options.InfoFramePoint, UIParent, self.Options.InfoFramePoint, self.Options.InfoFrameX, self.Options.InfoFrameY)
1699 end
1700end
1701
1702----------------------
1703-- Slash Commands --
1704----------------------
1705do
1706 local function Pull(timer)
1707 local LFGTankException = IsPartyLFG() and UnitGroupRolesAssigned("player") == "TANK"--Tanks in LFG need to be able to send pull timer even if someone refuses to pass lead. LFG locks roles so no one can abuse this.
1708 if (DBM:GetRaidRank(playerName) == 0 and IsInGroup() and not LFGTankException) or select(2, IsInInstance()) == "pvp" or IsEncounterInProgress() then
1709 return DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
1710 end
1711 local targetName = (UnitExists("target") and UnitIsEnemy("player", "target")) and UnitName("target") or nil--Filter non enemies in case player isn't targetting bos but another player/pet
1712 if targetName then
1713 sendSync("PT", timer.."\t"..LastInstanceMapID.."\t"..targetName)
1714 else
1715 sendSync("PT", timer.."\t"..LastInstanceMapID)
1716 end
1717 end
1718 local function Break(timer)
1719 if IsInGroup() and (DBM:GetRaidRank(playerName) == 0 or IsPartyLFG()) or IsEncounterInProgress() or select(2, IsInInstance()) == "pvp" then--No break timers if not assistant or if it's dungeon/raid finder/BG
1720 DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
1721 return
1722 end
1723 if timer > 60 then
1724 DBM:AddMsg(DBM_CORE_BREAK_USAGE)
1725 return
1726 end
1727 timer = timer * 60
1728 sendSync("BT", timer)
1729 end
1730
1731 SLASH_DEADLYBOSSMODS1 = "/dbm"
1732 SLASH_DEADLYBOSSMODSRPULL1 = "/rpull"
1733 SLASH_DEADLYBOSSMODSDWAY1 = "/dway"--/way not used because DBM would load before TomTom and can't check
1734 SlashCmdList["DEADLYBOSSMODSDWAY"] = function(msg)
1735 if DBM:HasMapRestrictions() then
1736 DBM:AddMsg(DBM_CORE_NO_ARROW)
1737 return
1738 end
1739 local x, y = string.split(" ", msg:sub(1):trim())
1740 local xNum, yNum = tonumber(x or ""), tonumber(y or "")
1741 local success
1742 if xNum and yNum then
1743 DBM.Arrow:ShowRunTo(xNum, yNum, 0.5, nil, true)
1744 success = true
1745 else--Check if they used , instead of space.
1746 x, y = string.split(",", msg:sub(1):trim())
1747 xNum, yNum = tonumber(x or ""), tonumber(y or "")
1748 if xNum and yNum then
1749 DBM.Arrow:ShowRunTo(xNum, yNum, 0.5, nil, true)
1750 success = true
1751 end
1752 end
1753 if not success then
1754 DBM:AddMsg(DBM_ARROW_WAY_USAGE)
1755 else
1756 DBM:AddMsg(DBM_ARROW_WAY_SUCCESS)
1757 end
1758 end
1759 if not BigWigs then
1760 --Register pull and break slash commands for BW converts, if BW isn't loaded
1761 --This shouldn't raise an issue since BW SHOULD load before DBM in any case they are both present.
1762 SLASH_DEADLYBOSSMODSPULL1 = "/pull"
1763 SLASH_DEADLYBOSSMODSBREAK1 = "/break"
1764 SlashCmdList["DEADLYBOSSMODSPULL"] = function(msg)
1765 Pull(tonumber(msg) or 10)
1766 end
1767 SlashCmdList["DEADLYBOSSMODSBREAK"] = function(msg)
1768 Break(tonumber(msg) or 10)
1769 end
1770 end
1771 SlashCmdList["DEADLYBOSSMODSRPULL"] = function(msg)
1772 Pull(30)
1773 end
1774 SlashCmdList["DEADLYBOSSMODS"] = function(msg)
1775 local cmd = msg:lower()
1776 if cmd == "ver" or cmd == "version" then
1777 DBM:ShowVersions(false)
1778 elseif cmd == "ver2" or cmd == "version2" then
1779 DBM:ShowVersions(true)
1780 elseif cmd == "unlock" or cmd == "move" then
1781 DBM.Bars:ShowMovableBar()
1782 elseif cmd == "help2" then
1783 for i, v in ipairs(DBM_CORE_SLASHCMD_HELP2) do DBM:AddMsg(v) end
1784 elseif cmd == "help" then
1785 for i, v in ipairs(DBM_CORE_SLASHCMD_HELP) do DBM:AddMsg(v) end
1786 elseif cmd:sub(1, 13) == "timer endloop" then
1787 DBM:CreatePizzaTimer(time, "", nil, nil, nil, nil, true)
1788 elseif cmd:sub(1, 5) == "timer" then
1789 local time, text = msg:match("^%w+ ([%d:]+) (.+)$")
1790 if not (time and text) then
1791 for i, v in ipairs(DBM_CORE_TIMER_USAGE) do DBM:AddMsg(v) end
1792 --DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
1793 return
1794 end
1795 local min, sec = string.split(":", time)
1796 min = tonumber(min or "") or 0
1797 sec = tonumber(sec or "")
1798 if min and not sec then
1799 sec = min
1800 min = 0
1801 end
1802 time = min * 60 + sec
1803 DBM:CreatePizzaTimer(time, text)
1804 elseif cmd:sub(1, 6) == "ctimer" then
1805 local time, text = msg:match("^%w+ ([%d:]+) (.+)$")
1806 if not (time and text) then
1807 DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
1808 return
1809 end
1810 local min, sec = string.split(":", time)
1811 min = tonumber(min or "") or 0
1812 sec = tonumber(sec or "")
1813 if min and not sec then
1814 sec = min
1815 min = 0
1816 end
1817 time = min * 60 + sec
1818 DBM:CreatePizzaTimer(time, text, nil, nil, true)
1819 elseif cmd:sub(1, 6) == "ltimer" then
1820 local time, text = msg:match("^%w+ ([%d:]+) (.+)$")
1821 if not (time and text) then
1822 DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
1823 return
1824 end
1825 local min, sec = string.split(":", time)
1826 min = tonumber(min or "") or 0
1827 sec = tonumber(sec or "")
1828 if min and not sec then
1829 sec = min
1830 min = 0
1831 end
1832 time = min * 60 + sec
1833 DBM:CreatePizzaTimer(time, text, nil, nil, nil, true)
1834 elseif cmd:sub(1, 7) == "cltimer" then
1835 local time, text = msg:match("^%w+ ([%d:]+) (.+)$")
1836 if not (time and text) then
1837 DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
1838 return
1839 end
1840 local min, sec = string.split(":", time)
1841 min = tonumber(min or "") or 0
1842 sec = tonumber(sec or "")
1843 if min and not sec then
1844 sec = min
1845 min = 0
1846 end
1847 time = min * 60 + sec
1848 DBM:CreatePizzaTimer(time, text, nil, nil, true, true)
1849 elseif cmd:sub(1, 15) == "broadcast timer" then--Standard Timer
1850 local permission = true
1851 if DBM:GetRaidRank(playerName) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then
1852 DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
1853 permission = false
1854 end
1855 local time, text = msg:match("^%w+ %w+ ([%d:]+) (.+)$")
1856 if not (time and text) then
1857 DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
1858 return
1859 end
1860 local min, sec = string.split(":", time)
1861 min = tonumber(min or "") or 0
1862 sec = tonumber(sec or "")
1863 if min and not sec then
1864 sec = min
1865 min = 0
1866 end
1867 time = min * 60 + sec
1868 DBM:CreatePizzaTimer(time, text, permission)
1869 elseif cmd:sub(1, 16) == "broadcast ctimer" then
1870 local permission = true
1871 if DBM:GetRaidRank(playerName) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then
1872 DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
1873 permission = false
1874 end
1875 local time, text = msg:match("^%w+ %w+ ([%d:]+) (.+)$")
1876 if not (time and text) then
1877 DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
1878 return
1879 end
1880 local min, sec = string.split(":", time)
1881 min = tonumber(min or "") or 0
1882 sec = tonumber(sec or "")
1883 if min and not sec then
1884 sec = min
1885 min = 0
1886 end
1887 time = min * 60 + sec
1888 DBM:CreatePizzaTimer(time, text, permission, nil, true)
1889 elseif cmd:sub(1, 16) == "broadcast ltimer" then
1890 local permission = true
1891 if DBM:GetRaidRank(playerName) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then
1892 DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
1893 permission = false
1894 end
1895 local time, text = msg:match("^%w+ %w+ ([%d:]+) (.+)$")
1896 if not (time and text) then
1897 DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
1898 return
1899 end
1900 local min, sec = string.split(":", time)
1901 min = tonumber(min or "") or 0
1902 sec = tonumber(sec or "")
1903 if min and not sec then
1904 sec = min
1905 min = 0
1906 end
1907 time = min * 60 + sec
1908 DBM:CreatePizzaTimer(time, text, permission, nil, nil, true)
1909 elseif cmd:sub(1, 17) == "broadcast cltimer" then
1910 local permission = true
1911 if DBM:GetRaidRank(playerName) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then
1912 DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
1913 permission = false
1914 end
1915 local time, text = msg:match("^%w+ %w+ ([%d:]+) (.+)$")
1916 if not (time and text) then
1917 DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
1918 return
1919 end
1920 local min, sec = string.split(":", time)
1921 min = tonumber(min or "") or 0
1922 sec = tonumber(sec or "")
1923 if min and not sec then
1924 sec = min
1925 min = 0
1926 end
1927 time = min * 60 + sec
1928 DBM:CreatePizzaTimer(time, text, permission, nil, true, true)
1929 elseif cmd:sub(0,5) == "break" then
1930 local timer = tonumber(cmd:sub(6)) or 5
1931 Break(timer)
1932 elseif cmd:sub(1, 4) == "pull" then
1933 local timer = tonumber(cmd:sub(5)) or 10
1934 Pull(timer)
1935 elseif cmd:sub(1, 5) == "rpull" then
1936 Pull(30)
1937 elseif cmd:sub(1, 3) == "lag" then
1938 if not LL then
1939 DBM:AddMsg(DBM_CORE_UPDATE_REQUIRES_RELAUNCH)
1940 return
1941 end
1942 LL:RequestLatency()
1943 DBM:AddMsg(DBM_CORE_LAG_CHECKING)
1944 C_TimerAfter(5, function() DBM:ShowLag() end)
1945 elseif cmd:sub(1, 3) == "hud" then
1946 if DBM:HasMapRestrictions() then
1947 DBM:AddMsg(DBM_CORE_NO_HUD)
1948 return
1949 end
1950 local hudType, target, duration = string.split(" ", msg:sub(4):trim())
1951 if hudType == "" then
1952 for i, v in ipairs(DBM_CORE_HUD_USAGE) do
1953 DBM:AddMsg(v)
1954 end
1955 return
1956 end
1957 local hudDuration = tonumber(duration) or 1200--if no duration defined. 20 minutes to cover even longest of fights
1958 local success = false
1959 if type(hudType) == "string" and hudType:trim() ~= "" then
1960 if hudType:upper() == "HIDE" then
1961 DBMHudMap:Disable()
1962 return
1963 end
1964 if not target then
1965 DBM:AddMsg(DBM_CORE_HUD_INVALID_TARGET)
1966 return
1967 end
1968 local uId
1969 if target:upper() == "TARGET" and UnitExists("target") then
1970 uId = "target"
1971 elseif target:upper() == "FOCUS" and UnitExists("focus") then
1972 uId = "focus"
1973 else--Try to use it as player name
1974 uId = DBM:GetRaidUnitId(target)
1975 end
1976 if not uId then
1977 DBM:AddMsg(DBM_CORE_HUD_INVALID_TARGET)
1978 return
1979 end
1980 if UnitIsUnit("player", uId) and not DBM.Options.DebugMode then--Don't allow hud to self, except if debug mode is enabled, then hud to self useful for testing
1981 DBM:AddMsg(DBM_CORE_HUD_INVALID_SELF)
1982 return
1983 end
1984 if hudType:upper() == "ARROW" then
1985 local _, targetClass = UnitClass(uId)
1986 local color2 = RAID_CLASS_COLORS[targetClass]
1987 local m1 = DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "party", playerName, 0.1, hudDuration, 0, 1, 0, 1, nil, false):Appear()
1988 local m2 = DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "party", UnitName(uId), 0.75, hudDuration, color2.r, color2.g, color2.b, 1, nil, false):Appear()
1989 m2:EdgeTo(m1, nil, hudDuration, 0, 1, 0, 1)
1990 success = true
1991 elseif hudType:upper() == "DOT" then
1992 local _, targetClass = UnitClass(uId)
1993 local color2 = RAID_CLASS_COLORS[targetClass]
1994 DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "party", UnitName(uId), 0.75, hudDuration, color2.r, color2.g, color2.b, 1, nil, false):Appear()
1995 success = true
1996 elseif hudType:upper() == "GREEN" then
1997 DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "highlight", UnitName(uId), 3.5, hudDuration, 0, 1, 0, 0.5, nil, false):Pulse(0.5, 0.5)
1998 success = true
1999 elseif hudType:upper() == "RED" then
2000 DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "highlight", UnitName(uId), 3.5, hudDuration, 1, 0, 0, 0.5, nil, false):Pulse(0.5, 0.5)
2001 success = true
2002 elseif hudType:upper() == "YELLOW" then
2003 DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "highlight", UnitName(uId), 3.5, hudDuration, 1, 1, 0, 0.5, nil, false):Pulse(0.5, 0.5)
2004 success = true
2005 elseif hudType:upper() == "BLUE" then
2006 DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "highlight", UnitName(uId), 3.5, hudDuration, 0, 0, 1, 0.5, nil, false):Pulse(0.5, 0.5)
2007 success = true
2008 elseif hudType:upper() == "ICON" then
2009 local icon = GetRaidTargetIndex(uId)
2010 if not icon then
2011 DBM:AddMsg(DBM_CORE_HUD_INVALID_ICON)
2012 return
2013 end
2014 if icon == 8 then
2015 DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "skull", UnitName(uId), 3.5, hudDuration, 1, 1, 1, 0.5, nil, false):Pulse(0.5, 0.5)
2016 elseif icon == 7 then
2017 DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "cross", UnitName(uId), 3.5, hudDuration, 1, 1, 1, 0.5, nil, false):Pulse(0.5, 0.5)
2018 elseif icon == 6 then
2019 DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "square", UnitName(uId), 3.5, hudDuration, 1, 1, 1, 0.5, nil, false):Pulse(0.5, 0.5)
2020 elseif icon == 5 then
2021 DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "moon", UnitName(uId), 3.5, hudDuration, 1, 1, 1, 0.5, nil, false):Pulse(0.5, 0.5)
2022 elseif icon == 4 then
2023 DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "triangle", UnitName(uId), 3.5, hudDuration, 1, 1, 1, 0.5, nil, false):Pulse(0.5, 0.5)
2024 elseif icon == 3 then
2025 DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "diamond", UnitName(uId), 3.5, hudDuration, 1, 1, 1, 0.5, nil, false):Pulse(0.5, 0.5)
2026 elseif icon == 2 then
2027 DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "circle", UnitName(uId), 3.5, hudDuration, 1, 1, 1, 0.5, nil, false):Pulse(0.5, 0.5)
2028 elseif icon == 1 then
2029 DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "star", UnitName(uId), 3.5, hudDuration, 1, 1, 1, 0.5, nil, false):Pulse(0.5, 0.5)
2030 end
2031 success = true
2032 else
2033 DBM:AddMsg(DBM_CORE_HUD_INVALID_TYPE)
2034 end
2035 end
2036 if success then
2037 DBM:AddMsg(DBM_CORE_HUD_SUCCESS:format(strFromTime(hudDuration)))
2038 end
2039 elseif cmd:sub(1, 5) == "arrow" then
2040 if DBM:HasMapRestrictions() then
2041 DBM:AddMsg(DBM_CORE_NO_ARROW)
2042 return
2043 end
2044 local x, y, z = string.split(" ", msg:sub(6):trim())
2045 local xNum, yNum, zNum = tonumber(x or ""), tonumber(y or ""), tonumber(z or "")
2046 local success
2047 if xNum and yNum then
2048 DBM.Arrow:ShowRunTo(xNum, yNum, 0)
2049 success = true
2050 elseif type(x) == "string" and x:trim() ~= "" then
2051 local subCmd = x:trim()
2052 if subCmd:upper() == "HIDE" then
2053 DBM.Arrow:Hide()
2054 success = true
2055 elseif subCmd:upper() == "MOVE" then
2056 DBM.Arrow:Move()
2057 success = true
2058 elseif subCmd:upper() == "TARGET" then
2059 DBM.Arrow:ShowRunTo("target")
2060 success = true
2061 elseif subCmd:upper() == "FOCUS" then
2062 DBM.Arrow:ShowRunTo("focus")
2063 success = true
2064 elseif subCmd:upper() == "MAP" then
2065 DBM.Arrow:ShowRunTo(yNum, zNum, 0, nil, true)
2066 success = true
2067 elseif DBM:GetRaidUnitId(subCmd) then
2068 DBM.Arrow:ShowRunTo(subCmd)
2069 success = true
2070 end
2071 end
2072 if not success then
2073 for i, v in ipairs(DBM_ARROW_ERROR_USAGE) do
2074 DBM:AddMsg(v)
2075 end
2076 end
2077 elseif cmd:sub(1, 7) == "lockout" or cmd:sub(1, 3) == "ids" then
2078 if DBM:GetRaidRank(playerName) == 0 then
2079 return DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
2080 end
2081 if not IsInRaid() then
2082 return DBM:AddMsg(DBM_ERROR_NO_RAID)
2083 end
2084 DBM:RequestInstanceInfo()
2085 elseif cmd:sub(1, 10) == "debuglevel" then
2086 local level = tonumber(cmd:sub(11)) or 1
2087 if level < 1 or level > 3 then
2088 DBM:AddMsg("Invalid Value. Debug Level must be between 1 and 3.")
2089 return
2090 end
2091 DBM.Options.DebugLevel = level
2092 DBM:AddMsg("Debug Level is " .. level)
2093 elseif cmd:sub(1, 5) == "debug" then
2094 DBM.Options.DebugMode = DBM.Options.DebugMode == false and true or false
2095 DBM:AddMsg("Debug Message is " .. (DBM.Options.DebugMode and "ON" or "OFF"))
2096 elseif cmd:sub(1, 8) == "whereiam" or cmd:sub(1, 8) == "whereami" then
2097 if DBM:HasMapRestrictions() then
2098 DBM:AddMsg("Location debug not available do to instance restrictions")
2099 return
2100 end
2101 local x, y, _, map = UnitPosition("player")
2102 SetMapToCurrentZone()
2103 local mapID = GetCurrentMapAreaID()
2104 local mapx, mapy = GetPlayerMapPosition("player")
2105 DBM:AddMsg(("Location Information\nYou are at zone %u (%s): x=%f, y=%f.\nLocal Map ID %u (%s): x=%f, y=%f"):format(map, GetRealZoneText(map), x, y, mapID, GetZoneText(), mapx, mapy))
2106 elseif cmd:sub(1, 7) == "request" then
2107 DBM:Unschedule(DBM.RequestTimers)
2108 DBM:RequestTimers(1)
2109 DBM:RequestTimers(2)
2110 DBM:RequestTimers(3)
2111 else
2112 DBM:LoadGUI()
2113 end
2114 end
2115end
2116
2117do
2118 local function updateRangeFrame(r, reverse)
2119 if DBM.RangeCheck:IsShown() then
2120 DBM.RangeCheck:Hide(true)
2121 else
2122 if DBM:HasMapRestrictions() then
2123 DBM:AddMsg(DBM_CORE_NO_RANGE)
2124 elseif IsInInstance() then
2125 DBM:AddMsg(DBM_CORE_NO_RANGE_SOON)
2126 end
2127 if r and (r < 201) then
2128 DBM.RangeCheck:Show(r, nil, true, nil, reverse)
2129 else
2130 DBM.RangeCheck:Show(10, nil, true, nil, reverse)
2131 end
2132 end
2133 end
2134 SLASH_DBMRANGE1 = "/range"
2135 SLASH_DBMRANGE2 = "/distance"
2136 SLASH_DBMHUDAR1 = "/hudar"
2137 SLASH_DBMRRANGE1 = "/rrange"
2138 SLASH_DBMRRANGE2 = "/rdistance"
2139 SlashCmdList["DBMRANGE"] = function(msg)
2140 local r = tonumber(msg) or 10
2141 updateRangeFrame(r, false)
2142 end
2143 SlashCmdList["DBMHUDAR"] = function(msg)
2144 local r = tonumber(msg) or 10
2145 DBMHudMap:ToggleHudar(r)
2146 end
2147 SlashCmdList["DBMRRANGE"] = function(msg)
2148 local r = tonumber(msg) or 10
2149 updateRangeFrame(r, true)
2150 end
2151end
2152
2153do
2154 local sortMe = {}
2155 local OutdatedUsers = {}
2156
2157 local function sort(v1, v2)
2158 if v1.revision and not v2.revision then
2159 return true
2160 elseif v2.revision and not v1.revision then
2161 return false
2162 elseif v1.revision and v2.revision then
2163 return v1.revision > v2.revision
2164 else
2165 return (v1.bwversion or 0) > (v2.bwversion or 0)
2166 end
2167 end
2168
2169 function DBM:ShowVersions(notify)
2170 for i, v in pairs(raid) do
2171 tinsert(sortMe, v)
2172 end
2173 tsort(sortMe, sort)
2174 twipe(OutdatedUsers)
2175 self:AddMsg(DBM_CORE_VERSIONCHECK_HEADER)
2176 for i, v in ipairs(sortMe) do
2177 local name = v.name
2178 local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(name)]
2179 if playerColor then
2180 name = ("|r|cff%.2x%.2x%.2x%s|r|cff%.2x%.2x%.2x"):format(playerColor.r * 255, playerColor.g * 255, playerColor.b * 255, name, 0.41 * 255, 0.8 * 255, 0.94 * 255)
2181 end
2182 if v.displayVersion and not v.bwversion then--DBM, no BigWigs
2183 if self.Options.ShowAllVersions then
2184 self:AddMsg(DBM_CORE_VERSIONCHECK_ENTRY:format(name, "DBM "..v.displayVersion, "r"..v.revision, v.VPVersion or ""), false)--Only display VP version if not running two mods
2185 end
2186 if notify and v.revision < self.ReleaseRevision then
2187 SendChatMessage(chatPrefixShort..DBM_CORE_YOUR_VERSION_OUTDATED, "WHISPER", nil, v.name)
2188 end
2189 elseif self.Options.ShowAllVersions and v.displayVersion and v.bwversion then--DBM & BigWigs
2190 self:AddMsg(DBM_CORE_VERSIONCHECK_ENTRY_TWO:format(name, "DBM "..v.displayVersion, "r"..v.revision, DBM_BIG_WIGS, versionResponseString:format(v.bwversion, v.bwhash)), false)
2191 elseif self.Options.ShowAllVersions and not v.displayVersion and v.bwversion then--BigWigs, No DBM
2192 self:AddMsg(DBM_CORE_VERSIONCHECK_ENTRY:format(name, DBM_BIG_WIGS, versionResponseString:format(v.bwversion, v.bwhash), ""), false)
2193 else
2194 if self.Options.ShowAllVersions then
2195 self:AddMsg(DBM_CORE_VERSIONCHECK_ENTRY_NO_DBM:format(name), false)
2196 end
2197 end
2198 end
2199 local TotalUsers = #sortMe
2200 local NoDBM = 0
2201 local NoBigwigs = 0
2202 local OldMod = 0
2203 for i = #sortMe, 1, -1 do
2204 if not sortMe[i].revision then
2205 NoDBM = NoDBM + 1
2206 end
2207 if not (sortMe[i].bwversion) then
2208 NoBigwigs = NoBigwigs + 1
2209 end
2210 --Table sorting sorts dbm to top, bigwigs underneath. Highest version dbm always at top. so sortMe[1]
2211 --This check compares all dbm version to highest RELEASE version in raid.
2212 if sortMe[i].revision and (sortMe[i].revision < sortMe[1].version) or sortMe[i].bwversion and (sortMe[i].bwversion < fakeBWVersion) then
2213 OldMod = OldMod + 1
2214 local name = sortMe[i].name
2215 local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(name)]
2216 if playerColor then
2217 name = ("|r|cff%.2x%.2x%.2x%s|r|cff%.2x%.2x%.2x"):format(playerColor.r * 255, playerColor.g * 255, playerColor.b * 255, name, 0.41 * 255, 0.8 * 255, 0.94 * 255)
2218 end
2219 tinsert(OutdatedUsers, name)
2220 end
2221 end
2222 local TotalDBM = TotalUsers - NoDBM
2223 local TotalBW = TotalUsers - NoBigwigs
2224 self:AddMsg("---", false)
2225 self:AddMsg(DBM_CORE_VERSIONCHECK_FOOTER:format(TotalDBM, TotalBW), false)
2226 self:AddMsg(DBM_CORE_VERSIONCHECK_OUTDATED:format(OldMod, #OutdatedUsers > 0 and tconcat(OutdatedUsers, ", ") or NONE), false)
2227 twipe(OutdatedUsers)
2228 twipe(sortMe)
2229 for i = #sortMe, 1, -1 do
2230 sortMe[i] = nil
2231 end
2232 end
2233end
2234
2235
2236-- Lag checking
2237do
2238 local sortLag = {}
2239 local nolagResponse = {}
2240 local function sortit(v1, v2)
2241 return (v1.worldlag or 0) < (v2.worldlag or 0)
2242 end
2243 function DBM:ShowLag()
2244 for i, v in pairs(raid) do
2245 tinsert(sortLag, v)
2246 end
2247 tsort(sortLag, sortit)
2248 self:AddMsg(DBM_CORE_LAG_HEADER)
2249 for i, v in ipairs(sortLag) do
2250 local name = v.name
2251 local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(name)]
2252 if playerColor then
2253 name = ("|r|cff%.2x%.2x%.2x%s|r|cff%.2x%.2x%.2x"):format(playerColor.r * 255, playerColor.g * 255, playerColor.b * 255, name, 0.41 * 255, 0.8 * 255, 0.94 * 255)
2254 end
2255 if v.worldlag then
2256 self:AddMsg(DBM_CORE_LAG_ENTRY:format(name, v.worldlag, v.homelag), false)
2257 else
2258 tinsert(nolagResponse, v.name)
2259 end
2260 end
2261 if #nolagResponse > 0 then
2262 self:AddMsg(DBM_CORE_LAG_FOOTER:format(tconcat(nolagResponse, ", ")), false)
2263 for i = #nolagResponse, 1, -1 do
2264 nolagResponse[i] = nil
2265 end
2266 end
2267 for i = #sortLag, 1, -1 do
2268 sortLag[i] = nil
2269 end
2270 end
2271 if LL then
2272 LL:Register("DBM", function(homelag, worldlag, sender, channel)
2273 if sender and raid[sender] then
2274 raid[sender].homelag = homelag
2275 raid[sender].worldlag = worldlag
2276 end
2277 end)
2278 else
2279 DBM:AddMsg(DBM_CORE_UPDATE_REQUIRES_RELAUNCH)
2280 end
2281
2282end
2283
2284-------------------
2285-- Pizza Timer --
2286-------------------
2287do
2288
2289 local function loopTimer(time, text, broadcast, sender, count)
2290 DBM:CreatePizzaTimer(time, text, broadcast, sender, count, true)
2291 end
2292
2293 local ignore = {}
2294 local fakeMod -- dummy mod for the count sound effects
2295 --Standard Pizza Timer
2296 function DBM:CreatePizzaTimer(time, text, broadcast, sender, count, loop, terminate)
2297 if not fakeMod then
2298 fakeMod = self:NewMod("CreateCountTimerDummy")
2299 self:GetModLocalization("CreateCountTimerDummy"):SetGeneralLocalization{ name = DBM_CORE_MINIMAP_TOOLTIP_HEADER }
2300 fakeMod.countdown = fakeMod:NewCountdown(0, 0, nil, nil, nil, true)
2301 end
2302 if terminate or time == 0 then
2303 self:Unschedule(loopTimer)
2304 fakeMod.countdown:Cancel()
2305 self.Bars:CancelBar(text)
2306 self:Unschedule(countDownTextDelay)
2307 TimerTracker_OnEvent(TimerTracker, "PLAYER_ENTERING_WORLD")
2308 return
2309 end
2310 if sender and ignore[sender] then return end
2311 text = text:sub(1, 16)
2312 text = text:gsub("%%t", UnitName("target") or "<no target>")
2313 if time < 3 then
2314 self:AddMsg(DBM_PIZZA_ERROR_USAGE)
2315 return
2316 end
2317 self.Bars:CreateBar(time, text, "Interface\\Icons\\Spell_Holy_BorrowedTime")
2318 if broadcast then
2319 if count then
2320 sendSync("CU", ("%s\t%s"):format(time, text))
2321 else
2322 sendSync("U", ("%s\t%s"):format(time, text))
2323 end
2324 end
2325 if sender then self:ShowPizzaInfo(text, sender) end
2326 if count then
2327 if not fakeMod then
2328 local threshold = self.Options.PTCountThreshold
2329 fakeMod = self:NewMod("CreateCountTimerDummy")
2330 self:GetModLocalization("CreateCountTimerDummy"):SetGeneralLocalization{ name = DBM_CORE_MINIMAP_TOOLTIP_HEADER }
2331 local adjustedThreshold = 5
2332 if threshold > 10 then
2333 adjustedThreshold = 10
2334 else
2335 adjustedThreshold = floor(threshold)
2336 end
2337 fakeMod.countdown = fakeMod:NewCountdown(0, 0, nil, nil, adjustedThreshold, true)
2338 end
2339 if not self.Options.DontPlayPTCountdown then
2340 fakeMod.countdown:Cancel()
2341 fakeMod.countdown:Start(time)
2342 end
2343 if not self.Options.DontShowPTCountdownText then
2344 self:Unschedule(countDownTextDelay)
2345 TimerTracker_OnEvent(TimerTracker, "PLAYER_ENTERING_WORLD")
2346 local threshold = self.Options.PTCountThreshold
2347 if time > threshold then
2348 self:Schedule(time-threshold, countDownTextDelay, threshold)
2349 else
2350 TimerTracker_OnEvent(TimerTracker, "START_TIMER", 2, time, time)
2351 end
2352 end
2353 end
2354 if loop then
2355 self:Unschedule(loopTimer)--Only one loop timer supported at once doing this, but much cleaner this way
2356 self:Schedule(time, loopTimer, time, text, broadcast, sender, count)
2357 end
2358 end
2359
2360 function DBM:AddToPizzaIgnore(name)
2361 ignore[name] = true
2362 end
2363end
2364
2365function DBM:ShowPizzaInfo(id, sender)
2366 if self.Options.ShowPizzaMessage then
2367 self:AddMsg(DBM_PIZZA_SYNC_INFO:format(sender, id))
2368 end
2369end
2370
2371------------------
2372-- Hyperlinks --
2373------------------
2374do
2375 local ignore, cancel
2376 local popuplevel = 0
2377 local function showPopupConfirmIgnore(ignore, cancel)
2378 local popup = CreateFrame("Frame", "DBMHyperLinks", UIParent)
2379 popup:SetBackdrop({bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
2380 edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
2381 tile = true, tileSize = 16, edgeSize = 16,
2382 insets = {left = 1, right = 1, top = 1, bottom = 1}}
2383 )
2384 popup:SetSize(500, 80)
2385 popup:SetPoint("TOP", UIParent, "TOP", 0, -200)
2386 popup:SetFrameStrata("DIALOG")
2387 popup:SetFrameLevel(popuplevel)
2388 popuplevel = popuplevel + 1
2389
2390 local text = popup:CreateFontString()
2391 text:SetFontObject(ChatFontNormal)
2392 text:SetWidth(470)
2393 text:SetWordWrap(true)
2394 text:SetPoint("TOP", popup, "TOP", 0, -15)
2395 text:SetText(DBM_PIZZA_CONFIRM_IGNORE:format(ignore))
2396
2397 local accept = CreateFrame("Button", nil, popup)
2398 accept:SetNormalTexture("Interface\\Buttons\\UI-DialogBox-Button-Up")
2399 accept:SetPushedTexture("Interface\\Buttons\\UI-DialogBox-Button-Down")
2400 accept:SetHighlightTexture("Interface\\Buttons\\UI-DialogBox-Button-Highlight", "ADD")
2401 accept:SetSize(128, 35)
2402 accept:SetPoint("BOTTOM", popup, "BOTTOM", -75, 0)
2403 accept:SetScript("OnClick", function(f) DBM:AddToPizzaIgnore(ignore) DBM.Bars:CancelBar(cancel) f:GetParent():Hide() end)
2404
2405 local atext = accept:CreateFontString()
2406 atext:SetFontObject(ChatFontNormal)
2407 atext:SetPoint("CENTER", accept, "CENTER", 0, 5)
2408 atext:SetText(YES)
2409
2410 local decline = CreateFrame("Button", nil, popup)
2411 decline:SetNormalTexture("Interface\\Buttons\\UI-DialogBox-Button-Up")
2412 decline:SetPushedTexture("Interface\\Buttons\\UI-DialogBox-Button-Down")
2413 decline:SetHighlightTexture("Interface\\Buttons\\UI-DialogBox-Button-Highlight", "ADD")
2414 decline:SetSize(128, 35)
2415 decline:SetPoint("BOTTOM", popup, "BOTTOM", 75, 0)
2416 decline:SetScript("OnClick", function(f) f:GetParent():Hide() end)
2417
2418 local dtext = decline:CreateFontString()
2419 dtext:SetFontObject(ChatFontNormal)
2420 dtext:SetPoint("CENTER", decline, "CENTER", 0, 5)
2421 dtext:SetText(NO)
2422 PlaySound("igMainMenuOpen")
2423 end
2424
2425 local function linkHook(self, link, string, button, ...)
2426 local linkType, arg1, arg2, arg3, arg4, arg5, arg6 = strsplit(":", link)
2427 if linkType ~= "DBM" then
2428 return
2429 end
2430 if arg1 == "cancel" then
2431 DBM.Bars:CancelBar(link:match("DBM:cancel:(.+):nil$"))
2432 elseif arg1 == "ignore" then
2433 cancel = link:match("DBM:ignore:(.+):[^%s:]+$")
2434 ignore = link:match(":([^:]+)$")
2435 showPopupConfirmIgnore(ignore, cancel)
2436 elseif arg1 == "update" then
2437 DBM:ShowUpdateReminder(arg2, arg3) -- displayVersion, revision
2438 elseif arg == "localizersneeded" then
2439 DBM:ShowUpdateReminder(nil, nil, DBM_FORUMS_COPY_URL_DIALOG, "http://www.deadlybossmods.com/forum/viewtopic.php?f=3&t=5")
2440 elseif arg1 == "forumsnews" then
2441 DBM:ShowUpdateReminder(nil, nil, DBM_FORUMS_COPY_URL_DIALOG_NEWS, "http://www.deadlybossmods.com/forum/viewtopic.php?f=3&p=171#p171")
2442 elseif arg1 == "forums" then
2443 DBM:ShowUpdateReminder(nil, nil, DBM_FORUMS_COPY_URL_DIALOG)
2444 elseif arg1 == "showRaidIdResults" then
2445 DBM:ShowRaidIDRequestResults()
2446 elseif arg1 == "noteshare" then
2447 local mod = DBM:GetModByName(arg2 or "")
2448 if mod then
2449 DBM:ShowNoteEditor(mod, arg3, arg4, arg5, arg6)
2450 else--Should not happen, since mod was verified before getting this far, but just in case
2451 DBM:Debug("Bad note share, mod not valid")
2452 end
2453 end
2454 end
2455
2456 DEFAULT_CHAT_FRAME:HookScript("OnHyperlinkClick", linkHook) -- handles the weird case that the default chat frame is not one of the normal chat frames (3rd party chat frames or whatever causes this)
2457 local i = 1
2458 while _G["ChatFrame" .. i] do
2459 if _G["ChatFrame" .. i] ~= DEFAULT_CHAT_FRAME then
2460 _G["ChatFrame" .. i]:HookScript("OnHyperlinkClick", linkHook)
2461 end
2462 i = i + 1
2463 end
2464end
2465
2466do
2467 local old = ItemRefTooltip.SetHyperlink -- we have to hook this function since the default ChatFrame code assumes that all links except for player and channel links are valid arguments for this function
2468 function ItemRefTooltip:SetHyperlink(link, ...)
2469 if link and link:sub(0, 4) == "DBM:" then
2470 return
2471 end
2472 return old(self, link, ...)
2473 end
2474end
2475
2476
2477-----------------
2478-- GUI Stuff --
2479-----------------
2480do
2481 local callOnLoad = {}
2482 function DBM:LoadGUI()
2483 if GetAddOnEnableState(playerName, "VEM-Core") >= 1 then
2484 self:AddMsg(DBM_CORE_VEM)
2485 return
2486 end
2487 if GetAddOnEnableState(playerName, "DBM-Profiles") >= 1 then
2488 self:AddMsg(DBM_CORE_3RDPROFILES)
2489 return
2490 end
2491 if GetAddOnEnableState(playerName, "DPMCore") >= 1 then
2492 self:AddMsg(DBM_CORE_DPMCORE)
2493 return
2494 end
2495 if not dbmIsEnabled then
2496 DBM:AddMsg(DBM_CORE_UPDATEREMINDER_DISABLE)
2497 return
2498 end
2499 if not IsAddOnLoaded("DBM-GUI") then
2500 if InCombatLockdown() then
2501 guiRequested = true
2502 self:AddMsg(DBM_CORE_LOAD_GUI_COMBAT)
2503 return
2504 end
2505 local enabled = GetAddOnEnableState(playerName, "DBM-GUI")
2506 if enabled == 0 then
2507 EnableAddOn("DBM-GUI")
2508 end
2509 local loaded, reason = LoadAddOn("DBM-GUI")
2510 if not loaded then
2511 if reason then
2512 self:AddMsg(DBM_CORE_LOAD_GUI_ERROR:format(tostring(_G["ADDON_"..reason or ""])))
2513 else
2514 self:AddMsg(DBM_CORE_LOAD_GUI_ERROR:format(DBM_CORE_UNKNOWN))
2515 end
2516 return false
2517 end
2518 tsort(callOnLoad, function(v1, v2) return v1[2] < v2[2] end)
2519 for i, v in ipairs(callOnLoad) do v[1]() end
2520 collectgarbage("collect")
2521 end
2522 return DBM_GUI:ShowHide()
2523 end
2524
2525 function DBM:RegisterOnGuiLoadCallback(f, sort)
2526 tinsert(callOnLoad, {f, sort or mhuge})
2527 end
2528end
2529
2530
2531----------------------
2532-- Minimap Button --
2533----------------------
2534do
2535 local dragMode = nil
2536
2537 local function moveButton(self)
2538 if dragMode == "free" then
2539 local centerX, centerY = Minimap:GetCenter()
2540 local x, y = GetCursorPosition()
2541 x, y = x / self:GetEffectiveScale() - centerX, y / self:GetEffectiveScale() - centerY
2542 self:ClearAllPoints()
2543 self:SetPoint("CENTER", x, y)
2544 else
2545 local centerX, centerY = Minimap:GetCenter()
2546 local x, y = GetCursorPosition()
2547 x, y = x / self:GetEffectiveScale() - centerX, y / self:GetEffectiveScale() - centerY
2548 centerX, centerY = math.abs(x), math.abs(y)
2549 centerX, centerY = (centerX / math.sqrt(centerX^2 + centerY^2)) * 80, (centerY / sqrt(centerX^2 + centerY^2)) * 80
2550 centerX = x < 0 and -centerX or centerX
2551 centerY = y < 0 and -centerY or centerY
2552 self:ClearAllPoints()
2553 self:SetPoint("CENTER", centerX, centerY)
2554 end
2555 end
2556
2557 local button = CreateFrame("Button", "DBMMinimapButton", Minimap)
2558 button:SetHeight(32)
2559 button:SetWidth(32)
2560 button:SetFrameStrata("MEDIUM")
2561 button:SetPoint("CENTER", -65.35, -38.8)
2562 button:SetMovable(true)
2563 button:SetUserPlaced(true)
2564 button:SetNormalTexture("Interface\\AddOns\\DBM-Core\\textures\\Minimap-Button-Up")
2565 button:SetPushedTexture("Interface\\AddOns\\DBM-Core\\textures\\Minimap-Button-Down")
2566 button:SetHighlightTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")
2567
2568 button:SetScript("OnMouseDown", function(self, button)
2569 if IsShiftKeyDown() and IsAltKeyDown() then
2570 dragMode = "free"
2571 self:SetScript("OnUpdate", moveButton)
2572 elseif IsShiftKeyDown() or button == "RightButton" then
2573 dragMode = nil
2574 self:SetScript("OnUpdate", moveButton)
2575 end
2576 end)
2577 button:SetScript("OnMouseUp", function(self)
2578 self:SetScript("OnUpdate", nil)
2579 end)
2580 button:SetScript("OnClick", function(self, button)
2581 if IsShiftKeyDown() or button == "RightButton" then return end
2582 DBM:LoadGUI()
2583 end)
2584 button:SetScript("OnEnter", function(self)
2585 GameTooltip_SetDefaultAnchor(GameTooltip, self)
2586 GameTooltip:SetText(DBM_CORE_MINIMAP_TOOLTIP_HEADER, 1, 1, 1)
2587 GameTooltip:AddLine(ver, NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, 1)
2588 GameTooltip:AddLine(" ")
2589 GameTooltip:AddLine(DBM_CORE_MINIMAP_TOOLTIP_FOOTER, RAID_CLASS_COLORS.MAGE.r, RAID_CLASS_COLORS.MAGE.g, RAID_CLASS_COLORS.MAGE.b, 1)
2590 GameTooltip:Show()
2591 end)
2592 button:SetScript("OnLeave", function(self)
2593 GameTooltip:Hide()
2594 end)
2595
2596 function DBM:ToggleMinimapButton()
2597 self.Options.ShowMinimapButton = not self.Options.ShowMinimapButton
2598 if self.Options.ShowMinimapButton then
2599 button:Show()
2600 else
2601 button:Hide()
2602 end
2603 end
2604
2605 function DBM:HideMinimapButton()
2606 return button:Hide()
2607 end
2608end
2609
2610-------------------------------------------------
2611-- Raid/Party Handling and Unit ID Utilities --
2612-------------------------------------------------
2613do
2614 local inRaid = false
2615
2616 local raidGuids = {}
2617 local iconSeter = {}
2618
2619 -- save playerinfo into raid table on load. (for solo raid)
2620 DBM:RegisterOnLoadCallback(function()
2621 C_TimerAfter(6, function()
2622 if not raid[playerName] then
2623 raid[playerName] = {}
2624 raid[playerName].name = playerName
2625 raid[playerName].shortname = playerName
2626 raid[playerName].guid = UnitGUID("player")
2627 raid[playerName].rank = 0
2628 raid[playerName].class = class
2629 raid[playerName].id = "player"
2630 raid[playerName].groupId = 0
2631 raid[playerName].revision = DBM.Revision
2632 raid[playerName].version = DBM.ReleaseRevision
2633 raid[playerName].displayVersion = DBM.DisplayVersion
2634 raid[playerName].locale = GetLocale()
2635 raid[playerName].enabledIcons = tostring(not DBM.Options.DontSetIcons)
2636 raidGuids[UnitGUID("player") or ""] = playerName
2637 end
2638 end)
2639 end)
2640
2641 local function updateAllRoster(self)
2642 if IsInRaid() then
2643 if not inRaid then
2644 inRaid = true
2645 sendSync("H")
2646 SendAddonMessage("BigWigs", versionQueryString:format(0, fakeBWHash), IsPartyLFG() and "INSTANCE_CHAT" or "RAID")
2647 self:Schedule(2, self.RoleCheck, false, self)
2648 fireEvent("raidJoin", playerName)
2649 if BigWigs and BigWigs.db.profile.raidicon and not self.Options.DontSetIcons then--Both DBM and bigwigs have raid icon marking turned on.
2650 self:AddMsg(DBM_CORE_BIGWIGS_ICON_CONFLICT)--Warn that one of them should be turned off to prevent conflict (which they turn off is obviously up to raid leaders preference, dbm accepts either ore turned off to stop this alert)
2651 end
2652 end
2653 for i = 1, GetNumGroupMembers() do
2654 local name, rank, subgroup, _, _, className = GetRaidRosterInfo(i)
2655 -- Maybe GetNumGroupMembers() bug? Seems that GetNumGroupMembers() rarely returns bad value, causing GetRaidRosterInfo() returns to nil.
2656 -- Filter name = nil to prevent nil table error.
2657 if name then
2658 local id = "raid" .. i
2659 local shortname = UnitName(id)
2660 if (not raid[name]) and inRaid then
2661 fireEvent("raidJoin", name)
2662 end
2663 raid[name] = raid[name] or {}
2664 raid[name].name = name
2665 raid[name].shortname = shortname
2666 raid[name].rank = rank
2667 raid[name].subgroup = subgroup
2668 raid[name].class = className
2669 raid[name].id = id
2670 raid[name].groupId = i
2671 raid[name].guid = UnitGUID(id) or ""
2672 raid[name].updated = true
2673 raidGuids[UnitGUID(id) or ""] = name
2674 end
2675 end
2676 enableIcons = false
2677 twipe(iconSeter)
2678 for i, v in pairs(raid) do
2679 if not v.updated then
2680 raidGuids[v.guid] = nil
2681 raid[i] = nil
2682 removeEntry(newerVersionPerson, i)
2683 fireEvent("raidLeave", i)
2684 else
2685 v.updated = nil
2686 if v.revision and v.rank > 0 and (v.enabledIcons or "") == "true" then
2687 iconSeter[#iconSeter + 1] = v.revision.." "..v.name
2688 end
2689 end
2690 end
2691 if #iconSeter > 0 then
2692 tsort(iconSeter, function(a, b) return a > b end)
2693 local elected = iconSeter[1]
2694 if playerName == elected:sub(elected:find(" ") + 1) then
2695 enableIcons = true
2696 end
2697 end
2698 elseif IsInGroup() then
2699 if not inRaid then
2700 -- joined a new party
2701 inRaid = true
2702 sendSync("H")
2703 SendAddonMessage("BigWigs", versionQueryString:format(0, fakeBWHash), IsPartyLFG() and "INSTANCE_CHAT" or "RAID")
2704 self:Schedule(2, self.RoleCheck, false, self)
2705 fireEvent("partyJoin", playerName)
2706 end
2707 for i = 0, GetNumSubgroupMembers() do
2708 local id
2709 if (i == 0) then
2710 id = "player"
2711 else
2712 id = "party"..i
2713 end
2714 local name = GetUnitName(id, true)
2715 local shortname = UnitName(id)
2716 local rank = UnitIsGroupLeader(id) and 2 or 0
2717 local _, className = UnitClass(id)
2718 if (not raid[name]) and inRaid then
2719 fireEvent("partyJoin", name)
2720 end
2721 raid[name] = raid[name] or {}
2722 raid[name].name = name
2723 raid[name].shortname = shortname
2724 raid[name].guid = UnitGUID(id) or ""
2725 raid[name].rank = rank
2726 raid[name].class = className
2727 raid[name].id = id
2728 raid[name].groupId = i
2729 raid[name].updated = true
2730 raidGuids[UnitGUID(id) or ""] = name
2731 end
2732 enableIcons = false
2733 twipe(iconSeter)
2734 for i, v in pairs(raid) do
2735 if not v.updated then
2736 raidGuids[v.guid] = nil
2737 raid[i] = nil
2738 removeEntry(newerVersionPerson, i)
2739 fireEvent("partyLeave", i)
2740 else
2741 v.updated = nil
2742 if v.revision and v.rank > 0 and (v.enabledIcons or "") == "true" then
2743 iconSeter[#iconSeter + 1] = v.revision.." "..v.name
2744 end
2745 end
2746 end
2747 if #iconSeter > 0 then
2748 tsort(iconSeter, function(a, b) return a > b end)
2749 local elected = iconSeter[1]
2750 if playerName == elected:sub(elected:find(" ") + 1) then
2751 enableIcons = true
2752 end
2753 end
2754 else
2755 -- left the current group/raid
2756 inRaid = false
2757 enableIcons = true
2758 fireEvent("raidLeave", playerName)
2759 twipe(raid)
2760 twipe(newerVersionPerson)
2761 -- restore playerinfo into raid table on raidleave. (for solo raid)
2762 raid[playerName] = {}
2763 raid[playerName].name = playerName
2764 raid[playerName].shortname = playerName
2765 raid[playerName].guid = UnitGUID("player")
2766 raid[playerName].rank = 0
2767 raid[playerName].class = class
2768 raid[playerName].id = "player"
2769 raid[playerName].groupId = 0
2770 raid[playerName].revision = DBM.Revision
2771 raid[playerName].version = DBM.ReleaseRevision
2772 raid[playerName].displayVersion = DBM.DisplayVersion
2773 raid[playerName].locale = GetLocale()
2774 raidGuids[UnitGUID("player")] = playerName
2775 end
2776 end
2777
2778 function DBM:GROUP_ROSTER_UPDATE()
2779 self:Unschedule(updateAllRoster)
2780 self:Schedule(1.5, updateAllRoster, self)
2781 end
2782
2783 function DBM:INSTANCE_GROUP_SIZE_CHANGED()
2784 local _, _, _, _, _, _, _, _, instanceGroupSize = GetInstanceInfo()
2785 LastGroupSize = instanceGroupSize
2786 end
2787
2788 function DBM:GetNumRealPlayersInZone()
2789 if not IsInGroup() then return 1 end
2790 local total = 0
2791 local _, _, _, currentMapId = UnitPosition("player")
2792 if IsInRaid() then
2793 for i = 1, GetNumGroupMembers() do
2794 local _, _, _, targetMapId = UnitPosition("raid"..i)
2795 if targetMapId == currentMapId then
2796 total = total + 1
2797 end
2798 end
2799 else
2800 total = 1--add player/self for "party" count
2801 for i = 1, GetNumSubgroupMembers() do
2802 local _, _, _, targetMapId = UnitPosition("party"..i)
2803 if targetMapId == currentMapId then
2804 total = total + 1
2805 end
2806 end
2807 end
2808 return total
2809 end
2810
2811 function DBM:GetRaidRank(name)
2812 local name = name or playerName
2813 if name == playerName then--If name is player, try to get actual rank. Because raid[name].rank sometimes seems returning 0 even player is promoted.
2814 return UnitIsGroupLeader("player") and 2 or UnitIsGroupAssistant("player") and 1 or 0
2815 else
2816 return (raid[name] and raid[name].rank) or 0
2817 end
2818 end
2819
2820 function DBM:GetRaidSubgroup(name)
2821 return (raid[name] and raid[name].subgroup) or 0
2822 end
2823
2824 function DBM:GetRaidClass(name)
2825 return (raid[name] and raid[name].class) or "UNKNOWN"
2826 end
2827
2828 function DBM:GetRaidUnitId(name)
2829 return raid[name] and raid[name].id
2830 end
2831
2832 function DBM:GetUnitFullName(uId)
2833 return GetUnitName(uId, true)
2834 end
2835
2836 function DBM:GetFullPlayerNameByGUID(guid)
2837 return raidGuids[guid]
2838 end
2839
2840 function DBM:GetPlayerNameByGUID(guid)
2841 return raidGuids[guid] and raidGuids[guid]:gsub("%-.*$", "")
2842 end
2843
2844 function DBM:GetGroupId(name)
2845 local raidMember = raid[name] or raid[GetUnitName(name, true) or ""]
2846 return raidMember and raidMember.groupId or 0
2847 end
2848end
2849
2850do
2851 -- yes, we still do avoid memory allocations during fights; so we don't use a closure around a counter here
2852 -- this seems to be the easiest way to write an iterator that returns the unit id *string* as first argument without a memory allocation
2853 local function raidIterator(groupMembers, uId)
2854 local a, b = uId:byte(-2, -1)
2855 local i = (a >= 0x30 and a <= 0x39 and (a - 0x30) * 10 or 0) + b - 0x30
2856 if i < groupMembers then
2857 return "raid" .. i + 1, i + 1
2858 end
2859 end
2860
2861 local function partyIterator(groupMembers, uId)
2862 if not uId then
2863 return "player", 0
2864 elseif uId == "player" then
2865 if groupMembers > 0 then
2866 return "party1", 1
2867 end
2868 else
2869 local i = uId:byte(-1) - 0x30
2870 if i < groupMembers then
2871 return "party" .. i + 1, i + 1
2872 end
2873 end
2874 end
2875
2876 local function soloIterator(_, state)
2877 if not state then -- no state == first call
2878 return "player", 0
2879 end
2880 end
2881
2882 -- returns the unit ids of all raid or party members, including the player's own id
2883 -- limitations: will break if there are ever raids with more than 99 players or partys with more than 10
2884 function DBM:GetGroupMembers()
2885 if IsInRaid() then
2886 return raidIterator, GetNumGroupMembers(), "raid0"
2887 elseif IsInGroup() then
2888 return partyIterator, GetNumSubgroupMembers(), nil
2889 else
2890 -- solo!
2891 return soloIterator, nil, nil
2892 end
2893 end
2894end
2895
2896function DBM:GetNumGroupMembers()
2897 return IsInGroup() and GetNumGroupMembers() or 1
2898end
2899
2900--For returning the number of players actually in zone with us for status functions
2901--This is very touchy though and will fail if everyone isn't in same SUB zone (ie same room/area)
2902--It should work for pretty much any case but outdoor
2903function DBM:GetNumRealGroupMembers()
2904 if not IsInInstance() then--Not accurate outside of instances (such as world bosses)
2905 return IsInGroup() and GetNumGroupMembers() or 1--So just return regular group members.
2906 end
2907 local _, _, _, currentMapId = UnitPosition("player")
2908 local realGroupMembers = 0
2909 if IsInGroup() then
2910 for uId in self:GetGroupMembers() do
2911 local _, _, _, targetMapId = UnitPosition(uId)
2912 if targetMapId == currentMapId then
2913 realGroupMembers = realGroupMembers + 1
2914 end
2915 end
2916 else
2917 return 1
2918 end
2919 return realGroupMembers
2920end
2921
2922function DBM:GetUnitCreatureId(uId)
2923 local guid = UnitGUID(uId)
2924 return self:GetCIDFromGUID(guid)
2925end
2926
2927--Creature/Vehicle/Pet
2928----<type>:<subtype>:<realmID>:<mapID>:<serverID>:<dbID>:<creationbits>
2929--Player/Item
2930----<type>:<realmID>:<dbID>
2931function DBM:GetCIDFromGUID(guid)
2932 local type, _, playerdbID, _, _, cid, creationbits = strsplit("-", guid or "")
2933 if type and (type == "Creature" or type == "Vehicle" or type == "Pet") then
2934 return tonumber(cid)
2935 elseif type and (type == "Player" or type == "Item") then
2936 return tonumber(playerdbID)
2937 end
2938 return 0
2939end
2940
2941function DBM:IsCreatureGUID(guid)
2942 local type = strsplit("-", guid or "")
2943 if type and (type == "Creature" or type == "Vehicle") then--To determine, add pet or not?
2944 return true
2945 end
2946 return false
2947end
2948
2949function DBM:GetBossUnitId(name)
2950 for i = 1, 5 do
2951 if UnitName("boss" .. i) == name then
2952 return "boss" .. i
2953 end
2954 end
2955 for uId in self:GetGroupMembers() do
2956 if UnitName(uId .. "target") == name and not UnitIsPlayer(uId .. "target") then
2957 return uId .. "target"
2958 end
2959 end
2960end
2961
2962---------------
2963-- Options --
2964---------------
2965function DBM:AddDefaultOptions(t1, t2)
2966 for i, v in pairs(t2) do
2967 if t1[i] == nil then
2968 t1[i] = v
2969 elseif type(v) == "table" and type(t1[i]) == "table" then
2970 self:AddDefaultOptions(t1[i], v)
2971 end
2972 end
2973end
2974
2975function DBM:LoadModOptions(modId, inCombat, first)
2976 local oldSavedVarsName = modId:gsub("-", "").."_SavedVars"
2977 local savedVarsName = modId:gsub("-", "").."_AllSavedVars"
2978 local savedStatsName = modId:gsub("-", "").."_SavedStats"
2979 local fullname = playerName.."-"..playerRealm
2980 local profileNum = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
2981 if not _G[savedVarsName] then _G[savedVarsName] = {} end
2982 local savedOptions = _G[savedVarsName][fullname] or {}
2983 local savedStats = _G[savedStatsName] or {}
2984 local existId = {}
2985 for i, id in ipairs(DBM.ModLists[modId]) do
2986 existId[id] = true
2987 -- init
2988 if not savedOptions[id] then savedOptions[id] = {} end
2989 local mod = DBM:GetModByName(id)
2990 -- migrate old option
2991 if _G[oldSavedVarsName] and _G[oldSavedVarsName][id] then
2992 self:Debug("LoadModOptions: Found old options, importing", 2)
2993 local oldTable = _G[oldSavedVarsName][id]
2994 _G[oldSavedVarsName][id] = nil
2995 savedOptions[id][profileNum] = oldTable
2996 end
2997 if not savedOptions[id][profileNum] and not first then--previous profile not found. load defaults
2998 self:Debug("LoadModOptions: No saved options, creating defaults for profile "..profileNum, 2)
2999 local defaultOptions = {}
3000 for option, optionValue in pairs(mod.DefaultOptions) do
3001 if type(optionValue) == "table" then
3002 optionValue = optionValue.value
3003 elseif type(optionValue) == "string" then
3004 optionValue = mod:GetRoleFlagValue(optionValue)
3005 end
3006 defaultOptions[option] = optionValue
3007 end
3008 savedOptions[id][profileNum] = defaultOptions
3009 else
3010 savedOptions[id][profileNum] = savedOptions[id][profileNum] or mod.Options
3011 --check new option
3012 for option, optionValue in pairs(mod.DefaultOptions) do
3013 if savedOptions[id][profileNum][option] == nil then
3014 if type(optionValue) == "table" then
3015 optionValue = optionValue.value
3016 elseif type(optionValue) == "string" then
3017 optionValue = mod:GetRoleFlagValue(optionValue)
3018 end
3019 savedOptions[id][profileNum][option] = optionValue
3020 end
3021 end
3022 --clean unused saved variables (do not work on combat load)
3023 if not inCombat then
3024 for option, optionValue in pairs(savedOptions[id][profileNum]) do
3025 if mod.DefaultOptions[option] == nil then
3026 savedOptions[id][profileNum][option] = nil
3027 elseif mod.DefaultOptions[option] and (type(mod.DefaultOptions[option]) == "table") then--recover broken dropdown option
3028 if savedOptions[id][profileNum][option] and (type(savedOptions[id][profileNum][option]) == "boolean") then
3029 savedOptions[id][profileNum][option] = mod.DefaultOptions[option].value
3030 end
3031 --Fix default options for colored bar by type that were set to 0 because no defaults existed at time they were created, but do now.
3032 elseif option:find("TColor") then
3033 if savedOptions[id][profileNum][option] and savedOptions[id][profileNum][option] == 0 and mod.DefaultOptions[option] and mod.DefaultOptions[option] ~= 0 then
3034 savedOptions[id][profileNum][option] = mod.DefaultOptions[option]
3035 end
3036 end
3037 end
3038 end
3039 end
3040 --apply saved option to actual option table
3041 mod.Options = savedOptions[id][profileNum]
3042 --stats init (only first load)
3043 if first then
3044 savedStats[id] = savedStats[id] or {}
3045 local stats = savedStats[id]
3046 stats.normalKills = stats.normalKills or 0
3047 stats.normalPulls = stats.normalPulls or 0
3048 stats.heroicKills = stats.heroicKills or 0
3049 stats.heroicPulls = stats.heroicPulls or 0
3050 stats.challengeKills = stats.challengeKills or 0
3051 stats.challengePulls = stats.challengePulls or 0
3052 stats.challengeBestRank = stats.challengeBestRank or 0
3053 stats.mythicKills = stats.mythicKills or 0
3054 stats.mythicPulls = stats.mythicPulls or 0
3055 stats.normal25Kills = stats.normal25Kills or 0
3056 stats.normal25Kills = stats.normal25Kills or 0
3057 stats.normal25Pulls = stats.normal25Pulls or 0
3058 stats.heroic25Kills = stats.heroic25Kills or 0
3059 stats.heroic25Pulls = stats.heroic25Pulls or 0
3060 stats.lfr25Kills = stats.lfr25Kills or 0
3061 stats.lfr25Pulls = stats.lfr25Pulls or 0
3062 stats.timewalkerKills = stats.timewalkerKills or 0
3063 stats.timewalkerPulls = stats.timewalkerPulls or 0
3064 mod.stats = stats
3065 --run OnInitialize function
3066 if mod.OnInitialize then mod:OnInitialize() end
3067 end
3068 end
3069 --clean unused saved variables (do not work on combat load)
3070 if not inCombat then
3071 for id, table in pairs(savedOptions) do
3072 if not existId[id] and not id:find("talent") then
3073 savedOptions[id] = nil
3074 end
3075 end
3076 for id, table in pairs(savedStats) do
3077 if not existId[id] then
3078 savedStats[id] = nil
3079 end
3080 end
3081 end
3082 _G[savedVarsName][fullname] = savedOptions
3083 if profileNum > 0 then
3084 _G[savedVarsName][fullname]["talent"..profileNum] = currentSpecName
3085 self:Debug("LoadModOptions: Finished loading "..(_G[savedVarsName][fullname]["talent"..profileNum] or DBM_CORE_UNKNOWN))
3086 end
3087 _G[savedStatsName] = savedStats
3088 if not first and DBM_GUI and DBM_GUI.currentViewing and DBM_GUI_OptionsFrame:IsShown() then
3089 DBM_GUI_OptionsFrame:DisplayFrame(DBM_GUI.currentViewing)
3090 end
3091end
3092
3093function DBM:SpecChanged(force)
3094 if not force and not DBM_UseDualProfile then return end
3095 --Load Options again.
3096 self:Debug("SpecChanged fired", 2)
3097 for modId, idTable in pairs(self.ModLists) do
3098 self:LoadModOptions(modId)
3099 end
3100end
3101
3102function DBM:PLAYER_LEVEL_UP()
3103 playerLevel = UnitLevel("player")
3104 if playerLevel < 15 and playerLevel > 9 then
3105 self:PLAYER_SPECIALIZATION_CHANGED()
3106 end
3107end
3108
3109function DBM:LoadAllModDefaultOption(modId)
3110 -- modId is string like "DBM-Highmaul"
3111 if not modId or not self.ModLists[modId] then return end
3112 -- prevent error
3113 if not currentSpecID then
3114 self:SetCurrentSpecInfo()
3115 end
3116 -- variable init
3117 local savedVarsName = modId:gsub("-", "").."_AllSavedVars"
3118 local fullname = playerName.."-"..playerRealm
3119 local profileNum = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
3120 -- prevent nil table error
3121 if not _G[savedVarsName] then _G[savedVarsName] = {} end
3122 for i, id in ipairs(self.ModLists[modId]) do
3123 -- prevent nil table error
3124 if not _G[savedVarsName][fullname][id] then _G[savedVarsName][fullname][id] = {} end
3125 -- actual do load default option
3126 local mod = self:GetModByName(id)
3127 local defaultOptions = {}
3128 for option, optionValue in pairs(mod.DefaultOptions) do
3129 if type(optionValue) == "table" then
3130 optionValue = optionValue.value
3131 elseif type(optionValue) == "string" then
3132 optionValue = mod:GetRoleFlagValue(optionValue)
3133 end
3134 defaultOptions[option] = optionValue
3135 end
3136 mod.Options = {}
3137 mod.Options = defaultOptions
3138 _G[savedVarsName][fullname][id][profileNum] = {}
3139 _G[savedVarsName][fullname][id][profileNum] = mod.Options
3140 end
3141 self:AddMsg(DBM_CORE_ALLMOD_DEFAULT_LOADED)
3142 -- update gui if showing
3143 if DBM_GUI and DBM_GUI.currentViewing and DBM_GUI_OptionsFrame:IsShown() then
3144 DBM_GUI_OptionsFrame:DisplayFrame(DBM_GUI.currentViewing)
3145 end
3146end
3147
3148function DBM:LoadModDefaultOption(mod)
3149 -- mod must be table
3150 if not mod then return end
3151 -- prevent error
3152 if not currentSpecID then
3153 self:SetCurrentSpecInfo()
3154 end
3155 -- variable init
3156 local savedVarsName = (mod.modId):gsub("-", "").."_AllSavedVars"
3157 local fullname = playerName.."-"..playerRealm
3158 local profileNum = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
3159 -- prevent nil table error
3160 if not _G[savedVarsName] then _G[savedVarsName] = {} end
3161 if not _G[savedVarsName][fullname] then _G[savedVarsName][fullname] = {} end
3162 if not _G[savedVarsName][fullname][mod.id] then _G[savedVarsName][fullname][mod.id] = {} end
3163 -- do load default
3164 local defaultOptions = {}
3165 for option, optionValue in pairs(mod.DefaultOptions) do
3166 if type(optionValue) == "table" then
3167 optionValue = optionValue.value
3168 elseif type(optionValue) == "string" then
3169 optionValue = mod:GetRoleFlagValue(optionValue)
3170 end
3171 defaultOptions[option] = optionValue
3172 end
3173 mod.Options = {}
3174 mod.Options = defaultOptions
3175 _G[savedVarsName][fullname][mod.id][profileNum] = {}
3176 _G[savedVarsName][fullname][mod.id][profileNum] = mod.Options
3177 self:AddMsg(DBM_CORE_MOD_DEFAULT_LOADED)
3178 -- update gui if showing
3179 if DBM_GUI and DBM_GUI.currentViewing and DBM_GUI_OptionsFrame:IsShown() then
3180 DBM_GUI_OptionsFrame:DisplayFrame(DBM_GUI.currentViewing)
3181 end
3182end
3183
3184function DBM:CopyAllModOption(modId, sourceName, sourceProfile)
3185 -- modId is string like "DBM-Highmaul"
3186 if not modId or not sourceName or not sourceProfile or not DBM.ModLists[modId] then return end
3187 -- prevent error
3188 if not currentSpecID then
3189 self:SetCurrentSpecInfo()
3190 end
3191 -- variable init
3192 local savedVarsName = modId:gsub("-", "").."_AllSavedVars"
3193 local targetName = playerName.."-"..playerRealm
3194 local targetProfile = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
3195 -- do not copy setting itself
3196 if targetName == sourceName and targetProfile == sourceProfile then
3197 self:AddMsg(DBM_CORE_MPROFILE_COPY_SELF_ERROR)
3198 return
3199 end
3200 -- prevent nil table error
3201 if not _G[savedVarsName] then _G[savedVarsName] = {} end
3202 -- check source is exist
3203 if not _G[savedVarsName][sourceName] then
3204 self:AddMsg(DBM_CORE_MPROFILE_COPY_S_ERROR)
3205 return
3206 end
3207 local targetOptions = _G[savedVarsName][targetName] or {}
3208 for i, id in ipairs(self.ModLists[modId]) do
3209 -- check source is exist
3210 if not _G[savedVarsName][sourceName][id] then
3211 self:AddMsg(DBM_CORE_MPROFILE_COPY_S_ERROR)
3212 return
3213 end
3214 if not _G[savedVarsName][sourceName][id][sourceProfile] then
3215 self:AddMsg(DBM_CORE_MPROFILE_COPY_S_ERROR)
3216 return
3217 end
3218 -- prevent nil table error
3219 if not _G[savedVarsName][targetName][id] then _G[savedVarsName][targetName][id] = {} end
3220 -- copy table
3221 _G[savedVarsName][targetName][id][targetProfile] = {}--clear before copy
3222 _G[savedVarsName][targetName][id][targetProfile] = _G[savedVarsName][sourceName][id][sourceProfile]
3223 --check new option
3224 local mod = self:GetModByName(id)
3225 for option, optionValue in pairs(mod.Options) do
3226 if _G[savedVarsName][targetName][id][targetProfile][option] == nil then
3227 _G[savedVarsName][targetName][id][targetProfile][option] = optionValue
3228 end
3229 end
3230 -- apply to options table
3231 mod.Options = {}
3232 mod.Options = _G[savedVarsName][targetName][id][targetProfile]
3233 end
3234 if targetProfile > 0 then
3235 _G[savedVarsName][targetName]["talent"..targetProfile] = currentSpecName
3236 end
3237 self:AddMsg(DBM_CORE_MPROFILE_COPY_SUCCESS:format(sourceName, sourceProfile))
3238 -- update gui if showing
3239 if DBM_GUI and DBM_GUI.currentViewing and DBM_GUI_OptionsFrame:IsShown() then
3240 DBM_GUI_OptionsFrame:DisplayFrame(DBM_GUI.currentViewing)
3241 end
3242end
3243
3244function DBM:CopyAllModTypeOption(modId, sourceName, sourceProfile, Type)
3245 -- modId is string like "DBM-Highmaul"
3246 if not modId or not sourceName or not sourceProfile or not self.ModLists[modId] or not Type then return end
3247 -- prevent error
3248 if not currentSpecID then
3249 self:SetCurrentSpecInfo()
3250 end
3251 -- variable init
3252 local savedVarsName = modId:gsub("-", "").."_AllSavedVars"
3253 local targetName = playerName.."-"..playerRealm
3254 local targetProfile = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
3255 -- do not copy setting itself
3256 if targetName == sourceName and targetProfile == sourceProfile then
3257 self:AddMsg(DBM_CORE_MPROFILE_COPYS_SELF_ERROR)
3258 return
3259 end
3260 -- prevent nil table error
3261 if not _G[savedVarsName] then _G[savedVarsName] = {} end
3262 -- check source is exist
3263 if not _G[savedVarsName][sourceName] then
3264 self:AddMsg(DBM_CORE_MPROFILE_COPYS_S_ERROR)
3265 return
3266 end
3267 local targetOptions = _G[savedVarsName][targetName] or {}
3268 for i, id in ipairs(self.ModLists[modId]) do
3269 -- check source is exist
3270 if not _G[savedVarsName][sourceName][id] then
3271 self:AddMsg(DBM_CORE_MPROFILE_COPYS_S_ERROR)
3272 return
3273 end
3274 if not _G[savedVarsName][sourceName][id][sourceProfile] then
3275 self:AddMsg(DBM_CORE_MPROFILE_COPYS_S_ERROR)
3276 return
3277 end
3278 -- prevent nil table error
3279 if not _G[savedVarsName][targetName][id] then _G[savedVarsName][targetName][id] = {} end
3280 -- copy table
3281 for option, optionValue in pairs(_G[savedVarsName][sourceName][id][sourceProfile]) do
3282 if option:find(Type) then
3283 _G[savedVarsName][targetName][id][targetProfile][option] = optionValue
3284 end
3285 end
3286 -- apply to options table
3287 local mod = self:GetModByName(id)
3288 mod.Options = {}
3289 mod.Options = _G[savedVarsName][targetName][id][targetProfile]
3290 end
3291 if targetProfile > 0 then
3292 _G[savedVarsName][targetName]["talent"..targetProfile] = currentSpecName
3293 end
3294 self:AddMsg(DBM_CORE_MPROFILE_COPYS_SUCCESS:format(sourceName, sourceProfile))
3295 -- update gui if showing
3296 if DBM_GUI and DBM_GUI.currentViewing and DBM_GUI_OptionsFrame:IsShown() then
3297 DBM_GUI_OptionsFrame:DisplayFrame(DBM_GUI.currentViewing)
3298 end
3299end
3300
3301function DBM:DeleteAllModOption(modId, name, profile)
3302 -- modId is string like "DBM-Highmaul"
3303 if not modId or not name or not profile or not self.ModLists[modId] then return end
3304 -- prevent error
3305 if not currentSpecID then
3306 self:SetCurrentSpecInfo()
3307 end
3308 -- variable init
3309 local savedVarsName = modId:gsub("-", "").."_AllSavedVars"
3310 local fullname = playerName.."-"..playerRealm
3311 local profileNum = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
3312 -- cannot delete current profile.
3313 if fullname == name and profileNum == profile then
3314 self:AddMsg(DBM_CORE_MPROFILE_DELETE_SELF_ERROR)
3315 return
3316 end
3317 -- prevent nil table error
3318 if not _G[savedVarsName] then _G[savedVarsName] = {} end
3319 if not _G[savedVarsName][name] then
3320 self:AddMsg(DBM_CORE_MPROFILE_DELETE_S_ERROR)
3321 return
3322 end
3323 for i, id in ipairs(self.ModLists[modId]) do
3324 -- prevent nil table error
3325 if not _G[savedVarsName][name][id] then
3326 self:AddMsg(DBM_CORE_MPROFILE_DELETE_S_ERROR)
3327 return
3328 end
3329 -- delete
3330 _G[savedVarsName][name][id][profile] = nil
3331 end
3332 _G[savedVarsName][name]["talent"..profile] = nil
3333 self:AddMsg(DBM_CORE_MPROFILE_DELETE_SUCCESS:format(name, profile))
3334end
3335
3336function DBM:ClearAllStats(modId)
3337 -- modId is string like "DBM-Highmaul"
3338 if not modId or not self.ModLists[modId] then return end
3339 -- variable init
3340 local savedStatsName = modId:gsub("-", "").."_SavedStats"
3341 -- prevent nil table error
3342 if not _G[savedStatsName] then _G[savedStatsName] = {} end
3343 for i, id in ipairs(self.ModLists[modId]) do
3344 local mod = self:GetModByName(id)
3345 -- prevent nil table error
3346 local defaultStats = {}
3347 defaultStats.normalKills = 0
3348 defaultStats.normalPulls = 0
3349 defaultStats.heroicKills = 0
3350 defaultStats.heroicPulls = 0
3351 defaultStats.challengeKills = 0
3352 defaultStats.challengePulls = 0
3353 defaultStats.challengeBestRank = 0
3354 defaultStats.mythicKills = 0
3355 defaultStats.mythicPulls = 0
3356 defaultStats.normal25Kills = 0
3357 defaultStats.normal25Kills = 0
3358 defaultStats.normal25Pulls = 0
3359 defaultStats.heroic25Kills = 0
3360 defaultStats.heroic25Pulls = 0
3361 defaultStats.lfr25Kills = 0
3362 defaultStats.lfr25Pulls = 0
3363 defaultStats.timewalkerKills = 0
3364 defaultStats.timewalkerPulls = 0
3365 mod.stats = {}
3366 mod.stats = defaultStats
3367 _G[savedStatsName][id] = {}
3368 _G[savedStatsName][id] = defaultStats
3369 end
3370 self:AddMsg(DBM_CORE_ALLMOD_STATS_RESETED)
3371 DBM_GUI:UpdateModList()
3372end
3373
3374do
3375 function loadOptions(self)
3376 --init
3377 if not DBM_AllSavedOptions then DBM_AllSavedOptions = {} end
3378 usedProfile = DBM_UsedProfile or usedProfile
3379 if not usedProfile or (usedProfile ~= "Default" and not DBM_AllSavedOptions[usedProfile]) then
3380 -- DBM.Option is not loaded. so use print function
3381 print(DBM_CORE_PROFILE_NOT_FOUND)
3382 usedProfile = "Default"
3383 end
3384 DBM_UsedProfile = usedProfile
3385 --migrate old options
3386 if DBM_SavedOptions and not DBM_AllSavedOptions[usedProfile] then
3387 DBM_AllSavedOptions[usedProfile] = DBM_SavedOptions
3388 end
3389 self.Options = DBM_AllSavedOptions[usedProfile] or {}
3390 dbmIsEnabled = true
3391 self:AddDefaultOptions(self.Options, self.DefaultOptions)
3392 DBM_AllSavedOptions[usedProfile] = self.Options
3393
3394 -- force enable dual profile (change default)
3395 if DBM_CharSavedRevision < 12976 then
3396 if class ~= "MAGE" and class ~= "WARLOCK" and class ~= "ROGUE" then
3397 DBM_UseDualProfile = true
3398 end
3399 end
3400 DBM_CharSavedRevision = self.Revision
3401
3402 -- load special warning options
3403 self:UpdateWarningOptions()
3404 self:UpdateSpecialWarningOptions()
3405 --Fix old options that use .wav instead of .ogg, to prevent no sounds bug as of 6.1+
3406 if self.Options.RaidWarningSound:find(".wav") then self.Options.RaidWarningSound = self.DefaultOptions.RaidWarningSound end
3407 if self.Options.SpecialWarningSound:find(".wav") then self.Options.SpecialWarningSound = self.DefaultOptions.SpecialWarningSound end
3408 if self.Options.SpecialWarningSound2:find(".wav") then self.Options.SpecialWarningSound2 = self.DefaultOptions.SpecialWarningSound2 end
3409 if self.Options.SpecialWarningSound3:find(".wav") then self.Options.SpecialWarningSound3 = self.DefaultOptions.SpecialWarningSound3 end
3410 if self.Options.SpecialWarningSound4:find(".wav") then self.Options.SpecialWarningSound4 = self.DefaultOptions.SpecialWarningSound4 end
3411 end
3412end
3413
3414do
3415 local lastLFGAlert = 0
3416 function DBM:LFG_ROLE_CHECK_SHOW()
3417 if not UnitIsGroupLeader("player") and self.Options.LFDEnhance and GetTime() - lastLFGAlert > 5 then
3418 self:FlashClientIcon()
3419 self:PlaySoundFile("Sound\\interface\\levelup2.ogg", true)--Because regular sound uses SFX channel which is too low of volume most of time
3420 lastLFGAlert = GetTime()
3421 end
3422 end
3423end
3424
3425function DBM:LFG_PROPOSAL_SHOW()
3426 if self.Options.ShowQueuePop and not self.Options.DontShowBossTimers then
3427 self.Bars:CreateBar(40, DBM_LFG_INVITE, "Interface\\Icons\\Spell_Holy_BorrowedTime")
3428 end
3429 if self.Options.LFDEnhance then
3430 self:FlashClientIcon()
3431 self:PlaySoundFile("Sound\\interface\\levelup2.ogg", true)--Because regular sound uses SFX channel which is too low of volume most of time
3432 end
3433end
3434
3435function DBM:LFG_PROPOSAL_FAILED()
3436 self.Bars:CancelBar(DBM_LFG_INVITE)
3437end
3438
3439function DBM:LFG_PROPOSAL_SUCCEEDED()
3440 self.Bars:CancelBar(DBM_LFG_INVITE)
3441end
3442
3443function DBM:READY_CHECK()
3444 if self.Options.RLReadyCheckSound then--readycheck sound, if ora3 not installed (bad to have 2 mods do it)
3445 self:FlashClientIcon()
3446 if not BINDING_HEADER_oRA3 then
3447 self:PlaySoundFile("Sound\\interface\\levelup2.ogg", true)--Because regular sound uses SFX channel which is too low of volume most of time
3448 end
3449 end
3450end
3451
3452function DBM:PLAYER_SPECIALIZATION_CHANGED()
3453 local lastSpecID = currentSpecID
3454 self:SetCurrentSpecInfo()
3455 if currentSpecID ~= lastSpecID then--Don't fire specchanged unless spec actually has changed.
3456 self:SpecChanged()
3457 if IsInGroup() then
3458 self:RoleCheck(false)
3459 end
3460 end
3461end
3462
3463do
3464 local function AcceptPartyInvite()
3465 AcceptGroup()
3466 for i=1, STATICPOPUP_NUMDIALOGS do
3467 local whichDialog = _G["StaticPopup"..i].which
3468 if whichDialog == "PARTY_INVITE" or whichDialog == "PARTY_INVITE_XREALM" then
3469 _G["StaticPopup"..i].inviteAccepted = 1
3470 StaticPopup_Hide(whichDialog)
3471 break
3472 end
3473 end
3474 end
3475
3476 function DBM:PARTY_INVITE_REQUEST(sender)
3477 --First off, if you are in queue for something, lets not allow guildies or friends boot you from it.
3478 if (IsInInstance() and not C_Garrison:IsOnGarrisonMap()) or GetLFGMode(1) or GetLFGMode(2) or GetLFGMode(3) or GetLFGMode(4) or GetLFGMode(5) then return end
3479 --First check realID
3480 if self.Options.AutoAcceptFriendInvite then
3481 local _, numBNetOnline = BNGetNumFriends()
3482 for i = 1, numBNetOnline do
3483 local presenceID, _, _, _, _, _, _, isOnline = BNGetFriendInfo(i)
3484 local friendIndex = BNGetFriendIndex(presenceID)--Check if they are on more than one client at once (very likely with new launcher)
3485 for i=1, BNGetNumFriendGameAccounts(friendIndex) do
3486 local _, toonName, client = BNGetFriendGameAccountInfo(friendIndex, i)
3487 if toonName and client == BNET_CLIENT_WOW then--Check if toon name exists and if client is wow. If yes to both, we found right client
3488 if toonName == sender then--Now simply see if this is sender
3489 AcceptPartyInvite()
3490 return
3491 end
3492 end
3493 end
3494 end
3495 -- Check regular non-BNet friends
3496 local nf = GetNumFriends()
3497 for i = 1, nf do
3498 local toonName = GetFriendInfo(i)
3499 if toonName == sender then
3500 AcceptPartyInvite()
3501 return
3502 end
3503 end
3504 end
3505 --Second check guildies
3506 if self.Options.AutoAcceptGuildInvite then
3507 local totalMembers, numOnlineGuildMembers, numOnlineAndMobileMembers = GetNumGuildMembers()
3508 local scanTotal = GetGuildRosterShowOffline() and totalMembers or numOnlineAndMobileMembers
3509 for i=1, scanTotal do
3510 --At this time, it's not easy to tell an officer from a non officer
3511 --since a guild might have ranks 1-3 or even 1-4 be officers/leader while another might only be 1-2
3512 --therefor, this feature is just a "yes/no" for if sender is a guildy
3513 local name, rank, rankIndex = GetGuildRosterInfo(i)
3514 if not name then break end
3515 name = Ambiguate(name, "none")
3516 if sender == name then
3517 AcceptPartyInvite()
3518 return
3519 end
3520 end
3521 end
3522 end
3523end
3524
3525function DBM:PLAYER_REGEN_ENABLED()
3526 if loadDelay then
3527 self:Debug("loadDelay is activating LoadMod again")
3528 self:LoadMod(loadDelay)
3529 end
3530 if loadDelay2 then
3531 self:Debug("loadDelay2 is activating LoadMod again")
3532 self:LoadMod(loadDelay2)
3533 end
3534 if guiRequested and not IsAddOnLoaded("DBM-GUI") then
3535 guiRequested = false
3536 self:LoadGUI()
3537 end
3538end
3539
3540function DBM:UPDATE_BATTLEFIELD_STATUS()
3541 for i = 1, 2 do
3542 if GetBattlefieldStatus(i) == "confirm" then
3543 if self.Options.ShowQueuePop and not self.Options.DontShowBossTimers then
3544 queuedBattlefield[i] = select(2, GetBattlefieldStatus(i))
3545 self.Bars:CreateBar(85, queuedBattlefield[i], "Interface\\Icons\\Spell_Holy_BorrowedTime") -- need to confirm the timer
3546 end
3547 if self.Options.LFDEnhance then
3548 self:PlaySoundFile("Sound\\interface\\levelup2.ogg", true)--Because regular sound uses SFX channel which is too low of volume most of time
3549 end
3550 elseif queuedBattlefield[i] then
3551 self.Bars:CancelBar(queuedBattlefield[i])
3552 queuedBattlefield[i] = nil
3553 end
3554 end
3555end
3556
3557function DBM:SCENARIO_CRITERIA_UPDATE()
3558 local _, currentStage, numStages = C_Scenario.GetInfo()
3559 if #inCombat > 0 and currentStage > numStages and C_Scenario.IsInScenario() then
3560 for i = #inCombat, 1, -1 do
3561 local v = inCombat[i]
3562 if v.inScenario then
3563 self:EndCombat(v)
3564 end
3565 end
3566 end
3567end
3568
3569--------------------------------
3570-- Load Boss Mods on Demand --
3571--------------------------------
3572do
3573 local function checkMods(self)
3574 if difficultyIndex == 24 then
3575 --Surely a less shitty way of checking "this is a BC dungeon"?
3576 if (LastInstanceMapID == 540 or LastInstanceMapID == 558 or LastInstanceMapID == 556 or LastInstanceMapID == 555 or LastInstanceMapID == 542 or LastInstanceMapID == 546 or LastInstanceMapID == 545 or LastInstanceMapID == 547 or LastInstanceMapID == 553 or LastInstanceMapID == 554 or LastInstanceMapID == 552 or LastInstanceMapID == 557 or LastInstanceMapID == 269 or LastInstanceMapID == 560 or LastInstanceMapID == 543 or LastInstanceMapID == 585) and not self.Options.BCTWMessageShown and not GetAddOnInfo("DBM-Party-BC") then
3577 self.Options.BCTWMessageShown = true
3578 self:AddMsg(DBM_CORE_MOD_AVAILABLE:format("DBM-Party-BC"))
3579 --Surely a less shitty way of checking "this is a wrath dungeon"?
3580 elseif (LastInstanceMapID == 619 or LastInstanceMapID == 601 or LastInstanceMapID == 595 or LastInstanceMapID == 600 or LastInstanceMapID == 604 or LastInstanceMapID == 602 or LastInstanceMapID == 599 or LastInstanceMapID == 576 or LastInstanceMapID == 578 or LastInstanceMapID == 574 or LastInstanceMapID == 575 or LastInstanceMapID == 608 or LastInstanceMapID == 658 or LastInstanceMapID == 632 or LastInstanceMapID == 668 or LastInstanceMapID == 650) and not self.Options.WOTLKTWMessageShown and not GetAddOnInfo("DBM-Party-WotLK") then
3581 self.Options.WOTLKTWMessageShown = true
3582 self:AddMsg(DBM_CORE_MOD_AVAILABLE:format("DBM-Party-WotLK"))
3583 elseif (LastInstanceMapID == 755 or LastInstanceMapID == 645 or LastInstanceMapID == 36 or LastInstanceMapID == 670 or LastInstanceMapID == 644 or LastInstanceMapID == 33 or LastInstanceMapID == 643 or LastInstanceMapID == 725 or LastInstanceMapID == 657 or LastInstanceMapID == 309 or LastInstanceMapID == 859 or LastInstanceMapID == 568 or LastInstanceMapID == 938 or LastInstanceMapID == 940 or LastInstanceMapID == 939) and not self.Options.CATATWMessageShown and not GetAddOnInfo("DBM-Party-Cataclysm") then
3584 self.Options.CATATWMessageShown = true--
3585 self:AddMsg(DBM_CORE_MOD_AVAILABLE:format("DBM-Party-Cataclysm"))
3586 end
3587 else
3588 if LastInstanceMapID == 1148 and not self.Options.PGMessageShown and not GetAddOnInfo("DBM-ProvingGrounds") then
3589 self.Options.PGMessageShown = true
3590 self:AddMsg(DBM_CORE_MOD_AVAILABLE:format("DBM-ProvingGrounds"))
3591 elseif LastInstanceMapID == 409 and not self.Options.MCMessageShown and not GetAddOnInfo("DBM-MC") then
3592 self.Options.MCMessageShown = true
3593 self:AddMsg(DBM_CORE_MOD_AVAILABLE:format("DBM-MC"))
3594 end
3595 end
3596 end
3597 local function SecondaryLoadCheck(self)
3598 local _, instanceType, difficulty, _, _, _, _, mapID, instanceGroupSize = GetInstanceInfo()
3599 self:Debug("Instance Check fired with mapID "..mapID.." and difficulty "..difficulty, 2)
3600 if LastInstanceMapID == mapID then
3601 self:Debug("No action taken because mapID hasn't changed since last check", 2)
3602 return
3603 end--ID hasn't changed, don't waste cpu doing anything else (example situation, porting into garrosh phase 4 is a loading screen)
3604 LastInstanceMapID = mapID
3605 LastGroupSize = instanceGroupSize
3606 difficultyIndex = difficulty
3607 if instanceType == "none" or C_Garrison:IsOnGarrisonMap() then
3608 LastInstanceType = "none"
3609 if not targetEventsRegistered then
3610 self:RegisterShortTermEvents("UPDATE_MOUSEOVER_UNIT")
3611 targetEventsRegistered = true
3612 end
3613 else
3614 LastInstanceType = instanceType
3615 if targetEventsRegistered then
3616 self:UnregisterShortTermEvents()
3617 targetEventsRegistered = false
3618 end
3619 if savedDifficulty == "worldboss" then
3620 for i = #inCombat, 1, -1 do
3621 self:EndCombat(inCombat[i], true)
3622 end
3623 end
3624 end
3625 -- LoadMod
3626 self:LoadModsOnDemand("mapId", mapID)
3627 checkMods(self)
3628 end
3629 --Faster and more accurate loading for instances, but useless outside of them
3630 function DBM:LOADING_SCREEN_DISABLED()
3631 self.Bars:CancelBar(DBM_LFG_INVITE)--Disable bar here since LFG_PROPOSAL_SUCCEEDED seems broken right now
3632 timerRequestInProgress = false
3633 self:Debug("LOADING_SCREEN_DISABLED fired")
3634 self:Unschedule(SecondaryLoadCheck)
3635 self:Schedule(1, SecondaryLoadCheck, self)
3636 self:Schedule(5, SecondaryLoadCheck, self)
3637 if DBM:HasMapRestrictions() then
3638 DBM.Arrow:Hide()
3639 DBMHudMap:Disable()
3640 if DBM.RangeCheck:IsRadarShown() then
3641 DBM.RangeCheck:Hide(true)
3642 end
3643 end
3644 end
3645
3646 function DBM:LoadModsOnDemand(checkTable, checkValue)
3647 self:Debug("LoadModsOnDemand fired")
3648 for i, v in ipairs(self.AddOns) do
3649 local modTable = v[checkTable]
3650 local enabled = GetAddOnEnableState(playerName, v.modId)
3651 --self:Debug(v.modId.." is "..enabled, 2)
3652 if not IsAddOnLoaded(v.modId) and modTable and checkEntry(modTable, checkValue) then
3653 if enabled ~= 0 then
3654 self:LoadMod(v)
3655 else
3656 self:Debug("Not loading "..v.name.." because it is not enabled")
3657 end
3658 end
3659 end
3660 self:ScenarioCheck()--Do not filter. Because ScenarioCheck function includes filter.
3661 end
3662end
3663
3664--Scenario mods
3665function DBM:ScenarioCheck()
3666 if dbmIsEnabled and combatInfo[LastInstanceMapID] then
3667 for i, v in ipairs(combatInfo[LastInstanceMapID]) do
3668 if (v.type == "scenario") and checkEntry(v.msgs, LastInstanceMapID) then
3669 self:StartCombat(v.mod, 0, "LOADING_SCREEN_DISABLED")
3670 end
3671 end
3672 end
3673end
3674
3675 --In combat and it's not a raid boss. We'll just delay mod load until we leave combat to avoid "script ran to long errors"
3676 --This should avoid most load problems (especially in LFR) When zoning in while in combat which causes the mod to fail to load/work correctly
3677 --IF we are fighting a boss, we don't have much of a choice but to try and load anyways since script ran too long isn't actually a guarentee.
3678 --The main place we should force a mod load in combat is for IsEncounterInProgress because i'm pretty sure blizzard waves "script ran too long" function for a small amount of time after a DC
3679 --Outdoor bosses will try to ignore check, if they fail, well, then we need to try and find ways to make the mods that can't load in combat smaller or load faster.
3680function DBM:LoadMod(mod, force)
3681 if type(mod) ~= "table" then
3682 self:Debug("LoadMod failed because mod table not valid")
3683 return false
3684 end
3685 if mod.isWorldBoss and not IsInInstance() and not force then
3686 return
3687 end--Don't load world boss mod this way.
3688 if mod.minRevision > self.Revision then
3689 if self:AntiSpam(60, "VER_MISMATCH") then--Throttle message in case person keeps trying to load mod (or it's a world boss player keeps targeting
3690 self:AddMsg(DBM_CORE_LOAD_MOD_VER_MISMATCH:format(mod.name))
3691 end
3692 return
3693 end
3694 if InCombatLockdown() and not IsEncounterInProgress() and IsInInstance() and not noDelay then
3695 self:Debug("LoadMod delayed do to combat")
3696 if not loadDelay then--Prevent duplicate DBM_CORE_LOAD_MOD_COMBAT message.
3697 self:AddMsg(DBM_CORE_LOAD_MOD_COMBAT:format(tostring(mod.name)))
3698 end
3699 if loadDelay and loadDelay ~= mod then--Check if load delay exists, but make sure this isn't a loop of same mod before making a second load delay
3700 loadDelay2 = mod
3701 else
3702 loadDelay = mod
3703 end
3704 return
3705 end
3706 if not currentSpecID then
3707 self:SetCurrentSpecInfo()
3708 end
3709 EJ_SetDifficulty(difficultyIndex)--Work around blizzard crash bug where other mods (like Boss) screw with Ej difficulty value, which makes EJ_GetSectionInfo crash the game when called with invalid difficulty index set.
3710 self:Debug("LoadAddOn should have fired for "..mod.name, 2)
3711 local loaded, reason = LoadAddOn(mod.modId)
3712 if not loaded then
3713 if reason then
3714 self:AddMsg(DBM_CORE_LOAD_MOD_ERROR:format(tostring(mod.name), tostring(_G["ADDON_"..reason or ""])))
3715 else
3716 self:Debug("LoadAddOn failed and did not give reason")
3717-- self:AddMsg(DBM_CORE_LOAD_MOD_ERROR:format(tostring(mod.name), DBM_CORE_UNKNOWN)) -- wtf, this should never happen....(but it does happen sometimes if you reload your UI in an instance...)
3718 end
3719 return false
3720 else
3721 self:Debug("LoadAddOn should have succeeded for "..mod.name, 2)
3722 self:AddMsg(DBM_CORE_LOAD_MOD_SUCCESS:format(tostring(mod.name)))
3723 self:LoadModOptions(mod.modId, InCombatLockdown(), true)
3724 if DBM_GUI then
3725 DBM_GUI:UpdateModList()
3726 end
3727 if LastInstanceType ~= "pvp" and #inCombat == 0 and IsInGroup() then--do timer recovery only mod load
3728 if not timerRequestInProgress then
3729 timerRequestInProgress = true
3730 -- Request timer to 3 person to prevent failure.
3731 self:Unschedule(self.RequestTimers)
3732 self:Schedule(7, self.RequestTimers, self, 1)
3733 self:Schedule(10, self.RequestTimers, self, 2)
3734 self:Schedule(13, self.RequestTimers, self, 3)
3735 C_TimerAfter(15, function() timerRequestInProgress = false end)
3736 end
3737 end
3738 if not InCombatLockdown() then--We loaded in combat because a raid boss was in process, but lets at least delay the garbage collect so at least load mod is half as bad, to do our best to avoid "script ran too long"
3739 collectgarbage("collect")
3740 end
3741 if loadDelay2 == mod then
3742 loadDelay2 = nil
3743 elseif loadDelay == mod then
3744 loadDelay = nil
3745 end
3746 return true
3747 end
3748end
3749
3750do
3751 local function loadModByUnit(uId)
3752 if not uId then
3753 uId = "mouseover"
3754 else
3755 uId = uId.."target"
3756 end
3757 if IsInInstance() or not UnitIsFriend("player", uId) and UnitIsDead("player") or UnitIsDead(uId) then return end--If you're in an instance no reason to waste cpu. If THE BOSS dead, no reason to load a mod for it. To prevent rare lua error, needed to filter on player dead.
3758 local guid = UnitGUID(uId)
3759 if guid and DBM:IsCreatureGUID(guid) then
3760 local cId = DBM:GetCIDFromGUID(guid)
3761 for bosscId, addon in pairs(loadcIds) do
3762 local enabled = GetAddOnEnableState(playerName, addon)
3763 if cId and bosscId and cId == bosscId and not IsAddOnLoaded(addon) and enabled ~= 0 then
3764 for i, v in ipairs(DBM.AddOns) do
3765 if v.modId == addon then
3766 DBM:LoadMod(v, true)
3767 break
3768 end
3769 end
3770 end
3771 end
3772 end
3773 end
3774
3775 --Loading routeens hacks for world bosses based on target or mouseover.
3776 function DBM:UPDATE_MOUSEOVER_UNIT()
3777 loadModByUnit()
3778 end
3779
3780 function DBM:UNIT_TARGET_UNFILTERED(uId)
3781 if targetEventsRegistered then--Allow outdoor mod loading
3782 loadModByUnit(uId)
3783 end
3784 --Debug options for seeing where BossUnitTargetScanner can be used.
3785 if (self.Options.DebugLevel > 2 or (Transcriptor and Transcriptor:IsLogging())) and (uId == "boss1" or uId == "boss2" or uId == "boss3" or uId == "boss4" or uId == "boss5") then
3786 local targetName = uId == "boss1" and UnitName("boss1target") or uId == "boss2" and UnitName("boss2target") or uId == "boss3" and UnitName("boss3target") or uId == "boss4" and UnitName("boss4target") or uId == "boss5" and UnitName("boss5target") or "nil"
3787 self:Debug(uId.." changed targets to "..targetName)
3788 end
3789 --Active BossUnitTargetScanner
3790 if targetMonitor and UnitExists(uId.."target") and UnitPlayerOrPetInRaid(uId.."target") then
3791 self:Debug("targetMonitor exists, target exists", 2)
3792 local modId, unitId, returnFunc = string.split("\t", targetMonitor)
3793 self:Debug("targetMonitor: "..modId..", "..unitId..", "..returnFunc, 2)
3794 local tanking, status = UnitDetailedThreatSituation(unitId, unitId.."target")--Tanking may return 0 if npc is temporarily looking at an NPC (IE fracture) but status will still be 3 on true tank
3795 if tanking or (status == 3) then
3796 self:Debug("targetMonitor ending, it's a tank", 2)
3797 return
3798 end--It's a tank/highest threat, this method ignores tanks
3799 local mod = self:GetModByName(modId)
3800 self:Debug("targetMonitor success, a valid target that's not a tank", 2)
3801 mod[returnFunc](mod, self:GetUnitFullName(unitId.."target"), unitId.."target", unitId)--Return results to warning function with all variables.
3802 targetMonitor = nil
3803 end
3804 end
3805end
3806
3807-----------------------------
3808-- Handle Incoming Syncs --
3809-----------------------------
3810
3811do
3812 local function checkForActualPull()
3813 if #inCombat == 0 then
3814 DBM:StopLogging()
3815 end
3816 end
3817
3818 local syncHandlers = {}
3819 local whisperSyncHandlers = {}
3820
3821 -- DBM uses the following prefixes since 4.1 as pre-4.1 sync code is going to be incompatible anways, so this is the perfect opportunity to throw away the old and long names
3822 -- M = Mod
3823 -- C = Combat start
3824 -- GC = Guild Combat Start
3825 -- IS = Icon set info
3826 -- K = Kill
3827 -- H = Hi!
3828 -- V = Incoming version information
3829 -- U = User Timer
3830 -- PT = Pull Timer (for sound effects, the timer itself is still sent as a normal timer)
3831 -- RT = Request Timers
3832 -- CI = Combat Info
3833 -- TI = Timer Info
3834 -- IR = Instance Info Request
3835 -- IRE = Instance Info Requested Ended/Canceled
3836 -- II = Instance Info
3837 -- WBE = World Boss engage info
3838 -- WBD = World Boss defeat info
3839 -- DSW = Disable Send Whisper
3840 -- NS = Note Share
3841
3842 syncHandlers["M"] = function(sender, mod, revision, event, ...)
3843 mod = DBM:GetModByName(mod or "")
3844 if mod and event and revision then
3845 revision = tonumber(revision) or 0
3846 mod:ReceiveSync(event, sender, revision, ...)
3847 end
3848 end
3849
3850 syncHandlers["NS"] = function(sender, modid, modvar, text, abilityName)
3851 if sender == playerName then return end
3852 if DBM.Options.BlockNoteShare or InCombatLockdown() then return end
3853 if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and IsInInstance() and not C_Garrison:IsOnGarrisonMap() then return end
3854 --^^You are in LFR, BG, or LFG. Block note syncs. They shouldn't be sendable, but in case someone edits DBM^^
3855 local mod = DBM:GetModByName(modid or "")
3856 local ability = abilityName or DBM_CORE_UNKNOWN
3857 if mod and modvar and text and text ~= "" then
3858 if DBM:AntiSpam(5, modvar) then--Don't allow calling same note more than once per 5 seconds
3859 DBM:AddMsg(DBM_CORE_NOTE_SHARE_SUCCESS:format(sender, abilityName))
3860 --Need to use modid in URL because we cannot insert a mod table into one
3861 DBM:AddMsg(("|HDBM:noteshare:%s:%s:%s:%s:%s|h|cff3588ff[%s]"):format(modid, modvar, ability, text, sender, DBM_CORE_NOTE_SHARE_LINK))
3862 else
3863 DBM:Debug(sender.." is attempting to send too many notes so notes are being throttled")
3864 end
3865 else
3866 DBM:AddMsg(DBM_CORE_NOTE_SHARE_FAIL:format(sender, ability))
3867 end
3868 end
3869
3870 syncHandlers["C"] = function(sender, delay, mod, modRevision, startHp, dbmRevision, modHFRevision)
3871 if not dbmIsEnabled or sender == playerName then return end
3872 if LastInstanceType == "pvp" then return end
3873 if LastInstanceType == "none" and (not UnitAffectingCombat("player") or #inCombat > 0) then--world boss
3874 local senderuId = DBM:GetRaidUnitId(sender)
3875 if not senderuId then return end--Should never happen, but just in case. If happens, MANY "C" syncs are sent. losing 1 no big deal.
3876 local _, _, _, playerZone = UnitPosition("player")
3877 local _, _, _, senderZone = UnitPosition(senderuId)
3878 if playerZone ~= senderZone then return end--not same zone
3879 local range = DBM.RangeCheck:GetDistance("player", senderuId)--Same zone, so check range
3880 if not range or range > 120 then return end
3881 end
3882 if not cSyncSender[sender] then
3883 cSyncSender[sender] = true
3884 cSyncReceived = cSyncReceived + 1
3885 if cSyncReceived > 2 then -- need at least 3 sync to combat start. (for security)
3886 local lag = select(4, GetNetStats()) / 1000
3887 delay = tonumber(delay or 0) or 0
3888 mod = DBM:GetModByName(mod or "")
3889 modRevision = tonumber(modRevision or 0) or 0
3890 dbmRevision = tonumber(dbmRevision or 0) or 0
3891 modHFRevision = tonumber(modHFRevision or 0) or 0
3892 startHp = tonumber(startHp or -1) or -1
3893 if dbmRevision < 10481 then return end
3894 if mod and delay and (not mod.zones or mod.zones[LastInstanceMapID]) and (not mod.minSyncRevision or modRevision >= mod.minSyncRevision) then
3895 DBM:StartCombat(mod, delay + lag, "SYNC from - "..sender, true, startHp)
3896 if (mod.revision < modHFRevision) and (mod.revision > 1000) then--mod.revision because we want to compare to OUR revision not senders
3897 if DBM:AntiSpam(3, "HOTFIX") then
3898 if DBM.HighestRelease < modHFRevision then--There is a newer RELEASE version of DBM out that has this mods fixes
3899 showConstantReminder = 2
3900 DBM:AddMsg(DBM_CORE_UPDATEREMINDER_HOTFIX)
3901 else--This mods fixes are in an alpha version
3902 DBM:AddMsg(DBM_CORE_UPDATEREMINDER_HOTFIX_ALPHA)
3903 end
3904 end
3905 end
3906 end
3907 end
3908 end
3909 end
3910
3911 syncHandlers["DSW"] = function(sender)
3912 if (DBM:GetRaidRank(sender) ~= 2 or not IsInGroup()) then return end--If not on group, we're probably sender, don't disable status. IF not leader, someone is trying to spoof this, block that too
3913 statusWhisperDisabled = true
3914 DBM:Debug("Raid leader has disabled status whispers")
3915 end
3916
3917 syncHandlers["IS"] = function(sender, guid, ver, optionName)
3918 ver = tonumber(ver)
3919 if ver > (iconSetRevision[optionName] or 0) then--Save first synced version and person, ignore same version. refresh occurs only above version (fastest person)
3920 iconSetRevision[optionName] = ver
3921 iconSetPerson[optionName] = guid
3922 end
3923 if iconSetPerson[optionName] == UnitGUID("player") then--Check if that highest version was from ourself
3924 canSetIcons[optionName] = true
3925 else--Not from self, it means someone with a higher version than us probably sent it
3926 canSetIcons[optionName] = false
3927 end
3928 local name = DBM:GetFullPlayerNameByGUID(iconSetPerson[optionName]) or DBM_CORE_UNKNOWN
3929 DBM:Debug(name.." was elected icon setter for "..optionName, 2)
3930 end
3931
3932 syncHandlers["K"] = function(sender, cId)
3933 if select(2, IsInInstance()) == "pvp" then return end
3934 cId = tonumber(cId or "")
3935 if cId then DBM:OnMobKill(cId, true) end
3936 end
3937
3938 syncHandlers["EE"] = function(sender, eId, success, mod, modRevision)
3939 if select(2, IsInInstance()) == "pvp" then return end
3940 eId = tonumber(eId or "")
3941 success = tonumber(success)
3942 mod = DBM:GetModByName(mod or "")
3943 modRevision = tonumber(modRevision or 0) or 0
3944 if mod and eId and success and (not mod.minSyncRevision or modRevision >= mod.minSyncRevision) and not eeSyncSender[sender] then
3945 eeSyncSender[sender] = true
3946 eeSyncReceived = eeSyncReceived + 1
3947 if eeSyncReceived > 2 then -- need at least 3 person to combat end. (for security)
3948 DBM:EndCombat(mod, success == 0)
3949 end
3950 end
3951 end
3952
3953 local dummyMod -- dummy mod for the pull timer
3954 local dummyMod2 -- dummy mod for the break timer
3955 syncHandlers["PT"] = function(sender, timer, lastMapID, target)
3956 DBM:Schedule(timer - 15, SendAddonMessage, "MyWAPrefix","StartTimer","RAID")
3957 if DBM.Options.DontShowUserTimers then return end
3958 local LFGTankException = IsPartyLFG() and UnitGroupRolesAssigned(sender) == "TANK"
3959 if (DBM:GetRaidRank(sender) == 0 and IsInGroup() and not LFGTankException) or select(2, IsInInstance()) == "pvp" or IsEncounterInProgress() then
3960 return
3961 end
3962 if (lastMapID and tonumber(lastMapID) ~= LastInstanceMapID) or (not lastMapID and DBM.Options.DontShowPTNoID) then return end
3963 timer = tonumber(timer or 0)
3964 if timer > 60 then
3965 return
3966 end
3967 if not dummyMod then
3968 local threshold = DBM.Options.PTCountThreshold
3969 local adjustedThreshold = 5
3970 if threshold > 10 then
3971 adjustedThreshold = 10
3972 else
3973 adjustedThreshold = floor(threshold)
3974 end
3975 dummyMod = DBM:NewMod("PullTimerCountdownDummy")
3976 DBM:GetModLocalization("PullTimerCountdownDummy"):SetGeneralLocalization{ name = DBM_CORE_MINIMAP_TOOLTIP_HEADER }
3977 dummyMod.countdown = dummyMod:NewCountdown(0, 0, nil, nil, adjustedThreshold, true)
3978 dummyMod.text = dummyMod:NewAnnounce("%s", 1, "Interface\\Icons\\ability_warrior_offensivestance")
3979 dummyMod.geartext = dummyMod:NewSpecialWarning(" %s ", nil, nil, nil, 3)
3980 end
3981 --Cancel any existing pull timers before creating new ones, we don't want double countdowns or mismatching blizz countdown text (cause you can't call another one if one is in progress)
3982 if not DBM.Options.DontShowPT2 and DBM.Bars:GetBar(DBM_CORE_TIMER_PULL) then
3983 DBM.Bars:CancelBar(DBM_CORE_TIMER_PULL)
3984 end
3985 if not DBM.Options.DontPlayPTCountdown then
3986 dummyMod.countdown:Cancel()
3987 end
3988 if not DBM.Options.DontShowPTCountdownText then
3989 DBM:Unschedule(countDownTextDelay)
3990 TimerTracker_OnEvent(TimerTracker, "PLAYER_ENTERING_WORLD")--easiest way to nil out timers on TimerTracker frame. This frame just has no actual star/stop functions
3991 end
3992 dummyMod.text:Cancel()
3993 if timer == 0 then return end--"/dbm pull 0" will strictly be used to cancel the pull timer (which is why we let above part of code run but not below)
3994 DBM:FlashClientIcon()
3995 if not DBM.Options.DontShowPT2 then
3996 DBM.Bars:CreateBar(timer, DBM_CORE_TIMER_PULL, "Interface\\Icons\\Spell_Holy_BorrowedTime")
3997 fireEvent("DBM_TimerStart", "pull", DBM_CORE_TIMER_PULL, timer, "Interface\\Icons\\Spell_Holy_BorrowedTime")--Most args missing because pull timer simply doesn't have them.
3998 end
3999 if not DBM.Options.DontPlayPTCountdown then
4000 dummyMod.countdown:Start(timer)
4001 end
4002 if not DBM.Options.DontShowPTCountdownText then
4003 TimerTracker_OnEvent(TimerTracker, "START_TIMER", 2, timer, timer)
4004 end
4005 if not DBM.Options.DontShowPTText then
4006 if target then
4007 dummyMod.text:Show(DBM_CORE_ANNOUNCE_PULL_TARGET:format(target, timer, sender))
4008 dummyMod.text:Schedule(timer, DBM_CORE_ANNOUNCE_PULL_NOW_TARGET:format(target))
4009 else
4010 dummyMod.text:Show(DBM_CORE_ANNOUNCE_PULL:format(timer, sender))
4011 dummyMod.text:Schedule(timer, DBM_CORE_ANNOUNCE_PULL_NOW)
4012 end
4013 end
4014 DBM:StartLogging(timer, checkForActualPull)
4015 if DBM.Options.CheckGear then
4016 local bagilvl, equippedilvl = GetAverageItemLevel()
4017 local difference = bagilvl - equippedilvl
4018 local weapon = GetInventoryItemLink("player", 16)
4019 local fishingPole = false
4020 if weapon then
4021 local _, _, _, _, _, _, type = GetItemInfo(weapon)
4022 if type and type == DBM_CORE_GEAR_FISHING_POLE then
4023 fishingPole = true
4024 end
4025 end
4026 if IsInRaid() and difference >= 40 then
4027 dummyMod.geartext:Show(DBM_CORE_GEAR_WARNING:format(floor(difference)))
4028 elseif IsInRaid() and (not weapon or fishingPole) then
4029 dummyMod.geartext:Show(DBM_CORE_GEAR_WARNING_WEAPON)
4030 end
4031 end
4032 end
4033
4034 function breakTimerStart(self, timer, sender)
4035 if not dummyMod2 then
4036 local threshold = DBM.Options.PTCountThreshold
4037 local adjustedThreshold = 5
4038 if threshold > 10 then
4039 adjustedThreshold = 10
4040 else
4041 adjustedThreshold = floor(threshold)
4042 end
4043 dummyMod2 = DBM:NewMod("BreakTimerCountdownDummy")
4044 DBM:GetModLocalization("BreakTimerCountdownDummy"):SetGeneralLocalization{ name = DBM_CORE_MINIMAP_TOOLTIP_HEADER }
4045 dummyMod2.countdown = dummyMod2:NewCountdown(0, 0, nil, nil, adjustedThreshold, true)
4046 dummyMod2.text = dummyMod2:NewAnnounce("%s", 1, "Interface\\Icons\\Spell_Holy_BorrowedTime")
4047 end
4048 --Cancel any existing break timers before creating new ones, we don't want double countdowns or mismatching blizz countdown text (cause you can't call another one if one is in progress)
4049 if not DBM.Options.DontShowPT2 and DBM.Bars:GetBar(DBM_CORE_TIMER_BREAK) then
4050 DBM.Bars:CancelBar(DBM_CORE_TIMER_BREAK)
4051 end
4052 if not DBM.Options.DontPlayPTCountdown then
4053 dummyMod2.countdown:Cancel()
4054 end
4055 dummyMod2.text:Cancel()
4056 DBM.Options.tempBreak2 = nil
4057 if timer == 0 then return end--"/dbm break 0" will strictly be used to cancel the break timer (which is why we let above part of code run but not below)
4058 self.Options.tempBreak2 = timer.."/"..time()
4059 if not self.Options.DontShowPT2 then
4060 self.Bars:CreateBar(timer, DBM_CORE_TIMER_BREAK, "Interface\\Icons\\Spell_Holy_BorrowedTime")
4061 fireEvent("DBM_TimerStart", "break", DBM_CORE_TIMER_BREAK, timer, "Interface\\Icons\\Spell_Holy_BorrowedTime")
4062 end
4063 if not self.Options.DontPlayPTCountdown then
4064 dummyMod2.countdown:Start(timer)
4065 end
4066 if not self.Options.DontShowPTText then
4067 local hour, minute = GetGameTime()
4068 minute = minute+(timer/60)
4069 if minute >= 60 then
4070 hour = hour + 1
4071 minute = minute - 60
4072 end
4073 minute = floor(minute)
4074 if minute < 10 then
4075 minute = tostring(0 .. minute)
4076 end
4077 dummyMod2.text:Show(DBM_CORE_BREAK_START:format(strFromTime(timer).." ("..hour..":"..minute..")", sender))
4078 if timer/60 > 10 then dummyMod2.text:Schedule(timer - 10*60, DBM_CORE_BREAK_MIN:format(10)) end
4079 if timer/60 > 5 then dummyMod2.text:Schedule(timer - 5*60, DBM_CORE_BREAK_MIN:format(5)) end
4080 if timer/60 > 2 then dummyMod2.text:Schedule(timer - 2*60, DBM_CORE_BREAK_MIN:format(2)) end
4081 if timer/60 > 1 then dummyMod2.text:Schedule(timer - 1*60, DBM_CORE_BREAK_MIN:format(1)) end
4082 dummyMod2.text:Schedule(timer, DBM_CORE_ANNOUNCE_BREAK_OVER:format(hour..":"..minute))
4083 end
4084 C_TimerAfter(timer, function() self.Options.tempBreak2 = nil end)
4085 end
4086
4087 syncHandlers["BT"] = function(sender, timer)
4088 if DBM.Options.DontShowUserTimers then return end
4089 timer = tonumber(timer or 0)
4090 if timer > 3600 then return end
4091 if (DBM:GetRaidRank(sender) == 0 and IsInGroup()) or select(2, IsInInstance()) == "pvp" or IsEncounterInProgress() then
4092 return
4093 end
4094 breakTimerStart(DBM, timer, sender)
4095 end
4096
4097 whisperSyncHandlers["BTR3"] = function(sender, timer)
4098 if DBM.Options.DontShowUserTimers or not DBM:GetRaidUnitId(sender) then return end
4099 timer = tonumber(timer or 0)
4100 if timer > 3600 then return end
4101 DBM:Unschedule(DBM.RequestTimers)--IF we got BTR3 sync, then we know immediately RequestTimers was successful, so abort others
4102 if #inCombat >= 1 then return end
4103 if DBM.Bars:GetBar(DBM_CORE_TIMER_BREAK) then return end--Already recovered. Prevent duplicate recovery
4104 breakTimerStart(DBM, timer, sender)
4105 end
4106
4107 local function SendVersion()
4108 if DBM.Options.FakeBWVersion then
4109 SendAddonMessage("BigWigs", versionResponseString:format(fakeBWVersion, fakeBWHash), IsInGroup(2) and "INSTANCE_CHAT" or "RAID")
4110 return
4111 end
4112 --(Note, faker isn't to screw with bigwigs nor is theirs to screw with dbm, but rathor raid leaders who don't let people run WTF they want to run)
4113 local VPVersion
4114 local VoicePack = DBM.Options.ChosenVoicePack
4115 if VoicePack ~= "None" then
4116 VPVersion = "/ VP"..VoicePack..": v"..DBM.VoiceVersions[VoicePack]
4117 end
4118 if VPVersion then
4119 sendSync("V", ("%d\t%s\t%s\t%s\t%s\t%s"):format(DBM.Revision, tostring(DBM.ReleaseRevision), DBM.DisplayVersion, GetLocale(), tostring(not DBM.Options.DontSetIcons), VPVersion))
4120 else
4121 sendSync("V", ("%d\t%s\t%s\t%s\t%s"):format(DBM.Revision, tostring(DBM.ReleaseRevision), DBM.DisplayVersion, GetLocale(), tostring(not DBM.Options.DontSetIcons)))
4122 end
4123 end
4124
4125 -- TODO: is there a good reason that version information is broadcasted and not unicasted?
4126 syncHandlers["H"] = function(sender)
4127 DBM:Unschedule(SendVersion)--Throttle so we don't needlessly send tons of comms during initial raid invites
4128 DBM:Schedule(3, SendVersion)--Send version if 3 seconds have past since last "Hi" sync
4129 end
4130
4131 syncHandlers["BV"] = function(sender, version, hash)--Parsed from bigwigs V7+
4132 if version and raid[sender] then
4133 raid[sender].bwversion = version
4134 raid[sender].bwhash = hash or ""
4135 end
4136 end
4137
4138 syncHandlers["V"] = function(sender, revision, version, displayVersion, locale, iconEnabled, VPVersion)
4139 revision, version = tonumber(revision), tonumber(version)
4140 if revision and version and displayVersion and raid[sender] then
4141 raid[sender].revision = revision
4142 raid[sender].version = version
4143 raid[sender].displayVersion = displayVersion
4144 raid[sender].VPVersion = VPVersion
4145 raid[sender].locale = locale
4146 raid[sender].enabledIcons = iconEnabled or "false"
4147 DBM:Debug("Received version info from "..sender.." : Rev - "..revision..", Ver - "..version..", Rev Diff - "..(revision - DBM.Revision), 3)
4148 if version > DBM.Revision then -- Update reminder
4149 if not checkEntry(newerVersionPerson, sender) then
4150 newerVersionPerson[#newerVersionPerson + 1] = sender
4151 DBM:Debug("Newer version detected from "..sender.." : Rev - "..revision..", Ver - "..version..", Rev Diff - "..(revision - DBM.Revision), 3)
4152 end
4153 if #newerVersionPerson < 4 then
4154 if #newerVersionPerson == 2 and updateNotificationDisplayed < 2 then--Only requires 2 for update notification.
4155 if DBM.HighestRelease < version then
4156 DBM.HighestRelease = version
4157 end
4158 DBM.NewerVersion = displayVersion
4159 --UGLY hack to get release version number instead of alpha one
4160 if DBM.NewerVersion:find("alpha") then
4161 local temp1, temp2 = string.split(" ", DBM.NewerVersion)--Strip down to just version, no alpha
4162 local temp3, temp4, temp5 = string.split(".", temp1)--Strip version down to 3 numbers
4163 if temp5 then
4164 temp5 = tonumber(temp5)
4165 temp5 = temp5 - 1
4166 temp5 = tostring(temp5)
4167 DBM.NewerVersion = temp3.."."..temp4.."."..temp5
4168 end
4169 end
4170 --Find min revision.
4171 updateNotificationDisplayed = 2
4172 DBM:AddMsg(DBM_CORE_UPDATEREMINDER_HEADER:match("([^\n]*)"))
4173 DBM:AddMsg(DBM_CORE_UPDATEREMINDER_HEADER:match("\n(.*)"):format(displayVersion, version))
4174 DBM:AddMsg(("|HDBM:update:%s:%s|h|cff3588ff[%s]"):format(displayVersion, version, DBM_CORE_UPDATEREMINDER_URL or "http://www.deadlybossmods.com"))
4175 showConstantReminder = 1
4176 elseif #newerVersionPerson == 3 then--Requires 3 for force disable.
4177 --Find min revision.
4178 local revDifference = mmin((raid[newerVersionPerson[1]].revision - DBM.Revision), (raid[newerVersionPerson[2]].revision - DBM.Revision), (raid[newerVersionPerson[3]].revision - DBM.Revision))
4179 --The following code requires at least THREE people to send that higher revision (I just upped it from 2). That should be more than adaquate.
4180 --Disable if out of date and it's a major patch.
4181 --[[if not testBuild and dbmToc < wowTOC then
4182 updateNotificationDisplayed = 3
4183 DBM:AddMsg(DBM_CORE_UPDATEREMINDER_MAJORPATCH)
4184 DBM:Disable(true)--]]
4185 --Disable if revision grossly out of date even if not major patch.
4186 if revDifference > 180 then
4187 --elseif revDifference > 180 then
4188 if updateNotificationDisplayed < 3 then
4189 updateNotificationDisplayed = 3
4190 DBM:AddMsg(DBM_CORE_UPDATEREMINDER_DISABLE)
4191 DBM:Disable(true)
4192 end
4193 end
4194 end
4195 end
4196 end
4197 if DBM.DisplayVersion:find("alpha") and #newerVersionPerson < 2 and #newerRevisionPerson < 2 and updateNotificationDisplayed < 2 and (revision - DBM.Revision) > 20 then
4198 if not checkEntry(newerRevisionPerson, sender) then
4199 newerRevisionPerson[#newerRevisionPerson + 1] = sender
4200 DBM:Debug("Newer revision detected from "..sender.." : Rev - "..revision..", Ver - "..version..", Rev Diff - "..(revision - DBM.Revision))
4201 end
4202 if #newerRevisionPerson == 2 then
4203 local revDifference = mmin((raid[newerRevisionPerson[1]].revision - DBM.Revision), (raid[newerRevisionPerson[2]].revision - DBM.Revision))
4204 if testBuild and revDifference > 5 then
4205 updateNotificationDisplayed = 3
4206 DBM:AddMsg(DBM_CORE_UPDATEREMINDER_DISABLE)
4207 DBM:Disable(true)
4208 else
4209 updateNotificationDisplayed = 2
4210 DBM:AddMsg(DBM_CORE_UPDATEREMINDER_HEADER_ALPHA:format(revDifference))
4211 end
4212 end
4213 end
4214 end
4215 DBM:GROUP_ROSTER_UPDATE()
4216 end
4217
4218 syncHandlers["U"] = function(sender, time, text)
4219 if select(2, IsInInstance()) == "pvp" then return end -- no pizza timers in battlegrounds
4220 if DBM.Options.DontShowUserTimers then return end
4221 if DBM:GetRaidRank(sender) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then return end
4222 if sender == playerName then return end
4223 time = tonumber(time or 0)
4224 text = tostring(text)
4225 if time and text then
4226 DBM:CreatePizzaTimer(time, text, nil, sender)
4227 end
4228 end
4229
4230 syncHandlers["CU"] = function(sender, time, text)
4231 if select(2, IsInInstance()) == "pvp" then return end -- no pizza timers in battlegrounds
4232 if DBM.Options.DontShowUserTimers then return end
4233 if DBM:GetRaidRank(sender) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then return end
4234 if sender == playerName then return end
4235 time = tonumber(time or 0)
4236 text = tostring(text)
4237 if time and text then
4238 DBM:CreatePizzaTimer(time, text, nil, sender, true)
4239 end
4240 end
4241
4242 -- beware, ugly and missplaced code ahead
4243 -- todo: move this somewhere else
4244 do
4245 local accessList = {}
4246 local savedSender
4247
4248 local inspopup = CreateFrame("Frame", "DBMPopupLockout", UIParent)
4249 inspopup:SetBackdrop({bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
4250 edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
4251 tile = true, tileSize = 16, edgeSize = 16,
4252 insets = {left = 1, right = 1, top = 1, bottom = 1}}
4253 )
4254 inspopup:SetSize(500, 120)
4255 inspopup:SetPoint("TOP", UIParent, "TOP", 0, -200)
4256 inspopup:SetFrameStrata("DIALOG")
4257
4258 local inspopuptext = inspopup:CreateFontString()
4259 inspopuptext:SetFontObject(ChatFontNormal)
4260 inspopuptext:SetWidth(470)
4261 inspopuptext:SetWordWrap(true)
4262 inspopuptext:SetPoint("TOP", inspopup, "TOP", 0, -15)
4263
4264 local buttonaccept = CreateFrame("Button", nil, inspopup)
4265 buttonaccept:SetNormalTexture("Interface\\Buttons\\UI-DialogBox-Button-Up")
4266 buttonaccept:SetPushedTexture("Interface\\Buttons\\UI-DialogBox-Button-Down")
4267 buttonaccept:SetHighlightTexture("Interface\\Buttons\\UI-DialogBox-Button-Highlight", "ADD")
4268 buttonaccept:SetSize(128, 35)
4269 buttonaccept:SetPoint("BOTTOM", inspopup, "BOTTOM", -75, 0)
4270
4271 local buttonatext = buttonaccept:CreateFontString()
4272 buttonatext:SetFontObject(ChatFontNormal)
4273 buttonatext:SetPoint("CENTER", buttonaccept, "CENTER", 0, 5)
4274 buttonatext:SetText(YES)
4275
4276 local buttondecline = CreateFrame("Button", nil, inspopup)
4277 buttondecline:SetNormalTexture("Interface\\Buttons\\UI-DialogBox-Button-Up")
4278 buttondecline:SetPushedTexture("Interface\\Buttons\\UI-DialogBox-Button-Down")
4279 buttondecline:SetHighlightTexture("Interface\\Buttons\\UI-DialogBox-Button-Highlight", "ADD")
4280 buttondecline:SetSize(128, 35)
4281 buttondecline:SetPoint("BOTTOM", inspopup, "BOTTOM", 75, 0)
4282
4283 local buttondtext = buttondecline:CreateFontString()
4284 buttondtext:SetFontObject(ChatFontNormal)
4285 buttondtext:SetPoint("CENTER", buttondecline, "CENTER", 0, 5)
4286 buttondtext:SetText(NO)
4287
4288 inspopup:Hide()
4289
4290 local function autoDecline(sender, force)
4291 inspopup:Hide()
4292 savedSender = nil
4293 if force then
4294 SendAddonMessage("D4", "II\t" .. "denied", "WHISPER", sender)
4295 else
4296 SendAddonMessage("D4", "II\t" .. "timeout", "WHISPER", sender)
4297 end
4298 end
4299
4300 local function showPopupInstanceIdPermission(sender)
4301 DBM:Unschedule(autoDecline)
4302 DBM:Schedule(59, autoDecline, sender)
4303 inspopup:Hide()
4304 if savedSender ~= sender then
4305 if savedSender then
4306 autoDecline(savedSender, 1) -- Do not allow multiple popups, so auto decline to previous sender.
4307 end
4308 savedSender = sender
4309 end
4310 inspopuptext:SetText(DBM_REQ_INSTANCE_ID_PERMISSION:format(sender, sender))
4311 buttonaccept:SetScript("OnClick", function(f) savedSender = nil DBM:Unschedule(autoDecline) accessList[sender] = true syncHandlers["IR"](sender) f:GetParent():Hide() end)
4312 buttondecline:SetScript("OnClick", function(f) autoDecline(sender, 1) end)
4313 PlaySound("igMainMenuOpen")
4314 inspopup:Show()
4315 end
4316
4317 syncHandlers["IR"] = function(sender)
4318 if DBM:GetRaidRank(sender) == 0 or sender == playerName then
4319 return
4320 end
4321 accessList = accessList or {}
4322 if not accessList[sender] then
4323 -- ask for permission
4324 showPopupInstanceIdPermission(sender)
4325 return
4326 end
4327 -- okay, send data
4328 local sentData = false
4329 for i = 1, GetNumSavedInstances() do
4330 local name, id, _, difficulty, locked, extended, instanceIDMostSig, isRaid, maxPlayers, textDiff, _, progress = GetSavedInstanceInfo(i)
4331 if (locked or extended) and isRaid then -- only report locked raid instances
4332 SendAddonMessage("D4", "II\tData\t" .. name .. "\t" .. id .. "\t" .. difficulty .. "\t" .. maxPlayers .. "\t" .. (progress or 0) .. "\t" .. textDiff, "WHISPER", sender)
4333 sentData = true
4334 end
4335 end
4336 if not sentData then
4337 -- send something even if there is nothing to report so the receiver is able to tell you apart from someone who just didn't respond...
4338 SendAddonMessage("D4", "II\tNoData", "WHISPER", sender)
4339 end
4340 end
4341
4342 syncHandlers["IRE"] = function(sender)
4343 local popup = inspopup:IsShown()
4344 if popup and savedSender == sender then -- found the popup with the correct data
4345 savedSender = nil
4346 DBM:Unschedule(autoDecline)
4347 inspopup:Hide()
4348 end
4349 end
4350
4351 syncHandlers["GCB"] = function(sender, modId, ver, difficulty)
4352 if not DBM.Options.ShowGuildMessages or not difficulty then return end
4353 if not ver or not (ver == "2") then return end--Ignore old versions
4354 if DBM:AntiSpam(5, "GCB") then
4355 if IsInInstance() then return end--Simple filter, if you are inside an instance, just filter it, if not in instance, good to go.
4356 local bossName = EJ_GetEncounterInfo(modId) or DBM_CORE_UNKNOWN
4357 local difficultyName = DBM_CORE_UNKNOWN
4358 difficulty = tonumber(difficulty)
4359 if difficulty == 16 then
4360 difficultyName = PLAYER_DIFFICULTY6
4361 elseif difficulty == 15 then
4362 difficultyName = PLAYER_DIFFICULTY2
4363 else
4364 difficultyName = PLAYER_DIFFICULTY1
4365 end
4366 DBM:AddMsg(DBM_CORE_GUILD_COMBAT_STARTED:format(difficultyName.."-"..bossName))
4367 end
4368 end
4369
4370 syncHandlers["GCE"] = function(sender, modId, ver, wipe, time, difficulty, wipeHP)
4371 if not DBM.Options.ShowGuildMessages or not difficulty then return end
4372 if not ver or not (ver == "3") then return end--Ignore old versions
4373 if DBM:AntiSpam(5, "GCE") then
4374 if IsInInstance() then return end--Simple filter, if you are inside an instance, just filter it, if not in instance, good to go.
4375 local bossName = EJ_GetEncounterInfo(modId) or DBM_CORE_UNKNOWN
4376 local difficultyName = DBM_CORE_UNKNOWN
4377 difficulty = tonumber(difficulty)
4378 if difficulty == 16 then
4379 difficultyName = PLAYER_DIFFICULTY6
4380 elseif difficulty == 15 then
4381 difficultyName = PLAYER_DIFFICULTY2
4382 else
4383 difficultyName = PLAYER_DIFFICULTY1
4384 end
4385 if wipe == "1" then
4386 DBM:AddMsg(DBM_CORE_GUILD_COMBAT_ENDED_AT:format(difficultyName.."-"..bossName, wipeHP, time))
4387 else
4388 DBM:AddMsg(DBM_CORE_GUILD_BOSS_DOWN:format(difficultyName.."-"..bossName, time))
4389 end
4390 end
4391 end
4392
4393 syncHandlers["WBE"] = function(sender, modId, realm, health, ver, name)
4394 if not ver or not (ver == "8") then return end--Ignore old versions
4395 if lastBossEngage[modId..realm] and (GetTime() - lastBossEngage[modId..realm] < 30) then return end--We recently got a sync about this boss on this realm, so do nothing.
4396 lastBossEngage[modId..realm] = GetTime()
4397 if realm == playerRealm and DBM.Options.WorldBossAlert and not IsEncounterInProgress() then
4398 modId = tonumber(modId)--If it fails to convert into number, this makes it nil
4399 local bossName = modId and EJ_GetEncounterInfo(modId) or name or DBM_CORE_UNKNOWN
4400 DBM:AddMsg(DBM_CORE_WORLDBOSS_ENGAGED:format(bossName, floor(health), sender))
4401 end
4402 end
4403
4404 syncHandlers["WBD"] = function(sender, modId, realm, ver, name)
4405 if not ver or not (ver == "8") then return end--Ignore old versions
4406 if lastBossDefeat[modId..realm] and (GetTime() - lastBossDefeat[modId..realm] < 30) then return end
4407 lastBossDefeat[modId..realm] = GetTime()
4408 if realm == playerRealm and DBM.Options.WorldBossAlert and not IsEncounterInProgress() then
4409 modId = tonumber(modId)--If it fails to convert into number, this makes it nil
4410 local bossName = modId and EJ_GetEncounterInfo(modId) or name or DBM_CORE_UNKNOWN
4411 DBM:AddMsg(DBM_CORE_WORLDBOSS_DEFEATED:format(bossName, sender))
4412 end
4413 end
4414
4415 whisperSyncHandlers["WBE"] = function(sender, modId, realm, health, ver, name)
4416 if not ver or not (ver == "8") then return end--Ignore old versions
4417 if lastBossEngage[modId..realm] and (GetTime() - lastBossEngage[modId..realm] < 30) then return end
4418 lastBossEngage[modId..realm] = GetTime()
4419 if realm == playerRealm and DBM.Options.WorldBossAlert and not IsEncounterInProgress() then
4420 local _, toonName = BNGetGameAccountInfo(sender)
4421 modId = tonumber(modId)--If it fails to convert into number, this makes it nil
4422 local bossName = modId and EJ_GetEncounterInfo(modId) or name or DBM_CORE_UNKNOWN
4423 DBM:AddMsg(DBM_CORE_WORLDBOSS_ENGAGED:format(bossName, floor(health), toonName))
4424 end
4425 end
4426
4427 whisperSyncHandlers["WBD"] = function(sender, modId, realm, ver, name)
4428 if not ver or not (ver == "8") then return end--Ignore old versions
4429 if lastBossDefeat[modId..realm] and (GetTime() - lastBossDefeat[modId..realm] < 30) then return end
4430 lastBossDefeat[modId..realm] = GetTime()
4431 if realm == playerRealm and DBM.Options.WorldBossAlert and not IsEncounterInProgress() then
4432 local _, toonName = BNGetGameAccountInfo(sender)
4433 modId = tonumber(modId)--If it fails to convert into number, this makes it nil
4434 local bossName = modId and EJ_GetEncounterInfo(modId) or name or DBM_CORE_UNKNOWN
4435 DBM:AddMsg(DBM_CORE_WORLDBOSS_DEFEATED:format(bossName, toonName))
4436 end
4437 end
4438
4439 local lastRequest = 0
4440 local numResponses = 0
4441 local expectedResponses = 0
4442 local allResponded = false
4443 local results
4444
4445 local updateInstanceInfo, showResults
4446
4447 whisperSyncHandlers["II"] = function(sender, result, name, id, diff, maxPlayers, progress, textDiff)
4448 if not DBM:GetRaidUnitId(sender) then return end
4449 if GetTime() - lastRequest > 62 or not results then
4450 return
4451 end
4452 if not result then
4453 return
4454 end
4455 name = name or DBM_CORE_UNKNOWN
4456 id = id or ""
4457 diff = tonumber(diff or 0) or 0
4458 maxPlayers = tonumber(maxPlayers or 0) or 0
4459 progress = tonumber(progress or 0) or 0
4460 textDiff = textDiff or ""
4461
4462 -- count responses
4463 if not results.responses[sender] then
4464 results.responses[sender] = result
4465 numResponses = numResponses + 1
4466 end
4467
4468 -- get localized difficulty text
4469 if textDiff ~= "" then
4470 results.difftext[diff] = textDiff
4471 end
4472
4473 if result == "Data" then
4474 -- got data in that response and not just a "no" or "i'm away"
4475 local instanceId = name.." "..maxPlayers.." "..diff -- locale-dependant dungeon ID
4476 results.data[instanceId] = results.data[instanceId] or {
4477 ids = {}, -- array of all ids of all raid members
4478 name = name,
4479 diff = diff,
4480 maxPlayers = maxPlayers,
4481 }
4482 if diff == 5 or diff == 6 or diff == 16 then
4483 results.data[instanceId].ids[id] = results.data[instanceId].ids[id] or { progress = progress, haveid = true }
4484 tinsert(results.data[instanceId].ids[id], sender)
4485 else
4486 results.data[instanceId].ids[progress] = results.data[instanceId].ids[progress] or { progress = progress }
4487 tinsert(results.data[instanceId].ids[progress], sender)
4488 end
4489 end
4490
4491 if numResponses >= expectedResponses then -- unlikely, lol
4492 DBM:Unschedule(updateInstanceInfo)
4493 DBM:Unschedule(showResults)
4494 if not allResponded then --Only display message once in case we get for example 4 syncs the last sender
4495 DBM:Schedule(0.99, DBM.AddMsg, DBM, DBM_INSTANCE_INFO_ALL_RESPONSES)
4496 allResponded = true
4497 end
4498 C_TimerAfter(1, showResults) --Delay results so we allow time for same sender to send more than 1 lockout, otherwise, if we get expectedResponses before all data is sent from 1 user, we clip some of their data.
4499 end
4500 end
4501
4502 function showResults()
4503 local resultCount = 0
4504 -- TODO: you could catch some localized instances by observing IDs if there are multiple players with the same instance ID but a different name ;) (not that useful if you are trying to get a fresh instance)
4505 DBM:AddMsg(DBM_INSTANCE_INFO_RESULTS, false)
4506 DBM:AddMsg("---", false)
4507 for i, v in pairs(results.data) do
4508 resultCount = resultCount + 1
4509 DBM:AddMsg(DBM_INSTANCE_INFO_DETAIL_HEADER:format(v.name, (results.difftext[v.diff] or v.diff)), false)
4510 for id, v in pairs(v.ids) do
4511 if v.haveid then
4512 DBM:AddMsg(DBM_INSTANCE_INFO_DETAIL_INSTANCE:format(id, v.progress, tconcat(v, ", ")), false)
4513 else
4514 DBM:AddMsg(DBM_INSTANCE_INFO_DETAIL_INSTANCE2:format(v.progress, tconcat(v, ", ")), false)
4515 end
4516 end
4517 DBM:AddMsg("---", false)
4518 end
4519 if resultCount == 0 then
4520 DBM:AddMsg(DBM_INSTANCE_INFO_NOLOCKOUT, false)
4521 end
4522 local denied = {}
4523 local away = {}
4524 local noResponse = {}
4525 for i = 1, GetNumGroupMembers() do
4526 if not UnitIsUnit("raid"..i, "player") then
4527 tinsert(noResponse, (GetRaidRosterInfo(i)))
4528 end
4529 end
4530 for i, v in pairs(results.responses) do
4531 if v == "Data" or v == "NoData" then
4532 elseif v == "timeout" then
4533 tinsert(away, i)
4534 else -- could be "clicked" or "override", in both cases we don't get the data because the dialog requesting it was dismissed
4535 tinsert(denied, i)
4536 end
4537 removeEntry(noResponse, i)
4538 end
4539 if #denied > 0 then
4540 DBM:AddMsg(DBM_INSTANCE_INFO_STATS_DENIED:format(tconcat(denied, ", ")), false)
4541 end
4542 if #away > 0 then
4543 DBM:AddMsg(DBM_INSTANCE_INFO_STATS_AWAY:format(tconcat(away, ", ")), false)
4544 end
4545 if #noResponse > 0 then
4546 DBM:AddMsg(DBM_INSTANCE_INFO_STATS_NO_RESPONSE:format(tconcat(noResponse, ", ")), false)
4547 end
4548 results = nil
4549 end
4550
4551 -- called when the chat link is clicked
4552 function DBM:ShowRaidIDRequestResults()
4553 if not results then -- check if we are currently querying raid IDs, results will be nil if we don't
4554 return
4555 end
4556 self:Unschedule(updateInstanceInfo)
4557 self:Unschedule(showResults)
4558 showResults() -- sets results to nil after the results are displayed, ending the current id request; future incoming data will be discarded
4559 sendSync("IRE")
4560 end
4561
4562 local function getResponseStats()
4563 local numResponses = 0
4564 local sent = 0
4565 local denied = 0
4566 local away = 0
4567 for k, v in pairs(results.responses) do
4568 numResponses = numResponses + 1
4569 if v == "Data" or v == "NoData" then
4570 sent = sent + 1
4571 elseif v == "timeout" then
4572 away = away + 1
4573 else -- could be "clicked" or "override", in both cases we don't get the data because the dialog requesting it was dismissed
4574 denied = denied + 1
4575 end
4576 end
4577 return numResponses, sent, denied, away
4578 end
4579
4580 local function getNumDBMUsers() -- without ourselves
4581 local r = 0
4582 for i, v in pairs(raid) do
4583 if v.revision and v.name ~= playerName and UnitIsConnected(v.id) then
4584 r = r + 1
4585 end
4586 end
4587 return r
4588 end
4589
4590 function updateInstanceInfo(timeRemaining, dontAddShowResultNowButton)
4591 local numResponses, sent, denied, away = getResponseStats()
4592 local dbmUsers = getNumDBMUsers()
4593 DBM:AddMsg(DBM_INSTANCE_INFO_STATUS_UPDATE:format(numResponses, dbmUsers, sent, denied, timeRemaining), false)
4594 if not dontAddShowResultNowButton then
4595 if dbmUsers - numResponses <= 7 then -- waiting for 7 or less players, show their names and the early result option
4596 -- copied from above, todo: implement a smarter way of keeping track of stuff like this
4597 local noResponse = {}
4598 for i = 1, GetNumGroupMembers() do
4599 if not UnitIsUnit("raid"..i, "player") and raid[GetRaidRosterInfo(i)] and raid[GetRaidRosterInfo(i)].revision then -- only show players who actually can respond (== DBM users)
4600 tinsert(noResponse, (GetRaidRosterInfo(i)))
4601 end
4602 end
4603 for i, v in pairs(results.responses) do
4604 removeEntry(noResponse, i)
4605 end
4606
4607 --[[
4608 -- this looked like the easiest way (for some reason?) to create the player string when writing this code -.-
4609 local function dup(...) if select("#", ...) == 0 then return else return ..., ..., dup(select(2, ...)) end end
4610 DBM:AddMsg(DBM_INSTANCE_INFO_SHOW_RESULTS:format(("|Hplayer:%s|h[%s]|h| "):rep(#noResponse):format(dup(unpack(noResponse)))), false)
4611 ]]
4612 -- code that one can actually read
4613 for i, v in ipairs(noResponse) do
4614 noResponse[i] = ("|Hplayer:%s|h[%s]|h|"):format(v, v)
4615 end
4616 DBM:AddMsg(DBM_INSTANCE_INFO_SHOW_RESULTS:format(tconcat(noResponse, ", ")), false)
4617 end
4618 end
4619 end
4620
4621 function DBM:RequestInstanceInfo()
4622 self:AddMsg(DBM_INSTANCE_INFO_REQUESTED)
4623 lastRequest = GetTime()
4624 allResponded = false
4625 results = {
4626 responses = { -- who responded to our request?
4627 },
4628 data = { -- the actual data
4629 },
4630 difftext = {
4631 }
4632 }
4633 numResponses = 0
4634 expectedResponses = getNumDBMUsers()
4635 sendSync("IR")
4636 self:Unschedule(updateInstanceInfo)
4637 self:Unschedule(showResults)
4638 self:Schedule(17, updateInstanceInfo, 45, true)
4639 self:Schedule(32, updateInstanceInfo, 30)
4640 self:Schedule(48, updateInstanceInfo, 15)
4641 C_TimerAfter(62, showResults)
4642 end
4643 end
4644
4645 whisperSyncHandlers["RT"] = function(sender)
4646 if not DBM:GetRaidUnitId(sender) then
4647 DBM:Debug(sender.." attempted to request timers but isn't in your group")
4648 return
4649 end
4650 DBM:SendTimers(sender)
4651 end
4652
4653 whisperSyncHandlers["CI"] = function(sender, mod, time)
4654 if not DBM:GetRaidUnitId(sender) then
4655 DBM:Debug(sender.." attempted to send you combat info but isn't in your group")
4656 return
4657 end
4658 mod = DBM:GetModByName(mod or "")
4659 time = tonumber(time or 0)
4660 if mod and time then
4661 DBM:ReceiveCombatInfo(sender, mod, time)
4662 end
4663 end
4664
4665 whisperSyncHandlers["TI"] = function(sender, mod, timeLeft, totalTime, id, ...)
4666 if not DBM:GetRaidUnitId(sender) then return end
4667 mod = DBM:GetModByName(mod or "")
4668 timeLeft = tonumber(timeLeft or 0)
4669 totalTime = tonumber(totalTime or 0)
4670 if mod and timeLeft and timeLeft > 0 and totalTime and totalTime > 0 and id then
4671 DBM:ReceiveTimerInfo(sender, mod, timeLeft, totalTime, id, ...)
4672 end
4673 end
4674
4675 whisperSyncHandlers["VI"] = function(sender, mod, name, value)
4676 if not DBM:GetRaidUnitId(sender) then return end
4677 mod = DBM:GetModByName(mod or "")
4678 value = tonumber(value) or value
4679 if mod and name and value then
4680 DBM:ReceiveVariableInfo(sender, mod, name, value)
4681 end
4682 end
4683
4684 local function handleSync(channel, sender, prefix, ...)
4685 if not prefix then
4686 return
4687 end
4688 local handler
4689 if channel == "WHISPER" and sender ~= playerName then -- separate between broadcast and unicast, broadcast must not be sent as unicast or vice-versa
4690 handler = whisperSyncHandlers[prefix]
4691 else
4692 handler = syncHandlers[prefix]
4693 end
4694 if handler then
4695 return handler(sender, ...)
4696 end
4697 end
4698
4699 function DBM:CHAT_MSG_ADDON(prefix, msg, channel, sender)
4700 if prefix == "D4" and msg and (channel == "PARTY" or channel == "RAID" or channel == "INSTANCE_CHAT" or channel == "WHISPER" or channel == "GUILD") then
4701 sender = Ambiguate(sender, "none")
4702 handleSync(channel, sender, strsplit("\t", msg))
4703 elseif prefix == "BigWigs" and msg and (channel == "PARTY" or channel == "RAID" or channel == "INSTANCE_CHAT") then
4704 local bwPrefix, bwMsg, extra = strsplit("^", msg)
4705 if bwPrefix and bwMsg and extra then--Nil check all 3 to avoid errors form older versions
4706 if bwPrefix == "V" then--Version information prefixes
4707 local verString, hash = bwMsg, extra
4708 local version = tonumber(verString) or 0
4709 if version == 0 then return end--Just a query
4710 sender = Ambiguate(sender, "none")
4711 handleSync(channel, sender, "BV", version, hash)--Prefix changed, so it's not handled by DBMs "V" handler
4712 if version > fakeBWVersion then--Newer revision found, upgrade!
4713 fakeBWVersion = version
4714 fakeBWHash = hash
4715 end
4716 elseif bwPrefix == "Q" then--Version request prefix
4717 self:Unschedule(SendVersion)
4718 self:Schedule(3, SendVersion)
4719 end
4720 end
4721 elseif prefix == "Transcriptor" and msg then
4722 if msg:find("spell:") and (DBM.Options.DebugLevel > 2 or (Transcriptor and Transcriptor:IsLogging())) then
4723 local spellId = string.match(msg, "spell:(%d+)") or DBM_CORE_UNKNOWN
4724 local spellName = string.match(msg, "h%[(.-)%]|h") or DBM_CORE_UNKNOWN
4725 local message = "RAID_BOSS_WHISPER on "..sender.." with spell of "..spellName.." ("..spellId..")"
4726 self:Debug(message)
4727 end
4728 end
4729 end
4730
4731 function DBM:BN_CHAT_MSG_ADDON(prefix, msg, channel, sender)
4732 if prefix == "D4" and msg then
4733 handleSync(channel, sender, strsplit("\t", msg))
4734 end
4735 end
4736end
4737
4738-----------------------
4739-- Update Reminder --
4740-----------------------
4741do
4742 local frame, fontstring, fontstringFooter, editBox, urlText
4743
4744 local function createFrame()
4745 frame = CreateFrame("Frame", "DBMUpdateReminder", UIParent)
4746 frame:SetFrameStrata("FULLSCREEN_DIALOG") -- yes, this isn't a fullscreen dialog, but I want it to be in front of other DIALOG frames (like DBM GUI which might open this frame...)
4747 frame:SetWidth(430)
4748 frame:SetHeight(140)
4749 frame:SetPoint("TOP", 0, -230)
4750 frame:SetBackdrop({
4751 bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
4752 edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", tile = true, tileSize = 32, edgeSize = 32,
4753 insets = {left = 11, right = 12, top = 12, bottom = 11},
4754 })
4755 fontstring = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
4756 fontstring:SetWidth(410)
4757 fontstring:SetHeight(0)
4758 fontstring:SetPoint("TOP", 0, -16)
4759 editBox = CreateFrame("EditBox", nil, frame)
4760 do
4761 local editBoxLeft = editBox:CreateTexture(nil, "BACKGROUND")
4762 local editBoxRight = editBox:CreateTexture(nil, "BACKGROUND")
4763 local editBoxMiddle = editBox:CreateTexture(nil, "BACKGROUND")
4764 editBoxLeft:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Left")
4765 editBoxLeft:SetHeight(32)
4766 editBoxLeft:SetWidth(32)
4767 editBoxLeft:SetPoint("LEFT", -14, 0)
4768 editBoxLeft:SetTexCoord(0, 0.125, 0, 1)
4769 editBoxRight:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Right")
4770 editBoxRight:SetHeight(32)
4771 editBoxRight:SetWidth(32)
4772 editBoxRight:SetPoint("RIGHT", 6, 0)
4773 editBoxRight:SetTexCoord(0.875, 1, 0, 1)
4774 editBoxMiddle:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Right")
4775 editBoxMiddle:SetHeight(32)
4776 editBoxMiddle:SetWidth(1)
4777 editBoxMiddle:SetPoint("LEFT", editBoxLeft, "RIGHT")
4778 editBoxMiddle:SetPoint("RIGHT", editBoxRight, "LEFT")
4779 editBoxMiddle:SetTexCoord(0, 0.9375, 0, 1)
4780 end
4781 editBox:SetHeight(32)
4782 editBox:SetWidth(250)
4783 editBox:SetPoint("TOP", fontstring, "BOTTOM", 0, -4)
4784 editBox:SetFontObject("GameFontHighlight")
4785 editBox:SetTextInsets(0, 0, 0, 1)
4786 editBox:SetFocus()
4787 editBox:SetText(urlText)
4788 editBox:HighlightText()
4789 editBox:SetScript("OnTextChanged", function(self)
4790 editBox:SetText(urlText)
4791 editBox:HighlightText()
4792 end)
4793 fontstringFooter = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
4794 fontstringFooter:SetWidth(410)
4795 fontstringFooter:SetHeight(0)
4796 fontstringFooter:SetPoint("TOP", editBox, "BOTTOM", 0, 0)
4797 local button = CreateFrame("Button", nil, frame)
4798 button:SetHeight(24)
4799 button:SetWidth(75)
4800 button:SetPoint("BOTTOM", 0, 13)
4801 button:SetNormalFontObject("GameFontNormal")
4802 button:SetHighlightFontObject("GameFontHighlight")
4803 button:SetNormalTexture(button:CreateTexture(nil, nil, "UIPanelButtonUpTexture"))
4804 button:SetPushedTexture(button:CreateTexture(nil, nil, "UIPanelButtonDownTexture"))
4805 button:SetHighlightTexture(button:CreateTexture(nil, nil, "UIPanelButtonHighlightTexture"))
4806 button:SetText(OKAY)
4807 button:SetScript("OnClick", function(self)
4808 frame:Hide()
4809 end)
4810
4811 end
4812
4813 function DBM:ShowUpdateReminder(newVersion, newRevision, text, url)
4814 urlText = url or DBM_CORE_UPDATEREMINDER_URL or "http://www.deadlybossmods.com"
4815 if not frame then
4816 createFrame()
4817 else
4818 editBox:SetText(urlText)
4819 editBox:HighlightText()
4820 end
4821 frame:Show()
4822 if newVersion then
4823 fontstring:SetText(DBM_CORE_UPDATEREMINDER_HEADER:format(newVersion, newRevision))
4824 fontstringFooter:SetText(DBM_CORE_UPDATEREMINDER_FOOTER)
4825 elseif text then
4826 fontstring:SetText(text)
4827 fontstringFooter:SetText(DBM_CORE_UPDATEREMINDER_FOOTER_GENERIC)
4828 end
4829 end
4830end
4831
4832--------------------
4833-- Notes Editor --
4834--------------------
4835do
4836 local frame, fontstring, fontstringFooter, editBox, button3
4837
4838 local function createFrame()
4839 frame = CreateFrame("Frame", "DBMNotesEditor", UIParent)
4840 frame:SetFrameStrata("FULLSCREEN_DIALOG") -- yes, this isn't a fullscreen dialog, but I want it to be in front of other DIALOG frames (like DBM GUI which might open this frame...)
4841 frame:SetWidth(430)
4842 frame:SetHeight(140)
4843 frame:SetPoint("TOP", 0, -230)
4844 frame:SetBackdrop({
4845 bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
4846 edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", tile = true, tileSize = 32, edgeSize = 32,
4847 insets = {left = 11, right = 12, top = 12, bottom = 11},
4848 })
4849 fontstring = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
4850 fontstring:SetWidth(410)
4851 fontstring:SetHeight(0)
4852 fontstring:SetPoint("TOP", 0, -16)
4853 editBox = CreateFrame("EditBox", nil, frame)
4854 do
4855 local editBoxLeft = editBox:CreateTexture(nil, "BACKGROUND")
4856 local editBoxRight = editBox:CreateTexture(nil, "BACKGROUND")
4857 local editBoxMiddle = editBox:CreateTexture(nil, "BACKGROUND")
4858 editBoxLeft:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Left")
4859 editBoxLeft:SetHeight(32)
4860 editBoxLeft:SetWidth(32)
4861 editBoxLeft:SetPoint("LEFT", -14, 0)
4862 editBoxLeft:SetTexCoord(0, 0.125, 0, 1)
4863 editBoxRight:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Right")
4864 editBoxRight:SetHeight(32)
4865 editBoxRight:SetWidth(32)
4866 editBoxRight:SetPoint("RIGHT", 6, 0)
4867 editBoxRight:SetTexCoord(0.875, 1, 0, 1)
4868 editBoxMiddle:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Right")
4869 editBoxMiddle:SetHeight(32)
4870 editBoxMiddle:SetWidth(1)
4871 editBoxMiddle:SetPoint("LEFT", editBoxLeft, "RIGHT")
4872 editBoxMiddle:SetPoint("RIGHT", editBoxRight, "LEFT")
4873 editBoxMiddle:SetTexCoord(0, 0.9375, 0, 1)
4874 end
4875 editBox:SetHeight(32)
4876 editBox:SetWidth(250)
4877 editBox:SetPoint("TOP", fontstring, "BOTTOM", 0, -4)
4878 editBox:SetFontObject("GameFontHighlight")
4879 editBox:SetTextInsets(0, 0, 0, 1)
4880 editBox:SetFocus()
4881 editBox:SetText("")
4882 fontstringFooter = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
4883 fontstringFooter:SetWidth(410)
4884 fontstringFooter:SetHeight(0)
4885 fontstringFooter:SetPoint("TOP", editBox, "BOTTOM", 0, 0)
4886 local button = CreateFrame("Button", nil, frame)
4887 button:SetHeight(24)
4888 button:SetWidth(75)
4889 button:SetPoint("BOTTOM", 80, 13)
4890 button:SetNormalFontObject("GameFontNormal")
4891 button:SetHighlightFontObject("GameFontHighlight")
4892 button:SetNormalTexture(button:CreateTexture(nil, nil, "UIPanelButtonUpTexture"))
4893 button:SetPushedTexture(button:CreateTexture(nil, nil, "UIPanelButtonDownTexture"))
4894 button:SetHighlightTexture(button:CreateTexture(nil, nil, "UIPanelButtonHighlightTexture"))
4895 button:SetText(OKAY)
4896 button:SetScript("OnClick", function(self)
4897 local mod = DBM.Noteframe.mod
4898 local modvar = DBM.Noteframe.modvar
4899 mod.Options[modvar .. "SWNote"] = editBox:GetText() or ""
4900 DBM.Noteframe.mod = nil
4901 DBM.Noteframe.modvar = nil
4902 DBM.Noteframe.abilityName = nil
4903 frame:Hide()
4904 end)
4905 local button2 = CreateFrame("Button", nil, frame)
4906 button2:SetHeight(24)
4907 button2:SetWidth(75)
4908 button2:SetPoint("BOTTOM", 0, 13)
4909 button2:SetNormalFontObject("GameFontNormal")
4910 button2:SetHighlightFontObject("GameFontHighlight")
4911 button2:SetNormalTexture(button2:CreateTexture(nil, nil, "UIPanelButtonUpTexture"))
4912 button2:SetPushedTexture(button2:CreateTexture(nil, nil, "UIPanelButtonDownTexture"))
4913 button2:SetHighlightTexture(button2:CreateTexture(nil, nil, "UIPanelButtonHighlightTexture"))
4914 button2:SetText(CANCEL)
4915 button2:SetScript("OnClick", function(self)
4916 DBM.Noteframe.mod = nil
4917 DBM.Noteframe.modvar = nil
4918 DBM.Noteframe.abilityName = nil
4919 frame:Hide()
4920 end)
4921 button3 = CreateFrame("Button", nil, frame)
4922 button3:SetHeight(24)
4923 button3:SetWidth(75)
4924 button3:SetPoint("BOTTOM", -80, 13)
4925 button3:SetNormalFontObject("GameFontNormal")
4926 button3:SetHighlightFontObject("GameFontHighlight")
4927 button3:SetNormalTexture(button3:CreateTexture(nil, nil, "UIPanelButtonUpTexture"))
4928 button3:SetPushedTexture(button3:CreateTexture(nil, nil, "UIPanelButtonDownTexture"))
4929 button3:SetHighlightTexture(button3:CreateTexture(nil, nil, "UIPanelButtonHighlightTexture"))
4930 button3:SetText(SHARE_QUEST_ABBREV)
4931 button3:SetScript("OnClick", function(self)
4932 local modid = DBM.Noteframe.mod.id
4933 local modvar = DBM.Noteframe.modvar
4934 local abilityName = DBM.Noteframe.abilityName
4935 local syncText = editBox:GetText() or ""
4936 if syncText == "" then
4937 DBM:AddMsg(DBM_CORE_NOTESHAREERRORBLANK)
4938 elseif IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and IsInInstance() and not C_Garrison:IsOnGarrisonMap() then--For BGs, LFR and LFG (we also check IsInInstance() so if you're in queue but fighting something outside like a world boss, it'll sync in "RAID" instead)
4939 DBM:AddMsg(DBM_CORE_NOTESHAREERRORGROUPFINDER)
4940 else
4941 local msg = modid.."\t"..modvar.."\t"..syncText.."\t"..abilityName
4942 if IsInRaid() then
4943 SendAddonMessage("D4", "NS\t" .. msg, "RAID")
4944 DBM:AddMsg(DBM_CORE_NOTESHARED)
4945 elseif IsInGroup(LE_PARTY_CATEGORY_HOME) then
4946 SendAddonMessage("D4", "NS\t" .. msg, "PARTY")
4947 DBM:AddMsg(DBM_CORE_NOTESHARED)
4948 else--Solo
4949 DBM:AddMsg(DBM_CORE_NOTESHAREERRORSOLO)
4950 end
4951 end
4952 end)
4953 end
4954
4955 function DBM:ShowNoteEditor(mod, modvar, abilityName, syncText, sender)
4956 if not frame then
4957 createFrame()
4958 self.Noteframe = frame
4959 else
4960 if frame:IsShown() and syncText then
4961 self:AddMsg(DBM_CORE_NOTESHAREERRORALREADYOPEN)
4962 return
4963 end
4964 end
4965 frame:Show()
4966 fontstringFooter:SetText(DBM_CORE_NOTEFOOTER)
4967 self.Noteframe.mod = mod
4968 self.Noteframe.modvar = modvar
4969 self.Noteframe.abilityName = abilityName
4970 if syncText then
4971 button3:Hide()--Don't show share button in shared notes
4972 fontstring:SetText(DBM_CORE_NOTESHAREDHEADER:format(sender, abilityName))
4973 editBox:SetText(syncText)
4974 else
4975 button3:Show()
4976 fontstring:SetText(DBM_CORE_NOTEHEADER:format(abilityName))
4977 if type(mod.Options[modvar .. "SWNote"]) == "string" then
4978 editBox:SetText(mod.Options[modvar .. "SWNote"])
4979 else
4980 editBox:SetText("")
4981 end
4982 end
4983 end
4984end
4985
4986----------------------
4987-- Pull Detection --
4988----------------------
4989do
4990 local targetList = {}
4991 local function buildTargetList()
4992 local uId = (IsInRaid() and "raid") or "party"
4993 for i = 0, GetNumGroupMembers() do
4994 local id = (i == 0 and "target") or uId..i.."target"
4995 local guid = UnitGUID(id)
4996 if guid and DBM:IsCreatureGUID(guid) then
4997 local cId = DBM:GetCIDFromGUID(guid)
4998 targetList[cId] = id
4999 end
5000 end
5001 end
5002
5003 local function clearTargetList()
5004 twipe(targetList)
5005 end
5006
5007 local function scanForCombat(mod, mob, delay)
5008 if not checkEntry(inCombat, mob) then
5009 buildTargetList()
5010 if targetList[mob] then
5011 if delay > 0 and UnitAffectingCombat(targetList[mob]) and not (UnitPlayerOrPetInRaid(targetList[mob]) or UnitPlayerOrPetInParty(targetList[mob])) then
5012 DBM:StartCombat(mod, delay, "PLAYER_REGEN_DISABLED")
5013 elseif (delay == 0) then
5014 DBM:StartCombat(mod, 0, "PLAYER_REGEN_DISABLED_AND_MESSAGE")
5015 end
5016 end
5017 clearTargetList()
5018 end
5019 end
5020
5021
5022 local function checkForPull(mob, combatInfo)
5023 healthCombatInitialized = false
5024 --This just can't be avoided, tryig to save cpu by using C_TimerAfter broke this
5025 --This needs the redundancy and ability to pass args.
5026 DBM:Schedule(0.5, scanForCombat, combatInfo.mod, mob, 0.5)
5027 DBM:Schedule(2, scanForCombat, combatInfo.mod, mob, 2)
5028 C_TimerAfter(2.1, function()
5029 healthCombatInitialized = true
5030 end)
5031 end
5032
5033 -- TODO: fix the duplicate code that was added for quick & dirty support of zone IDs
5034
5035 -- detects a boss pull based on combat state, this is required for pre-ICC bosses that do not fire INSTANCE_ENCOUNTER_ENGAGE_UNIT events on engage
5036 function DBM:PLAYER_REGEN_DISABLED()
5037 lastCombatStarted = GetTime()
5038 if not combatInitialized then return end
5039 if dbmIsEnabled and combatInfo[LastInstanceMapID] then
5040 for i, v in ipairs(combatInfo[LastInstanceMapID]) do
5041 if v.type:find("combat") and not v.noRegenDetection then
5042 if v.multiMobPullDetection then
5043 for _, mob in ipairs(v.multiMobPullDetection) do
5044 if checkForPull(mob, v) then
5045 break
5046 end
5047 end
5048 else
5049 checkForPull(v.mob, v)
5050 end
5051 end
5052 end
5053 end
5054 if self.Options.AFKHealthWarning and not IsEncounterInProgress() and UnitIsAFK("player") and self:AntiSpam(5, "AFK") then--You are afk and losing health, some griever is trying to kill you while you are afk/tabbed out.
5055 self:FlashClientIcon()
5056 local voice = DBM.Options.ChosenVoicePack
5057 local path = "Sound\\Creature\\CThun\\CThunYouWillDIe.ogg"
5058 if voice ~= "None" then
5059 path = "Interface\\AddOns\\DBM-VP"..voice.."\\checkhp.ogg"
5060 end
5061 self:PlaySoundFile(path)
5062 if UnitHealthMax("player") ~= 0 then
5063 local health = UnitHealth("player") / UnitHealthMax("player") * 100
5064 self:AddMsg(DBM_CORE_AFK_WARNING:format(health))
5065 end
5066 end
5067 end
5068
5069 local function isBossEngaged(cId)
5070 -- note that this is designed to work with any number of bosses, but it might be sufficient to check the first 5 unit ids
5071 -- TODO: check if the client supports more than 5 boss unit IDs...just because the default boss health frame is limited to 5 doesn't mean there can't be more
5072 local i = 1
5073 repeat
5074 local bossUnitId = "boss"..i
5075 local bossGUID = not UnitIsDead(bossUnitId) and UnitGUID(bossUnitId) -- check for UnitIsVisible maybe?
5076 local bossCId = bossGUID and DBM:GetCIDFromGUID(bossGUID)
5077 if bossCId and (type(cId) == "number" and cId == bossCId or type(cId) == "table" and checkEntry(cId, bossCId)) then
5078 return true
5079 end
5080 i = i + 1
5081 until not bossGUID
5082 end
5083
5084 function DBM:INSTANCE_ENCOUNTER_ENGAGE_UNIT()
5085 if timerRequestInProgress then return end--do not start ieeu combat if timer request is progressing. (not to break Timer Recovery stuff)
5086 if dbmIsEnabled and combatInfo[LastInstanceMapID] then
5087 for i, v in ipairs(combatInfo[LastInstanceMapID]) do
5088 if v.type:find("combat") and isBossEngaged(v.multiMobPullDetection or v.mob) then
5089 self:StartCombat(v.mod, 0, "IEEU")
5090 end
5091 end
5092 end
5093 end
5094
5095 function DBM:UNIT_TARGETABLE_CHANGED(uId)
5096 if self.Options.DebugLevel > 2 or (Transcriptor and Transcriptor:IsLogging()) then
5097 local active = UnitExists(uId) and "true" or "false"
5098 self:Debug("UNIT_TARGETABLE_CHANGED event fired for "..UnitName(uId)..". Active: "..active)
5099 end
5100 end
5101
5102 function DBM:UNIT_SPELLCAST_SUCCEEDED(uId, spellName, _, spellGUID, spellId)
5103 local correctSpellId = tonumber(select(5, strsplit("-", spellGUID)), 10)
5104 self:Debug("UNIT_SPELLCAST_SUCCEEDED fired: "..UnitName(uId).."'s "..spellName.."("..correctSpellId..")", 3)
5105 end
5106
5107 function DBM:ENCOUNTER_START(encounterID, name, difficulty, size)
5108 self:Debug("ENCOUNTER_START event fired: "..encounterID.." "..name.." "..difficulty.." "..size)
5109 if dbmIsEnabled and combatInfo[LastInstanceMapID] then
5110 for i, v in ipairs(combatInfo[LastInstanceMapID]) do
5111 if not v.noESDetection then
5112 if v.multiEncounterPullDetection then
5113 for _, eId in ipairs(v.multiEncounterPullDetection) do
5114 if encounterID == eId then
5115 self:StartCombat(v.mod, 0, "ENCOUNTER_START")
5116 return
5117 end
5118 end
5119 elseif encounterID == v.eId then
5120 self:StartCombat(v.mod, 0, "ENCOUNTER_START")
5121 return
5122 end
5123 end
5124 end
5125 end
5126 end
5127
5128 function DBM:ENCOUNTER_END(encounterID, name, difficulty, size, success)
5129 self:Debug("ENCOUNTER_END event fired: "..encounterID.." "..name.." "..difficulty.." "..size.." "..success)
5130 for i = #inCombat, 1, -1 do
5131 local v = inCombat[i]
5132 if not v.combatInfo then return end
5133 if v.noEEDetection then return end
5134 if v.respawnTime and success == 0 and self.Options.ShowRespawn and not self.Options.DontShowBossTimers then--No special hacks needed for bad wrath ENCOUNTER_END. Only mods that define respawnTime have a timer, since variable per boss.
5135 local name = string.split(",", name)
5136 self.Bars:CreateBar(v.respawnTime, DBM_CORE_TIMER_RESPAWN:format(name), "Interface\\Icons\\Spell_Holy_BorrowedTime")
5137 end
5138 if v.multiEncounterPullDetection then
5139 for _, eId in ipairs(v.multiEncounterPullDetection) do
5140 if encounterID == eId then
5141 self:EndCombat(v, success == 0)
5142 sendSync("EE", encounterID.."\t"..success.."\t"..v.id.."\t"..(v.revision or 0))
5143 return
5144 end
5145 end
5146 elseif encounterID == v.combatInfo.eId then
5147 self:EndCombat(v, success == 0)
5148 sendSync("EE", encounterID.."\t"..success.."\t"..v.id.."\t"..(v.revision or 0))
5149 return
5150 end
5151 end
5152 end
5153
5154 function DBM:BOSS_KILL(encounterID, name)
5155 self:Debug("BOSS_KILL event fired: "..encounterID.." "..name)
5156 for i = #inCombat, 1, -1 do
5157 local v = inCombat[i]
5158 if not v.combatInfo then return end
5159 if v.multiEncounterPullDetection then
5160 for _, eId in ipairs(v.multiEncounterPullDetection) do
5161 if encounterID == eId then
5162 self:EndCombat(v)
5163 sendSync("EE", encounterID.."\t1\t"..v.id.."\t"..(v.revision or 0))
5164 return
5165 end
5166 end
5167 elseif encounterID == v.combatInfo.eId then
5168 self:EndCombat(v)
5169 sendSync("EE", encounterID.."\t1\t"..v.id.."\t"..(v.revision or 0))
5170 return
5171 end
5172 end
5173 end
5174
5175 local function checkExpressionList(exp, str)
5176 for i, v in ipairs(exp) do
5177 if str:match(v) then
5178 return true
5179 end
5180 end
5181 return false
5182 end
5183
5184 -- called for all mob chat events
5185 local function onMonsterMessage(self, type, msg)
5186 -- pull detection
5187 if dbmIsEnabled and combatInfo[LastInstanceMapID] then
5188 for i, v in ipairs(combatInfo[LastInstanceMapID]) do
5189 if v.type == type and checkEntry(v.msgs, msg) or v.type == type .. "_regex" and checkExpressionList(v.msgs, msg) then
5190 self:StartCombat(v.mod, 0, "MONSTER_MESSAGE")
5191 elseif v.type == "combat_" .. type .. "find" and findEntry(v.msgs, msg) or v.type == "combat_" .. type and checkEntry(v.msgs, msg) then
5192 if IsInInstance() then--Indoor boss that uses both combat and yell for combat, so in other words (such as hodir), don't require "target" of boss for yell like scanForCombat does for World Bosses
5193 self:StartCombat(v.mod, 0, "MONSTER_MESSAGE")
5194 else--World Boss
5195 scanForCombat(v.mod, v.mob, 0)
5196 if v.mod.readyCheckQuestId and (self.Options.WorldBossNearAlert or v.mod.Options.ReadyCheck) and not IsQuestFlaggedCompleted(v.mod.readyCheckQuestId) then
5197 self:FlashClientIcon()
5198 self:PlaySoundFile("Sound\\interface\\levelup2.ogg", true)
5199 end
5200 end
5201 end
5202 end
5203 end
5204 -- kill detection (wipe detection would also be nice to have)
5205 -- todo: add sync
5206 for i = #inCombat, 1, -1 do
5207 local v = inCombat[i]
5208 if not v.combatInfo then return end
5209 if v.combatInfo.killType == type and v.combatInfo.killMsgs[msg] then
5210 self:EndCombat(v)
5211 end
5212 end
5213 end
5214
5215 function DBM:CHAT_MSG_MONSTER_YELL(msg, npc, _, _, target)
5216 if IsEncounterInProgress() or (IsInInstance() and InCombatLockdown()) then--Too many 5 mans/old raids don't properly return encounterinprogress
5217 local targetName = target or "nil"
5218 self:Debug("CHAT_MSG_MONSTER_YELL from "..npc.." while looking at "..targetName, 2)
5219 end
5220 return onMonsterMessage(self, "yell", msg)
5221 end
5222
5223 function DBM:CHAT_MSG_MONSTER_EMOTE(msg)
5224 return onMonsterMessage(self, "emote", msg)
5225 end
5226
5227 function DBM:CHAT_MSG_RAID_BOSS_EMOTE(msg, ...)
5228 onMonsterMessage(self, "emote", msg)
5229 return self:FilterRaidBossEmote(msg, ...)
5230 end
5231
5232 function DBM:RAID_BOSS_EMOTE(msg, ...)--This is a mirror of above prototype only it has less args, both still exist for some reason.
5233 onMonsterMessage(self, "emote", msg)
5234 return self:FilterRaidBossEmote(msg, ...)
5235 end
5236
5237 function DBM:RAID_BOSS_WHISPER(msg)
5238 --Make it easier for devs to detect whispers they are unable to see
5239 --TINTERFACE\\ICONS\\ability_socererking_arcanewrath.blp:20|t You have been branded by |cFFF00000|Hspell:156238|h[Arcane Wrath]|h|r!"
5240 if IsInGroup() then
5241 SendAddonMessage("Transcriptor", msg, IsInGroup(2) and "INSTANCE_CHAT" or IsInRaid() and "RAID" or "PARTY")--Send any emote to transcriptor, even if no spellid
5242 end
5243 end
5244
5245 function DBM:CHAT_MSG_MONSTER_SAY(msg)
5246 return onMonsterMessage(self, "say", msg)
5247 end
5248end
5249
5250---------------------------
5251-- Kill/Wipe Detection --
5252---------------------------
5253
5254function checkWipe(self, confirm)
5255 if #inCombat > 0 then
5256 if not savedDifficulty or not difficultyText or not difficultyIndex then--prevent error if savedDifficulty or difficultyText is nil
5257 savedDifficulty, difficultyText, difficultyIndex = self:GetCurrentInstanceDifficulty()
5258 end
5259 --hack for no iEEU information is provided.
5260 if not bossuIdFound then
5261 for i = 1, 5 do
5262 if UnitExists("boss"..i) then
5263 bossuIdFound = true
5264 break
5265 end
5266 end
5267 end
5268 local wipe = 1 -- 0: no wipe, 1: normal wipe, 2: wipe by UnitExists check.
5269 if IsInScenarioGroup() or (difficultyIndex == 11) or (difficultyIndex == 12) then -- Scenario mod uses special combat start and must be enabled before sceniro end. So do not wipe.
5270 wipe = 0
5271 elseif IsEncounterInProgress() then -- Encounter Progress marked, you obviously in combat with boss. So do not Wipe
5272 wipe = 0
5273 elseif savedDifficulty == "worldboss" and UnitIsDeadOrGhost("player") then -- On dead or ghost, unit combat status detection would be fail. If you ghost in instance, that means wipe. But in worldboss, ghost means not wipe. So do not wipe.
5274 wipe = 0
5275 elseif bossuIdFound and LastInstanceType == "raid" then -- Combat started by IEEU and no boss exist and no EncounterProgress marked, that means wipe
5276 wipe = 2
5277 for i = 1, 5 do
5278 if UnitExists("boss"..i) then
5279 wipe = 0 -- Boss found. No wipe
5280 break
5281 end
5282 end
5283 else -- Unit combat status detection. No combat unit in your party and no EncounterProgress marked, that means wipe
5284 wipe = 1
5285 local uId = (IsInRaid() and "raid") or "party"
5286 for i = 0, GetNumGroupMembers() do
5287 local id = (i == 0 and "player") or uId..i
5288 if UnitAffectingCombat(id) and not UnitIsDeadOrGhost(id) then
5289 wipe = 0 -- Someone still in combat. No wipe
5290 break
5291 end
5292 end
5293 end
5294 if wipe == 0 then
5295 self:Schedule(3, checkWipe, self)
5296 elseif confirm then
5297 for i = #inCombat, 1, -1 do
5298 local reason = (wipe == 1 and "No combat unit found in your party." or "No boss found : "..(wipe or "nil"))
5299 self:Debug("You wiped. Reason : "..reason)
5300 self:EndCombat(inCombat[i], true)
5301 end
5302 else
5303 local maxDelayTime = (savedDifficulty == "worldboss" and 15) or 5 --wait 10s more on worldboss do actual wipe.
5304 for i, v in ipairs(inCombat) do
5305 maxDelayTime = v.combatInfo and v.combatInfo.wipeTimer and v.combatInfo.wipeTimer > maxDelayTime and v.combatInfo.wipeTimer or maxDelayTime
5306 end
5307 self:Schedule(maxDelayTime, checkWipe, self, true)
5308 end
5309 end
5310end
5311
5312function checkBossHealth(self)
5313 if #inCombat > 0 then
5314 for i, v in ipairs(inCombat) do
5315 if not v.multiMobPullDetection or v.mainBoss then
5316 self:GetBossHP(v.mainBoss or v.combatInfo.mob or -1)
5317 else
5318 for _, mob in ipairs(v.multiMobPullDetection) do
5319 self:GetBossHP(mob)
5320 end
5321 end
5322 end
5323 self:Schedule(1, checkBossHealth, self)
5324 end
5325end
5326
5327function checkCustomBossHealth(self, mod)
5328 mod:CustomHealthUpdate()
5329 self:Schedule(1, checkCustomBossHealth, self, mod)
5330end
5331
5332function loopCRTimer(self, timer, mod)
5333 local crTimer = mod:NewTimer(timer, DBM_COMBAT_RES_TIMER_TEXT, "Interface\\Icons\\Spell_Nature_Reincarnation", nil, false)
5334 crTimer:Start()
5335 self:Schedule(timer, loopCRTimer, self, timer, mod)
5336end
5337
5338do
5339 local statVarTable = {
5340 --6.0
5341 ["event5"] = "normal",
5342 ["event20"] = "lfr25",
5343 ["event40"] = "lfr25",
5344 ["normal5"] = "normal",
5345 ["heroic5"] = "heroic",
5346 ["challenge5"] = "challenge",
5347 ["lfr"] = "lfr25",
5348 ["normal"] = "normal",
5349 ["heroic"] = "heroic",
5350 ["mythic"] = "mythic",
5351 ["worldboss"] = "normal",
5352 ["timewalker"] = "timewalker",
5353 --Legacy
5354 ["lfr25"] = "lfr25",
5355 ["normal10"] = "normal",
5356 ["normal25"] = "normal25",
5357 ["heroic10"] = "heroic",
5358 ["heroic25"] = "heroic25",
5359 }
5360
5361 function DBM:StartCombat(mod, delay, event, synced, syncedStartHp)
5362 cSyncSender = {}
5363 cSyncReceived = 0
5364 if not checkEntry(inCombat, mod) then
5365 if not mod.Options.Enabled then return end
5366 if not mod.combatInfo then return end
5367 if mod.combatInfo.noCombatInVehicle and UnitInVehicle("player") then -- HACK
5368 return
5369 end
5370 --HACK: makes sure that we don't detect a false pull if the event fires again when the boss dies...
5371 if mod.lastKillTime and GetTime() - mod.lastKillTime < (mod.reCombatTime or 120) and event ~= "LOADING_SCREEN_DISABLED" then return end
5372 if mod.lastWipeTime and GetTime() - mod.lastWipeTime < (event == "ENCOUNTER_START" and 3 or mod.reCombatTime2 or 20) and event ~= "LOADING_SCREEN_DISABLED" then return end
5373 if event then
5374 self:Debug("StartCombat called by : "..event..". LastInstanceMapID is "..LastInstanceMapID)
5375 if event ~= "ENCOUNTER_START" then
5376 self:Debug("This event is started by"..event..". Review ENCOUNTER_START event to ensure if this is still needed", 2)
5377 end
5378 else
5379 self:Debug("StartCombat called by individual mod or unknown reason. LastInstanceMapID is "..LastInstanceMapID)
5380 end
5381 --check completed. starting combat
5382 tinsert(inCombat, mod)
5383 if mod.inCombatOnlyEvents and not mod.inCombatOnlyEventsRegistered then
5384 mod.inCombatOnlyEventsRegistered = 1
5385 mod:RegisterEvents(unpack(mod.inCombatOnlyEvents))
5386 end
5387 --Fix for "attempt to perform arithmetic on field 'stats' (a nil value)"
5388 if not mod.stats then
5389 self:AddMsg(DBM_CORE_BAD_LOAD)--Warn user that they should reload ui soon as they leave combat to get their mod to load correctly as soon as possible
5390 mod.ignoreBestkill = true--Force this to true so we don't check any more occurances of "stats"
5391 elseif event == "TIMER_RECOVERY" then --add a lag time to delay when TIMER_RECOVERY
5392 delay = delay + select(4, GetNetStats()) / 1000
5393 end
5394 --set mod default info
5395 savedDifficulty, difficultyText, difficultyIndex, LastGroupSize = self:GetCurrentInstanceDifficulty()
5396 local name = mod.combatInfo.name
5397 local modId = mod.id
5398 if C_Scenario.IsInScenario() and (mod.addon.type == "SCENARIO") then
5399 mod.inScenario = true
5400 end
5401 mod.inCombat = true
5402 mod.blockSyncs = nil
5403 mod.combatInfo.pull = GetTime() - (delay or 0)
5404 bossuIdFound = (event or "") == "IEEU"
5405 if mod.minCombatTime then
5406 self:Schedule(mmax((mod.minCombatTime - delay), 3), checkWipe, self)
5407 else
5408 self:Schedule(3, checkWipe, self)
5409 end
5410 --get boss hp at pull
5411 if syncedStartHp and syncedStartHp < 1 then
5412 syncedStartHp = syncedStartHp * 100
5413 end
5414 local startHp = syncedStartHp or mod:GetBossHP(mod.mainBoss or mod.combatInfo.mob or -1) or 100
5415 --check boss engaged first?
5416 if (savedDifficulty == "worldboss" and startHp < 98) or (event == "UNIT_HEALTH" and delay > 4) or event == "TIMER_RECOVERY" then--Boss was not full health when engaged, disable combat start timer and kill record
5417 mod.ignoreBestkill = true
5418 elseif mod.inScenario then
5419 local _, currentStage, numStages = C_Scenario.GetInfo()
5420 if currentStage > 1 and numStages > 1 then
5421 mod.ignoreBestkill = true
5422 end
5423 else--Reset ignoreBestkill after wipe
5424 mod.ignoreBestkill = false
5425 --It was a clean pull, so cancel any RequestTimers which might fire after boss was pulled if boss was pulled right after mod load
5426 --Only want timer recovery on in progress bosses, not clean pulls
5427 if startHp > 98 and (savedDifficulty == "worldboss" or event == "IEEU") or event == "ENCOUNTER_START" then
5428 self:Unschedule(self.RequestTimers)
5429 end
5430 end
5431 --show health frame
5432 if not mod.inScenario then
5433 if self.Options.HideTooltips then
5434 --Better or cleaner way?
5435 tooltipsHidden = true
5436 GameTooltip.Temphide = function() GameTooltip:Hide() end; GameTooltip:SetScript("OnShow", GameTooltip.Temphide)
5437 end
5438 if self.Options.DisableSFX and GetCVar("Sound_EnableSFX") == "1" then
5439 self.Options.sfxDisabled = true
5440 SetCVar("Sound_EnableSFX", 0)
5441 end
5442 if (self.Options.AlwaysShowHealthFrame or mod.Options.HealthFrame) then
5443 if not self.BossHealth:IsShown() then
5444 self.BossHealth:Show(mod.localization.general.name)
5445 else-- pulled other boss during combat, set header text.
5446 self.BossHealth:SetHeaderText(BOSS)
5447 end
5448 if mod.bossHealthInfo then
5449 if mod.bossHealthInfo[2] and type(mod.bossHealthInfo[2]) == "number" then
5450 for i = 1, #mod.bossHealthInfo do--boss name gets from UnitName
5451 self.BossHealth:AddBoss(mod.bossHealthInfo[i])
5452 end
5453 else
5454 for i = 1, #mod.bossHealthInfo, 2 do
5455 self.BossHealth:AddBoss(mod.bossHealthInfo[i], mod.bossHealthInfo[i + 1])
5456 end
5457 end
5458 else
5459 self.BossHealth:AddBoss(mod.combatInfo.mob, mod.localization.general.name)
5460 end
5461 --boss health info update scheduler if boss health frame is not enabled.
5462 elseif not mod.CustomHealthUpdate then
5463 self:Schedule(1, checkBossHealth, self)
5464 end
5465 --this function must be scheduled if boss health frame enabled.
5466 if mod.CustomHealthUpdate then
5467 self:Schedule(1, checkCustomBossHealth, self, mod)
5468 end
5469 end
5470 --process global options
5471 self:HideBlizzardEvents(1)
5472 self:StartLogging(0, nil)
5473 if self.Options.HideObjectivesFrame and mod.addon.type ~= "SCENARIO" and GetNumTrackedAchievements() == 0 and difficultyIndex ~= 8 then
5474 if ObjectiveTrackerFrame:IsVisible() then
5475 ObjectiveTrackerFrame:Hide()
5476 watchFrameRestore = true
5477 end
5478 end
5479 fireEvent("pull", mod, delay, synced, startHp)
5480 self:FlashClientIcon()
5481 --serperate timer recovery and normal start.
5482 if event ~= "TIMER_RECOVERY" then
5483 --add pull count
5484 if mod.stats then
5485 if not mod.stats[statVarTable[savedDifficulty].."Pulls"] then mod.stats[statVarTable[savedDifficulty].."Pulls"] = 0 end
5486 mod.stats[statVarTable[savedDifficulty].."Pulls"] = mod.stats[statVarTable[savedDifficulty].."Pulls"] + 1
5487 end
5488 --show speed timer
5489 if self.Options.AlwaysShowSpeedKillTimer and mod.stats and not mod.ignoreBestkill then
5490 --TODO, add code here to only pull best kull for CURRENT mythic+ rank
5491 local bestTime = mod.stats[statVarTable[savedDifficulty].."BestTime"]
5492 if bestTime and bestTime > 0 then
5493 local speedTimer = mod:NewTimer(bestTime, DBM_SPEED_KILL_TIMER_TEXT, "Interface\\Icons\\Spell_Holy_BorrowedTime", nil, false)
5494 speedTimer:Start()
5495 end
5496 end
5497 --Combat Rez timer, if not a world boss or 5 man dungeon.
5498 if self.Options.CRT_Enabled and difficultyIndex ~= 0 and difficultyIndex ~= 1 and difficultyIndex ~= 2 and difficultyIndex ~= 19 and difficultyIndex ~= 24 and not self.Options.DontShowBossTimers then
5499 local charges, maxCharges, started, duration = GetSpellCharges(20484)
5500 if charges then
5501 local time = duration - (GetTime() - started)
5502 loopCRTimer(self, time, mod)
5503 self:Debug("CRT started by charges", 2)
5504 elseif difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 17 then--Flexible difficulties
5505 local time = 90/LastGroupSize
5506 time = time * 60
5507 loopCRTimer(self, time, mod)
5508 self:Debug("CRT started by Flexible code", 2)
5509 else--Fixed difficulties (LastGroupSize cannot be trusted, this INCLUDES mythic. If you underman mythic then it is NOT 90/20)
5510 local realGroupSize = self:GetNumRealPlayersInZone()
5511 if realGroupSize > 1 then
5512 local time = 90/realGroupSize
5513 time = time * 60
5514 loopCRTimer(self, time, mod)
5515 self:Debug("CRT started by iffy fixed size code", 2)
5516 end
5517 end
5518 end
5519 --update boss left
5520 if mod.numBoss then
5521 mod.vb.bossLeft = mod.numBoss
5522 end
5523 --elect icon person
5524 if mod.findFastestComputer and not self.Options.DontSetIcons then
5525 if self:GetRaidRank() > 0 then
5526 for i = 1, #mod.findFastestComputer do
5527 local option = mod.findFastestComputer[i]
5528 if mod.Options[option] then
5529 sendSync("IS", UnitGUID("player").."\t"..DBM.Revision.."\t"..option)
5530 end
5531 end
5532 elseif not IsInGroup() then
5533 for i = 1, #mod.findFastestComputer do
5534 local option = mod.findFastestComputer[i]
5535 if mod.Options[option] then
5536 canSetIcons[option] = true
5537 end
5538 end
5539 end
5540 end
5541 if testBuild and difficultyIndex == 16 then
5542 self:AddMsg(DBM_CORE_NEED_LOGS)
5543 end
5544 --call OnCombatStart
5545 if mod.OnCombatStart and not mod.ignoreBestkill then
5546 mod:OnCombatStart(delay or 0, event == "PLAYER_REGEN_DISABLED_AND_MESSAGE" or event == "SPELL_CAST_SUCCESS")
5547 end
5548 --send "C" sync
5549 if not synced then
5550 sendSync("C", (delay or 0).."\t"..modId.."\t"..(mod.revision or 0).."\t"..startHp.."\t"..DBM.Revision.."\t"..(mod.hotfixNoticeRev or 0))
5551 end
5552 if self.Options.DisableStatusWhisper and UnitIsGroupLeader("player") and (difficultyIndex == 8 or difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16 or difficultyIndex == 23) then
5553 sendSync("DSW")
5554 end
5555 --show bigbrother check
5556 if self.Options.ShowBigBrotherOnCombatStart and BigBrother and type(BigBrother.ConsumableCheck) == "function" then
5557 if self.Options.BigBrotherAnnounceToRaid then
5558 BigBrother:ConsumableCheck("RAID")
5559 else
5560 BigBrother:ConsumableCheck("SELF")
5561 end
5562 end
5563 --show enage message
5564 if self.Options.ShowEngageMessage then
5565 if mod.ignoreBestkill and (savedDifficulty == "worldboss") then--Should only be true on in progress field bosses, not in progress raid bosses we did timer recovery on.
5566 self:AddMsg(DBM_CORE_COMBAT_STARTED_IN_PROGRESS:format(difficultyText..name))
5567 elseif mod.ignoreBestkill and mod.inScenario then
5568 self:AddMsg(DBM_CORE_SCENARIO_STARTED_IN_PROGRESS:format(difficultyText..name))
5569 else
5570 if mod.addon.type == "SCENARIO" then
5571 self:AddMsg(DBM_CORE_SCENARIO_STARTED:format(difficultyText..name))
5572 else
5573 self:AddMsg(DBM_CORE_COMBAT_STARTED:format(difficultyText..name))
5574 if difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16 then--Only send relevant content, not guild beating down lich king or LFR.
5575 if InGuildParty() then--Guild Group
5576 SendAddonMessage("D4", "GCB\t"..modId.."\t2\t"..difficultyIndex, "GUILD")
5577 end
5578 end
5579 end
5580 end
5581 end
5582 --stop pull count
5583 local dummyMod = self:GetModByName("PullTimerCountdownDummy")
5584 if dummyMod then--stop pull timer, warning, countdowns
5585 dummyMod.countdown:Cancel()
5586 dummyMod.text:Cancel()
5587 self.Bars:CancelBar(DBM_CORE_TIMER_PULL)
5588 TimerTracker_OnEvent(TimerTracker, "PLAYER_ENTERING_WORLD")
5589 end
5590 else
5591 self:AddMsg(DBM_CORE_COMBAT_STATE_RECOVERED:format(difficultyText..name, strFromTime(delay)))
5592 end
5593 if savedDifficulty == "worldboss" and not mod.noWBEsync then
5594 if lastBossEngage[modId..playerRealm] and (GetTime() - lastBossEngage[modId..playerRealm] < 30) then return end--Someone else synced in last 10 seconds so don't send out another sync to avoid needless sync spam.
5595 lastBossEngage[modId..playerRealm] = GetTime()--Update last engage time, that way we ignore our own sync
5596 if IsInGuild() then
5597 SendAddonMessage("D4", "WBE\t"..modId.."\t"..playerRealm.."\t"..startHp.."\t8\t"..name, "GUILD")--Even guild syncs send realm so we can keep antispam the same across realid as well.
5598 end
5599 local _, numBNetOnline = BNGetNumFriends()
5600 for i = 1, numBNetOnline do
5601 local sameRealm = false
5602 local presenceID, _, _, _, _, _, client, isOnline = BNGetFriendInfo(i)
5603 if isOnline and client == BNET_CLIENT_WOW then
5604 local _, _, _, userRealm = BNGetGameAccountInfo(presenceID)
5605 if connectedServers then
5606 for i = 1, #connectedServers do
5607 if userRealm == connectedServers[i] then
5608 sameRealm = true
5609 break
5610 end
5611 end
5612 else
5613 if userRealm == playerRealm then
5614 sameRealm = true
5615 end
5616 end
5617 if sameRealm then
5618 BNSendGameData(presenceID, "D4", "WBE\t"..modId.."\t"..userRealm.."\t"..startHp.."\t8\t"..name)--Just send users realm for pull, so we can eliminate connectedServers checks on sync handler
5619 end
5620 end
5621 end
5622 end
5623 end
5624 end
5625
5626 function DBM:UNIT_HEALTH(uId)
5627 local cId = self:GetCIDFromGUID(UnitGUID(uId))
5628 local health
5629 if UnitHealthMax(uId) ~= 0 then
5630 health = UnitHealth(uId) / UnitHealthMax(uId) * 100
5631 end
5632 if not health or health < 2 then return end -- no worthy of combat start if health is below 2%
5633 if dbmIsEnabled and InCombatLockdown() then
5634 if cId ~= 0 and not bossHealth[cId] and bossIds[cId] and UnitAffectingCombat(uId) and not (UnitPlayerOrPetInRaid(uId) or UnitPlayerOrPetInParty(uId)) and healthCombatInitialized then -- StartCombat by UNIT_HEALTH.
5635 if combatInfo[LastInstanceMapID] then
5636 for i, v in ipairs(combatInfo[LastInstanceMapID]) do
5637 if v.mod.Options.Enabled and not v.mod.disableHealthCombat and v.type:find("combat") and (v.multiMobPullDetection and checkEntry(v.multiMobPullDetection, cId) or v.mob == cId) then
5638 -- Delay set, > 97% = 0.5 (consider as normal pulling), max dealy limited to 20s.
5639 self:StartCombat(v.mod, health > 97 and 0.5 or mmin(GetTime() - lastCombatStarted, 20), "UNIT_HEALTH", nil, health)
5640 end
5641 end
5642 end
5643 end
5644 if self.Options.AFKHealthWarning and UnitIsUnit(uId, "player") and (health < 85) and not IsEncounterInProgress() and UnitIsAFK("player") and self:AntiSpam(5, "AFK") then--You are afk and losing health, some griever is trying to kill you while you are afk/tabbed out.
5645 self:PlaySoundFile("Sound\\Creature\\CThun\\CThunYouWillDIe.ogg")--So fire an alert sound to save yourself from this person's behavior.
5646 self:AddMsg(DBM_CORE_AFK_WARNING:format(health))
5647 end
5648 end
5649 end
5650
5651 function DBM:EndCombat(mod, wipe)
5652 if removeEntry(inCombat, mod) then
5653 local scenario = mod.addon.type == "SCENARIO"
5654 if mod.inCombatOnlyEvents and mod.inCombatOnlyEventsRegistered then
5655 -- unregister all events except for SPELL_AURA_REMOVED events (might still be needed to remove icons etc...)
5656 mod:UnregisterInCombatEvents()
5657 self:Schedule(2, mod.UnregisterInCombatEvents, mod, true) -- 2 seconds should be enough for all auras to fade
5658 self:Schedule(2.1, mod.Stop, mod) -- Remove accident started timers.
5659 mod.inCombatOnlyEventsRegistered = nil
5660 end
5661 if mod.updateInterval then
5662 mod:UnregisterOnUpdateHandler()
5663 end
5664 mod:Stop()
5665 if enableIcons and not self.Options.DontSetIcons and not self.Options.DontRestoreIcons then
5666 -- restore saved previous icon
5667 for uId, icon in pairs(mod.iconRestore) do
5668 SetRaidTarget(uId, icon)
5669 end
5670 twipe(mod.iconRestore)
5671 end
5672 mod.inCombat = false
5673 mod.blockSyncs = true
5674 if mod.combatInfo.killMobs then
5675 for i, v in pairs(mod.combatInfo.killMobs) do
5676 mod.combatInfo.killMobs[i] = true
5677 end
5678 end
5679 if not savedDifficulty or not difficultyText or not difficultyIndex then--prevent error if savedDifficulty or difficultyText is nil
5680 savedDifficulty, difficultyText, difficultyIndex = DBM:GetCurrentInstanceDifficulty()
5681 end
5682 if not mod.stats then--This will be nil if the mod for this intance failed to load fully because "script ran too long" (it tried to load in combat and failed)
5683 self:AddMsg(DBM_CORE_BAD_LOAD)--Warn user that they should reload ui soon as they leave combat to get their mod to load correctly as soon as possible
5684 return--Don't run any further, stats are nil on a bad load so rest of this code will also error out.
5685 end
5686 local name = mod.combatInfo.name
5687 local modId = mod.id
5688 if wipe then
5689 mod.lastWipeTime = GetTime()
5690 --Fix for "attempt to perform arithmetic on field 'pull' (a nil value)" (which was actually caused by stats being nil, so we never did getTime on pull, fixing one SHOULD fix the other)
5691 local thisTime = GetTime() - mod.combatInfo.pull
5692 local hp = mod.highesthealth and mod:GetHighestBossHealth() or mod:GetLowestBossHealth()
5693 local wipeHP = mod.CustomHealthUpdate and mod:CustomHealthUpdate() or hp and ("%d%%"):format(hp) or DBM_CORE_UNKNOWN
5694 if mod.vb.phase then
5695 wipeHP = wipeHP.." ("..SCENARIO_STAGE:format(mod.vb.phase)..")"
5696 end
5697 if mod.numBoss then
5698 local bossesKilled = mod.numBoss - mod.vb.bossLeft
5699 wipeHP = wipeHP.." ("..BOSSES_KILLED:format(bossesKilled, mod.numBoss)..")"
5700 end
5701 local totalPulls = mod.stats[statVarTable[savedDifficulty].."Pulls"]
5702 local totalKills = mod.stats[statVarTable[savedDifficulty].."Kills"]
5703 if thisTime < 30 then -- Normally, one attempt will last at least 30 sec.
5704 totalPulls = totalPulls - 1
5705 mod.stats[statVarTable[savedDifficulty].."Pulls"] = totalPulls
5706 if self.Options.ShowDefeatMessage then
5707 if scenario then
5708 self:AddMsg(DBM_CORE_SCENARIO_ENDED_AT:format(difficultyText..name, strFromTime(thisTime)))
5709 else
5710 self:AddMsg(DBM_CORE_COMBAT_ENDED_AT:format(difficultyText..name, wipeHP, strFromTime(thisTime)))
5711 --No reason to GCE it here, so omited on purpose.
5712 end
5713 end
5714 else
5715 if self.Options.ShowDefeatMessage then
5716 if scenario then
5717 self:AddMsg(DBM_CORE_SCENARIO_ENDED_AT_LONG:format(difficultyText..name, strFromTime(thisTime), totalPulls - totalKills))
5718 else
5719 self:AddMsg(DBM_CORE_COMBAT_ENDED_AT_LONG:format(difficultyText..name, wipeHP, strFromTime(thisTime), totalPulls - totalKills))
5720 if (difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16) and InGuildParty() then--Maybe add mythic plus/CM?
5721 SendAddonMessage("D4", "GCE\t"..modId.."\t3\t1\t"..strFromTime(thisTime).."\t"..difficultyIndex.."\t"..wipeHP, "GUILD")
5722 end
5723 end
5724 end
5725 end
5726 if showConstantReminder == 2 and IsInGroup() and savedDifficulty ~= "lfr" and savedDifficulty ~= "lfr25" then
5727 showConstantReminder = 1
5728 --Show message any time this is a mod that has a newer hotfix revision
5729 --These people need to know the wipe could very well be their fault.
5730 self:AddMsg(DBM_CORE_OUT_OF_DATE_NAG)
5731 end
5732 local msg
5733 for k, v in pairs(autoRespondSpam) do
5734 if self.Options.WhisperStats then
5735 if scenario then
5736 msg = msg or chatPrefixShort..DBM_CORE_WHISPER_SCENARIO_END_WIPE_STATS:format(playerName, difficultyText..(name or ""), totalPulls - totalKills)
5737 else
5738 msg = msg or chatPrefixShort..DBM_CORE_WHISPER_COMBAT_END_WIPE_STATS_AT:format(playerName, difficultyText..(name or ""), wipeHP, totalPulls - totalKills)
5739 end
5740 else
5741 if scenario then
5742 msg = msg or chatPrefixShort..DBM_CORE_WHISPER_SCENARIO_END_WIPE:format(playerName, difficultyText..(name or ""))
5743 else
5744 msg = msg or chatPrefixShort..DBM_CORE_WHISPER_COMBAT_END_WIPE_AT:format(playerName, difficultyText..(name or ""), wipeHP)
5745 end
5746 end
5747 sendWhisper(k, msg)
5748 end
5749 fireEvent("wipe", mod)
5750 else
5751 mod.lastKillTime = GetTime()
5752 local thisTime = GetTime() - (mod.combatInfo.pull or 0)
5753 local lastTime = mod.stats[statVarTable[savedDifficulty].."LastTime"]
5754 local bestTime = mod.stats[statVarTable[savedDifficulty].."BestTime"]
5755 if not mod.stats[statVarTable[savedDifficulty].."Kills"] or mod.stats[statVarTable[savedDifficulty].."Kills"] < 0 then mod.stats[statVarTable[savedDifficulty].."Kills"] = 0 end
5756 --Fix logical error i've seen where for some reason we have more kills then pulls for boss as seen by - stats for wipe messages.
5757 mod.stats[statVarTable[savedDifficulty].."Kills"] = mod.stats[statVarTable[savedDifficulty].."Kills"] + 1
5758 if mod.stats[statVarTable[savedDifficulty].."Kills"] > mod.stats[statVarTable[savedDifficulty].."Pulls"] then mod.stats[statVarTable[savedDifficulty].."Kills"] = mod.stats[statVarTable[savedDifficulty].."Pulls"] end
5759 if not mod.ignoreBestkill and mod.combatInfo.pull then
5760 mod.stats[statVarTable[savedDifficulty].."LastTime"] = thisTime
5761 --Just to prevent pre mature end combat calls from broken mods from saving bad time stats.
5762 if bestTime and bestTime > 0 and bestTime < 1.5 then
5763 mod.stats[statVarTable[savedDifficulty].."BestTime"] = thisTime
5764 else
5765 if difficultyIndex == 8 then--Mythic+/Challenge Mode
5766 --TODO, figure out how to get current mythic plus rank, compare to our best rank.
5767 local currentMPRank = C_ChallengeMode.GetActiveKeystoneInfo() or 0
5768 local bestMPRank = mod.stats.challengeBestRank or 0
5769 if mod.stats.challengeBestRank > currentMPRank then--Don't save time stats at all
5770 --DO nothing
5771 elseif mod.stats.challengeBestRank < currentMPRank then--Update best time and best rank, even if best time is lower (for a lower rank)
5772 mod.stats.challengeBestRank = currentMPRank--Update best rank
5773 mod.stats[statVarTable[savedDifficulty].."BestTime"] = thisTime--Write this time no matter what.
5774 else--Best rank must match current rank, so update time normally
5775 mod.stats[statVarTable[savedDifficulty].."BestTime"] = mmin(bestTime or mhuge, thisTime)
5776 end
5777 else
5778 mod.stats[statVarTable[savedDifficulty].."BestTime"] = mmin(bestTime or mhuge, thisTime)
5779 end
5780 end
5781 end
5782 local totalKills = mod.stats[statVarTable[savedDifficulty].."Kills"]
5783 if self.Options.ShowDefeatMessage then
5784 local msg = ""
5785 if not mod.combatInfo.pull then--was a bad pull so we ignored thisTime, should never happen
5786 if scenario then
5787 msg = DBM_CORE_SCENARIO_COMPLETE:format(difficultyText..name, DBM_CORE_UNKNOWN)
5788 else
5789 msg = DBM_CORE_BOSS_DOWN:format(difficultyText..name, DBM_CORE_UNKNOWN)
5790 end
5791 elseif mod.ignoreBestkill then--Should never happen in a scenario so no need for scenario check.
5792 if scenario then
5793 msg = DBM_CORE_SCENARIO_COMPLETE_I:format(difficultyText..name, totalKills)
5794 else
5795 msg = DBM_CORE_BOSS_DOWN_I:format(difficultyText..name, totalKills)
5796 end
5797 elseif not lastTime then
5798 if scenario then
5799 msg = DBM_CORE_SCENARIO_COMPLETE:format(difficultyText..name, strFromTime(thisTime))
5800 else
5801 msg = DBM_CORE_BOSS_DOWN:format(difficultyText..name, strFromTime(thisTime))
5802 if difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16 then
5803 if InGuildParty() then--Guild Group
5804 SendAddonMessage("D4", "GCE\t"..modId.."\t3\t0\t"..strFromTime(thisTime).."\t"..difficultyIndex, "GUILD")
5805 end
5806 end
5807 end
5808 elseif thisTime < (bestTime or mhuge) then
5809 if scenario then
5810 msg = DBM_CORE_SCENARIO_COMPLETE_NR:format(difficultyText..name, strFromTime(thisTime), strFromTime(bestTime), totalKills)
5811 else
5812 msg = DBM_CORE_BOSS_DOWN_NR:format(difficultyText..name, strFromTime(thisTime), strFromTime(bestTime), totalKills)
5813 if difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16 then
5814 if InGuildParty() then--Guild Group
5815 SendAddonMessage("D4", "GCE\t"..modId.."\t3\t0\t"..strFromTime(thisTime).."\t"..difficultyIndex, "GUILD")
5816 end
5817 end
5818 end
5819 else
5820 if scenario then
5821 msg = DBM_CORE_SCENARIO_COMPLETE_L:format(difficultyText..name, strFromTime(thisTime), strFromTime(lastTime), strFromTime(bestTime), totalKills)
5822 else
5823 msg = DBM_CORE_BOSS_DOWN_L:format(difficultyText..name, strFromTime(thisTime), strFromTime(lastTime), strFromTime(bestTime), totalKills)
5824 if difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16 then
5825 if InGuildParty() then--Guild Group
5826 SendAddonMessage("D4", "GCE\t"..modId.."\t3\t0\t"..strFromTime(thisTime).."\t"..difficultyIndex, "GUILD")
5827 end
5828 end
5829 end
5830 end
5831 self:Schedule(1, self.AddMsg, self, msg)
5832 end
5833 local msg
5834 for k, v in pairs(autoRespondSpam) do
5835 if self.Options.WhisperStats then
5836 if scenario then
5837 msg = msg or chatPrefixShort..DBM_CORE_WHISPER_SCENARIO_END_KILL_STATS:format(playerName, difficultyText..(name or ""), totalKills)
5838 else
5839 msg = msg or chatPrefixShort..DBM_CORE_WHISPER_COMBAT_END_KILL_STATS:format(playerName, difficultyText..(name or ""), totalKills)
5840 end
5841 else
5842 if scenario then
5843 msg = msg or chatPrefixShort..DBM_CORE_WHISPER_SCENARIO_END_KILL:format(playerName, difficultyText..(name or ""))
5844 else
5845 msg = msg or chatPrefixShort..DBM_CORE_WHISPER_COMBAT_END_KILL:format(playerName, difficultyText..(name or ""))
5846 end
5847 end
5848 sendWhisper(k, msg)
5849 end
5850 fireEvent("kill", mod)
5851 if savedDifficulty == "worldboss" and not mod.noWBEsync then
5852 if lastBossDefeat[modId..playerRealm] and (GetTime() - lastBossDefeat[modId..playerRealm] < 30) then return end--Someone else synced in last 10 seconds so don't send out another sync to avoid needless sync spam.
5853 lastBossDefeat[modId..playerRealm] = GetTime()--Update last defeat time before we send it, so we don't handle our own sync
5854 if IsInGuild() then
5855 SendAddonMessage("D4", "WBD\t"..modId.."\t"..playerRealm.."\t8\t"..name, "GUILD")--Even guild syncs send realm so we can keep antispam the same across realid as well.
5856 end
5857 local _, numBNetOnline = BNGetNumFriends()
5858 for i = 1, numBNetOnline do
5859 local sameRealm = false
5860 local presenceID, _, _, _, _, _, client, isOnline = BNGetFriendInfo(i)
5861 if isOnline and client == BNET_CLIENT_WOW then
5862 local _, _, _, userRealm = BNGetGameAccountInfo(presenceID)
5863 if connectedServers then
5864 for i = 1, #connectedServers do
5865 if userRealm == connectedServers[i] then
5866 sameRealm = true
5867 break
5868 end
5869 end
5870 else
5871 if userRealm == playerRealm then
5872 sameRealm = true
5873 end
5874 end
5875 if sameRealm then
5876 BNSendGameData(presenceID, "D4", "WBD\t"..modId.."\t"..userRealm.."\t8\t"..name)
5877 end
5878 end
5879 end
5880 end
5881 end
5882 if mod.OnCombatEnd then mod:OnCombatEnd(wipe) end
5883 if #inCombat == 0 then--prevent error if you pulled multiple boss. (Earth, Wind and Fire)
5884 statusWhisperDisabled = false
5885 self:Schedule(10, self.StopLogging, self)--small delay to catch kill/died combatlog events
5886 self:HideBlizzardEvents(0)
5887 self:Unschedule(checkBossHealth)
5888 self:Unschedule(checkCustomBossHealth)
5889 self:Unschedule(loopCRTimer)
5890 self.BossHealth:Hide()
5891 self.Arrow:Hide(true)
5892 if watchFrameRestore then
5893 ObjectiveTrackerFrame:Show()
5894 watchFrameRestore = false
5895 end
5896 if tooltipsHidden then
5897 --Better or cleaner way?
5898 tooltipsHidden = false
5899 GameTooltip:SetScript("OnShow", GameTooltip.Show)
5900 end
5901 if self.Options.sfxDisabled then
5902 self.Options.sfxDisabled = nil
5903 SetCVar("Sound_EnableSFX", 1)
5904 end
5905 --cache table
5906 twipe(autoRespondSpam)
5907 twipe(bossHealth)
5908 twipe(bossHealthuIdCache)
5909 twipe(bossuIdCache)
5910 --sync table
5911 twipe(canSetIcons)
5912 twipe(iconSetRevision)
5913 twipe(iconSetPerson)
5914 twipe(addsGUIDs)
5915 bossuIdFound = false
5916 eeSyncSender = {}
5917 eeSyncReceived = 0
5918 targetMonitor = nil
5919 self:CreatePizzaTimer(time, "", nil, nil, nil, nil, true)--Auto Terminate infinite loop timers on combat end
5920 elseif self.BossHealth:IsShown() then
5921 if mod.bossHealthInfo then
5922 if mod.bossHealthInfo[2] and type(mod.bossHealthInfo[2]) == "number" then
5923 for i = 1, #mod.bossHealthInfo do
5924 self.BossHealth:RemoveBoss(mod.bossHealthInfo[i])
5925 end
5926 else
5927 for i = 1, #mod.bossHealthInfo, 2 do
5928 self.BossHealth:RemoveBoss(mod.bossHealthInfo[i])
5929 end
5930 end
5931 else
5932 self.BossHealth:RemoveBoss(mod.combatInfo.mob)
5933 end
5934 end
5935 end
5936 end
5937end
5938
5939function DBM:OnMobKill(cId, synced)
5940 for i = #inCombat, 1, -1 do
5941 local v = inCombat[i]
5942 if not v.combatInfo then
5943 return
5944 end
5945 if v.combatInfo.killMobs and v.combatInfo.killMobs[cId] then
5946 if not synced then
5947 sendSync("K", cId)
5948 end
5949 v.combatInfo.killMobs[cId] = false
5950 self.BossHealth:RemoveBoss(cId)
5951 if v.numBoss then
5952 v.vb.bossLeft = (v.vb.bossLeft or v.numBoss) - 1
5953 self:Debug("Boss left - "..v.vb.bossLeft.."/"..v.numBoss, 2)
5954 end
5955 local allMobsDown = true
5956 for i, v in pairs(v.combatInfo.killMobs) do
5957 if v then
5958 allMobsDown = false
5959 break
5960 end
5961 end
5962 if allMobsDown then
5963 self:EndCombat(v)
5964 end
5965 elseif cId == v.combatInfo.mob and not v.combatInfo.killMobs and not v.combatInfo.multiMobPullDetection then
5966 if not synced then
5967 sendSync("K", cId)
5968 end
5969 self:EndCombat(v)
5970 end
5971 end
5972end
5973
5974do
5975 local autoLog = false
5976 local autoTLog = false
5977
5978 local function isCurrentContent()
5979 if LastInstanceMapID == 1520 or LastInstanceMapID == 1530 or LastInstanceMapID == 1220 or LastInstanceMapID == 1648 or LastInstanceMapID == 1676 then--Legion
5980 return true
5981 end
5982 return false
5983 end
5984
5985 function DBM:StartLogging(timer, checkFunc)
5986 self:Unschedule(DBM.StopLogging)
5987 if self.Options.LogOnlyRaidBosses and ((LastInstanceType ~= "raid") or IsPartyLFG() or not isCurrentContent()) then return end
5988 if self.Options.AutologBosses then--Start logging here to catch pre pots.
5989 if not LoggingCombat() then
5990 autoLog = true
5991 self:AddMsg("|cffffff00"..COMBATLOGENABLED.."|r")
5992 LoggingCombat(true)
5993 if checkFunc then
5994 self:Unschedule(checkFunc)
5995 self:Schedule(timer+10, checkFunc)--But if pull was canceled and we don't have a boss engaged within 10 seconds of pull timer ending, abort log
5996 end
5997 end
5998 end
5999 if self.Options.AdvancedAutologBosses and Transcriptor then
6000 if not Transcriptor:IsLogging() then
6001 autoTLog = true
6002 self:AddMsg("|cffffff00"..DBM_CORE_TRANSCRIPTOR_LOG_START.."|r")
6003 Transcriptor:StartLog(1)
6004 end
6005 if checkFunc then
6006 self:Unschedule(checkFunc)
6007 self:Schedule(timer+10, checkFunc)--But if pull was canceled and we don't have a boss engaged within 10 seconds of pull timer ending, abort log
6008 end
6009 end
6010 end
6011
6012 function DBM:StopLogging()
6013 if self.Options.AutologBosses and LoggingCombat() and autoLog then
6014 autoLog = false
6015 self:AddMsg("|cffffff00"..COMBATLOGDISABLED.."|r")
6016 LoggingCombat(false)
6017 end
6018 if self.Options.AdvancedAutologBosses and Transcriptor and autoTLog then
6019 if Transcriptor:IsLogging() then
6020 autoTLog = false
6021 self:AddMsg("|cffffff00"..DBM_CORE_TRANSCRIPTOR_LOG_END.."|r")
6022 Transcriptor:StopLog(1)
6023 end
6024 end
6025 end
6026end
6027
6028function DBM:SetCurrentSpecInfo()
6029 currentSpecGroup = GetSpecialization() or 1
6030 currentSpecID, currentSpecName = GetSpecializationInfo(currentSpecGroup)--give temp first spec id for non-specialization char. no one should use dbm with no specialization, below level 10, should not need dbm.
6031 currentSpecID = tonumber(currentSpecID)
6032end
6033
6034function DBM:GetCurrentInstanceDifficulty()
6035 local _, instanceType, difficulty, difficultyName, _, _, _, _, instanceGroupSize = GetInstanceInfo()
6036 if difficulty == 0 or (difficulty == 1 and instanceType == "none") or C_Garrison:IsOnGarrisonMap() then--draenor field returns 1, causing world boss mod bug.
6037 return "worldboss", RAID_INFO_WORLD_BOSS.." - ", difficulty
6038 elseif difficulty == 1 then
6039 return "normal5", difficultyName.." - ", difficulty, instanceGroupSize
6040 elseif difficulty == 2 then
6041 return "heroic5", difficultyName.." - ", difficulty, instanceGroupSize
6042 elseif difficulty == 3 then
6043 return "normal10", difficultyName.." - ", difficulty, instanceGroupSize
6044 elseif difficulty == 4 then
6045 return "normal25", difficultyName.." - ", difficulty, instanceGroupSize
6046 elseif difficulty == 5 then
6047 return "heroic10", difficultyName.." - ", difficulty, instanceGroupSize
6048 elseif difficulty == 6 then
6049 return "heroic25", difficultyName.." - ", difficulty, instanceGroupSize
6050 elseif difficulty == 7 then--Fixed LFR (ie pre WoD zones)
6051 return "lfr25", difficultyName.." - ", difficulty, instanceGroupSize
6052 elseif difficulty == 8 then
6053 return "challenge5", PLAYER_DIFFICULTY6.."+ - ", difficulty, instanceGroupSize
6054 elseif difficulty == 9 then--40 man raids have their own difficulty now, no longer returned as normal 10man raids
6055 return "normal10", difficultyName.." - ",difficulty, instanceGroupSize--Just use normal10 anyways, since that's where we been saving 40 man stuff for so long anyways, no reason to change it now, not like any 40 mans can be toggled between 10 and 40 where we NEED to tell the difference.
6056 elseif difficulty == 11 then
6057 return "heroic5", difficultyName.." - ", difficulty, instanceGroupSize
6058 elseif difficulty == 12 then
6059 return "normal5", difficultyName.." - ", difficulty, instanceGroupSize
6060 elseif difficulty == 14 then
6061 return "normal", difficultyName.." - ", difficulty, instanceGroupSize
6062 elseif difficulty == 15 then
6063 return "heroic", difficultyName.." - ", difficulty, instanceGroupSize
6064 elseif difficulty == 16 then
6065 return "mythic", difficultyName.." - ", difficulty, instanceGroupSize
6066 elseif difficulty == 17 then--Variable LFR (ie post WoD zones)
6067 return "lfr", difficultyName.." - ", difficulty, instanceGroupSize
6068 elseif difficulty == 18 then
6069 return "event40", difficultyName.." - ", difficulty, instanceGroupSize
6070 elseif difficulty == 19 then
6071 return "event5", difficultyName.." - ", difficulty, instanceGroupSize
6072 elseif difficulty == 20 then
6073 return "event20", difficultyName.." - ", difficulty, instanceGroupSize
6074 elseif difficulty == 23 then
6075 return "mythic", difficultyName.." - ", difficulty, instanceGroupSize
6076 elseif difficulty == 24 then
6077 return "timewalker", difficultyName.." - ", difficulty, instanceGroupSize
6078-- elseif difficulty == 25 then--Used by Ashran in 7.x.
6079-- return "pvpscenario", difficultyName.." - ", difficulty, instanceGroupSize
6080 else--failsafe
6081 return "normal5", "", difficulty, instanceGroupSize
6082 end
6083end
6084
6085function DBM:GetCurrentArea()
6086 return LastInstanceMapID
6087end
6088
6089function DBM:GetGroupSize()
6090 return LastGroupSize
6091end
6092
6093function DBM:HasMapRestrictions()
6094 --Check playerX and playerY. if they are nil restrictions are active
6095 --Restrictions active in all party, raid, pvp, arena maps. No restrictions in "none" or "scenario"
6096 local playerX, playerY = UnitPosition("player")
6097 if not playerX or not playerY then
6098 return true
6099 end
6100 return false
6101end
6102
6103function DBM:PlaySoundFile(path, ignoreSFX)
6104 local soundSetting = self.Options.UseSoundChannel
6105 if soundSetting == "Dialog" then
6106 PlaySoundFile(path, "Dialog")
6107 elseif ignoreSFX or soundSetting == "Master" then
6108 PlaySoundFile(path, "Master")
6109 else
6110 PlaySoundFile(path)
6111 end
6112end
6113
6114function DBM:PlaySound(path)
6115 local soundSetting = self.Options.UseSoundChannel
6116 if soundSetting == "Master" then
6117 PlaySound(path, "Master")
6118 elseif soundSetting == "Dialog" then
6119 PlaySound(path, "Dialog")
6120 else
6121 PlaySound(path)
6122 end
6123end
6124
6125function DBM:UNIT_DIED(args)
6126 local GUID = args.destGUID
6127 if self:IsCreatureGUID(GUID) then
6128 self:OnMobKill(self:GetCIDFromGUID(GUID))
6129 end
6130 if self.Options.AFKHealthWarning and GUID == UnitGUID("player") and not IsEncounterInProgress() and UnitIsAFK("player") and self:AntiSpam(5, "AFK") then--You are afk and losing health, some griever is trying to kill you while you are afk/tabbed out.
6131 self:FlashClientIcon()
6132 self:PlaySoundFile("Sound\\Creature\\CThun\\CThunYouWillDIe.ogg")--So fire an alert sound to save yourself from this person's behavior.
6133 self:AddMsg(DBM_CORE_AFK_WARNING:format(0))
6134 end
6135end
6136DBM.UNIT_DESTROYED = DBM.UNIT_DIED
6137
6138----------------------
6139-- Timer recovery --
6140----------------------
6141do
6142 local requestedFrom = {}
6143 local requestTime = 0
6144 local clientUsed = {}
6145 local sortMe = {}
6146
6147 local function sort(v1, v2)
6148 if v1.revision and not v2.revision then
6149 return true
6150 elseif v2.revision and not v1.revision then
6151 return false
6152 elseif v1.revision and v2.revision then
6153 return v1.revision > v2.revision
6154 else
6155 return (v1.bwversion or 0) > (v2.bwversion or 0)
6156 end
6157 end
6158
6159 function DBM:RequestTimers(requestNum)
6160 twipe(sortMe)
6161 for i, v in pairs(raid) do
6162 tinsert(sortMe, v)
6163 end
6164 tsort(sortMe, sort)
6165 self:Debug("RequestTimers Running", 2)
6166 local selectedClient
6167 local listNum = 0
6168 for i, v in ipairs(sortMe) do
6169 -- If selectedClient player's realm is not same with your's, timer recovery by selectedClient not works at all.
6170 -- SendAddonMessage target channel is "WHISPER" and target player is other realm, no msg sends at all. At same realm, message sending works fine. (Maybe bliz bug or SendAddonMessage function restriction?)
6171 if v.name ~= playerName and UnitIsConnected(v.id) and (not UnitIsGhost(v.id)) and UnitRealmRelationship(v.id) ~= 2 and (GetTime() - (clientUsed[v.name] or 0)) > 10 then
6172 listNum = listNum + 1
6173 if listNum == requestNum then
6174 selectedClient = v
6175 clientUsed[v.name] = GetTime()
6176 break
6177 end
6178 end
6179 end
6180 if not selectedClient then return end
6181 self:Debug("Requesting timer recovery to "..selectedClient.name)
6182 requestedFrom[selectedClient.name] = true
6183 requestTime = GetTime()
6184 SendAddonMessage("D4", "RT", "WHISPER", selectedClient.name)
6185 end
6186
6187 function DBM:ReceiveCombatInfo(sender, mod, time)
6188 if dbmIsEnabled and requestedFrom[sender] and (GetTime() - requestTime) < 5 and #inCombat == 0 then
6189 self:StartCombat(mod, time, "TIMER_RECOVERY")
6190 --Recovery successful, someone sent info, abort other recovery requests
6191 self:Unschedule(self.RequestTimers)
6192 twipe(requestedFrom)
6193 end
6194 end
6195
6196 function DBM:ReceiveTimerInfo(sender, mod, timeLeft, totalTime, id, ...)
6197 if requestedFrom[sender] and (GetTime() - requestTime) < 5 then
6198 local lag = select(4, GetNetStats()) / 1000
6199 for i, v in ipairs(mod.timers) do
6200 if v.id == id then
6201 v:Start(totalTime, ...)
6202 v:Update(totalTime - timeLeft + lag, totalTime, ...)
6203 end
6204 end
6205 end
6206 end
6207
6208 function DBM:ReceiveVariableInfo(sender, mod, name, value)
6209 if requestedFrom[sender] and (GetTime() - requestTime) < 5 then
6210 if value == "true" then
6211 mod.vb[name] = true
6212 elseif value == "false" then
6213 mod.vb[name] = false
6214 else
6215 mod.vb[name] = value
6216 end
6217 end
6218 end
6219end
6220
6221do
6222 local spamProtection = {}
6223 function DBM:SendTimers(target)
6224 self:Debug("SendTimers requested by "..target, 2)
6225 local spamForTarget = spamProtection[target] or 0
6226 -- just try to clean up the table, that should keep the hash table at max. 4 entries or something :)
6227 for k, v in pairs(spamProtection) do
6228 if GetTime() - v >= 1 then
6229 spamProtection[k] = nil
6230 end
6231 end
6232 if GetTime() - spamForTarget < 1 then -- just to prevent players from flooding this on purpose
6233 return
6234 end
6235 spamProtection[target] = GetTime()
6236 if UnitInBattleground("player") then
6237 self:SendBGTimers(target)
6238 return
6239 end
6240 if #inCombat < 1 then
6241 --Break timer is up, so send that
6242 --But only if we are not in combat with a boss
6243 if self.Bars:GetBar(DBM_CORE_TIMER_BREAK) then
6244 local remaining = self.Bars:GetBar(DBM_CORE_TIMER_BREAK).timer
6245 SendAddonMessage("D4", "BTR3\t"..remaining, "WHISPER", target)
6246 end
6247 return
6248 end
6249 local mod
6250 for i, v in ipairs(inCombat) do
6251 mod = not v.isCustomMod and v
6252 end
6253 mod = mod or inCombat[1]
6254 self:SendCombatInfo(mod, target)
6255 self:SendVariableInfo(mod, target)
6256 self:SendTimerInfo(mod, target)
6257 end
6258end
6259
6260function DBM:SendBGTimers(target)
6261 local mod
6262 if IsActiveBattlefieldArena() then
6263 mod = self:GetModByName("Arenas")
6264 else
6265 -- FIXME: this doesn't work for non-english clients
6266 local zone = GetRealZoneText():gsub(" ", "")--Does this need updating to mapid arta?
6267 mod = self:GetModByName(zone)
6268 end
6269 if mod and mod.timers then
6270 self:SendTimerInfo(mod, target)
6271 end
6272end
6273
6274function DBM:SendCombatInfo(mod, target)
6275 return SendAddonMessage("D4", ("CI\t%s\t%s"):format(mod.id, GetTime() - mod.combatInfo.pull), "WHISPER", target)
6276end
6277
6278function DBM:SendTimerInfo(mod, target)
6279 for i, v in ipairs(mod.timers) do
6280 for _, uId in ipairs(v.startedTimers) do
6281 local elapsed, totalTime, timeLeft
6282 if select("#", string.split("\t", uId)) > 1 then
6283 elapsed, totalTime = v:GetTime(select(2, string.split("\t", uId)))
6284 else
6285 elapsed, totalTime = v:GetTime()
6286 end
6287 timeLeft = totalTime - elapsed
6288 if timeLeft > 0 and totalTime > 0 then
6289 SendAddonMessage("D4", ("TI\t%s\t%s\t%s\t%s"):format(mod.id, timeLeft, totalTime, uId), "WHISPER", target)
6290 end
6291 end
6292 end
6293end
6294
6295function DBM:SendVariableInfo(mod, target)
6296 for vname, v in pairs(mod.vb) do
6297 local v2 = tostring(v)
6298 if v2 then
6299 SendAddonMessage("D4", ("VI\t%s\t%s\t%s"):format(mod.id, vname, v2), "WHISPER", target)
6300 end
6301 end
6302end
6303
6304do
6305 local soundFiles = {
6306 "Sound\\Creature\\Rhonin\\UR_Rhonin_Event01.ogg",--5
6307 "Sound\\Creature\\Rhonin\\UR_Rhonin_Event02.ogg",--5
6308 "Sound\\Creature\\Rhonin\\UR_Rhonin_Event03.ogg",--5.5
6309 "Sound\\Creature\\Rhonin\\UR_Rhonin_Event04.ogg",--9
6310 "Sound\\Creature\\Rhonin\\UR_Rhonin_Event05.ogg",--4
6311 "Sound\\Creature\\Rhonin\\UR_Rhonin_Event06.ogg",--10
6312 "Sound\\Creature\\Rhonin\\UR_Rhonin_Event07.ogg",--15
6313 "Sound\\Creature\\Rhonin\\UR_Rhonin_Event08.ogg",
6314 }
6315 local function playDelay(self, count)
6316 self:PlaySoundFile(soundFiles[count])
6317 end
6318
6319 function DBM:AprilFools()
6320 self:Unschedule(self.AprilFools)
6321 local currentMapId = GetPlayerMapAreaID("player")
6322 if not currentMapId then
6323 SetMapToCurrentZone()
6324 currentMapId = GetCurrentMapAreaID()
6325 end
6326 self:Schedule(120 + math.random(0, 300) , self.AprilFools, self)
6327 if currentMapId ~= 1014 then return end--Legion Dalaran
6328 playDelay(self, 1)
6329 self:Schedule(5, playDelay, self, 2)
6330 self:Schedule(10, playDelay, self, 3)
6331 self:Schedule(15.5, playDelay, self, 4)
6332 self:Schedule(24.5, playDelay, self, 5)
6333 self:Schedule(28, playDelay, self, 6)
6334 self:Schedule(37.5, playDelay, self, 7)
6335 self:Schedule(50.5, playDelay, self, 8)
6336 end
6337 function DBM:PLAYER_ENTERING_WORLD()
6338 local weekday, month, day, year = CalendarGetDate()--Must be called after PLAYER_ENTERING_WORLD
6339 if month == 4 and day == 1 then--April 1st
6340 self:Schedule(180 + math.random(0, 300) , self.AprilFools, self)
6341 end
6342 if GetLocale() == "ptBR" or GetLocale() == "frFR" or GetLocale() == "itIT" or GetLocale() == "esES" or GetLocale() == "ruRU" then
6343 C_TimerAfter(10, function() if self.Options.HelpMessageVersion < 4 then self.Options.HelpMessageVersion = 4 self:AddMsg(DBM_CORE_NEED_LOCALS) end end)
6344 end
6345 C_TimerAfter(20, function() if not self.Options.ForumsMessageShown then self.Options.ForumsMessageShown = self.ReleaseRevision self:AddMsg(DBM_FORUMS_MESSAGE) end end)
6346 C_TimerAfter(30, function() if not self.Options.SettingsMessageShown then self.Options.SettingsMessageShown = true self:AddMsg(DBM_HOW_TO_USE_MOD) end end)
6347 C_TimerAfter(40, function() if self.Options.NewsMessageShown < 8 then self.Options.NewsMessageShown = 8 self:AddMsg(DBM_CORE_WHATS_NEW_LINK) end end)
6348 if type(RegisterAddonMessagePrefix) == "function" then
6349 if not RegisterAddonMessagePrefix("D4") then -- main prefix for DBM4
6350 self:AddMsg("Error: unable to register DBM addon message prefix (reached client side addon message filter limit), synchronization will be unavailable") -- TODO: confirm that this actually means that the syncs won't show up
6351 end
6352 if not RegisterAddonMessagePrefix("BigWigs") then
6353 self:AddMsg("Error: unable to register BigWigs addon message prefix (reached client side addon message filter limit), BigWigs version checks will be unavailable")
6354 end
6355 if not RegisterAddonMessagePrefix("Transcriptor") then
6356 self:AddMsg("Error: unable to register Transcriptor addon message prefix (reached client side addon message filter limit)")
6357 end
6358 end
6359 if self.Options.sfxDisabled then--Check if sound was disabled by previous session and not re-enabled.
6360 self.Options.sfxDisabled = nil
6361 SetCVar("Sound_EnableSFX", 1)
6362 end
6363 if self.Options.RestoreRange then self.Options.RestoreRange = nil end--User DCed while this was true, clear it
6364 end
6365end
6366
6367------------------------------------
6368-- Auto-respond/Status whispers --
6369------------------------------------
6370do
6371 local function getNumAlivePlayers()
6372 local alive = 0
6373 if IsInRaid() then
6374 for i = 1, GetNumGroupMembers() do
6375 alive = alive + ((UnitIsDeadOrGhost("raid"..i) and 0) or 1)
6376 end
6377 else
6378 alive = (UnitIsDeadOrGhost("player") and 0) or 1
6379 for i = 1, GetNumSubgroupMembers() do
6380 alive = alive + ((UnitIsDeadOrGhost("party"..i) and 0) or 1)
6381 end
6382 end
6383 return alive
6384 end
6385
6386 local function getNumRealAlivePlayers()
6387 local alive = 0
6388 local currentMapId = GetPlayerMapAreaID("player")
6389 if not currentMapId then
6390 SetMapToCurrentZone()
6391 currentMapId = GetCurrentMapAreaID()
6392 end
6393 local currentMapName = GetMapNameByID(currentMapId)
6394 if IsInRaid() then
6395 for i = 1, GetNumGroupMembers() do
6396 if select(7, GetRaidRosterInfo(i)) == currentMapName then
6397 alive = alive + ((UnitIsDeadOrGhost("raid"..i) and 0) or 1)
6398 end
6399 end
6400 else
6401 alive = (UnitIsDeadOrGhost("player") and 0) or 1
6402 for i = 1, GetNumSubgroupMembers() do
6403 if select(7, GetRaidRosterInfo(i)) == currentMapName then
6404 alive = alive + ((UnitIsDeadOrGhost("party"..i) and 0) or 1)
6405 end
6406 end
6407 end
6408 return alive
6409 end
6410
6411 local function isOnSameServer(presenceId)
6412 local toonID, client = select(6, BNGetFriendInfoByID(presenceId))
6413 if client ~= "WoW" then
6414 return false
6415 end
6416 return GetRealmName() == select(4, BNGetGameAccountInfo(toonID))
6417 end
6418
6419 -- sender is a presenceId for real id messages, a character name otherwise
6420 local function onWhisper(msg, sender, isRealIdMessage)
6421 if statusWhisperDisabled then return end--RL has disabled status whispers for entire raid.
6422 if msg:find(chatPrefix) and not InCombatLockdown() and DBM:AntiSpam(60, "Ogron") and DBM.Options.AutoReplySound then
6423 --Might need more validation if people figure out they can just whisper people with chatPrefix to trigger it.
6424 --However if I have to add more validation it probably won't work in most languages :\ So lets hope antispam and combat check is enough
6425 DBM:PlaySoundFile("sound\\creature\\aggron1\\VO_60_HIGHMAUL_AGGRON_1_AGGRO_1.ogg")
6426 elseif msg == "status" and #inCombat > 0 and DBM.Options.StatusEnabled then
6427 if not difficultyText then -- prevent error when timer recovery function worked and etc (StartCombat not called)
6428 difficultyText = select(2, DBM:GetCurrentInstanceDifficulty())
6429 end
6430 if IsInScenarioGroup() then return end--status not really useful on scenario mods since there is no way to report progress as a percent. We just ignore it.
6431 local mod
6432 for i, v in ipairs(inCombat) do
6433 mod = not v.isCustomMod and v
6434 end
6435 mod = mod or inCombat[1]
6436 local hp = mod.highesthealth and mod:GetHighestBossHealth() or mod:GetLowestBossHealth()
6437 local hpText = mod.CustomHealthUpdate and mod:CustomHealthUpdate() or hp and ("%d%%"):format(hp) or DBM_CORE_UNKNOWN
6438 if mod.vb.phase then
6439 hpText = hpText.." ("..SCENARIO_STAGE:format(mod.vb.phase)..")"
6440 end
6441 if mod.numBoss then
6442 local bossesKilled = mod.numBoss - mod.vb.bossLeft
6443 hpText = hpText.." ("..BOSSES_KILLED:format(bossesKilled, mod.numBoss)..")"
6444 end
6445 sendWhisper(sender, chatPrefix..DBM_CORE_STATUS_WHISPER:format(difficultyText..(mod.combatInfo.name or ""), hpText, IsInInstance() and getNumRealAlivePlayers() or getNumAlivePlayers(), DBM:GetNumRealGroupMembers()))
6446 elseif #inCombat > 0 and DBM.Options.AutoRespond and (isRealIdMessage and (not isOnSameServer(sender) or not DBM:GetRaidUnitId(select(5, BNGetFriendInfoByID(sender)))) or not isRealIdMessage and not DBM:GetRaidUnitId(sender)) then
6447 if not difficultyText then -- prevent error when timer recovery function worked and etc (StartCombat not called)
6448 difficultyText = select(2, DBM:GetCurrentInstanceDifficulty())
6449 end
6450 local mod
6451 for i, v in ipairs(inCombat) do
6452 mod = not v.isCustomMod and v
6453 end
6454 mod = mod or inCombat[1]
6455 local hp = mod.highesthealth and mod:GetHighestBossHealth() or mod:GetLowestBossHealth()
6456 local hpText = mod.CustomHealthUpdate and mod:CustomHealthUpdate() or hp and ("%d%%"):format(hp) or DBM_CORE_UNKNOWN
6457 if mod.vb.phase then
6458 hpText = hpText.." ("..SCENARIO_STAGE:format(mod.vb.phase)..")"
6459 end
6460 if mod.numBoss then
6461 local bossesKilled = mod.numBoss - mod.vb.bossLeft
6462 hpText = hpText.." ("..BOSSES_KILLED:format(bossesKilled, mod.numBoss)..")"
6463 end
6464 if not autoRespondSpam[sender] then
6465 if IsInScenarioGroup() then
6466 sendWhisper(sender, chatPrefix..DBM_CORE_AUTO_RESPOND_WHISPER_SCENARIO:format(playerName, difficultyText..(mod.combatInfo.name or ""), getNumAlivePlayers(), DBM:GetNumGroupMembers()))
6467 else
6468 sendWhisper(sender, chatPrefix..DBM_CORE_AUTO_RESPOND_WHISPER:format(playerName, difficultyText..(mod.combatInfo.name or ""), hpText, IsInInstance() and getNumRealAlivePlayers() or getNumAlivePlayers(), DBM:GetNumRealGroupMembers()))
6469 end
6470 DBM:AddMsg(DBM_CORE_AUTO_RESPONDED)
6471 end
6472 autoRespondSpam[sender] = true
6473 end
6474 end
6475
6476 function DBM:CHAT_MSG_WHISPER(msg, name, _, _, _, status)
6477 if status ~= "GM" then
6478 name = Ambiguate(name, "none")
6479 return onWhisper(msg, name, false)
6480 end
6481 end
6482
6483 function DBM:CHAT_MSG_BN_WHISPER(msg, ...)
6484 local presenceId = select(12, ...) -- srsly?
6485 return onWhisper(msg, presenceId, true)
6486 end
6487end
6488
6489-------------------
6490-- Chat Filter --
6491-------------------
6492do
6493 local function filterOutgoing(self, event, ...)
6494 local msg = ...
6495 if not msg and self then -- compatibility mode!
6496 -- we also check if self exists to prevent a possible freeze if the function is called without arguments at all
6497 -- as this would be even worse than the issue with missing whisper messages ;)
6498 return filterOutgoing(nil, nil, self, event)
6499 end
6500 return msg:sub(1, chatPrefix:len()) == chatPrefix or msg:sub(1, chatPrefixShort:len()) == chatPrefixShort, ...
6501 end
6502
6503 local function filterIncoming(self, event, ...)
6504 local msg = ...
6505 if not msg and self then -- compatibility mode!
6506 return filterIncoming(nil, nil, self, event)
6507 end
6508 if DBM.Options.SpamBlockBossWhispers then
6509 return #inCombat > 0 and (msg == "status" or msg:sub(1, chatPrefix:len()) == chatPrefix or msg:sub(1, chatPrefixShort:len()) == chatPrefixShort), ...
6510 else
6511 return msg == "status" and #inCombat > 0, ...
6512 end
6513 end
6514
6515 local function filterSayYell(self, event, ...)
6516 return DBM.Options.FilterSayAndYell and #inCombat > 0, ...
6517 end
6518
6519 --This is the source of the taints. As well as function DBM:AddMsg(text, prefix) function
6520 --Which is why we embed libchatanims to fix those taints.
6521 ChatFrame_AddMessageEventFilter("CHAT_MSG_WHISPER_INFORM", filterOutgoing)
6522 ChatFrame_AddMessageEventFilter("CHAT_MSG_BN_WHISPER_INFORM", filterOutgoing)
6523 ChatFrame_AddMessageEventFilter("CHAT_MSG_WHISPER", filterIncoming)
6524 ChatFrame_AddMessageEventFilter("CHAT_MSG_BN_WHISPER", filterIncoming)
6525 ChatFrame_AddMessageEventFilter("CHAT_MSG_SAY", filterSayYell)
6526 ChatFrame_AddMessageEventFilter("CHAT_MSG_YELL", filterSayYell)
6527end
6528
6529--Raid Boss Emote frame handler for core and BG mods.
6530--This completely unregisteres or registers event so frame simply does or doesn't show events
6531--No dirty hooking. Least invasive way to do it. Uses lowest CPU
6532--Toggle is for if we are turning off or on.
6533--Custom is for pvp mods to call function without needing global option turned on (such as BG mods option)
6534--All also handled by core so both core AND pvp mods aren't trying to hook/hide it. Should all be done HERE
6535do
6536 local blizzEventsUnregistered = false
6537 function DBM:HideBlizzardEvents(toggle, custom)
6538 if toggle == 1 and not blizzEventsUnregistered then
6539 blizzEventsUnregistered = true
6540 if self.Options.HideQuestTooltips then
6541 SetCVar("showQuestTrackingTooltips", 0)
6542 end
6543 if self.Options.HideBossEmoteFrame or custom then
6544 RaidBossEmoteFrame:UnregisterEvent("RAID_BOSS_EMOTE")
6545 RaidBossEmoteFrame:UnregisterEvent("RAID_BOSS_WHISPER")
6546 RaidBossEmoteFrame:UnregisterEvent("CLEAR_BOSS_EMOTES")
6547 end
6548 if self.Options.HideGarrisonToasts or custom then
6549 AlertFrame:UnregisterEvent("GARRISON_MISSION_FINISHED")
6550 AlertFrame:UnregisterEvent("GARRISON_BUILDING_ACTIVATABLE")
6551 end
6552 if self.Options.HideGuildChallengeUpdates or custom then
6553 AlertFrame:UnregisterEvent("GUILD_CHALLENGE_COMPLETED")
6554 end
6555 elseif toggle == 0 and blizzEventsUnregistered then
6556 blizzEventsUnregistered = false
6557 if self.Options.HideQuestTooltips then
6558 SetCVar("showQuestTrackingTooltips", 1)
6559 end
6560 if self.Options.HideBossEmoteFrame or custom then
6561 RaidBossEmoteFrame:RegisterEvent("RAID_BOSS_EMOTE")
6562 RaidBossEmoteFrame:RegisterEvent("RAID_BOSS_WHISPER")
6563 RaidBossEmoteFrame:RegisterEvent("CLEAR_BOSS_EMOTES")
6564 end
6565 if self.Options.HideGarrisonToasts then
6566 AlertFrame:RegisterEvent("GARRISON_MISSION_FINISHED")
6567 AlertFrame:RegisterEvent("GARRISON_BUILDING_ACTIVATABLE")
6568 end
6569 if self.Options.HideGuildChallengeUpdates then
6570 AlertFrame:RegisterEvent("GUILD_CHALLENGE_COMPLETED")
6571 end
6572 end
6573 end
6574end
6575
6576--------------------------
6577-- Enable/Disable DBM --
6578--------------------------
6579function DBM:Disable(forced)
6580 unscheduleAll()
6581 dbmIsEnabled = false
6582end
6583
6584function DBM:Enable()
6585 dbmIsEnabled = true
6586end
6587
6588function DBM:IsEnabled()
6589 return dbmIsEnabled
6590end
6591
6592-----------------------
6593-- Misc. Functions --
6594-----------------------
6595function DBM:AddMsg(text, prefix)
6596 local tag = prefix or (self.localization and self.localization.general.name) or "DBM"
6597 local frame = _G[tostring(DBM.Options.ChatFrame)]
6598 frame = frame and frame:IsShown() and frame or DEFAULT_CHAT_FRAME
6599 if prefix ~= false then
6600 frame:AddMessage(("|cffff7d0a<|r|cffffd200%s|r|cffff7d0a>|r %s"):format(tostring(tag), tostring(text)), 0.41, 0.8, 0.94)
6601 else
6602 frame:AddMessage(text, 0.41, 0.8, 0.94)
6603 end
6604end
6605
6606function DBM:Debug(text, level)
6607 if not self.Options or not self.Options.DebugMode then return end
6608 if (level or 1) <= DBM.Options.DebugLevel then
6609 local frame = _G[tostring(DBM.Options.ChatFrame)]
6610 frame = frame and frame:IsShown() and frame or DEFAULT_CHAT_FRAME
6611 frame:AddMessage("|cffff7d0aDBM Debug:|r "..text, 1, 1, 1)
6612 end
6613end
6614
6615do
6616 local testMod
6617 local testWarning1, testWarning2, testWarning3
6618 local testTimer1, testTimer2, testTimer3, testTimer4, testTimer5, testTimer6, testTimer7, testTimer8
6619 local testCount1, testCount2
6620 local testSpecialWarning1, testSpecialWarning2, testSpecialWarning3
6621 function DBM:DemoMode()
6622 if not testMod then
6623 testMod = self:NewMod("TestMod")
6624 self:GetModLocalization("TestMod"):SetGeneralLocalization{ name = "Test Mod" }
6625 testWarning1 = testMod:NewAnnounce("%s", 1, "Interface\\Icons\\Spell_Nature_WispSplode")
6626 testWarning2 = testMod:NewAnnounce("%s", 2, "Interface\\Icons\\Spell_Shadow_ShadesOfDarkness")
6627 testWarning3 = testMod:NewAnnounce("%s", 3, "Interface\\Icons\\Spell_Fire_SelfDestruct")
6628 testTimer1 = testMod:NewTimer(20, "%s", "Interface\\Icons\\Spell_Nature_WispSplode", nil, nil)
6629 testTimer2 = testMod:NewTimer(20, "%s ", "Interface\\ICONS\\INV_Misc_Head_Orc_01.blp", nil, nil, 1)
6630 testTimer3 = testMod:NewTimer(20, "%s ", "Interface\\Icons\\Spell_Shadow_ShadesOfDarkness", nil, nil, 3, DBM_CORE_MAGIC_ICON)
6631 testTimer4 = testMod:NewTimer(20, "%s ", "Interface\\Icons\\Spell_Nature_WispSplode", nil, nil, 4, DBM_CORE_INTERRUPT_ICON)
6632 testTimer5 = testMod:NewTimer(20, "%s ", "Interface\\Icons\\Spell_Fire_SelfDestruct", nil, nil, 2, DBM_CORE_HEALER_ICON)
6633 testTimer6 = testMod:NewTimer(20, "%s ", "Interface\\Icons\\Spell_Nature_WispSplode", nil, nil, 5, DBM_CORE_TANK_ICON)
6634 testTimer7 = testMod:NewTimer(20, "%s ", "Interface\\Icons\\Spell_Nature_WispSplode", nil, nil, 6)
6635 testTimer8 = testMod:NewTimer(20, "%s ", "Interface\\Icons\\Spell_Nature_WispSplode", nil, nil, 7)
6636 testCount1 = testMod:NewCountdown(0, 0, nil, nil, nil, true)
6637 testCount2 = testMod:NewCountdown(0, 0, nil, nil, nil, true, true)
6638 testSpecialWarning1 = testMod:NewSpecialWarning("%s")
6639 testSpecialWarning2 = testMod:NewSpecialWarning(" %s ", nil, nil, nil, 2)
6640 testSpecialWarning3 = testMod:NewSpecialWarning(" %s ", nil, nil, nil, 3) -- hack: non auto-generated special warnings need distinct names (we could go ahead and give them proper names with proper localization entries, but this is much easier)
6641 end
6642 testTimer1:Start(10, "Test Bar")
6643 testTimer2:Start(30, "Adds")
6644 testTimer3:Start(43, "Evil Debuff")
6645 testTimer4:Start(20, "Important Interrupt")
6646 testTimer5:Start(60, "Boom!")
6647 testTimer6:Start(35, "Handle your Role")
6648 testTimer7:Start(50, "Next Phase")
6649 testTimer8:Start(55, "Custom User Bar")
6650 testCount1:Cancel()
6651 testCount1:Start(43)
6652 testCount2:Cancel()
6653 testCount2:Start(60)
6654 testWarning1:Cancel()
6655 testWarning2:Cancel()
6656 testWarning3:Cancel()
6657 testSpecialWarning1:Cancel()
6658 testSpecialWarning2:Cancel()
6659 testSpecialWarning3:Cancel()
6660 testWarning1:Show("Test-mode started...")
6661 testWarning1:Schedule(62, "Test-mode finished!")
6662 testWarning3:Schedule(50, "Boom in 10 sec!")
6663 testWarning3:Schedule(20, "Pew Pew Laser Owl!")
6664 testWarning2:Schedule(38, "Evil Spell in 5 sec!")
6665 testWarning2:Schedule(43, "Evil Spell!")
6666 testWarning1:Schedule(10, "Test bar expired!")
6667 testSpecialWarning1:Schedule(20, "Pew Pew Laser Owl")
6668 testSpecialWarning2:Schedule(43, "Evil Spell!")
6669 testSpecialWarning3:Schedule(60, "Boom!")
6670 end
6671end
6672
6673DBM.Bars:SetAnnounceHook(function(bar)
6674 local prefix
6675 if bar.color and bar.color.r == 1 and bar.color.g == 0 and bar.color.b == 0 then
6676 prefix = DBM_CORE_HORDE or FACTION_HORDE
6677 elseif bar.color and bar.color.r == 0 and bar.color.g == 0 and bar.color.b == 1 then
6678 prefix = DBM_CORE_ALLIANCE or FACTION_ALLIANCE
6679 end
6680 if prefix then
6681 return ("%s: %s %d:%02d"):format(prefix, _G[bar.frame:GetName().."BarName"]:GetText(), floor(bar.timer / 60), bar.timer % 60)
6682 end
6683end)
6684
6685function DBM:Capitalize(str)
6686 local firstByte = str:byte(1, 1)
6687 local numBytes = 1
6688 if firstByte >= 0xF0 then -- firstByte & 0b11110000
6689 numBytes = 4
6690 elseif firstByte >= 0xE0 then -- firstByte & 0b11100000
6691 numBytes = 3
6692 elseif firstByte >= 0xC0 then -- firstByte & 0b11000000
6693 numBytes = 2
6694 end
6695 return str:sub(1, numBytes):upper()..str:sub(numBytes + 1):lower()
6696end
6697
6698--copied from big wigs with permission from funkydude. Modified by MysticalOS
6699function DBM:RoleCheck(ignoreLoot)
6700 local spec = GetSpecialization()
6701 if not spec then return end
6702 local role = GetSpecializationRole(spec)
6703 if not role then return end
6704 local specID = GetLootSpecialization()
6705 local _, _, _, _, lootrole = GetSpecializationInfoByID(specID)
6706 if not InCombatLockdown() and ((IsPartyLFG() and (difficultyIndex == 14 or difficultyIndex == 15)) or not IsPartyLFG()) then
6707 if UnitGroupRolesAssigned("player") ~= role then
6708 UnitSetRole("player", role)
6709 end
6710 end
6711 --Loot reminder even if spec isn't known or we are in LFR where we have a valid for role without us being ones that set us.
6712 if not ignoreLoot and lootrole and (role ~= lootrole) and self.Options.RoleSpecAlert then
6713 self:AddMsg(DBM_CORE_LOOT_SPEC_REMINDER:format(_G[role] or DBM_CORE_UNKNOWN, _G[lootrole]))
6714 end
6715end
6716
6717-- An anti spam function to throttle spammy events (e.g. SPELL_AURA_APPLIED on all group members)
6718-- @param time the time to wait between two events (optional, default 2.5 seconds)
6719-- @param id the id to distinguish different events (optional, only necessary if your mod keeps track of two different spam events at the same time)
6720function DBM:AntiSpam(time, id)
6721 if GetTime() - (id and (self["lastAntiSpam" .. tostring(id)] or 0) or self.lastAntiSpam or 0) > (time or 2.5) then
6722 if id then
6723 self["lastAntiSpam" .. tostring(id)] = GetTime()
6724 else
6725 self.lastAntiSpam = GetTime()
6726 end
6727 return true
6728 else
6729 return false
6730 end
6731end
6732
6733function DBM:GetTOC()
6734 return wowTOC
6735end
6736
6737function DBM:FlashClientIcon()
6738 if self:AntiSpam(5, "FLASH") then
6739 FlashClientIcon()
6740 end
6741end
6742
6743--To speed up creating new mods.
6744function DBM:FindDungeonIDs()
6745 for i=1, 2000 do
6746 local dungeon = GetRealZoneText(i)
6747 if dungeon and dungeon ~= "" then
6748 self:AddMsg(i..": "..dungeon)
6749 end
6750 end
6751end
6752
6753function DBM:FindInstanceIDs()
6754 for i=1, 2000 do
6755 local instance = EJ_GetInstanceInfo(i)
6756 if instance then
6757 self:AddMsg(i..": "..instance)
6758 end
6759 end
6760end
6761
6762--/run DBM:FindEncounterIDs(875)--Tomb of Sargeras
6763--/run DBM:FindEncounterIDs(900, 23)--Cathedral of Eternal Night
6764function DBM:FindEncounterIDs(instanceID, diff)
6765 if not instanceID then
6766 self:AddMsg("Error: Function requires instanceID be provided")
6767 end
6768 if not diff then diff = 14 end--Default to "normal" in 6.0+ if diff arg not given.
6769 EJ_SetDifficulty(diff)--Make sure it's set to right difficulty or it'll ignore mobs (ie ra-den if it's not set to heroic). Use user specified one as primary, with curernt zone difficulty as fallback
6770 for i=1, 25 do
6771 local name, _, encounterID = EJ_GetEncounterInfoByIndex(i, instanceID)
6772 if name then
6773 self:AddMsg(encounterID..": "..name)
6774 end
6775 end
6776end
6777
6778--Taint the script that disables /run /dump, etc
6779--ScriptsDisallowedForBeta = function() return false end
6780
6781-------------------
6782-- Movie Filter --
6783-------------------
6784do
6785 local neverFilter = {
6786 [486] = true, -- Tomb of Sarg Intro
6787 [487] = true, -- Alliance Broken Shore cut-scene
6788 [488] = true, -- Horde Broken Shore cut-scene
6789 [489] = true, -- Unknown, currently encrypted
6790 [490] = true, -- Unknown, currently encrypted
6791 }
6792 MovieFrame:HookScript("OnEvent", function(self, event, id)
6793 if event == "PLAY_MOVIE" and id and not neverFilter[id] then
6794 DBM:Debug("PLAY_MOVIE fired for ID: "..id, 2)
6795 local isInstance, instanceType = IsInInstance()
6796 if not isInstance or C_Garrison:IsOnGarrisonMap() or instanceType == "scenario" or DBM.Options.MovieFilter == "Never" then return end
6797 if DBM.Options.MovieFilter == "Block" or DBM.Options.MovieFilter == "AfterFirst" and DBM.Options.MoviesSeen[id] then
6798 MovieFrame_OnMovieFinished(self)
6799 DBM:AddMsg(DBM_CORE_MOVIE_SKIPPED)
6800 else
6801 DBM.Options.MoviesSeen[id] = true
6802 end
6803 end
6804 end)
6805
6806 function DBM:CINEMATIC_START()
6807 self:Debug("CINEMATIC_START fired", 2)
6808 local isInstance, instanceType = IsInInstance()
6809 if not isInstance or C_Garrison:IsOnGarrisonMap() or instanceType == "scenario" or self.Options.MovieFilter == "Never" then return end
6810 SetMapToCurrentZone()
6811 local currentFloor = GetCurrentMapDungeonLevel() or 0
6812 if self.Options.MovieFilter == "Block" or self.Options.MovieFilter == "AfterFirst" and self.Options.MoviesSeen[LastInstanceMapID..currentFloor] then
6813 CinematicFrame_CancelCinematic()
6814 self:AddMsg(DBM_CORE_MOVIE_SKIPPED)
6815 else
6816 self.Options.MoviesSeen[LastInstanceMapID..currentFloor] = true
6817 end
6818 end
6819end
6820
6821----------------------------
6822-- Boss Mod Constructor --
6823----------------------------
6824do
6825 local modsById = setmetatable({}, {__mode = "v"})
6826 local mt = {__index = bossModPrototype}
6827
6828 function DBM:NewMod(name, modId, modSubTab, instanceId, nameModifier)
6829 name = tostring(name) -- the name should never be a number of something as it confuses sync handlers that just receive some string and try to get the mod from it
6830 if name == "DBM-ProfilesDummy" then return end
6831 if modsById[name] then error("DBM:NewMod(): Mod names are used as IDs and must therefore be unique.", 2) end
6832 local obj = setmetatable(
6833 {
6834 Options = {
6835 Enabled = true,
6836 },
6837 DefaultOptions = {
6838 Enabled = true,
6839 },
6840 subTab = modSubTab,
6841 optionCategories = {
6842 },
6843 categorySort = {"announce", "announceother", "announcepersonal", "announcerole", "timer", "sound", "misc"},
6844 id = name,
6845 announces = {},
6846 specwarns = {},
6847 timers = {},
6848 countdowns = {},
6849 vb = {},
6850 iconRestore = {},
6851 modId = modId,
6852 instanceId = instanceId,
6853 revision = 0,
6854 SyncThreshold = 8,
6855 localization = self:GetModLocalization(name)
6856 },
6857 mt
6858 )
6859 for i, v in ipairs(self.AddOns) do
6860 if v.modId == modId then
6861 obj.addon = v
6862 break
6863 end
6864 end
6865
6866 if tonumber(name) then
6867 local t = EJ_GetEncounterInfo(tonumber(name))
6868 if type(nameModifier) == "number" then--Get name form EJ_GetCreatureInfo
6869 t = select(2, EJ_GetCreatureInfo(nameModifier, tonumber(name)))
6870 elseif type(nameModifier) == "function" then--custom name modify function
6871 t = nameModifier(t or name)
6872 else--default name modify
6873 t = tostring(t)
6874 t = string.split(",", t or name)
6875 end
6876 obj.localization.general.name = t or name
6877 obj.modelId = select(4, EJ_GetCreatureInfo(1, tonumber(name)))
6878 elseif name:match("z%d+") then
6879 local t = GetRealZoneText(string.sub(name, 2))
6880 if type(nameModifier) == "number" then--do nothing
6881 elseif type(nameModifier) == "function" then--custom name modify function
6882 t = nameModifier(t or name)
6883 else--default name modify
6884 t = string.split(",", t or name)
6885 end
6886 obj.localization.general.name = t or name
6887 elseif name:match("d%d+") then
6888 local t = GetDungeonInfo(string.sub(name, 2))
6889 if type(nameModifier) == "number" then--do nothing
6890 elseif type(nameModifier) == "function" then--custom name modify function
6891 t = nameModifier(t or name)
6892 else--default name modify
6893 t = string.split(",", t or name)
6894 end
6895 obj.localization.general.name = t or name
6896 else
6897 obj.localization.general.name = obj.localization.general.name or name
6898 end
6899 tinsert(self.Mods, obj)
6900 if modId then
6901 self.ModLists[modId] = self.ModLists[modId] or {}
6902 tinsert(self.ModLists[modId], name)
6903 end
6904 modsById[name] = obj
6905 obj:AddBoolOption("HealthFrame", false, "misc")
6906 obj:SetZone()
6907 return obj
6908 end
6909
6910 function DBM:GetModByName(name)
6911 return modsById[tostring(name)]
6912 end
6913end
6914
6915-----------------------
6916-- General Methods --
6917-----------------------
6918bossModPrototype.RegisterEvents = DBM.RegisterEvents
6919bossModPrototype.UnregisterInCombatEvents = DBM.UnregisterInCombatEvents
6920bossModPrototype.AddMsg = DBM.AddMsg
6921bossModPrototype.RegisterShortTermEvents = DBM.RegisterShortTermEvents
6922bossModPrototype.UnregisterShortTermEvents = DBM.UnregisterShortTermEvents
6923
6924function bossModPrototype:SetZone(...)
6925 if select("#", ...) == 0 then
6926 self.zones = {}
6927 if self.addon and self.addon.mapId then
6928 for i, v in ipairs(self.addon.mapId) do
6929 self.zones[v] = true
6930 end
6931 end
6932 elseif select(1, ...) ~= DBM_DISABLE_ZONE_DETECTION then
6933 self.zones = {}
6934 for i = 1, select("#", ...) do
6935 self.zones[select(i, ...)] = true
6936 end
6937 else -- disable zone detection
6938 self.zones = nil
6939 end
6940end
6941
6942function bossModPrototype:Toggle()
6943 if self.Options.Enabled then
6944 self:DisableMod()
6945 else
6946 self:EnableMod()
6947 end
6948end
6949
6950function bossModPrototype:EnableMod()
6951 self.Options.Enabled = true
6952end
6953
6954function bossModPrototype:DisableMod()
6955 self:Stop()
6956 self.Options.Enabled = false
6957end
6958
6959function bossModPrototype:Stop()
6960 for i, v in ipairs(self.timers) do
6961 v:Stop()
6962 end
6963 for i, v in ipairs(self.countdowns) do
6964 v:Stop()
6965 end
6966 self:Unschedule()
6967end
6968
6969function bossModPrototype:SetUsedIcons(...)
6970 self.usedIcons = {}
6971 for i = 1, select("#", ...) do
6972 self.usedIcons[select(i, ...)] = true
6973 end
6974end
6975
6976function bossModPrototype:RegisterOnUpdateHandler(func, interval)
6977 startScheduler()
6978 if type(func) ~= "function" then return end
6979 self.elapsed = 0
6980 self.updateInterval = interval or 0
6981 updateFunctions[self] = func
6982end
6983
6984function bossModPrototype:UnregisterOnUpdateHandler()
6985 self.elapsed = nil
6986 self.updateInterval = nil
6987 twipe(updateFunctions)
6988end
6989
6990--------------
6991-- Events --
6992--------------
6993function bossModPrototype:RegisterEventsInCombat(...)
6994 if self.inCombatOnlyEvents then
6995 geterrorhandler()("combat events already set")
6996 end
6997 self.inCombatOnlyEvents = {...}
6998 for k, v in pairs(self.inCombatOnlyEvents) do
6999 if v:sub(0, 5) == "UNIT_" and v:sub(v:len() - 10) ~= "_UNFILTERED" and not v:find(" ") and v ~= "UNIT_DIED" and v ~= "UNIT_DESTROYED" then
7000 -- legacy event, oh noes
7001 self.inCombatOnlyEvents[k] = v .. " boss1 boss2 boss3 boss4 boss5 target focus"
7002 end
7003 end
7004end
7005
7006-----------------------
7007-- Utility Methods --
7008-----------------------
7009
7010function bossModPrototype:IsDifficulty(...)
7011 local diff = DBM:GetCurrentInstanceDifficulty()
7012 for i = 1, select("#", ...) do
7013 if diff == select(i, ...) then
7014 return true
7015 end
7016 end
7017 return false
7018end
7019
7020function bossModPrototype:IsLFR()
7021 local diff = DBM:GetCurrentInstanceDifficulty()
7022 if diff == "lfr" or diff == "lfr25" then
7023 return true
7024 end
7025 return false
7026end
7027
7028function bossModPrototype:IsEasy()
7029 local diff = DBM:GetCurrentInstanceDifficulty()
7030 if diff == "normal" or diff == "lfr" then
7031 return true
7032 end
7033 return false
7034end
7035
7036function bossModPrototype:IsHard()
7037 local diff = DBM:GetCurrentInstanceDifficulty()
7038 if diff == "mythic" or diff == "challenge5" then
7039 return true
7040 end
7041 return false
7042end
7043
7044function bossModPrototype:IsNormal()
7045 local diff = DBM:GetCurrentInstanceDifficulty()
7046 if diff == "normal" or diff == "normal5" or diff == "normal10" or diff == "normal25" then
7047 return true
7048 end
7049 return false
7050end
7051
7052function bossModPrototype:IsHeroic()
7053 local diff = DBM:GetCurrentInstanceDifficulty()
7054 if diff == "heroic" or diff == "heroic5" or diff == "heroic10" or diff == "heroic25" then
7055 return true
7056 end
7057 return false
7058end
7059
7060function bossModPrototype:IsMythic()
7061 local diff = DBM:GetCurrentInstanceDifficulty()
7062 if diff == "mythic" then
7063 return true
7064 end
7065 return false
7066end
7067
7068function bossModPrototype:IsEvent()
7069 local diff = DBM:GetCurrentInstanceDifficulty()
7070 if diff == "event5" or diff == "event20" or diff == "event40" then
7071 return true
7072 end
7073 return false
7074end
7075
7076function bossModPrototype:IsTrivial(level)
7077 if difficultyIndex == 24 then return false end--Timewalker dungeon, ignore level and return false for trivial
7078 if playerLevel >= level then
7079 return true
7080 end
7081 return false
7082end
7083
7084function bossModPrototype:CheckInterruptFilter(sourceGUID, skip)
7085 if not DBM.Options.FilterInterrupt and not skip then return true end
7086 if UnitGUID("target") == sourceGUID or UnitGUID("focus") == sourceGUID then
7087 return true
7088 end
7089 return false
7090end
7091
7092function bossModPrototype:CheckDispelFilter()
7093 if not DBM.Options.FilterDispel then return true end
7094 --Druid: Nature's Cure (88423), Remove Corruption (2782), Monk: Detox (115450), Priest: Purify (527), Plaadin: Cleanse (4987), Shaman: Cleanse Spirit (51886), Purify Spirit (77130), Mage: Remove Curse (475)
7095 --start, duration, enable = GetSpellCooldown
7096 --start & duration == 0 if spell not on cd
7097 if (GetSpellCooldown(88423)) ~= 0 or (GetSpellCooldown(2782)) ~= 0 or (GetSpellCooldown(115450)) ~= 0 or (GetSpellCooldown(527)) ~= 0 or (GetSpellCooldown(4987)) ~= 0 or (GetSpellCooldown(51886)) ~= 0 or (GetSpellCooldown(77130)) ~= 0 or (GetSpellCooldown(475)) ~= 0 then
7098 return false
7099 end
7100 return true
7101end
7102
7103function bossModPrototype:IsCriteriaCompleted(criteriaIDToCheck)
7104 if not criteriaIDToCheck then
7105 geterrorhandler()("usage: mod:IsCriteriaComplected(criteriaId)")
7106 return false
7107 end
7108 local _, _, numCriteria = C_Scenario.GetStepInfo()
7109 for i = 1, numCriteria do
7110 local _, _, criteriaCompleted, _, _, _, _, _, criteriaID = C_Scenario.GetCriteriaInfo(i)
7111 if criteriaID == criteriaIDToCheck and criteriaCompleted then
7112 return true
7113 end
7114 end
7115 return false
7116end
7117
7118function bossModPrototype:LatencyCheck()
7119 return select(4, GetNetStats()) < DBM.Options.LatencyThreshold
7120end
7121
7122function bossModPrototype:CheckBigWigs(name)
7123 if raid[name] and raid[name].bwversion then
7124 return raid[name].bwversion
7125 else
7126 return false
7127 end
7128end
7129
7130do
7131 local iconStrings = {[1] = RAID_TARGET_1, [2] = RAID_TARGET_2, [3] = RAID_TARGET_3, [4] = RAID_TARGET_4, [5] = RAID_TARGET_5, [6] = RAID_TARGET_6, [7] = RAID_TARGET_7, [8] = RAID_TARGET_8,}
7132 function bossModPrototype:IconNumToString(number)
7133 return iconStrings[number] or number
7134 end
7135end
7136
7137bossModPrototype.AntiSpam = DBM.AntiSpam
7138bossModPrototype.HasMapRestrictions = DBM.HasMapRestrictions
7139bossModPrototype.GetUnitCreatureId = DBM.GetUnitCreatureId
7140bossModPrototype.GetCIDFromGUID = DBM.GetCIDFromGUID
7141bossModPrototype.IsCreatureGUID = DBM.IsCreatureGUID
7142
7143do
7144 local bossTargetuIds = {
7145 "boss1", "boss2", "boss3", "boss4", "boss5", "focus", "target"
7146 }
7147 local targetScanCount = {}
7148 local repeatedScanEnabled = {}
7149
7150 local function getBossTarget(guid, scanOnlyBoss)
7151 local name, uid, bossuid
7152 local cacheuid = bossuIdCache[guid] or "boss1"
7153 if UnitGUID(cacheuid) == guid then
7154 bossuid = cacheuid
7155 name = DBM:GetUnitFullName(cacheuid.."target")
7156 uid = cacheuid.."target"
7157 bossuIdCache[guid] = bossuid
7158 end
7159 if name then return name, uid, bossuid end
7160 for i, uId in ipairs(bossTargetuIds) do
7161 if UnitGUID(uId) == guid then
7162 bossuid = uId
7163 name = DBM:GetUnitFullName(uId.."target")
7164 uid = uId.."target"
7165 bossuIdCache[guid] = bossuid
7166 break
7167 end
7168 end
7169 if name or scanOnlyBoss then return name, uid, bossuid end
7170 -- Now lets check nameplates
7171 for i = 1, 40 do
7172 if UnitGUID("nameplate"..i) == guid then
7173 bossuid = "nameplate"..i
7174 name = DBM:GetUnitFullName("nameplate"..i.."target")
7175 uid = "nameplate"..i.."target"
7176 bossuIdCache[guid] = bossuid
7177 break
7178 end
7179 end
7180 if name then return name, uid, bossuid end
7181 -- failed to detect from default uIds, scan all group members's target.
7182 if IsInRaid() then
7183 for i = 1, GetNumGroupMembers() do
7184 if UnitGUID("raid"..i.."target") == guid then
7185 bossuid = "raid"..i.."target"
7186 name = DBM:GetUnitFullName("raid"..i.."targettarget")
7187 uid = "raid"..i.."targettarget"
7188 bossuIdCache[guid] = bossuid
7189 break
7190 end
7191 end
7192 elseif IsInGroup() then
7193 for i = 1, GetNumSubgroupMembers() do
7194 if UnitGUID("party"..i.."target") == guid then
7195 bossuid = "party"..i.."target"
7196 name = DBM:GetUnitFullName("party"..i.."targettarget")
7197 uid = "party"..i.."targettarget"
7198 bossuIdCache[guid] = bossuid
7199 break
7200 end
7201 end
7202 end
7203 return name, uid, bossuid
7204 end
7205
7206 function bossModPrototype:GetBossTarget(cidOrGuid, scanOnlyBoss)
7207 local name, uid, bossuid
7208 if type(cidOrGuid) == "number" then
7209 local cidOrGuid = cidOrGuid or self.creatureId
7210 local cacheuid = bossuIdCache[cidOrGuid] or "boss1"
7211 if self:GetUnitCreatureId(cacheuid) == cidOrGuid then
7212 bossuIdCache[cidOrGuid] = cacheuid
7213 bossuIdCache[UnitGUID(cacheuid)] = cacheuid
7214 name, uid, bossuid = getBossTarget(UnitGUID(cacheuid), scanOnlyBoss)
7215 else
7216 local found = false
7217 for i, uId in ipairs(bossTargetuIds) do
7218 if self:GetUnitCreatureId(uId) == cidOrGuid then
7219 found = true
7220 bossuIdCache[cidOrGuid] = uId
7221 bossuIdCache[UnitGUID(uId)] = uId
7222 name, uid, bossuid = getBossTarget(UnitGUID(uId), scanOnlyBoss)
7223 break
7224 end
7225 end
7226 if not found and not scanOnlyBoss then
7227 if IsInRaid() then
7228 for i = 1, GetNumGroupMembers() do
7229 if self:GetUnitCreatureId("raid"..i.."target") == cidOrGuid then
7230 bossuIdCache[cidOrGuid] = "raid"..i.."target"
7231 bossuIdCache[UnitGUID("raid"..i.."target")] = "raid"..i.."target"
7232 name, uid, bossuid = getBossTarget(UnitGUID("raid"..i.."target"))
7233 break
7234 end
7235 end
7236 elseif IsInGroup() then
7237 for i = 1, GetNumSubgroupMembers() do
7238 if self:GetUnitCreatureId("party"..i.."target") == cidOrGuid then
7239 bossuIdCache[cidOrGuid] = "party"..i.."target"
7240 bossuIdCache[UnitGUID("party"..i.."target")] = "party"..i.."target"
7241 name, uid, bossuid = getBossTarget(UnitGUID("party"..i.."target"))
7242 break
7243 end
7244 end
7245 end
7246 end
7247 end
7248 else
7249 name, uid, bossuid = getBossTarget(cidOrGuid, scanOnlyBoss)
7250 end
7251 if uid then
7252 local cid = DBM:GetUnitCreatureId(uid)
7253 if cid == 24207 or cid == 80258 or cid == 87519 then--filter army of the dead/Garrison Footman (basically same thing as army)
7254 return nil, nil, nil
7255 end
7256 end
7257 return name, uid, bossuid
7258 end
7259
7260 function bossModPrototype:BossTargetScannerAbort(cidOrGuid, returnFunc)
7261 targetScanCount[cidOrGuid] = nil--Reset count for later use.
7262 self:UnscheduleMethod("BossTargetScanner", cidOrGuid, returnFunc)
7263 DBM:Debug("Boss target scan for "..cidOrGuid.." should be aborting.", 3)
7264 end
7265
7266 function bossModPrototype:BossUnitTargetScannerAbort()
7267 targetMonitor = nil
7268 DBM:Debug("Boss unit target scan should be aborting.", 3)
7269 end
7270
7271 function bossModPrototype:BossUnitTargetScanner(unitId, returnFunc, scanTime)
7272 --UNIT_TARGET technique was originally used by DXE on heroic lich king back in wrath to give most accurate defile/shadow trap warnings. Recently bigwigs started using it.
7273 --This is fastest and most accurate method for getting the target and probably should be used where it does work 100% of time.
7274 --This method fails if boss is already looking at correct target!! This method needs to monitor a target change so it must start before that target change
7275 --In most cases, using BossTargetScanner is probably still better, especially if boss is expected to look at target before or immediately on cast start
7276 --Limited to only one unitTarget scanner at a time. TODO, maybe make targetMonitor a table or something to support more than one scan at a time?
7277 --This code is much prettier if it's in mod, but then it'd require copying and pasting it all the time. SO ugly code in core more convinient.
7278 local modId = self.id
7279 local scanDuration = scanTime or 1.5
7280 targetMonitor = modId.."\t"..unitId.."\t"..returnFunc
7281 self:ScheduleMethod(scanDuration, "BossUnitTargetScannerAbort")--In case of BossUnitTargetScanner firing too late, and boss already having changed target before monitor started, it needs to abort after x seconds
7282 end
7283
7284 function bossModPrototype:BossTargetScanner(cidOrGuid, returnFunc, scanInterval, scanTimes, scanOnlyBoss, isEnemyScan, isFinalScan, targetFilter, tankFilter)
7285 --Increase scan count
7286 local cidOrGuid = cidOrGuid or self.creatureId
7287 if not cidOrGuid then return end
7288 if not targetScanCount[cidOrGuid] then targetScanCount[cidOrGuid] = 0 end
7289 targetScanCount[cidOrGuid] = targetScanCount[cidOrGuid] + 1
7290 --Set default values
7291 local scanInterval = scanInterval or 0.05
7292 local scanTimes = scanTimes or 16
7293 local targetname, targetuid, bossuid = self:GetBossTarget(cidOrGuid, scanOnlyBoss)
7294 DBM:Debug("Boss target scan "..targetScanCount[cidOrGuid].." of "..scanTimes..", found target "..(targetname or "nil").." using "..(bossuid or "nil"), 3)--Doesn't hurt to keep this, as level 3
7295 --Do scan
7296 if targetname and targetname ~= DBM_CORE_UNKNOWN and (not targetFilter or (targetFilter and targetFilter ~= targetname)) then
7297 if not IsInGroup() then scanTimes = 1 end--Solo, no reason to keep scanning, give faster warning. But only if first scan is actually a valid target, which is why i have this check HERE
7298 if (isEnemyScan and UnitIsFriend("player", targetuid) or self:IsTanking(targetuid, bossuid)) and not isFinalScan then--On player scan, ignore tanks. On enemy scan, ignore friendly player.
7299 if targetScanCount[cidOrGuid] < scanTimes then--Make sure no infinite loop.
7300 self:ScheduleMethod(scanInterval, "BossTargetScanner", cidOrGuid, returnFunc, scanInterval, scanTimes, scanOnlyBoss, isEnemyScan, nil, targetFilter, tankFilter)--Scan multiple times to be sure it's not on something other then tank (or friend on enemy scan).
7301 else--Go final scan.
7302 self:BossTargetScanner(cidOrGuid, returnFunc, scanInterval, scanTimes, scanOnlyBoss, isEnemyScan, true, targetFilter, tankFilter)
7303 end
7304 else--Scan success. (or failed to detect right target.) But some spells can be used on tanks, anyway warns tank if player scan. (enemy scan block it)
7305 targetScanCount[cidOrGuid] = nil--Reset count for later use.
7306 self:UnscheduleMethod("BossTargetScanner", cidOrGuid, returnFunc)--Unschedule all checks just to be sure none are running, we are done.
7307 if (tankFilter and self:IsTanking(targetuid, bossuid)) or (isFinalScan and isEnemyScan) then return end--If enemyScan and playerDetected, return nothing
7308 self[returnFunc](self, targetname, targetuid, bossuid)--Return results to warning function with all variables.
7309 end
7310 else--target was nil, lets schedule a rescan here too.
7311 if targetScanCount[cidOrGuid] < scanTimes then--Make sure not to infinite loop here as well.
7312 self:ScheduleMethod(scanInterval, "BossTargetScanner", cidOrGuid, returnFunc, scanInterval, scanTimes, scanOnlyBoss, isEnemyScan, nil, targetFilter, tankFilter)
7313 else
7314 targetScanCount[cidOrGuid] = nil--Reset count for later use.
7315 self:UnscheduleMethod("BossTargetScanner", cidOrGuid, returnFunc)--Unschedule all checks just to be sure none are running, we are done.
7316 end
7317 end
7318 end
7319
7320 --infinite scanner. so use this carefully.
7321 local function repeatedScanner(cidOrGuid, returnFunc, scanInterval, scanOnlyBoss, includeTank, mod)
7322 if repeatedScanEnabled[returnFunc] then
7323 local cidOrGuid = cidOrGuid or mod.creatureId
7324 local scanInterval = scanInterval or 0.1
7325 local targetname, targetuid, bossuid = mod:GetBossTarget(cidOrGuid, scanOnlyBoss)
7326 if targetname and (includeTank or not mod:IsTanking(targetuid, bossuid)) then
7327 mod[returnFunc](mod, targetname, targetuid, bossuid)
7328 end
7329 DBM:Schedule(scanInterval, repeatedScanner, cidOrGuid, returnFunc, scanInterval, scanOnlyBoss, includeTank, mod)
7330 end
7331 end
7332
7333 function bossModPrototype:StartRepeatedScan(cidOrGuid, returnFunc, scanInterval, scanOnlyBoss, includeTank)
7334 repeatedScanEnabled[returnFunc] = true
7335 repeatedScanner(cidOrGuid, returnFunc, scanInterval, scanOnlyBoss, includeTank, self)
7336 end
7337
7338 function bossModPrototype:StopRepeatedScan(returnFunc)
7339 repeatedScanEnabled[returnFunc] = nil
7340 end
7341end
7342
7343function bossModPrototype:CheckNearby(range, targetname)
7344 local uId = DBM:GetRaidUnitId(targetname)
7345 if uId and not UnitIsUnit("player", uId) then
7346 local inRange = DBM.RangeCheck:GetDistance(uId)
7347 if inRange and inRange < range then
7348 return true
7349 end
7350 end
7351 return false
7352end
7353
7354do
7355 local bossCache = {}
7356 local lastTank = nil
7357
7358 function bossModPrototype:GetCurrentTank(cidOrGuid)
7359 if lastTank and GetTime() - (bossCache[cidOrGuid] or 0) < 2 then -- return last tank within 2 seconds of call
7360 return lastTank
7361 else
7362 local cidOrGuid = cidOrGuid or self.creatureId--GetBossTarget supports GUID or CID and it will automatically return correct values with EITHER ONE
7363 local uId
7364 local _, fallbackuId, mobuId = self:GetBossTarget(cidOrGuid)
7365 if mobuId then--Have a valid mob unit ID
7366 --First, use trust threat more than fallbackuId and see what we pull from it first.
7367 --This is because for GetCurrentTank we want to know who is tanking it, not who it's targeting.
7368 local unitId = (IsInRaid() and "raid") or "party"
7369 for i = 0, GetNumGroupMembers() do
7370 local id = (i == 0 and "target") or unitId..i
7371 local tanking, status = UnitDetailedThreatSituation(id, mobuId)--Tanking may return 0 if npc is temporarily looking at an NPC (IE fracture) but status will still be 3 on true tank
7372 if tanking or (status == 3) then uId = id end--Found highest threat target, make them uId
7373 if uId then break end
7374 end
7375 --Did not get anything useful from threat, so use who the boss was looking at, at time of cast (ie fallbackuId)
7376 if fallbackuId and not uId then
7377 uId = fallbackuId
7378 end
7379 end
7380 if uId then--Now we have a valid uId
7381 bossCache[cidOrGuid] = GetTime()
7382 lastTank = UnitName(uId)
7383 return UnitName(lastTank), uId
7384 end
7385 return false
7386 end
7387 end
7388end
7389
7390--Now this function works perfectly. But have some limitation due to DBM.RangeCheck:GetDistance() function.
7391--Unfortunely, DBM.RangeCheck:GetDistance() function cannot reflects altitude difference. This makes range unreliable.
7392--So, we need to cafefully check range in difference altitude (Especially, tower top and bottom)
7393do
7394 local rangeCache = {}
7395 local rangeUpdated = {}
7396
7397 function bossModPrototype:CheckTankDistance(cidOrGuid, distance, defaultReturn)
7398 if not DBM.Options.DontShowFarWarnings then return true end--Global disable.
7399 if rangeCache[cidOrGuid] and (GetTime() - (rangeUpdated[cidOrGuid] or 0)) < 2 then -- return same range within 2 sec call
7400 if rangeCache[cidOrGuid] > distance then
7401 return false
7402 else
7403 return true
7404 end
7405 else
7406 local cidOrGuid = cidOrGuid or self.creatureId--GetBossTarget supports GUID or CID and it will automatically return correct values with EITHER ONE
7407 local distance = distance or 40
7408 local uId
7409 local _, fallbackuId, mobuId = self:GetBossTarget(cidOrGuid)
7410 if mobuId then--Have a valid mob unit ID
7411 --First, use trust threat more than fallbackuId and see what we pull from it first.
7412 --This is because for CheckTankDistance we want to know who is tanking it, not who it's targeting.
7413 local unitId = (IsInRaid() and "raid") or "party"
7414 for i = 0, GetNumGroupMembers() do
7415 local id = (i == 0 and "target") or unitId..i
7416 local tanking, status = UnitDetailedThreatSituation(id, mobuId)--Tanking may return 0 if npc is temporarily looking at an NPC (IE fracture) but status will still be 3 on true tank
7417 if tanking or (status == 3) then uId = id end--Found highest threat target, make them uId
7418 if uId then break end
7419 end
7420 --Did not get anything useful from threat, so use who the boss was looking at, at time of cast (ie fallbackuId)
7421 if fallbackuId and not uId then
7422 uId = fallbackuId
7423 end
7424 end
7425 if uId then--Now we have a valid uId
7426 if UnitIsUnit(uId, "player") then return true end--If "player" is target, avoid doing any complicated stuff
7427 local inRange = 0
7428 if not UnitIsPlayer(uId) then
7429 local inRange2, checkedRange = UnitInRange(uId)
7430 if checkedRange then--checkedRange only returns true if api worked, so if we get false, true then we are not near npc
7431 return inRange2 and true or false
7432 else--Its probably a totem or just something we can't assess. Fall back to no filtering
7433 return true
7434 end
7435 else
7436 inRange = DBM.RangeCheck:GetDistance("player", uId)--We check how far we are from the tank who has that boss
7437 end
7438 rangeCache[cidOrGuid] = inRange
7439 rangeUpdated[cidOrGuid] = GetTime()
7440 if inRange and (inRange > distance) then--You are not near the person tanking boss
7441 return false
7442 end
7443 --Tank in range, return true.
7444 return true
7445 end
7446 return (defaultReturn == nil) or defaultReturn--When we simply can't figure anything out, return true and allow warnings using this filter to fire. But some spells will prefer not to fire(i.e : Galakras tower spell), we can define it on this function calling.
7447 end
7448 end
7449end
7450
7451do
7452 local frame = CreateFrame("Frame", "DBMShields") -- frame for CLEU events, we don't want to run all *_MISSED events through the whole DBM event system...
7453
7454 local activeShields = {}
7455 local shieldsByGuid = {}
7456
7457 frame:SetScript("OnEvent", function(self, _, _, subEvent, _, _, _, _, _, destGUID, _, _, _, ...)
7458 local info = destGUID and shieldsByGuid[destGUID]
7459 if info then
7460 if info.maxAbsorb then
7461 local _, absorbed
7462 if subEvent == "SWING_MISSED" then
7463 _, _, _, absorbed = ...
7464 elseif subEvent == "RANGE_MISSED" or subEvent == "SPELL_MISSED" or subEvent == "SPELL_PERIODIC_MISSED" then
7465 _, _, _, _, _, _, absorbed = ...
7466 end
7467 if absorbed then
7468 info.absorbRemaining = info.absorbRemaining - absorbed
7469 end
7470 elseif info.maxDamage then
7471 local _, damage
7472 if subEvent == "SWING_DAMAGE" then
7473 damage = ...
7474 elseif subEvent == "RANGE_DAMAGE" or subEvent == "SPELL_DAMAGE" or subEvent == "SPELL_PERIODIC_DAMAGE" then
7475 _, _, _, damage = ...
7476 end
7477 if damage then
7478 info.damageRemaining = info.damageRemaining - damage
7479 end
7480 elseif info.maxHeal then
7481 local _, absorbed
7482 if subEvent == "SPELL_HEAL" or subEvent == "SPELL_PERIODIC_HEAL" then
7483 _, _, _, _, _, absorbed = ...
7484 end
7485 if absorbed then
7486 info.healed = info.healed + absorbed
7487 end
7488 end
7489 end
7490 end)
7491
7492 local function getShieldHPFunc(info)
7493 return function()
7494 return mmax(1, floor(info.absorbRemaining / info.maxAbsorb * 100))
7495 end
7496 end
7497
7498 function bossModPrototype:ShowShieldHealthBar(guid, name, absorb)
7499 self:RemoveShieldHealthBar(guid, name)
7500 if #activeShields == 0 then
7501 frame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
7502 end
7503 local obj = {
7504 mod = self.id,
7505 name = name,
7506 guid = guid,
7507 absorbRemaining = absorb,
7508 maxAbsorb = absorb,
7509 }
7510 obj.func = getShieldHPFunc(obj)
7511 activeShields[#activeShields + 1] = obj
7512 shieldsByGuid[guid] = obj
7513 if DBM.BossHealth:IsShown() then
7514 DBM.BossHealth:AddBoss(obj.func, name)
7515 end
7516 end
7517
7518 function bossModPrototype:RemoveShieldHealthBar(guid, name)
7519 if not guid then
7520 self:RemoveAllSpecialHealthBars()
7521 else
7522 shieldsByGuid[guid] = nil
7523 for i = #activeShields, 1, -1 do
7524 if activeShields[i].guid == guid and activeShields[i].mod == self.id and (not name or activeShields[i].name == name) then
7525 if DBM.BossHealth:IsShown() then
7526 DBM.BossHealth:RemoveBoss(activeShields[i].func)
7527 end
7528 tremove(activeShields, i)
7529 end
7530 end
7531 if #activeShields == 0 then
7532 frame:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
7533 end
7534 end
7535 end
7536
7537 local function getDamagedHPFunc(info)
7538 return function()
7539 return mmax(1, floor(info.damageRemaining / info.maxDamage * 100))
7540 end
7541 end
7542
7543 function bossModPrototype:ShowDamagedHealthBar(guid, name, damage)
7544 self:RemoveDamagedHealthBar(guid, name)
7545 if #activeShields == 0 then
7546 frame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
7547 end
7548 local obj = {
7549 mod = self.id,
7550 name = name,
7551 guid = guid,
7552 damageRemaining = damage,
7553 maxDamage = damage,
7554 }
7555 obj.func = getDamagedHPFunc(obj)
7556 activeShields[#activeShields + 1] = obj
7557 shieldsByGuid[guid] = obj
7558 if DBM.BossHealth:IsShown() then
7559 DBM.BossHealth:AddBoss(obj.func, name)
7560 end
7561 end
7562 bossModPrototype.RemoveDamagedHealthBar = bossModPrototype.RemoveShieldHealthBar
7563
7564 local function getHealedHPFunc(info)
7565 return function()
7566 return mmax(1, floor(info.healed / info.maxHeal * 100))
7567 end
7568 end
7569
7570 function bossModPrototype:ShowAbsorbedHealHealthBar(guid, name, heal)
7571 self:RemoveAbsorbedHealHealthBar(guid, name)
7572 if #activeShields == 0 then
7573 frame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
7574 end
7575 local obj = {
7576 mod = self.id,
7577 name = name,
7578 guid = guid,
7579 healed = 0,
7580 maxHeal = heal,
7581 }
7582 obj.func = getHealedHPFunc(obj)
7583 activeShields[#activeShields + 1] = obj
7584 shieldsByGuid[guid] = obj
7585 if DBM.BossHealth:IsShown() then
7586 DBM.BossHealth:AddBoss(obj.func, name)
7587 end
7588 end
7589 bossModPrototype.RemoveAbsorbedHealHealthBar = bossModPrototype.RemoveShieldHealthBar
7590
7591 -- removes all shield, damaged, healed bars of a boss mod
7592 function bossModPrototype:RemoveAllSpecialHealthBars()
7593 frame:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
7594 for i = #activeShields, 1, -1 do
7595 if activeShields[i].mod == self.id then
7596 if DBM.BossHealth:IsShown() then
7597 DBM.BossHealth:RemoveBoss(activeShields[i].func)
7598 end
7599 shieldsByGuid[activeShields[i].guid] = nil
7600 tremove(activeShields, i)
7601 end
7602 end
7603 end
7604end
7605
7606---------------------
7607-- Class Methods --
7608---------------------
7609do
7610 --[[local specFlags ={
7611 ["Tank"] = true,
7612 ["Dps"] = true,
7613 ["Healer"] = true,
7614 ["Melee"] = true,
7615 ["MeleeDps"] = true,
7616 ["Physical"] = true,
7617 ["Ranged"] = true,
7618 ["RangedDps"] = true,
7619 ["ManaUser"] = true,--Affected by things like mana drains, or mana detonation, etc
7620 ["SpellCaster"] = true,--Has channeled casts, can be interrupted/spell locked by roars, etc
7621 ["RaidCooldown"] = true,
7622 ["RemovePoison"] = true,
7623 ["RemoveDisease"] = true,
7624 ["RemoveEnrage"] = true,--Depricated, no one can remove enrage anymore
7625 ["RemoveCurse"] = true,
7626 ["MagicDispeller"] = true--Buffs on targets, not debuffs on players
7627 ["HasInterrupt"] = true,--Has an interrupt that is 24 seconds or less CD.
7628 }]]
7629
7630 local specRoleTable = {
7631 [62] = { --Arcane Mage
7632 ["Dps"] = true,
7633 ["Ranged"] = true,
7634 ["RangedDps"] = true,
7635 ["ManaUser"] = true,
7636 ["SpellCaster"] = true,
7637 ["MagicDispeller"] = true,
7638 ["HasInterrupt"] = true,
7639 },
7640 [65] = { --Holy Paladin
7641 ["Healer"] = true,
7642 ["Ranged"] = true,
7643 ["ManaUser"] = true,
7644 ["SpellCaster"] = true,
7645 ["RaidCooldown"] = true,--Devotion Aura
7646 ["RemovePoison"] = true,
7647 ["RemoveDisease"] = true,
7648 },
7649 [66] = { --Protection Paladin
7650 ["Tank"] = true,
7651 ["Melee"] = true,
7652 ["ManaUser"] = true,
7653 ["Physical"] = true,
7654 ["RemovePoison"] = true,
7655 ["RemoveDisease"] = true,
7656 ["HasInterrupt"] = true,
7657 },
7658 [70] = { --Retribution Paladin
7659 ["Dps"] = true,
7660 ["Melee"] = true,
7661 ["MeleeDps"] = true,
7662 ["ManaUser"] = true,
7663 ["Physical"] = true,
7664 ["RemovePoison"] = true,
7665 ["RemoveDisease"] = true,
7666 ["HasInterrupt"] = true,
7667 },
7668 [71] = { --Arms Warrior
7669 ["Dps"] = true,
7670 ["Melee"] = true,
7671 ["MeleeDps"] = true,
7672 ["RaidCooldown"] = true,--Rallying Cry
7673 ["Physical"] = true,
7674 ["HasInterrupt"] = true,
7675 },
7676 [73] = { --Protection Warrior
7677 ["Tank"] = true,
7678 ["Melee"] = true,
7679 ["Physical"] = true,
7680 ["HasInterrupt"] = true,
7681 },
7682 [102] = { --Balance Druid
7683 ["Dps"] = true,
7684 ["Ranged"] = true,
7685 ["RangedDps"] = true,
7686 ["ManaUser"] = true,
7687 ["SpellCaster"] = true,
7688 ["RemoveCurse"] = true,
7689 ["RemovePoison"] = true,
7690 },
7691 [103] = { --Feral Druid
7692 ["Dps"] = true,
7693 ["Melee"] = true,
7694 ["MeleeDps"] = true,
7695 ["Physical"] = true,
7696 ["RemoveCurse"] = true,
7697 ["RemovePoison"] = true,
7698 ["HasInterrupt"] = true,
7699 },
7700 [104] = { --Guardian Druid
7701 ["Tank"] = true,
7702 ["Melee"] = true,
7703 ["Physical"] = true,
7704 ["RemoveCurse"] = true,
7705 ["RemovePoison"] = true,
7706 ["HasInterrupt"] = true,
7707 },
7708 [105] = { -- Restoration Druid
7709 ["Healer"] = true,
7710 ["Ranged"] = true,
7711 ["ManaUser"] = true,
7712 ["SpellCaster"] = true,
7713 ["RaidCooldown"] = true,--Tranquility
7714 ["RemoveCurse"] = true,
7715 ["RemovePoison"] = true,
7716 },
7717 [250] = { --Blood DK
7718 ["Tank"] = true,
7719 ["Melee"] = true,
7720 ["Physical"] = true,
7721 ["HasInterrupt"] = true,
7722 },
7723 [251] = { --Frost DK
7724 ["Dps"] = true,
7725 ["Melee"] = true,
7726 ["MeleeDps"] = true,
7727 ["Physical"] = true,
7728 ["HasInterrupt"] = true,
7729 },
7730 [253] = { --Beastmaster Hunter
7731 ["Dps"] = true,
7732 ["Ranged"] = true,
7733 ["RangedDps"] = true,
7734 ["Physical"] = true,
7735 ["HasInterrupt"] = true,
7736 },
7737 [255] = { --Survival Hunter (Legion)
7738 ["Dps"] = true,
7739 ["Melee"] = true,
7740 ["MeleeDps"] = true,
7741 ["Physical"] = true,
7742 ["HasInterrupt"] = true,
7743 },
7744 [256] = { --Discipline Priest
7745 ["Healer"] = true,
7746 ["Ranged"] = true,
7747 ["ManaUser"] = true,
7748 ["SpellCaster"] = true,
7749 ["RaidCooldown"] = true,--Power Word: Barrier(Discipline) / Divine Hymn (Holy)
7750 ["RemoveDisease"] = true,
7751 ["MagicDispeller"] = true,
7752 },
7753 [258] = { --Shadow Priest
7754 ["Dps"] = true,
7755 ["Ranged"] = true,
7756 ["RangedDps"] = true,
7757 ["ManaUser"] = true,
7758 ["SpellCaster"] = true,
7759 ["MagicDispeller"] = true,
7760 },
7761 [259] = { --Assassination Rogue
7762 ["Dps"] = true,
7763 ["Melee"] = true,
7764 ["MeleeDps"] = true,
7765 ["Physical"] = true,
7766 ["HasInterrupt"] = true,
7767 },
7768 [262] = { --Elemental Shaman
7769 ["Dps"] = true,
7770 ["Ranged"] = true,
7771 ["RangedDps"] = true,
7772 ["ManaUser"] = true,
7773 ["SpellCaster"] = true,
7774 ["RemoveCurse"] = true,
7775 ["MagicDispeller"] = true,
7776 ["HasInterrupt"] = true,
7777 },
7778 [263] = { --Enhancement Shaman
7779 ["Dps"] = true,
7780 ["Melee"] = true,
7781 ["MeleeDps"] = true,
7782 ["ManaUser"] = true,
7783 ["SpellCaster"] = true,
7784 ["Physical"] = true,
7785 ["RemoveCurse"] = true,
7786 ["MagicDispeller"] = true,
7787 ["HasInterrupt"] = true,
7788 },
7789 [264] = { --Restoration Shaman
7790 ["Healer"] = true,
7791 ["Ranged"] = true,
7792 ["ManaUser"] = true,
7793 ["SpellCaster"] = true,
7794 ["RaidCooldown"] = true,--Spirit Link Totem
7795 ["RemoveCurse"] = true,
7796 ["MagicDispeller"] = true,
7797 ["HasInterrupt"] = true,
7798 },
7799 [265] = { --Affliction Warlock
7800 ["Dps"] = true,
7801 ["Ranged"] = true,
7802 ["RangedDps"] = true,
7803 ["ManaUser"] = true,
7804 ["SpellCaster"] = true,
7805 },
7806 [268] = { --Brewmaster Monk
7807 ["Tank"] = true,
7808 ["Melee"] = true,
7809 ["Physical"] = true,
7810 ["RemovePoison"] = true,
7811 ["RemoveDisease"] = true,
7812 ["HasInterrupt"] = true,
7813 },
7814 [269] = { --Windwalker Monk
7815 ["Dps"] = true,
7816 ["Melee"] = true,
7817 ["MeleeDps"] = true,
7818 ["Physical"] = true,
7819 ["RemovePoison"] = true,
7820 ["RemoveDisease"] = true,
7821 ["HasInterrupt"] = true,
7822 },
7823 [270] = { --Mistweaver Monk
7824 ["Healer"] = true,
7825 ["Melee"] = true,
7826 ["Ranged"] = true,
7827 ["ManaUser"] = true,
7828 ["SpellCaster"] = true,
7829 ["RaidCooldown"] = true,--Revival
7830 ["RemovePoison"] = true,
7831 ["RemoveDisease"] = true,
7832 },
7833 [577] = { --Havok Demon Hunter
7834 ["Dps"] = true,
7835 ["Melee"] = true,
7836 ["MeleeDps"] = true,
7837 ["Physical"] = true,
7838 ["HasInterrupt"] = true,
7839 },
7840 [581] = { --Vengeance Demon Hunter
7841 ["Tank"] = true,
7842 ["Melee"] = true,
7843 ["Physical"] = true,
7844 ["HasInterrupt"] = true,
7845 },
7846 }
7847 specRoleTable[63] = specRoleTable[62]--Frost Mage
7848 specRoleTable[64] = specRoleTable[62]--Fire Mage
7849 specRoleTable[72] = specRoleTable[71]--Fury Warrior
7850 specRoleTable[252] = specRoleTable[251]--Unholy DK
7851 specRoleTable[254] = specRoleTable[253]--Markmanship Hunter
7852 specRoleTable[257] = specRoleTable[256]--Holy Priest
7853 specRoleTable[260] = specRoleTable[259]--Combat Rogue
7854 specRoleTable[261] = specRoleTable[259]--Subtlety Rogue
7855 specRoleTable[266] = specRoleTable[265]--Demonology Warlock
7856 specRoleTable[267] = specRoleTable[265]--Destruction Warlock
7857
7858 --[[function bossModPrototype:GetRoleFlagValue(flag)
7859 if not flag then return false end
7860 local flags = {strsplit("|", flag)}
7861 for i = 1, #flags do
7862 local flagText = flags[i]
7863 flagText = flagText:gsub("-", "")
7864 if not specFlags[flagText] then
7865 print("bad flag found : "..flagText)
7866 end
7867 end
7868 self:GetRoleFlagValue2(flag)
7869 end]]
7870
7871 --to check flag is correct, remove comment block specFlags table and GetRoleFlagValue function, change this to GetRoleFlagValue2
7872 --disable flag check normally because double flag check comsumes more cpu on mod load.
7873 function bossModPrototype:GetRoleFlagValue(flag)
7874 if not flag then return false end
7875 if not currentSpecID then
7876 DBM:SetCurrentSpecInfo()
7877 end
7878 local flags = {strsplit("|", flag)}
7879 for i = 1, #flags do
7880 local flagText = flags[i]
7881 if flagText:match("^-") then
7882 flagText = flagText:gsub("-", "")
7883 if not specRoleTable[currentSpecID][flagText] then
7884 return true
7885 end
7886 else
7887 if specRoleTable[currentSpecID][flagText] then
7888 return true
7889 end
7890 end
7891 end
7892 return false
7893 end
7894
7895 function bossModPrototype:IsMeleeDps(uId)
7896 if uId then--This version includes ONLY melee dps
7897 local role = UnitGroupRolesAssigned(uId)
7898 if role == "HEALER" or role == "TANK" then--Auto filter healer from dps check
7899 return false
7900 end
7901 local _, class = UnitClass(uId)
7902 if class == "WARRIOR" or class == "ROGUE" or class == "DEATHKNIGHT" then
7903 return true
7904 end
7905 --Inspect throttle exists, so have to do it this way
7906 if class == "DRUID" or class == "SHAMAN" or class == "PALADIN" or class == "MONK" or class == "HUNTER" then
7907 local unitMaxPower = UnitPowerMax(uId)
7908 --Mark and beast have 120 base focus, survival has 100 base focus. Not sure if this is best way to do it or if it breaks with talent/artifact weapon
7909 --Elemental shaman have 100 unit power base, while enhancement have 150 power base, so a shaman with > 150 but less tha 35000 is the melee one
7910 if (unitMaxPower < 101 and class == "HUNTER") or (unitMaxPower >= 150 and class == "SHAMAN" and unitMaxPower < 35000) or unitMaxPower < 35000 then
7911 return true
7912 end
7913 end
7914 return false
7915 end
7916 if not currentSpecID then
7917 DBM:SetCurrentSpecInfo()
7918 end
7919 if specRoleTable[currentSpecID]["MeleeDps"] then
7920 return true
7921 else
7922 return false
7923 end
7924 end
7925
7926 function bossModPrototype:IsMelee(uId)
7927 if uId then--This version includes monk healers as melee and tanks as melee
7928 local _, class = UnitClass(uId)
7929 if class == "WARRIOR" or class == "ROGUE" or class == "DEATHKNIGHT" or class == "MONK" then
7930 return true
7931 end
7932 --Inspect throttle exists, so have to do it this way
7933 if class == "DRUID" or class == "SHAMAN" or class == "PALADIN" then
7934 if UnitPowerMax(uId) < 35000 then
7935 return true
7936 end
7937 end
7938 return false
7939 end
7940 if not currentSpecID then
7941 DBM:SetCurrentSpecInfo()
7942 end
7943 if specRoleTable[currentSpecID]["Melee"] then
7944 return true
7945 else
7946 return false
7947 end
7948 end
7949
7950 function bossModPrototype:IsRanged()
7951 if not currentSpecID then
7952 DBM:SetCurrentSpecInfo()
7953 end
7954 if specRoleTable[currentSpecID]["Ranged"] then
7955 return true
7956 else
7957 return false
7958 end
7959 end
7960
7961 function bossModPrototype:IsSpellCaster()
7962 if not currentSpecID then
7963 DBM:SetCurrentSpecInfo()
7964 end
7965 if specRoleTable[currentSpecID]["SpellCaster"] then
7966 return true
7967 else
7968 return false
7969 end
7970 end
7971
7972 function bossModPrototype:IsMagicDispeller()
7973 if not currentSpecID then
7974 DBM:SetCurrentSpecInfo()
7975 end
7976 if specRoleTable[currentSpecID]["MagicDispeller"] then
7977 return true
7978 else
7979 return false
7980 end
7981 end
7982end
7983
7984function bossModPrototype:IsTank()
7985 --IsTanking already handles external calls, no need here.
7986 if not currentSpecID then
7987 DBM:SetCurrentSpecInfo()
7988 end
7989 local _, _, _, _, role = GetSpecializationInfoByID(currentSpecID)
7990 if role == "TANK" then
7991 return true
7992 else
7993 return false
7994 end
7995end
7996
7997function bossModPrototype:IsDps(uId)
7998 if uId then--External unit call.
7999 if UnitGroupRolesAssigned(uId) == "DAMAGER" then
8000 return true
8001 end
8002 return false
8003 end
8004 if not currentSpecID then
8005 DBM:SetCurrentSpecInfo()
8006 end
8007 local _, _, _, _, role = GetSpecializationInfoByID(currentSpecID)
8008 if role == "DAMAGER" then
8009 return true
8010 else
8011 return false
8012 end
8013end
8014
8015function bossModPrototype:IsHealer(uId)
8016 if uId then--External unit call.
8017 if UnitGroupRolesAssigned(uId) == "HEALER" then
8018 return true
8019 end
8020 return false
8021 end
8022 if not currentSpecID then
8023 DBM:SetCurrentSpecInfo()
8024 end
8025 local _, _, _, _, role = GetSpecializationInfoByID(currentSpecID)
8026 if role == "HEALER" then
8027 return true
8028 else
8029 return false
8030 end
8031end
8032
8033function bossModPrototype:IsTanking(unit, boss)
8034 if not unit then
8035 DBM:Debug("IsTanking passed with invalid unit", 2)
8036 return false
8037 end
8038 --Prefer threat target first
8039 if boss and UnitExists(boss) then--Only checking one bossID as requested
8040 local tanking, status = UnitDetailedThreatSituation(unit, boss)
8041 if tanking or (status == 3) then
8042 return true
8043 end
8044 else--Check all of them if one isn't defined
8045 for i = 1, 5 do
8046 if UnitExists("boss"..i) then
8047 local tanking, status = UnitDetailedThreatSituation(unit, "boss"..i)
8048 if tanking or (status == 3) then
8049 return true
8050 end
8051 end
8052 end
8053 end
8054 --Use these as fallback if threat target not found
8055 if GetPartyAssignment("MAINTANK", unit, 1) then
8056 return true
8057 end
8058 if UnitGroupRolesAssigned(unit) == "TANK" then
8059 return true
8060 end
8061 return false
8062end
8063
8064function bossModPrototype:GetNumAliveTanks()
8065 if not IsInGroup() then return 1 end--Solo raid, you're the "tank"
8066 local count = 0
8067 local uId = (IsInRaid() and "raid") or "party"
8068 for i = 1, DBM:GetNumRealGroupMembers() do
8069 if UnitGroupRolesAssigned(uId..i) == "TANK" and not UnitIsDeadOrGhost(uId..i) then
8070 count = count + 1
8071 end
8072 end
8073 return count
8074end
8075
8076----------------------------
8077-- Boss Health Function --
8078----------------------------
8079function DBM:GetBossHP(cId)
8080 local uId = bossHealthuIdCache[cId] or "target"
8081 if self:GetCIDFromGUID(UnitGUID(uId)) == cId and UnitHealthMax(uId) ~= 0 then
8082 if bossHealth[cId] and (UnitHealth(uId) == 0 and not UnitIsDead(uId)) then return bossHealth[cId], uId, UnitName(uId) end--Return last non 0 value if value is 0, since it's last valid value we had.
8083 local hp = UnitHealth(uId) / UnitHealthMax(uId) * 100
8084 bossHealth[cId] = hp
8085 return hp, uId, UnitName(uId)
8086 elseif self:GetCIDFromGUID(UnitGUID("focus")) == cId and UnitHealthMax("focus") ~= 0 then
8087 if bossHealth[cId] and (UnitHealth("focus") == 0 and not UnitIsDead("focus")) then return bossHealth[cId], "focus", UnitName("focus") end--Return last non 0 value if value is 0, since it's last valid value we had.
8088 local hp = UnitHealth("focus") / UnitHealthMax("focus") * 100
8089 bossHealth[cId] = hp
8090 return hp, "focus", UnitName("focus")
8091 else
8092 for i = 1, 5 do
8093 local guid = UnitGUID("boss"..i)
8094 if self:GetCIDFromGUID(guid) == cId and UnitHealthMax("boss"..i) ~= 0 then
8095 if bossHealth[cId] and (UnitHealth("boss"..i) == 0 and not UnitIsDead("boss"..i)) then return bossHealth[cId], "boss"..i, UnitName("boss"..i) end--Return last non 0 value if value is 0, since it's last valid value we had.
8096 local hp = UnitHealth("boss"..i) / UnitHealthMax("boss"..i) * 100
8097 bossHealth[cId] = hp
8098 bossHealthuIdCache[cId] = "boss"..i
8099 return hp, "boss"..i, UnitName("boss"..i)
8100 end
8101 end
8102 local idType = (IsInRaid() and "raid") or "party"
8103 for i = 0, GetNumGroupMembers() do
8104 local unitId = ((i == 0) and "target") or idType..i.."target"
8105 local guid = UnitGUID(unitId)
8106 if self:GetCIDFromGUID(guid) == cId and UnitHealthMax(unitId) ~= 0 then
8107 if bossHealth[cId] and (UnitHealth(unitId) == 0 and not UnitIsDead(unitId)) then return bossHealth[cId], unitId, UnitName(unitId) end--Return last non 0 value if value is 0, since it's last valid value we had.
8108 local hp = UnitHealth(unitId) / UnitHealthMax(unitId) * 100
8109 bossHealth[cId] = hp
8110 bossHealthuIdCache[cId] = unitId
8111 return hp, unitId, UnitName(unitId)
8112 end
8113 end
8114 end
8115 return nil
8116end
8117
8118function DBM:GetBossHPByGUID(guid)
8119 local uId = bossHealthuIdCache[guid] or "target"
8120 if UnitGUID(uId) == guid and UnitHealthMax(uId) ~= 0 then
8121 if bossHealth[guid] and (UnitHealth(uId) == 0 and not UnitIsDead(uId)) then return bossHealth[guid], uId, UnitName(uId) end--Return last non 0 value if value is 0, since it's last valid value we had.
8122 local hp = UnitHealth(uId) / UnitHealthMax(uId) * 100
8123 bossHealth[guid] = hp
8124 return hp, uId, UnitName(uId)
8125 elseif UnitGUID("focus") == guid and UnitHealthMax("focus") ~= 0 then
8126 if bossHealth[guid] and (UnitHealth("focus") == 0 and not UnitIsDead("focus")) then return bossHealth[guid], "focus", UnitName("focus") end--Return last non 0 value if value is 0, since it's last valid value we had.
8127 local hp = UnitHealth("focus") / UnitHealthMax("focus") * 100
8128 bossHealth[guid] = hp
8129 return hp, "focus", UnitName("focus")
8130 else
8131 for i = 1, 5 do
8132 local guid2 = UnitGUID("boss"..i)
8133 if guid == guid2 and UnitHealthMax("boss"..i) ~= 0 then
8134 if bossHealth[guid] and (UnitHealth("boss"..i) == 0 and not UnitIsDead("boss"..i)) then return bossHealth[guid], "boss"..i, UnitName("boss"..i) end--Return last non 0 value if value is 0, since it's last valid value we had.
8135 local hp = UnitHealth("boss"..i) / UnitHealthMax("boss"..i) * 100
8136 bossHealth[guid] = hp
8137 bossHealthuIdCache[guid] = "boss"..i
8138 return hp, "boss"..i, UnitName("boss"..i)
8139 end
8140 end
8141 local idType = (IsInRaid() and "raid") or "party"
8142 for i = 0, GetNumGroupMembers() do
8143 local unitId = ((i == 0) and "target") or idType..i.."target"
8144 local guid2 = UnitGUID(unitId)
8145 if guid == guid2 and UnitHealthMax(unitId) ~= 0 then
8146 if bossHealth[guid] and (UnitHealth(unitId) == 0 and not UnitIsDead(unitId)) then return bossHealth[guid], unitId, UnitName(unitId) end--Return last non 0 value if value is 0, since it's last valid value we had.
8147 local hp = UnitHealth(unitId) / UnitHealthMax(unitId) * 100
8148 bossHealth[guid] = hp
8149 bossHealthuIdCache[guid] = unitId
8150 return hp, unitId, UnitName(unitId)
8151 end
8152 end
8153 end
8154 return nil
8155end
8156
8157function DBM:GetBossHPByUnitID(uId)
8158 if UnitHealthMax(uId) ~= 0 then
8159 local hp = UnitHealth(uId) / UnitHealthMax(uId) * 100
8160 bossHealth[uId] = hp
8161 return hp, uId, UnitName(uId)
8162 end
8163 return nil
8164end
8165
8166function bossModPrototype:SetBossHealthInfo(...)
8167 self.bossHealthInfo = {...}
8168end
8169
8170function bossModPrototype:SetMainBossID(cid)
8171 self.mainBoss = cid
8172end
8173
8174function bossModPrototype:SetBossHPInfoToHighest(numBoss)
8175 if numBoss ~= false then
8176 self.numBoss = numBoss or (self.multiMobPullDetection and #self.multiMobPullDetection)
8177 end
8178 self.highesthealth = true
8179end
8180
8181function bossModPrototype:GetHighestBossHealth()
8182 local hp
8183 if not self.multiMobPullDetection or self.mainBoss then
8184 hp = bossHealth[self.mainBoss or self.combatInfo.mob or -1]
8185 if hp and (hp > 100 or hp <= 0) then
8186 hp = nil
8187 end
8188 else
8189 for _, mob in ipairs(self.multiMobPullDetection) do
8190 if (bossHealth[mob] or 0) > (hp or 0) and (bossHealth[mob] or 0) < 100 then-- ignore full health.
8191 hp = bossHealth[mob]
8192 end
8193 end
8194 end
8195 return hp
8196end
8197
8198function bossModPrototype:GetLowestBossHealth()
8199 local hp
8200 if not self.multiMobPullDetection or self.mainBoss then
8201 hp = bossHealth[self.mainBoss or self.combatInfo.mob or -1]
8202 if hp and (hp > 100 or hp <= 0) then
8203 hp = nil
8204 end
8205 else
8206 for _, mob in ipairs(self.multiMobPullDetection) do
8207 if (bossHealth[mob] or 100) < (hp or 100) and (bossHealth[mob] or 100) > 0 then-- ignore zero health.
8208 hp = bossHealth[mob]
8209 end
8210 end
8211 end
8212 return hp
8213end
8214
8215bossModPrototype.GetBossHP = DBM.GetBossHP
8216bossModPrototype.GetBossHPByGUID = DBM.GetBossHPByGUID
8217
8218-----------------------
8219-- Announce Object --
8220-----------------------
8221do
8222 local frame = CreateFrame("Frame", "DBMWarning", UIParent)
8223 local font1u = CreateFrame("Frame", "DBMWarning1Updater", UIParent)
8224 local font2u = CreateFrame("Frame", "DBMWarning2Updater", UIParent)
8225 local font3u = CreateFrame("Frame", "DBMWarning3Updater", UIParent)
8226 local font1 = frame:CreateFontString("DBMWarning1", "OVERLAY", "GameFontNormal")
8227 font1:SetWidth(1024)
8228 font1:SetHeight(0)
8229 font1:SetPoint("TOP", 0, 0)
8230 local font2 = frame:CreateFontString("DBMWarning2", "OVERLAY", "GameFontNormal")
8231 font2:SetWidth(1024)
8232 font2:SetHeight(0)
8233 font2:SetPoint("TOP", font1, "BOTTOM", 0, 0)
8234 local font3 = frame:CreateFontString("DBMWarning3", "OVERLAY", "GameFontNormal")
8235 font3:SetWidth(1024)
8236 font3:SetHeight(0)
8237 font3:SetPoint("TOP", font2, "BOTTOM", 0, 0)
8238 frame:SetMovable(1)
8239 frame:SetWidth(1)
8240 frame:SetHeight(1)
8241 frame:SetFrameStrata("HIGH")
8242 frame:SetClampedToScreen()
8243 frame:SetPoint("CENTER", UIParent, "CENTER", 0, 300)
8244 font1u:Hide()
8245 font2u:Hide()
8246 font3u:Hide()
8247
8248 local font1elapsed, font2elapsed, font3elapsed, moving
8249
8250 local function fontHide1()
8251 local duration = DBM.Options.WarningDuration
8252 if font1elapsed > duration * 1.3 then
8253 font1u:Hide()
8254 font1:Hide()
8255 if frame.font1ticker then
8256 frame.font1ticker:Cancel()
8257 frame.font1ticker = nil
8258 end
8259 elseif font1elapsed > duration then
8260 font1elapsed = font1elapsed + 0.05
8261 local alpha = 1 - (font1elapsed - duration) / (duration * 0.3)
8262 font1:SetAlpha(alpha > 0 and alpha or 0)
8263 else
8264 font1elapsed = font1elapsed + 0.05
8265 font1:SetAlpha(1)
8266 end
8267 end
8268
8269 local function fontHide2()
8270 local duration = DBM.Options.WarningDuration
8271 if font2elapsed > duration * 1.3 then
8272 font2u:Hide()
8273 font2:Hide()
8274 if frame.font2ticker then
8275 frame.font2ticker:Cancel()
8276 frame.font2ticker = nil
8277 end
8278 elseif font2elapsed > duration then
8279 font2elapsed = font2elapsed + 0.05
8280 local alpha = 1 - (font2elapsed - duration) / (duration * 0.3)
8281 font2:SetAlpha(alpha > 0 and alpha or 0)
8282 else
8283 font2elapsed = font2elapsed + 0.05
8284 font2:SetAlpha(1)
8285 end
8286 end
8287
8288 local function fontHide3()
8289 local duration = DBM.Options.WarningDuration
8290 if font3elapsed > duration * 1.3 then
8291 font3u:Hide()
8292 font3:Hide()
8293 if frame.font3ticker then
8294 frame.font3ticker:Cancel()
8295 frame.font3ticker = nil
8296 end
8297 elseif font3elapsed > duration then
8298 font3elapsed = font3elapsed + 0.05
8299 local alpha = 1 - (font3elapsed - duration) / (duration * 0.3)
8300 font3:SetAlpha(alpha > 0 and alpha or 0)
8301 else
8302 font3elapsed = font3elapsed + 0.05
8303 font3:SetAlpha(1)
8304 end
8305 end
8306
8307 font1u:SetScript("OnUpdate", function(self)
8308 local diff = GetTime() - font1.lastUpdate
8309 local origSize = DBM.Options.WarningFontSize
8310 if diff > 0.4 then
8311 font1:SetTextHeight(origSize)
8312 self:Hide()
8313 elseif diff > 0.2 then
8314 font1:SetTextHeight(origSize * (1.5 - (diff-0.2) * 2.5))
8315 else
8316 font1:SetTextHeight(origSize * (1 + diff * 2.5))
8317 end
8318 end)
8319
8320 font2u:SetScript("OnUpdate", function(self)
8321 local diff = GetTime() - font2.lastUpdate
8322 local origSize = DBM.Options.WarningFontSize
8323 if diff > 0.4 then
8324 font2:SetTextHeight(origSize)
8325 self:Hide()
8326 elseif diff > 0.2 then
8327 font2:SetTextHeight(origSize * (1.5 - (diff-0.2) * 2.5))
8328 else
8329 font2:SetTextHeight(origSize * (1 + diff * 2.5))
8330 end
8331 end)
8332
8333 font3u:SetScript("OnUpdate", function(self)
8334 local diff = GetTime() - font3.lastUpdate
8335 local origSize = DBM.Options.WarningFontSize
8336 if diff > 0.4 then
8337 font3:SetTextHeight(origSize)
8338 self:Hide()
8339 elseif diff > 0.2 then
8340 font3:SetTextHeight(origSize * (1.5 - (diff-0.2) * 2.5))
8341 else
8342 font3:SetTextHeight(origSize * (1 + diff * 2.5))
8343 end
8344 end)
8345
8346 function DBM:UpdateWarningOptions()
8347 frame:ClearAllPoints()
8348 frame:SetPoint(self.Options.WarningPoint, UIParent, self.Options.WarningPoint, self.Options.WarningX, self.Options.WarningY)
8349 font1:SetFont(self.Options.WarningFont, self.Options.WarningFontSize, self.Options.WarningFontStyle == "None" and nil or self.Options.WarningFontStyle)
8350 font2:SetFont(self.Options.WarningFont, self.Options.WarningFontSize, self.Options.WarningFontStyle == "None" and nil or self.Options.WarningFontStyle)
8351 font3:SetFont(self.Options.WarningFont, self.Options.WarningFontSize, self.Options.WarningFontStyle == "None" and nil or self.Options.WarningFontStyle)
8352 if self.Options.WarningFontShadow then
8353 font1:SetShadowOffset(1, -1)
8354 font2:SetShadowOffset(1, -1)
8355 font3:SetShadowOffset(1, -1)
8356 else
8357 font1:SetShadowOffset(0, 0)
8358 font2:SetShadowOffset(0, 0)
8359 font3:SetShadowOffset(0, 0)
8360 end
8361 end
8362
8363 function DBM:AddWarning(text, force)
8364 local added = false
8365 if not frame.font1ticker then
8366 font1elapsed = 0
8367 font1.lastUpdate = GetTime()
8368 font1:SetText(text)
8369 font1:Show()
8370 font1u:Show()
8371 added = true
8372 frame.font1ticker = frame.font1ticker or C_TimerNewTicker(0.05, fontHide1)
8373 elseif not frame.font2ticker then
8374 font2elapsed = 0
8375 font2.lastUpdate = GetTime()
8376 font2:SetText(text)
8377 font2:Show()
8378 font2u:Show()
8379 added = true
8380 frame.font2ticker = frame.font2ticker or C_TimerNewTicker(0.05, fontHide2)
8381 elseif not frame.font3ticker or force then
8382 font3elapsed = 0
8383 font3.lastUpdate = GetTime()
8384 font3:SetText(text)
8385 font3:Show()
8386 font3u:Show()
8387 fontHide3()
8388 added = true
8389 frame.font3ticker = frame.font3ticker or C_TimerNewTicker(0.05, fontHide3)
8390 end
8391 if not added then
8392 local prevText1 = font2:GetText()
8393 local prevText2 = font3:GetText()
8394 font1:SetText(prevText1)
8395 font1elapsed = font2elapsed
8396 font2:SetText(prevText2)
8397 font2elapsed = font3elapsed
8398 self:AddWarning(text, true)
8399 end
8400 end
8401
8402 do
8403 local anchorFrame
8404 local function moveEnd(self)
8405 moving = false
8406 anchorFrame:Hide()
8407 if anchorFrame.ticker then
8408 anchorFrame.ticker:Cancel()
8409 anchorFrame.ticker = nil
8410 end
8411 font1elapsed = self.Options.WarningDuration
8412 font2elapsed = self.Options.WarningDuration
8413 font3elapsed = self.Options.WarningDuration
8414 frame:SetFrameStrata("HIGH")
8415 self:Unschedule(moveEnd)
8416 self.Bars:CancelBar(DBM_CORE_MOVE_WARNING_BAR)
8417 end
8418
8419 function DBM:MoveWarning()
8420 if not anchorFrame then
8421 anchorFrame = CreateFrame("Frame", nil, frame)
8422 anchorFrame:SetWidth(32)
8423 anchorFrame:SetHeight(32)
8424 anchorFrame:EnableMouse(true)
8425 anchorFrame:SetPoint("TOP", frame, "TOP", 0, 32)
8426 anchorFrame:RegisterForDrag("LeftButton")
8427 anchorFrame:SetClampedToScreen()
8428 anchorFrame:Hide()
8429 local texture = anchorFrame:CreateTexture()
8430 texture:SetTexture("Interface\\Addons\\DBM-GUI\\textures\\dot.blp")
8431 texture:SetPoint("CENTER", anchorFrame, "CENTER", 0, 0)
8432 texture:SetWidth(32)
8433 texture:SetHeight(32)
8434 anchorFrame:SetScript("OnDragStart", function()
8435 frame:StartMoving()
8436 self:Unschedule(moveEnd)
8437 self.Bars:CancelBar(DBM_CORE_MOVE_WARNING_BAR)
8438 end)
8439 anchorFrame:SetScript("OnDragStop", function()
8440 frame:StopMovingOrSizing()
8441 local point, _, _, xOfs, yOfs = frame:GetPoint(1)
8442 self.Options.WarningPoint = point
8443 self.Options.WarningX = xOfs
8444 self.Options.WarningY = yOfs
8445 self:Schedule(15, moveEnd, self)
8446 self.Bars:CreateBar(15, DBM_CORE_MOVE_WARNING_BAR)
8447 end)
8448 end
8449 if anchorFrame:IsShown() then
8450 moveEnd()
8451 else
8452 moving = true
8453 anchorFrame:Show()
8454 anchorFrame.ticker = anchorFrame.ticker or C_TimerNewTicker(5, function() self:AddWarning(DBM_CORE_MOVE_WARNING_MESSAGE) end)
8455 self:AddWarning(DBM_CORE_MOVE_WARNING_MESSAGE)
8456 self:Schedule(15, moveEnd, self)
8457 self.Bars:CreateBar(15, DBM_CORE_MOVE_WARNING_BAR)
8458 frame:Show()
8459 frame:SetFrameStrata("TOOLTIP")
8460 frame:SetAlpha(1)
8461 end
8462 end
8463 end
8464
8465 local textureCode = " |T%s:12:12|t "
8466 local textureExp = " |T(%S+......%S+):12:12|t "--Fix texture file including blank not strips(example: Interface\\Icons\\Spell_Frost_Ring of Frost). But this have limitations. Since I'm poor at regular expressions, this is not good fix. Do you have another good regular expression, tandanu?
8467 local announcePrototype = {}
8468 local mt = {__index = announcePrototype}
8469
8470 -- TODO: is there a good reason that this is a weak table?
8471 local cachedColorFunctions = setmetatable({}, {__mode = "kv"})
8472
8473 -- TODO: this function is an abomination, it needs to be rewritten. Also: check if these work-arounds are still necessary
8474 function announcePrototype:Show(...) -- todo: reduce amount of unneeded strings
8475 if not self.option or self.mod.Options[self.option] then
8476 if DBM.Options.DontShowBossAnnounces then return end -- don't show the announces if the spam filter option is set
8477 local argTable = {...}
8478 local colorCode = ("|cff%.2x%.2x%.2x"):format(self.color.r * 255, self.color.g * 255, self.color.b * 255)
8479 if #self.combinedtext > 0 then
8480 --Throttle spam.
8481 if DBM.Options.WarningAlphabetical then
8482 tsort(self.combinedtext)
8483 end
8484 local combinedText = tconcat(self.combinedtext, "<, >")
8485 if self.combinedcount == 1 then
8486 combinedText = combinedText.." "..DBM_CORE_GENERIC_WARNING_OTHERS
8487 elseif self.combinedcount > 1 then
8488 combinedText = combinedText.." "..DBM_CORE_GENERIC_WARNING_OTHERS2:format(self.combinedcount)
8489 end
8490 --Process
8491 for i = 1, #argTable do
8492 if type(argTable[i]) == "string" then
8493 argTable[i] = combinedText
8494 end
8495 end
8496 end
8497 local message = pformat(self.text, unpack(argTable))
8498 local text = ("%s%s%s|r%s"):format(
8499 (DBM.Options.WarningIconLeft and self.icon and textureCode:format(self.icon)) or "",
8500 colorCode,
8501 message,
8502 (DBM.Options.WarningIconRight and self.icon and textureCode:format(self.icon)) or ""
8503 )
8504 self.combinedcount = 0
8505 self.combinedtext = {}
8506 if not cachedColorFunctions[self.color] then
8507 local color = self.color -- upvalue for the function to colorize names, accessing self in the colorize closure is not safe as the color of the announce object might change (it would also prevent the announce from being garbage-collected but announce objects are never destroyed)
8508 cachedColorFunctions[color] = function(cap)
8509 cap = cap:sub(2, -2)
8510 local noStrip = cap:match("noStrip ")
8511 if not noStrip then
8512 local name = cap
8513 if DBM.Options.StripServerName then
8514 cap = Ambiguate(cap, "short")
8515 end
8516 local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(name)] or color
8517 if playerColor then
8518 cap = ("|r|cff%.2x%.2x%.2x%s|r|cff%.2x%.2x%.2x"):format(playerColor.r * 255, playerColor.g * 255, playerColor.b * 255, cap, color.r * 255, color.g * 255, color.b * 255)
8519 end
8520 else
8521 cap = cap:sub(9)
8522 end
8523 return cap
8524 end
8525 end
8526 text = text:gsub(">.-<", cachedColorFunctions[self.color])
8527 DBM:AddWarning(text)
8528 if DBM.Options.ShowWarningsInChat then
8529 if not DBM.Options.WarningIconChat then
8530 text = text:gsub(textureExp, "") -- textures @ chat frame can (and will) distort the font if using certain combinations of UI scale, resolution and font size TODO: is this still true as of cataclysm?
8531 end
8532 self.mod:AddMsg(text, nil)
8533 end
8534 if self.sound then
8535 DBM:PlaySoundFile(DBM.Options.RaidWarningSound)
8536 end
8537 --Message: Full message text
8538 --Icon: Texture path for icon
8539 --Type: Announce type
8540 --SpellId: Raw spell or encounter journal Id if available.
8541 --Mod ID: Encounter ID as string, or a generic string for mods that don't have encounter ID (such as trash, dummy/test mods)
8542 fireEvent("DBM_Announce", message, self.icon, self.type, self.spellId, self.mod.id)
8543 else
8544 self.combinedcount = 0
8545 self.combinedtext = {}
8546 end
8547 end
8548
8549 function announcePrototype:CombinedShow(delay, ...)
8550 local argTable = {...}
8551 for i = 1, #argTable do
8552 if type(argTable[i]) == "string" then
8553 if #self.combinedtext < 8 then--Throttle spam. We may not need more than 9 targets..
8554 if not checkEntry(self.combinedtext, argTable[i]) then
8555 self.combinedtext[#self.combinedtext + 1] = argTable[i]
8556 end
8557 else
8558 self.combinedcount = self.combinedcount + 1
8559 end
8560 end
8561 end
8562 unschedule(self.Show, self.mod, self)
8563 schedule(delay or 0.5, self.Show, self.mod, self, ...)
8564 end
8565
8566 function announcePrototype:Schedule(t, ...)
8567 return schedule(t, self.Show, self.mod, self, ...)
8568 end
8569
8570 function announcePrototype:Cancel(...)
8571 return unschedule(self.Show, self.mod, self, ...)
8572 end
8573
8574 -- old constructor (no auto-localize)
8575 function bossModPrototype:NewAnnounce(text, color, icon, optionDefault, optionName, noSound)
8576 if not text then
8577 error("NewAnnounce: you must provide announce text", 2)
8578 return
8579 end
8580 local obj = setmetatable(
8581 {
8582 text = self.localization.warnings[text],
8583 combinedtext = {},
8584 combinedcount = 0,
8585 color = DBM.Options.WarningColors[color or 1] or DBM.Options.WarningColors[1],
8586 sound = not noSound,
8587 mod = self,
8588 icon = (type(icon) == "string" and icon:match("ej%d+") and select(4, EJ_GetSectionInfo(string.sub(icon, 3))) ~= "" and select(4, EJ_GetSectionInfo(string.sub(icon, 3)))) or (type(icon) == "number" and GetSpellTexture(icon)) or icon or "Interface\\Icons\\Spell_Nature_WispSplode",
8589 },
8590 mt
8591 )
8592 if optionName then
8593 obj.option = optionName
8594 self:AddBoolOption(obj.option, optionDefault, "announce")
8595 elseif not (optionName == false) then
8596 obj.option = text
8597 self:AddBoolOption(obj.option, optionDefault, "announce")
8598 end
8599 tinsert(self.announces, obj)
8600 return obj
8601 end
8602
8603 -- new constructor (auto-localized warnings and options, yay!)
8604 local function newAnnounce(self, announceType, spellId, color, icon, optionDefault, optionName, castTime, preWarnTime, noSound)
8605 if not spellId then
8606 error("newAnnounce: you must provide spellId", 2)
8607 return
8608 end
8609 local optionVersion
8610 if type(optionName) == "number" then
8611 optionVersion = optionName
8612 optionName = nil
8613 end
8614 if type(spellId) == "string" and spellId:match("OptionVersion") then
8615 print("newAnnounce for "..color.." is using OptionVersion hack. this is depricated")
8616 return
8617 end
8618 local unparsedId = spellId
8619 local spellName
8620 if type(spellId) == "string" and spellId:match("ej%d+") then
8621 spellId = string.sub(spellId, 3)
8622 spellName = EJ_GetSectionInfo(spellId) or DBM_CORE_UNKNOWN
8623 else
8624 spellName = GetSpellInfo(spellId) or DBM_CORE_UNKNOWN
8625 end
8626 icon = icon or unparsedId
8627 local text
8628 if announceType == "cast" then
8629 local spellHaste = select(4, GetSpellInfo(53142)) / 10000 -- 53142 = Dalaran Portal, should have 10000 ms cast time
8630 local timer = (select(4, GetSpellInfo(spellId)) or 1000) / spellHaste
8631 text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName, castTime or (timer / 1000))
8632 elseif announceType == "prewarn" then
8633 if type(preWarnTime) == "string" then
8634 text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName, preWarnTime)
8635 else
8636 text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName, DBM_CORE_SEC_FMT:format(tostring(preWarnTime or 5)))
8637 end
8638 elseif announceType == "phase" or announceType == "prephase" then
8639 text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(tostring(spellId))
8640 elseif announceType == "phasechange" then
8641 text = DBM_CORE_AUTO_ANNOUNCE_TEXTS.spell
8642 else
8643 text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName)
8644 end
8645 local obj = setmetatable( -- todo: fix duplicate code
8646 {
8647 text = text,
8648 combinedtext = {},
8649 combinedcount = 0,
8650 announceType = announceType,
8651 color = DBM.Options.WarningColors[color or 1] or DBM.Options.WarningColors[1],
8652 mod = self,
8653 icon = (type(icon) == "string" and icon:match("ej%d+") and select(4, EJ_GetSectionInfo(string.sub(icon, 3))) ~= "" and select(4, EJ_GetSectionInfo(string.sub(icon, 3)))) or (type(icon) == "number" and GetSpellTexture(icon)) or icon or "Interface\\Icons\\Spell_Nature_WispSplode",
8654 sound = not noSound,
8655 type = announceType,
8656 spellId = unparsedId,
8657 },
8658 mt
8659 )
8660 local catType = "announce"--Default to General announce
8661 --Change if Personal or Other
8662 if announceType == "target" or announceType == "targetcount" or announceType == "stack" then
8663 catType = "announceother"
8664 end
8665 if optionName then
8666 obj.option = optionName
8667 self:AddBoolOption(obj.option, optionDefault, catType)
8668 elseif not (optionName == false) then
8669 obj.option = catType..unparsedId..announceType..(optionVersion or "")
8670 self:AddBoolOption(obj.option, optionDefault, catType)
8671 self.localization.options[obj.option] = DBM_CORE_AUTO_ANNOUNCE_OPTIONS[announceType]:format(unparsedId)
8672 end
8673 tinsert(self.announces, obj)
8674 return obj
8675 end
8676
8677 function bossModPrototype:NewYouAnnounce(spellId, color, ...)
8678 return newAnnounce(self, "you", spellId, color or 1, ...)
8679 end
8680
8681 function bossModPrototype:NewTargetAnnounce(spellId, color, ...)
8682 return newAnnounce(self, "target", spellId, color or 3, ...)
8683 end
8684
8685 function bossModPrototype:NewTargetCountAnnounce(spellId, color, ...)
8686 return newAnnounce(self, "targetcount", spellId, color or 3, ...)
8687 end
8688
8689 function bossModPrototype:NewSpellAnnounce(spellId, color, ...)
8690 return newAnnounce(self, "spell", spellId, color or 2, ...)
8691 end
8692
8693 function bossModPrototype:NewEndAnnounce(spellId, color, ...)
8694 return newAnnounce(self, "ends", spellId, color or 2, ...)
8695 end
8696
8697 function bossModPrototype:NewEndTargetAnnounce(spellId, color, ...)
8698 return newAnnounce(self, "endtarget", spellId, color or 2, ...)
8699 end
8700
8701 function bossModPrototype:NewFadesAnnounce(spellId, color, ...)
8702 return newAnnounce(self, "fades", spellId, color or 2, ...)
8703 end
8704
8705 function bossModPrototype:NewAddsLeftAnnounce(spellId, color, ...)
8706 return newAnnounce(self, "adds", spellId, color or 3, ...)
8707 end
8708
8709 function bossModPrototype:NewCountAnnounce(spellId, color, ...)
8710 return newAnnounce(self, "count", spellId, color or 2, ...)
8711 end
8712
8713 function bossModPrototype:NewStackAnnounce(spellId, color, ...)
8714 return newAnnounce(self, "stack", spellId, color or 2, ...)
8715 end
8716
8717 function bossModPrototype:NewCastAnnounce(spellId, color, castTime, icon, optionDefault, optionName, noArg, noSound)
8718 local optionVersion
8719 if type(optionName) == "number" then
8720 optionVersion = optionName
8721 optionName = nil
8722 end
8723 if type(spellId) == "string" and spellId:match("OptionVersion") then--LEGACY hack, remove when new DBM core and other mods released
8724 print("NewCastAnnounce is using OptionVersion and this is depricated for "..color)
8725 return
8726 end
8727 return newAnnounce(self, "cast", spellId, color or 3, icon, optionDefault, optionName, castTime, nil, noSound)
8728 end
8729
8730 function bossModPrototype:NewSoonAnnounce(spellId, color, ...)
8731 return newAnnounce(self, "soon", spellId, color or 1, ...)
8732 end
8733
8734 function bossModPrototype:NewPreWarnAnnounce(spellId, time, color, icon, optionDefault, optionName, noArg, noSound)
8735 local optionVersion
8736 if type(optionName) == "number" then
8737 optionVersion = optionName
8738 optionName = nil
8739 end
8740 if type(spellId) == "string" and spellId:match("OptionVersion") then--LEGACY hack, remove when new DBM core and other mods released
8741 print("NewCastAnnounce is using OptionVersion and this is depricated for "..color)
8742 return
8743 end
8744 return newAnnounce(self, "prewarn", spellId, color or 1, icon, optionDefault, optionName, nil, time, noSound)
8745 end
8746
8747 function bossModPrototype:NewPhaseAnnounce(phase, color, icon, ...)
8748 return newAnnounce(self, "phase", phase, color or 1, icon or "Interface\\Icons\\Spell_Nature_WispSplode", ...)
8749 end
8750
8751 function bossModPrototype:NewPhaseChangeAnnounce(color, icon, ...)
8752 return newAnnounce(self, "phasechange", 0, color or 1, icon or "Interface\\Icons\\Spell_Nature_WispSplode", ...)
8753 end
8754
8755 function bossModPrototype:NewPrePhaseAnnounce(phase, color, icon, ...)
8756 return newAnnounce(self, "prephase", phase, color or 1, icon or "Interface\\Icons\\Spell_Nature_WispSplode", ...)
8757 end
8758end
8759
8760--------------------
8761-- Sound Object --
8762--------------------
8763do
8764 --Sound Object (basically only used by countdown now)
8765 local soundPrototype = {}
8766 local mt = { __index = soundPrototype }
8767 function bossModPrototype:NewSound(spellId, optionDefault, optionName)
8768 if not (optionName == false) then--Basically, all mods still using NewSound.
8769 -- Because there are going to be users who update core and not old mods, we need to check and alert
8770 --I'll try to avoid this as much as possible by removing NewSound from all old mods in advance of dbm core update
8771 print("Error, NewSound depricated. Update your old DBM mods to remove this error")
8772 return
8773 end
8774 self.numSounds = self.numSounds and self.numSounds + 1 or 1
8775 local obj = setmetatable(
8776 {
8777 mod = self,
8778 },
8779 mt
8780 )
8781 return obj
8782 end
8783
8784 function soundPrototype:Play(file)
8785 DBM:PlaySoundFile(file)
8786 end
8787
8788 function soundPrototype:Schedule(t, ...)
8789 return schedule(t, self.Play, self.mod, self, ...)
8790 end
8791
8792 function soundPrototype:Cancel(...)
8793 return unschedule(self.Play, self.mod, self, ...)
8794 end
8795
8796 --Voice Object
8797 --Individual options still generated in case a person likes to enable voice, but not for ALL warnings (they can pick and choose what is enabled/disabled"
8798 local soundPrototype2 = {}
8799 local mt = { __index = soundPrototype2 }
8800 function bossModPrototype:NewVoice(spellId, optionDefault, optionName, optionVersion)
8801 if not spellId and not optionName then
8802 print("NewVoice: you must provide either spellId or optionName", 2)
8803 return
8804 end
8805 if type(spellId) == "string" and spellId:match("OptionVersion") then
8806 print("NewVoice for "..optionDefault.." is using OptionVersion hack. this is not needed, this only has 4 args, do this properly")
8807 return
8808 end
8809 self.numSounds = self.numSounds and self.numSounds + 1 or 1
8810 local obj = setmetatable(
8811 {
8812 mod = self,
8813 },
8814 mt
8815 )
8816 if #DBM.Voices < 2 then optionName = false end--Hide options if no voice packs are installed
8817 if optionName then
8818 obj.option = optionName
8819 self:AddBoolOption(obj.option, optionDefault, "sound")
8820 elseif not (optionName == false) then
8821 obj.option = "Voice"..spellId..(optionVersion or "")
8822 self:AddBoolOption(obj.option, optionDefault, "sound")
8823 self.localization.options[obj.option] = DBM_CORE_AUTO_VOICE_OPTION_TEXT:format(spellId)
8824 end
8825 return obj
8826 end
8827
8828 --If no file at path, it should silenty fail. However, I want to try to only add NewVoice to mods for files that already exist.
8829 function soundPrototype2:Play(name, customPath)
8830 local voice = DBM.Options.ChosenVoicePack
8831 local always = DBM.Options.AlwaysPlayVoice
8832 if voice == "None" then return end
8833 --Filter tank specific voice alerts for non tanks if tank filter enabled
8834 --But still allow AlwaysPlayVoice to play as well.
8835 if (name == "changemt" or name == "tauntboss") and DBM.Options.FilterTankSpec and not self.mod:IsTank() and not always then return end
8836 if not self.option or self.mod.Options[self.option] or always then
8837 local path = customPath or "Interface\\AddOns\\DBM-VP"..voice.."\\"..name..".ogg"
8838 DBM:PlaySoundFile(path)
8839 end
8840 end
8841
8842 function soundPrototype2:Schedule(t, ...)
8843 if DBM.Options.ChosenVoicePack == "None" then return end
8844 return schedule(t, self.Play, self.mod, self, ...)
8845 end
8846
8847 function soundPrototype2:Cancel(...)
8848 if DBM.Options.ChosenVoicePack == "None" then return end
8849 return unschedule(self.Play, self.mod, self, ...)
8850 end
8851end
8852
8853----------------------------
8854-- Countdown/out object --
8855----------------------------
8856do
8857 local countdownProtoType = {}
8858 local voice1, voice2, voice3 = nil, nil, nil
8859 local voice1max, voice2max, voice3max = 5, 5, 5
8860 local path1, path2, path3 = nil, nil, nil
8861 local mt = {__index = countdownProtoType}
8862
8863 function DBM:BuildVoiceCountdownCache()
8864 voice1 = self.Options.CountdownVoice
8865 voice2 = self.Options.CountdownVoice2
8866 voice3 = self.Options.CountdownVoice3v2
8867 local voicesFound = 0
8868 for i = 1, #self.Counts do
8869 if voicesFound == 3 then return end
8870 local curVoice = self.Counts[i]
8871 if curVoice.value == voice1 then
8872 path1 = curVoice.path
8873 voice1max = curVoice.max
8874 voicesFound = voicesFound + 1
8875 elseif curVoice.value == voice2 then
8876 path2 = curVoice.path
8877 voice2max = curVoice.max
8878 voicesFound = voicesFound + 1
8879 elseif curVoice.value == voice3 then
8880 path3 = curVoice.path
8881 voice3max = curVoice.max
8882 voicesFound = voicesFound + 1
8883 end
8884 end
8885 end
8886
8887 local function showCountdown(timer)
8888 TimerTracker_OnEvent(TimerTracker, "START_TIMER", 2, timer+1, timer+1)
8889 end
8890
8891 local function stopCountdown()
8892 TimerTracker_OnEvent(TimerTracker, "PLAYER_ENTERING_WORLD")
8893 end
8894
8895 function countdownProtoType:Start(timer, count)
8896 if not self.option or self.mod.Options[self.option] then
8897 timer = timer or self.timer or 10
8898 timer = timer < 2 and self.timer or timer
8899 count = count or self.count or 5
8900 if timer <= count then count = floor(timer) end
8901 if DBM.Options.ShowCountdownText and not (self.textDisabled or self.alternateVoice) and self.type ~= "Countout" then
8902 stopCountdown()
8903 if timer >= count then
8904 DBM:Schedule(timer-count, showCountdown, count)
8905 else
8906 DBM:Schedule(timer%1, showCountdown, floor(timer))
8907 end
8908 end
8909 if DBM.Options.DontPlayCountdowns then return end
8910 if not path1 or not path2 or not path3 then
8911 DBM:Debug("Voice cache not built at time of countdownProtoType:Start. On fly caching.")
8912 DBM:BuildVoiceCountdownCache()
8913 end
8914 local voice, maxCount, path
8915 if self.alternateVoice == 2 then
8916 voice = voice2 or DBM.DefaultOptions.CountdownVoice2
8917 maxCount = voice2max or 10
8918 path = path2 or "Interface\\AddOns\\DBM-Core\\Sounds\\Kolt\\"
8919 elseif self.alternateVoice == 3 then
8920 voice = voice3 or DBM.DefaultOptions.CountdownVoice3v2
8921 maxCount = voice3max or 5
8922 path = path3 or "Interface\\AddOns\\DBM-Core\\Sounds\\Heroes\\Necromancer\\"
8923 else
8924 voice = voice1 or DBM.DefaultOptions.CountdownVoice
8925 maxCount = voice1max or 10
8926 path = path1 or "Interface\\AddOns\\DBM-Core\\Sounds\\Corsica\\"
8927 end
8928 if not path then--Should not happen but apparently it does somehow
8929 DBM:Debug("Voice path failed in countdownProtoType:Start.")
8930 return
8931 end
8932 if self.type == "Countout" then
8933 for i = 1, timer do
8934 if i < maxCount then
8935 self.sound5:Schedule(i, path..i..".ogg")
8936 end
8937 end
8938 else
8939 for i = count, 1, -1 do
8940 if i <= maxCount then
8941 self.sound5:Schedule(timer-i, path..i..".ogg")
8942 end
8943 end
8944 end
8945 end
8946 end
8947 countdownProtoType.Show = countdownProtoType.Start
8948
8949 function countdownProtoType:Schedule(t)
8950 return schedule(t, self.Start, self.mod, self)
8951 end
8952
8953 function countdownProtoType:Cancel()
8954 if DBM.Options.ShowCountdownText and not self.textDisabled then
8955 DBM:Unschedule(showCountdown)
8956 stopCountdown()
8957 end
8958 self.mod:Unschedule(self.Start, self)
8959 self.sound5:Cancel()
8960 end
8961 countdownProtoType.Stop = countdownProtoType.Cancel
8962
8963 local function newCountdown(self, countdownType, timer, spellId, optionDefault, optionName, count, textDisabled, altVoice)
8964 if not spellId and not optionName then
8965 print("NewCountdown: you must provide either spellId or optionName", 2)
8966 return
8967 end
8968 if type(timer) == "string" and timer:match("OptionVersion") then
8969 print("OptionVersion depricated for newCountdown :"..optionDefault)
8970 return
8971 end
8972 local optionVersion
8973 if type(optionName) == "number" then
8974 optionVersion = optionName
8975 optionName = nil
8976 end
8977 if altVoice == true then altVoice = 2 end--Compat
8978 if type(timer) == "string" then
8979 if timer:match("AltTwo") then
8980 altVoice = 3
8981 timer = tonumber(string.sub(timer, 7))
8982 elseif timer:match("Alt") then
8983 altVoice = 2
8984 timer = tonumber(string.sub(timer, 4))
8985 end
8986 end
8987 --TODO, maybe make this not use an entire sound object?
8988 local sound5 = self:NewSound(5, true, false)
8989 timer = timer or 10
8990 count = count or 4
8991 spellId = spellId or 39505
8992 local obj = setmetatable(
8993 {
8994 id = optionName or countdownType..spellId..(optionVersion or ""),
8995 type = countdownType,
8996 sound5 = sound5,
8997 timer = timer,
8998 count = count,
8999 textDisabled = textDisabled,
9000 alternateVoice = altVoice,
9001 mod = self
9002 },
9003 mt
9004 )
9005 if optionName then
9006 obj.option = obj.id
9007 self:AddBoolOption(obj.option, optionDefault, "sound")
9008 elseif not (optionName == false) then
9009 obj.option = obj.id
9010 self:AddBoolOption(obj.option, optionDefault, "sound")
9011 if countdownType == "Countdown" then
9012 self.localization.options[obj.option] = DBM_CORE_AUTO_COUNTDOWN_OPTION_TEXT:format(spellId)
9013 elseif countdownType == "CountdownFades" then
9014 self.localization.options[obj.option] = DBM_CORE_AUTO_COUNTDOWN_OPTION_TEXT2:format(spellId)
9015 elseif countdownType == "Countout" then
9016 self.localization.options[obj.option] = DBM_CORE_AUTO_COUNTOUT_OPTION_TEXT:format(spellId)
9017 end
9018 end
9019 tinsert(self.countdowns, obj)
9020 return obj
9021 end
9022
9023 function bossModPrototype:NewCountdown(...)
9024 return newCountdown(self, "Countdown", ...)
9025 end
9026
9027 function bossModPrototype:NewCountdownFades(...)
9028 return newCountdown(self, "CountdownFades", ...)
9029 end
9030
9031 function bossModPrototype:NewCountout(...)
9032 return newCountdown(self, "Countout", ...)
9033 end
9034end
9035
9036--------------------
9037-- Yell Object --
9038--------------------
9039do
9040 local yellPrototype = {}
9041 local mt = { __index = yellPrototype }
9042 local function newYell(self, yellType, spellId, yellText, optionDefault, optionName, chatType)
9043 if not spellId and not yellText then
9044 error("NewYell: you must provide either spellId or yellText", 2)
9045 return
9046 end
9047 if type(spellId) == "string" and spellId:match("OptionVersion") then
9048 print("newYell for: "..yellText.." is using OptionVersion hack. This is depricated")
9049 return
9050 end
9051 local optionVersion
9052 if type(optionName) == "number" then
9053 optionVersion = optionName
9054 optionName = nil
9055 end
9056 local displayText
9057 if not yellText then
9058 if type(spellId) == "string" and spellId:match("ej%d+") then
9059 displayText = DBM_CORE_AUTO_YELL_ANNOUNCE_TEXT[yellType]:format(EJ_GetSectionInfo(string.sub(spellId, 3)) or DBM_CORE_UNKNOWN)
9060 else
9061 displayText = DBM_CORE_AUTO_YELL_ANNOUNCE_TEXT[yellType]:format(GetSpellInfo(spellId) or DBM_CORE_UNKNOWN)
9062 end
9063 end
9064 --Passed spellid as yellText.
9065 --Auto localize spelltext using yellText instead
9066 if yellText and type(yellText) == "number" then
9067 displayText = DBM_CORE_AUTO_YELL_ANNOUNCE_TEXT[yellType]:format(GetSpellInfo(yellText) or DBM_CORE_UNKNOWN)
9068 end
9069 local obj = setmetatable(
9070 {
9071 text = displayText or yellText,
9072 mod = self,
9073 chatType = chatType
9074 },
9075 mt
9076 )
9077 if optionName then
9078 obj.option = optionName
9079 self:AddBoolOption(obj.option, optionDefault, "misc")
9080 elseif not (optionName == false) then
9081 obj.option = "Yell"..(spellId or yellText)..(yellType ~= "yell" and yellType or "")..(optionVersion or "")
9082 self:AddBoolOption(obj.option, optionDefault, "misc")
9083 self.localization.options[obj.option] = DBM_CORE_AUTO_YELL_OPTION_TEXT[yellType]:format(spellId)
9084 end
9085 return obj
9086 end
9087
9088 function yellPrototype:Yell(...)
9089 if DBM.Options.DontSendYells then return end
9090 if not self.option or self.mod.Options[self.option] then
9091 SendChatMessage(pformat(self.text, ...), self.chatType or "SAY")
9092 end
9093 end
9094 yellPrototype.Show = yellPrototype.Yell
9095
9096 function yellPrototype:Schedule(t, ...)
9097 return schedule(t, self.Yell, self.mod, self, ...)
9098 end
9099
9100 function yellPrototype:Cancel(...)
9101 return unschedule(self.Yell, self.mod, self, ...)
9102 end
9103
9104 function bossModPrototype:NewYell(...)
9105 return newYell(self, "yell", ...)
9106 end
9107
9108 function bossModPrototype:NewCountYell(...)
9109 return newYell(self, "count", ...)
9110 end
9111
9112 function bossModPrototype:NewFadesYell(...)
9113 return newYell(self, "fade", ...)
9114 end
9115
9116 function bossModPrototype:NewPosYell(...)
9117 return newYell(self, "position", ...)
9118 end
9119end
9120
9121------------------------------
9122-- Special Warning Object --
9123------------------------------
9124do
9125 local frame = CreateFrame("Frame", "DBMSpecialWarning", UIParent)
9126 local font1 = frame:CreateFontString("DBMSpecialWarning1", "OVERLAY", "ZoneTextFont")
9127 font1:SetWidth(1024)
9128 font1:SetHeight(0)
9129 font1:SetPoint("TOP", 0, 0)
9130 local font2 = frame:CreateFontString("DBMSpecialWarning2", "OVERLAY", "ZoneTextFont")
9131 font2:SetWidth(1024)
9132 font2:SetHeight(0)
9133 font2:SetPoint("TOP", font1, "BOTTOM", 0, 0)
9134 frame:SetMovable(1)
9135 frame:SetWidth(1)
9136 frame:SetHeight(1)
9137 frame:SetFrameStrata("HIGH")
9138 frame:SetClampedToScreen()
9139 frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
9140
9141 local font1elapsed, font2elapsed, moving
9142
9143 local function fontHide1()
9144 local duration = DBM.Options.SpecialWarningDuration
9145 if font1elapsed > duration * 1.3 then
9146 font1:Hide()
9147 if frame.font1ticker then
9148 frame.font1ticker:Cancel()
9149 frame.font1ticker = nil
9150 end
9151 elseif font1elapsed > duration then
9152 font1elapsed = font1elapsed + 0.05
9153 local alpha = 1 - (font1elapsed - duration) / (duration * 0.3)
9154 font1:SetAlpha(alpha > 0 and alpha or 0)
9155 else
9156 font1elapsed = font1elapsed + 0.05
9157 font1:SetAlpha(1)
9158 end
9159 end
9160
9161 local function fontHide2()
9162 local duration = DBM.Options.SpecialWarningDuration
9163 if font2elapsed > duration * 1.3 then
9164 font2:Hide()
9165 if frame.font2ticker then
9166 frame.font2ticker:Cancel()
9167 frame.font2ticker = nil
9168 end
9169 elseif font2elapsed > duration then
9170 font2elapsed = font2elapsed + 0.05
9171 local alpha = 1 - (font2elapsed - duration) / (duration * 0.3)
9172 font2:SetAlpha(alpha > 0 and alpha or 0)
9173 else
9174 font2elapsed = font2elapsed + 0.05
9175 font2:SetAlpha(1)
9176 end
9177 end
9178
9179 function DBM:UpdateSpecialWarningOptions()
9180 frame:ClearAllPoints()
9181 frame:SetPoint(self.Options.SpecialWarningPoint, UIParent, self.Options.SpecialWarningPoint, self.Options.SpecialWarningX, self.Options.SpecialWarningY)
9182 font1:SetFont(self.Options.SpecialWarningFont, self.Options.SpecialWarningFontSize2, self.Options.SpecialWarningFontStyle == "None" and nil or self.Options.SpecialWarningFontStyle)
9183 font2:SetFont(self.Options.SpecialWarningFont, self.Options.SpecialWarningFontSize2, self.Options.SpecialWarningFontStyle == "None" and nil or self.Options.SpecialWarningFontStyle)
9184 font1:SetTextColor(unpack(self.Options.SpecialWarningFontCol))
9185 font2:SetTextColor(unpack(self.Options.SpecialWarningFontCol))
9186 if self.Options.SpecialWarningFontShadow then
9187 font1:SetShadowOffset(1, -1)
9188 font2:SetShadowOffset(1, -1)
9189 else
9190 font1:SetShadowOffset(0, 0)
9191 font2:SetShadowOffset(0, 0)
9192 end
9193 end
9194
9195 function DBM:AddSpecialWarning(text, force)
9196 local added = false
9197 if not frame.font1ticker then
9198 font1elapsed = 0
9199 font1.lastUpdate = GetTime()
9200 font1:SetText(text)
9201 font1:Show()
9202 added = true
9203 frame.font1ticker = frame.font1ticker or C_TimerNewTicker(0.05, fontHide1)
9204 elseif not frame.font2ticker or force then
9205 font2elapsed = 0
9206 font2.lastUpdate = GetTime()
9207 font2:SetText(text)
9208 font2:Show()
9209 added = true
9210 frame.font2ticker = frame.font2ticker or C_TimerNewTicker(0.05, fontHide2)
9211 end
9212 if not added then
9213 local prevText1 = font2:GetText()
9214 font1:SetText(prevText1)
9215 font1elapsed = font2elapsed
9216 self:AddSpecialWarning(text, true)
9217 end
9218 end
9219
9220 do
9221 local anchorFrame
9222 local function moveEnd(self)
9223 moving = false
9224 anchorFrame:Hide()
9225 font1elapsed = self.Options.SpecialWarningDuration
9226 font2elapsed = self.Options.SpecialWarningDuration
9227 frame:SetFrameStrata("HIGH")
9228 self:Unschedule(moveEnd)
9229 self.Bars:CancelBar(DBM_CORE_MOVE_SPECIAL_WARNING_BAR)
9230 end
9231
9232 function DBM:MoveSpecialWarning()
9233 if not anchorFrame then
9234 anchorFrame = CreateFrame("Frame", nil, frame)
9235 anchorFrame:SetWidth(32)
9236 anchorFrame:SetHeight(32)
9237 anchorFrame:EnableMouse(true)
9238 anchorFrame:SetPoint("TOP", frame, "TOP", 0, 32)
9239 anchorFrame:RegisterForDrag("LeftButton")
9240 anchorFrame:SetClampedToScreen()
9241 anchorFrame:Hide()
9242 local texture = anchorFrame:CreateTexture()
9243 texture:SetTexture("Interface\\Addons\\DBM-GUI\\textures\\dot.blp")
9244 texture:SetPoint("CENTER", anchorFrame, "CENTER", 0, 0)
9245 texture:SetWidth(32)
9246 texture:SetHeight(32)
9247 anchorFrame:SetScript("OnDragStart", function()
9248 frame:StartMoving()
9249 self:Unschedule(moveEnd)
9250 self.Bars:CancelBar(DBM_CORE_MOVE_SPECIAL_WARNING_BAR)
9251 end)
9252 anchorFrame:SetScript("OnDragStop", function()
9253 frame:StopMovingOrSizing()
9254 local point, _, _, xOfs, yOfs = frame:GetPoint(1)
9255 self.Options.SpecialWarningPoint = point
9256 self.Options.SpecialWarningX = xOfs
9257 self.Options.SpecialWarningY = yOfs
9258 self:Schedule(15, moveEnd, self)
9259 self.Bars:CreateBar(15, DBM_CORE_MOVE_SPECIAL_WARNING_BAR)
9260 end)
9261 end
9262 if anchorFrame:IsShown() then
9263 moveEnd(self)
9264 else
9265 moving = true
9266 anchorFrame:Show()
9267 DBM:AddSpecialWarning(DBM_CORE_MOVE_SPECIAL_WARNING_TEXT)
9268 DBM:AddSpecialWarning(DBM_CORE_MOVE_SPECIAL_WARNING_TEXT)
9269 self:Schedule(15, moveEnd, self)
9270 self.Bars:CreateBar(15, DBM_CORE_MOVE_SPECIAL_WARNING_BAR)
9271 frame:Show()
9272 frame:SetFrameStrata("TOOLTIP")
9273 frame:SetAlpha(1)
9274 end
9275 end
9276 end
9277
9278 local specialWarningPrototype = {}
9279 local mt = {__index = specialWarningPrototype}
9280
9281 local function classColoringFunction(cap)
9282 cap = cap:sub(2, -2)
9283 local noStrip = cap:match("noStrip ")
9284 if not noStrip then
9285 local name = cap
9286 if DBM.Options.StripServerName then
9287 cap = Ambiguate(cap, "short")
9288 end
9289 if DBM.Options.SWarnClassColor then
9290 local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(name)]
9291 if playerColor then
9292 cap = ("|r|cff%.2x%.2x%.2x%s|r|cff%.2x%.2x%.2x"):format(playerColor.r * 255, playerColor.g * 255, playerColor.b * 255, cap, DBM.Options.SpecialWarningFontCol[1] * 255, DBM.Options.SpecialWarningFontCol[2] * 255, DBM.Options.SpecialWarningFontCol[3] * 255)
9293 end
9294 end
9295 else
9296 cap = cap:sub(9)
9297 end
9298 return cap
9299 end
9300
9301 local textureCode = " |T%s:12:12|t "
9302
9303 function specialWarningPrototype:Show(...)
9304 if not DBM.Options.DontShowSpecialWarnings and (not self.option or self.mod.Options[self.option]) and not moving and frame then
9305 if self.announceType == "taunt" and DBM.Options.FilterTankSpec and not self.mod:IsTank() then return end--Don't tell non tanks to taunt, ever.
9306 local argTable = {...}
9307 if #self.combinedtext > 0 then
9308 --Throttle spam.
9309 if DBM.Options.SWarningAlphabetical then
9310 tsort(self.combinedtext)
9311 end
9312 local combinedText = tconcat(self.combinedtext, "<, >")
9313 if self.combinedcount == 1 then
9314 combinedText = combinedText.." "..DBM_CORE_GENERIC_WARNING_OTHERS
9315 elseif self.combinedcount > 1 then
9316 combinedText = combinedText.." "..DBM_CORE_GENERIC_WARNING_OTHERS2:format(self.combinedcount)
9317 end
9318 --Process
9319 for i = 1, #argTable do
9320 if type(argTable[i]) == "string" then
9321 argTable[i] = combinedText
9322 end
9323 end
9324 end
9325 local message = pformat(self.text, unpack(argTable))
9326 local text = ("%s%s%s"):format(
9327 (DBM.Options.SpecialWarningIcon and self.icon and textureCode:format(self.icon)) or "",
9328 message,
9329 (DBM.Options.SpecialWarningIcon and self.icon and textureCode:format(self.icon)) or ""
9330 )
9331 local noteHasName = false
9332 if self.option then
9333 local noteText = self.mod.Options[self.option .. "SWNote"]
9334 if noteText and type(noteText) == "string" and noteText ~= "" then--Filter false bool and empty strings
9335 local count1 = self.announceType == "count" or self.announceType == "switchcount" or self.announceType == "targetcount"
9336 local count2 = self.announceType == "interruptcount"
9337 if count1 or count2 then--Counts support different note for EACH count
9338 local noteCount
9339 local notesTable = {string.split("/", noteText)}
9340 if count1 then
9341 noteCount = argTable[1]--Count should be first arg in table
9342 elseif count2 then
9343 noteCount = argTable[2]--Count should be second arg in table
9344 end
9345 if type(noteCount) == "string" then
9346 --Probably a hypehnated double count like inferno slice or marked for death
9347 local mainCount = string.split("-", noteCount)
9348 noteCount = tonumber(mainCount)
9349 end
9350 noteText = notesTable[noteCount]
9351 if noteText and type(noteText) == "string" and noteText ~= "" then--Refilter after string split to make sure a note for this count exists
9352 local hasPlayerName = noteText:find(playerName)
9353 if DBM.Options.SWarnNameInNote and hasPlayerName then
9354 noteHasName = 5
9355 end
9356 --Terminate special warning, it's an interrupt count warning without player name and filter enabled
9357 if count2 and DBM.Options.FilterInterruptNoteName and not hasPlayerName then return end
9358 noteText = " ("..noteText..")"
9359 text = text..noteText
9360 end
9361 else--Non count warnings will have one note, period
9362 if DBM.Options.SWarnNameInNote and noteText:find(playerName) then
9363 noteHasName = 5
9364 end
9365 if self.announceType and self.announceType:find("switch") then
9366 noteText = noteText:gsub(">.-<", classColoringFunction)--Class color note text before combining with warning text.
9367 end
9368 noteText = " ("..noteText..")"
9369 text = text..noteText
9370 end
9371 end
9372 end
9373 --No stripping on switch warnings, ever. They will NEVER have player name, but often have adds with "-" in name
9374 if self.announceType and not self.announceType:find("switch") then
9375 text = text:gsub(">.-<", classColoringFunction)
9376 end
9377 DBM:AddSpecialWarning(text)
9378 self.combinedcount = 0
9379 self.combinedtext = {}
9380 if DBM.Options.ShowSWarningsInChat then
9381 local colorCode = ("|cff%.2x%.2x%.2x"):format(DBM.Options.SpecialWarningFontCol[1] * 255, DBM.Options.SpecialWarningFontCol[2] * 255, DBM.Options.SpecialWarningFontCol[3] * 255)
9382 self.mod:AddMsg(colorCode.."["..DBM_CORE_MOVE_SPECIAL_WARNING_TEXT.."] "..text.."|r", nil)
9383 end
9384 if not UnitIsDeadOrGhost("player") and DBM.Options.ShowFlashFrame then
9385 if noteHasName then
9386 local repeatCount = DBM.Options.SpecialWarningFlashRepeat5 and DBM.Options.SpecialWarningFlashRepeatAmount or 0
9387 DBM.Flash:Show(DBM.Options.SpecialWarningFlashCol5[1],DBM.Options.SpecialWarningFlashCol5[2], DBM.Options.SpecialWarningFlashCol5[3], DBM.Options.SpecialWarningFlashDura5, DBM.Options.SpecialWarningFlashAlph5, repeatCount)
9388 elseif self.flash == 1 then
9389 local repeatCount = DBM.Options.SpecialWarningFlashRepeat1 and DBM.Options.SpecialWarningFlashRepeatAmount or 0
9390 DBM.Flash:Show(DBM.Options.SpecialWarningFlashCol1[1],DBM.Options.SpecialWarningFlashCol1[2], DBM.Options.SpecialWarningFlashCol1[3], DBM.Options.SpecialWarningFlashDura1, DBM.Options.SpecialWarningFlashAlph1, repeatCount)
9391 elseif self.flash == 2 then
9392 local repeatCount = DBM.Options.SpecialWarningFlashRepeat2 and DBM.Options.SpecialWarningFlashRepeatAmount or 0
9393 DBM.Flash:Show(DBM.Options.SpecialWarningFlashCol2[1],DBM.Options.SpecialWarningFlashCol2[2], DBM.Options.SpecialWarningFlashCol2[3], DBM.Options.SpecialWarningFlashDura2, DBM.Options.SpecialWarningFlashAlph2, repeatCount)
9394 elseif self.flash == 3 then
9395 local repeatCount = DBM.Options.SpecialWarningFlashRepeat3 and DBM.Options.SpecialWarningFlashRepeatAmount or 0
9396 DBM.Flash:Show(DBM.Options.SpecialWarningFlashCol3[1],DBM.Options.SpecialWarningFlashCol3[2], DBM.Options.SpecialWarningFlashCol3[3], DBM.Options.SpecialWarningFlashDura3, DBM.Options.SpecialWarningFlashAlph3, repeatCount)
9397 elseif self.flash == 4 then
9398 local repeatCount = DBM.Options.SpecialWarningFlashRepeat4 and DBM.Options.SpecialWarningFlashRepeatAmount or 0
9399 DBM.Flash:Show(DBM.Options.SpecialWarningFlashCol4[1],DBM.Options.SpecialWarningFlashCol4[2], DBM.Options.SpecialWarningFlashCol4[3], DBM.Options.SpecialWarningFlashDura4, DBM.Options.SpecialWarningFlashAlph3, repeatCount)
9400 end
9401 end
9402 --Text: Full message text
9403 --Type: Announce type
9404 --SpellId: Raw spell or encounter journal Id if available.
9405 --Mod ID: Encounter ID as string, or a generic string for mods that don't have encounter ID (such as trash, dummy/test mods)
9406 fireEvent("DBM_Announce", text, self.type, self.spellId, self.mod.id)
9407 if self.sound then
9408 local soundId = self.option and self.mod.Options[self.option .. "SWSound"] or self.flash
9409 if noteHasName and type(soundId) == "number" then soundId = noteHasName end--Change number to 5 if it's not a custom sound, else, do nothing with it
9410 if self.hasVoice and DBM.Options.ChosenVoicePack ~= "None" and self.hasVoice <= SWFilterDisabed and (type(soundId) == "number" and soundId < 5 and DBM.Options.VoiceOverSpecW2 == "DefaultOnly" or DBM.Options.VoiceOverSpecW2 == "All") and (DBM.Options.AlwaysPlayVoice or (self.mod.Options[self.voiceOptionId] or self.mod.Options[self.voiceOptionId.."2"] or self.mod.Options[self.voiceOptionId.."3"])) then return end
9411 if not self.option or self.mod.Options[self.option.."SWSound"] ~= "None" then
9412 DBM:PlaySpecialWarningSound(soundId or 1)
9413 end
9414 end
9415 else
9416 self.combinedcount = 0
9417 self.combinedtext = {}
9418 end
9419 end
9420
9421 function specialWarningPrototype:CombinedShow(delay, ...)
9422 local argTable = {...}
9423 for i = 1, #argTable do
9424 if type(argTable[i]) == "string" then
9425 if #self.combinedtext < 8 then--Throttle spam. We may not need more than 9 targets..
9426 if not checkEntry(self.combinedtext, argTable[i]) then
9427 self.combinedtext[#self.combinedtext + 1] = argTable[i]
9428 end
9429 else
9430 self.combinedcount = self.combinedcount + 1
9431 end
9432 end
9433 end
9434 unschedule(self.Show, self.mod, self)
9435 schedule(delay or 0.5, self.Show, self.mod, self, ...)
9436 end
9437
9438 function specialWarningPrototype:DelayedShow(delay, ...)
9439 unschedule(self.Show, self.mod, self, ...)
9440 schedule(delay or 0.5, self.Show, self.mod, self, ...)
9441 end
9442
9443 function specialWarningPrototype:Schedule(t, ...)
9444 return schedule(t, self.Show, self.mod, self, ...)
9445 end
9446
9447 function specialWarningPrototype:Cancel(t, ...)
9448 return unschedule(self.Show, self.mod, self, ...)
9449 end
9450
9451 function bossModPrototype:NewSpecialWarning(text, optionDefault, optionName, optionVersion, runSound, hasVoice)
9452 if not text then
9453 error("NewSpecialWarning: you must provide special warning text", 2)
9454 return
9455 end
9456 if type(text) == "string" and text:match("OptionVersion") then
9457 print("NewSpecialWarning: you must provide remove optionversion hack for "..optionDefault)
9458 return
9459 end
9460 if runSound == true then
9461 runSound = 2
9462 elseif not runSound then
9463 runSound = 1
9464 end
9465 if hasVoice == true then--if not a number, set it to 2, old mods that don't use new numbered system
9466 hasVoice = 2
9467 end
9468 local obj = setmetatable(
9469 {
9470 text = self.localization.warnings[text],
9471 combinedtext = {},
9472 combinedcount = 0,
9473 mod = self,
9474 sound = runSound>0,
9475 flash = runSound,--Set flash color to hard coded runsound (even if user sets custom sounds)
9476 hasVoice = hasVoice,
9477 },
9478 mt
9479 )
9480 local optionId = optionName or optionName ~= false and text
9481 if optionId then
9482 obj.voiceOptionId = hasVoice and "Voice"..optionId or nil
9483 obj.option = optionId..(optionVersion or "")
9484 self:AddSpecialWarningOption(optionId, optionDefault, runSound, "announce")
9485 end
9486 tinsert(self.specwarns, obj)
9487 return obj
9488 end
9489
9490 local function newSpecialWarning(self, announceType, spellId, stacks, optionDefault, optionName, optionVersion, runSound, hasVoice)
9491 if not spellId then
9492 error("newSpecialWarning: you must provide spellId", 2)
9493 return
9494 end
9495 if type(spellId) == "string" and spellId:match("OptionVersion") then
9496 print("NewSpecialWarning: you must provide remove optionversion hack for"..stacks)
9497 return
9498 end
9499 if runSound == true then
9500 runSound = 2
9501 elseif not runSound then
9502 runSound = 1
9503 end
9504 if hasVoice == true then--if not a number, set it to 2, old mods that don't use new numbered system
9505 hasVoice = 2
9506 end
9507 local spellName
9508 local unparsedId = spellId
9509 if type(spellId) == "string" and spellId:match("ej%d+") then
9510 spellName = EJ_GetSectionInfo(string.sub(spellId, 3)) or DBM_CORE_UNKNOWN
9511 else
9512 spellName = GetSpellInfo(spellId) or DBM_CORE_UNKNOWN
9513 end
9514 local text
9515 if announceType == "prewarn" then
9516 if type(stacks) == "string" then
9517 text = DBM_CORE_AUTO_SPEC_WARN_TEXTS[announceType]:format(spellName, stacks)
9518 else
9519 text = DBM_CORE_AUTO_SPEC_WARN_TEXTS[announceType]:format(spellName, DBM_CORE_SEC_FMT:format(tostring(stacks or 5)))
9520 end
9521 else
9522 text = DBM_CORE_AUTO_SPEC_WARN_TEXTS[announceType]:format(spellName)
9523 end
9524 local obj = setmetatable( -- todo: fix duplicate code
9525 {
9526 text = text,
9527 combinedtext = {},
9528 combinedcount = 0,
9529 announceType = announceType,
9530 mod = self,
9531 sound = runSound>0,
9532 flash = runSound,--Set flash color to hard coded runsound (even if user sets custom sounds)
9533 hasVoice = hasVoice,
9534 type = announceType,
9535 spellId = unparsedId,
9536 icon = (type(spellId) == "string" and spellId:match("ej%d+") and select(4, EJ_GetSectionInfo(string.sub(spellId, 3))) ~= "" and select(4, EJ_GetSectionInfo(string.sub(spellId, 3)))) or (type(spellId) == "number" and GetSpellTexture(spellId)) or nil
9537 },
9538 mt
9539 )
9540 if optionName then
9541 obj.option = optionName
9542 elseif not (optionName == false) then
9543 obj.option = "SpecWarn"..spellId..announceType..(optionVersion or "")
9544 if announceType == "stack" then
9545 self.localization.options[obj.option] = DBM_CORE_AUTO_SPEC_WARN_OPTIONS[announceType]:format(stacks or 3, spellId)
9546 elseif announceType == "prewarn" then
9547 self.localization.options[obj.option] = DBM_CORE_AUTO_SPEC_WARN_OPTIONS[announceType]:format(tostring(stacks or 5), spellId)
9548 else
9549 self.localization.options[obj.option] = DBM_CORE_AUTO_SPEC_WARN_OPTIONS[announceType]:format(spellId)
9550 end
9551 end
9552 if obj.option then
9553 local catType = "announce"--Default to General announce
9554 --Directly affects another target (boss or player) that you need to know about
9555 if announceType == "target" or announceType == "targetcount" or announceType == "close" or announceType == "reflect" or announceType == "switch" or announceType == "switchcount" then
9556 catType = "announceother"
9557 --Directly affects you
9558 elseif announceType == "you" or announceType == "youcount" or announceType == "move" or announceType == "dodge" or announceType == "moveaway" or announceType == "run" or announceType == "stack" or announceType == "moveto" then
9559 catType = "announcepersonal"
9560 --Things you have to do to fulfil your role
9561 elseif announceType == "taunt" or announceType == "dispel" or announceType == "interrupt" or announceType == "interruptcount" then
9562 catType = "announcerole"
9563 end
9564 self:AddSpecialWarningOption(obj.option, optionDefault, runSound, catType)
9565 end
9566 obj.voiceOptionId = hasVoice and "Voice"..spellId or nil
9567 tinsert(self.specwarns, obj)
9568 return obj
9569 end
9570
9571 function bossModPrototype:NewSpecialWarningSpell(text, optionDefault, ...)
9572 return newSpecialWarning(self, "spell", text, nil, optionDefault, ...)
9573 end
9574
9575 function bossModPrototype:NewSpecialWarningEnd(text, optionDefault, ...)
9576 return newSpecialWarning(self, "ends", text, nil, optionDefault, ...)
9577 end
9578
9579 function bossModPrototype:NewSpecialWarningFades(text, optionDefault, ...)
9580 return newSpecialWarning(self, "fades", text, nil, optionDefault, ...)
9581 end
9582
9583 function bossModPrototype:NewSpecialWarningSoon(text, optionDefault, ...)
9584 return newSpecialWarning(self, "soon", text, nil, optionDefault, ...)
9585 end
9586
9587 function bossModPrototype:NewSpecialWarningDispel(text, optionDefault, ...)
9588 return newSpecialWarning(self, "dispel", text, nil, optionDefault, ...)
9589 end
9590
9591 function bossModPrototype:NewSpecialWarningInterrupt(text, optionDefault, ...)
9592 return newSpecialWarning(self, "interrupt", text, nil, optionDefault, ...)
9593 end
9594
9595 function bossModPrototype:NewSpecialWarningInterruptCount(text, optionDefault, ...)
9596 return newSpecialWarning(self, "interruptcount", text, nil, optionDefault, ...)
9597 end
9598
9599 function bossModPrototype:NewSpecialWarningYou(text, optionDefault, ...)
9600 return newSpecialWarning(self, "you", text, nil, optionDefault, ...)
9601 end
9602
9603 function bossModPrototype:NewSpecialWarningYouCount(text, optionDefault, ...)
9604 return newSpecialWarning(self, "youcount", text, nil, optionDefault, ...)
9605 end
9606
9607 function bossModPrototype:NewSpecialWarningYouPos(text, optionDefault, ...)
9608 return newSpecialWarning(self, "youpos", text, nil, optionDefault, ...)
9609 end
9610
9611 function bossModPrototype:NewSpecialWarningSoakPos(text, optionDefault, ...)
9612 return newSpecialWarning(self, "soakpos", text, nil, optionDefault, ...)
9613 end
9614
9615 function bossModPrototype:NewSpecialWarningTarget(text, optionDefault, ...)
9616 return newSpecialWarning(self, "target", text, nil, optionDefault, ...)
9617 end
9618
9619 function bossModPrototype:NewSpecialWarningTargetCount(text, optionDefault, ...)
9620 return newSpecialWarning(self, "targetcount", text, nil, optionDefault, ...)
9621 end
9622
9623 function bossModPrototype:NewSpecialWarningDefensive(text, optionDefault, ...)
9624 return newSpecialWarning(self, "defensive", text, nil, optionDefault, ...)
9625 end
9626
9627 function bossModPrototype:NewSpecialWarningTaunt(text, optionDefault, ...)
9628 return newSpecialWarning(self, "taunt", text, nil, optionDefault, ...)
9629 end
9630
9631 function bossModPrototype:NewSpecialWarningClose(text, optionDefault, ...)
9632 return newSpecialWarning(self, "close", text, nil, optionDefault, ...)
9633 end
9634
9635 function bossModPrototype:NewSpecialWarningMove(text, optionDefault, ...)
9636 return newSpecialWarning(self, "move", text, nil, optionDefault, ...)
9637 end
9638
9639 function bossModPrototype:NewSpecialWarningDodge(text, optionDefault, ...)
9640 return newSpecialWarning(self, "dodge", text, nil, optionDefault, ...)
9641 end
9642
9643 function bossModPrototype:NewSpecialWarningMoveAway(text, optionDefault, ...)
9644 return newSpecialWarning(self, "moveaway", text, nil, optionDefault, ...)
9645 end
9646
9647 function bossModPrototype:NewSpecialWarningMoveTo(text, optionDefault, ...)
9648 return newSpecialWarning(self, "moveto", text, nil, optionDefault, ...)
9649 end
9650
9651 function bossModPrototype:NewSpecialWarningJump(text, optionDefault, ...)
9652 return newSpecialWarning(self, "jump", text, nil, optionDefault, ...)
9653 end
9654
9655 function bossModPrototype:NewSpecialWarningRun(text, optionDefault, ...)
9656 return newSpecialWarning(self, "run", text, nil, optionDefault, ...)
9657 end
9658
9659 function bossModPrototype:NewSpecialWarningCast(text, optionDefault, ...)
9660 return newSpecialWarning(self, "cast", text, nil, optionDefault, ...)
9661 end
9662
9663 function bossModPrototype:NewSpecialWarningReflect(text, optionDefault, ...)
9664 return newSpecialWarning(self, "reflect", text, nil, optionDefault, ...)
9665 end
9666
9667 function bossModPrototype:NewSpecialWarningCount(text, optionDefault, ...)
9668 return newSpecialWarning(self, "count", text, nil, optionDefault, ...)
9669 end
9670
9671 function bossModPrototype:NewSpecialWarningStack(text, optionDefault, stacks, ...)
9672 if type(text) == "string" and text:match("OptionVersion") then
9673 print("NewSpecialWarning: you must provide remove optionversion hack for "..optionDefault)
9674 end
9675 return newSpecialWarning(self, "stack", text, stacks, optionDefault, ...)
9676 end
9677
9678 function bossModPrototype:NewSpecialWarningSwitch(text, optionDefault, ...)
9679 return newSpecialWarning(self, "switch", text, nil, optionDefault, ...)
9680 end
9681
9682 function bossModPrototype:NewSpecialWarningSwitchCount(text, optionDefault, ...)
9683 return newSpecialWarning(self, "switchcount", text, nil, optionDefault, ...)
9684 end
9685
9686 function bossModPrototype:NewSpecialWarningPreWarn(text, optionDefault, time, ...)
9687 if type(text) == "string" and text:match("OptionVersion") then
9688 print("NewSpecialWarning: you must provide remove optionversion hack for "..optionDefault)
9689 end
9690 return newSpecialWarning(self, "prewarn", text, time, optionDefault, ...)
9691 end
9692
9693 function DBM:PlayCountSound(number, forceVoice)
9694 if number > 10 then return end
9695 local voice
9696 if forceVoice then--For options example
9697 voice = forceVoice
9698 else
9699 voice = self.Options.CountdownVoice
9700 end
9701 local path
9702 local maxCount = 5
9703 for i = 1, #self.Counts do
9704 if self.Counts[i].value == voice then
9705 path = self.Counts[i].path
9706 maxCount = self.Counts[i].max
9707 break
9708 end
9709 end
9710 if not path or (number > maxCount) then return end
9711 self:PlaySoundFile(path..number..".ogg")
9712 end
9713
9714 function DBM:RegisterCountSound(t, v, p, m)
9715 --Prevent duplicate insert
9716 for i = 1, #self.Counts do
9717 if self.Counts[i].value == v then return end
9718 end
9719 --Insert into counts table.
9720 if t and v and p and m then
9721 tinsert(self.Counts, { text = t, value = v, path = p, max = m })
9722 end
9723 end
9724
9725 function DBM:CheckVoicePackVersion(value)
9726 local activeVP = self.Options.ChosenVoicePack
9727 --Check if voice pack out of date
9728 if activeVP ~= "None" and activeVP == value then
9729 if self.VoiceVersions[value] < 6 then--Version will be bumped when new voice packs released that contain new voices.
9730 self:AddMsg(DBM_CORE_VOICE_PACK_OUTDATED)
9731 SWFilterDisabed = self.VoiceVersions[value]--Set disable to version on current voice pack
9732 else
9733 SWFilterDisabed = 6
9734 end
9735 end
9736 end
9737
9738 function DBM:PlaySpecialWarningSound(soundId)
9739 local sound = type(soundId) == "number" and self.Options["SpecialWarningSound" .. (soundId == 1 and "" or soundId)] or soundId or self.Options.SpecialWarningSound
9740 self:PlaySoundFile(sound)
9741 end
9742
9743 local function testWarningEnd()
9744 frame:SetFrameStrata("HIGH")
9745 end
9746
9747 function DBM:ShowTestSpecialWarning(text, number, noSound)
9748 if moving then
9749 return
9750 end
9751 self:AddSpecialWarning(DBM_CORE_MOVE_SPECIAL_WARNING_TEXT)
9752 frame:SetFrameStrata("TOOLTIP")
9753 self:Unschedule(testWarningEnd)
9754 self:Schedule(self.Options.SpecialWarningDuration * 1.3, testWarningEnd)
9755 if number and not noSound then
9756 self:PlaySpecialWarningSound(number)
9757 end
9758 if self.Options.ShowFlashFrame and number then
9759 if number == 1 then
9760 local repeatCount = self.Options.SpecialWarningFlashRepeat1 and self.Options.SpecialWarningFlashRepeatAmount or 0
9761 self.Flash:Show(self.Options.SpecialWarningFlashCol1[1],self.Options.SpecialWarningFlashCol1[2], self.Options.SpecialWarningFlashCol1[3], self.Options.SpecialWarningFlashDura1, self.Options.SpecialWarningFlashAlph1, repeatCount)
9762 elseif number == 2 then
9763 local repeatCount = self.Options.SpecialWarningFlashRepeat2 and self.Options.SpecialWarningFlashRepeatAmount or 0
9764 self.Flash:Show(self.Options.SpecialWarningFlashCol2[1],self.Options.SpecialWarningFlashCol2[2], self.Options.SpecialWarningFlashCol2[3], self.Options.SpecialWarningFlashDura2, self.Options.SpecialWarningFlashAlph2, repeatCount)
9765 elseif number == 3 then
9766 local repeatCount = self.Options.SpecialWarningFlashRepeat3 and self.Options.SpecialWarningFlashRepeatAmount or 0
9767 self.Flash:Show(self.Options.SpecialWarningFlashCol3[1],self.Options.SpecialWarningFlashCol3[2], self.Options.SpecialWarningFlashCol3[3], self.Options.SpecialWarningFlashDura3, self.Options.SpecialWarningFlashAlph3, repeatCount)
9768 elseif number == 4 then
9769 local repeatCount = self.Options.SpecialWarningFlashRepeat4 and self.Options.SpecialWarningFlashRepeatAmount or 0
9770 self.Flash:Show(self.Options.SpecialWarningFlashCol4[1],self.Options.SpecialWarningFlashCol4[2], self.Options.SpecialWarningFlashCol4[3], self.Options.SpecialWarningFlashDura4, self.Options.SpecialWarningFlashAlph4, repeatCount)
9771 elseif number == 5 then
9772 local repeatCount = self.Options.SpecialWarningFlashRepeat5 and self.Options.SpecialWarningFlashRepeatAmount or 0
9773 self.Flash:Show(self.Options.SpecialWarningFlashCol5[1],self.Options.SpecialWarningFlashCol5[2], self.Options.SpecialWarningFlashCol5[3], self.Options.SpecialWarningFlashDura5, self.Options.SpecialWarningFlashAlph5, repeatCount)
9774 end
9775 end
9776 end
9777
9778 function DBM:ShowTestHUD()
9779 local x, y = UnitPosition("player")
9780 DBMHudMap:RegisterPositionMarker(10000, "Test1", "highlight", x, y-20, 5, 10, 1, 1, 0, 0.5, nil, 1):Pulse(0.5, 0.5)
9781 DBMHudMap:RegisterPositionMarker(20000, "Test2", "highlight", x-20, y, 5, 10, 1, 0, 0, 0.5, nil, 2):Pulse(0.5, 0.5)
9782 DBMHudMap:RegisterPositionMarker(30000, "Test3", "highlight", x+20, y, 5, 10, 1, 0.5, 0, 0.5, nil, 3):Pulse(0.5, 0.5)
9783 DBMHudMap:RegisterPositionMarker(40000, "Test4", "highlight", x, y+20, 5, 10, 0, 1, 0, 0.5, nil, 4):Pulse(0.5, 0.5)
9784 end
9785end
9786
9787--------------------
9788-- Timer Object --
9789--------------------
9790do
9791 local timerPrototype = {}
9792 local mt = {__index = timerPrototype}
9793
9794 function timerPrototype:Start(timer, ...)
9795 if DBM.Options.DontShowBossTimers then return end
9796 if timer and type(timer) ~= "number" then
9797 return self:Start(nil, timer, ...) -- first argument is optional!
9798 end
9799 if not self.option or self.mod.Options[self.option] then
9800 if self.type and self.type:find("count") and not self.allowdouble then--cdcount, nextcount. remove previous timer.
9801 for i = #self.startedTimers, 1, -1 do
9802 if DBM.Options.AutoCorrectTimer or (DBM.Options.DebugMode and DBM.Options.DebugLevel > 1) then
9803 local bar = DBM.Bars:GetBar(self.startedTimers[i])
9804 if bar then
9805 local remaining = ("%.1f"):format(bar.timer)
9806 local ttext = _G[bar.frame:GetName().."BarName"]:GetText() or ""
9807 ttext = ttext.."("..self.id..")"
9808 if bar.timer > 0.2 then
9809 if timer then
9810 self.correctedCast = timer - bar.timer--Store what lowest timer is in timer object
9811 self.correctedDiff = difficultyIndex--Store index of correction to ensure the change is only used in one difficulty (so a mythic timer doesn't alter heroic for example)
9812 end
9813 DBM:Debug("Timer "..ttext.. " refreshed before expired. Remaining time is : "..remaining, 2)
9814 end
9815 end
9816 end
9817 DBM.Bars:CancelBar(self.startedTimers[i])
9818 self.startedTimers[i] = nil
9819 end
9820 end
9821 local timer = timer and ((timer > 0 and timer) or self.timer + timer) or self.timer
9822 --AI timer api:
9823 --Starting ai timer with (1) indicates it's a first timer after pull
9824 --Starting timer with (2) or (3) indicates it's a phase 2 or phase 3 first timer
9825 --Starting AI timer with anything above 3 indicarets it's a regular timer and to use shortest time in between two regular casts
9826 if self.type == "ai" then--A learning timer
9827 if not DBM.Options.AITimer then return end
9828 if timer > 4 then--Normal behavior.
9829 if self.firstCastTimer and type(self.firstCastTimer) == "string" then--This is first cast of spell, we need to generate self.firstPullTimer
9830 self.firstCastTimer = tonumber(self.firstCastTimer)
9831 self.firstCastTimer = GetTime() - self.firstCastTimer--We have generated a self.firstCastTimer! Next pull, DBM should know timer for first cast next pull. FANCY!
9832 DBM:Debug("AI timer learned a first timer for pull of "..self.firstCastTimer, 2)
9833 end
9834 if self.phase4CastTimer and type(self.phase4CastTimer) == "string" then--This is first cast of spell after a phase transition, we need to generate self.phaseCastTimer
9835 self.phase4CastTimer = tonumber(self.phase4CastTimer)
9836 self.phase4CastTimer = GetTime() - self.phase4CastTimer--We have generated a self.phaseCastTimer!
9837 DBM:Debug("AI timer learned a first timer for phase of "..self.phase4CastTimer, 2)
9838 end
9839 if self.phase3CastTimer and type(self.phase3CastTimer) == "string" then--This is first cast of spell after a phase transition, we need to generate self.phaseCastTimer
9840 self.phase3CastTimer = tonumber(self.phase3CastTimer)
9841 self.phase3CastTimer = GetTime() - self.phase3CastTimer--We have generated a self.phaseCastTimer!
9842 DBM:Debug("AI timer learned a first timer for phase of "..self.phase3CastTimer, 2)
9843 end
9844 if self.phase2CastTimer and type(self.phase2CastTimer) == "string" then--This is first cast of spell after a phase transition, we need to generate self.phaseCastTimer
9845 self.phase2CastTimer = tonumber(self.phase2CastTimer)
9846 self.phase2CastTimer = GetTime() - self.phase2CastTimer--We have generated a self.phaseCastTimer!
9847 DBM:Debug("AI timer learned a first timer for phase of "..self.phase2CastTimer, 2)
9848 end
9849 if self.lastCast then--We have a GetTime() on last cast
9850 local timeLastCast = GetTime() - self.lastCast--Get time between current cast and last cast
9851 if timeLastCast > 4 then--Prevent infinite loop cpu hang. Plus anything shorter than 5 seconds doesn't need a timer
9852 if not self.lowestSeenCast or (self.lowestSeenCast and self.lowestSeenCast > timeLastCast) then--Always use lowest seen cast for a timer
9853 self.lowestSeenCast = timeLastCast
9854 DBM:Debug("AI timer learned a new lowest timer of "..self.lowestSeenCast, 2)
9855 end
9856 end
9857 end
9858 self.lastCast = GetTime()
9859 if self.lowestSeenCast then--Always use lowest seen cast for timer
9860 timer = self.lowestSeenCast
9861 else
9862 return--Don't start the bogus timer shoved into timer field in the mod
9863 end
9864 elseif timer == 4 then
9865 if self.phase4CastTimer and type(self.phase4CastTimer) == "number" then
9866 timer = self.phase4CastTimer
9867 else--No first pull timer generated yet, set it to GetTime, as a string
9868 self.phase4CastTimer = tostring(GetTime())
9869 return--Don't start the 4 second timer
9870 end
9871 elseif timer == 3 then
9872 if self.phase3CastTimer and type(self.phase3CastTimer) == "number" then
9873 timer = self.phase3CastTimer
9874 else--No first pull timer generated yet, set it to GetTime, as a string
9875 self.phase3CastTimer = tostring(GetTime())
9876 return--Don't start the 3 second timer
9877 end
9878 elseif timer == 2 then
9879 if self.phase2CastTimer and type(self.phase2CastTimer) == "number" then
9880 timer = self.phase2CastTimer
9881 else--No first pull timer generated yet, set it to GetTime, as a string
9882 self.phase2CastTimer = tostring(GetTime())
9883 return--Don't start the 2 second timer
9884 end
9885 else--1 was sent, trigger a first Cast timer
9886 if self.firstCastTimer and type(self.firstCastTimer) == "number" then
9887 timer = self.firstCastTimer
9888 else--No first pull timer generated yet, set it to GetTime, as a string
9889 self.firstCastTimer = tostring(GetTime())
9890 return--Don't start the 1 second timer
9891 end
9892 end
9893 end
9894 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
9895 if DBM.Options.AutoCorrectTimer or (DBM.Options.DebugMode and DBM.Options.DebugLevel > 1) then
9896 if not self.type or (self.type ~= "target" and self.type ~= "active" and self.type ~= "fades" and self.type ~= "ai") then
9897 local bar = DBM.Bars:GetBar(id)
9898 if bar then
9899 local remaining = ("%.1f"):format(bar.timer)
9900 local ttext = _G[bar.frame:GetName().."BarName"]:GetText() or ""
9901 ttext = ttext.."("..self.id..")"
9902 if bar.timer > 0.2 then
9903 self.correctedCast = timer - bar.timer--Store what lowest timer is for advanced user feature
9904 self.correctedDiff = difficultyIndex--Store index of correction to ensure the change is only used in one difficulty (so a mythic timer doesn't alter heroic for example
9905 DBM:Debug("Timer "..ttext.. " refreshed before expired. Remaining time is : "..remaining, 2)
9906 end
9907 end
9908 end
9909 end
9910 if DBM.Options.AutoCorrectTimer and self.correctedCast and self.correctedDiff and self.correctedDiff == difficultyIndex and self.correctedCast < timer then
9911 local debugtemp = timer - self.correctedCast
9912 DBM:Debug("Timer autocorrected by "..debugtemp, 2)
9913 timer = self.correctedCast
9914 end
9915 local colorId = 0
9916 if self.option then
9917 colorId = self.mod.Options[self.option .. "TColor"]
9918 end
9919 local bar = DBM.Bars:CreateBar(timer, id, self.icon, nil, nil, nil, nil, colorId)
9920 if not bar then
9921 return false, "error" -- creating the timer failed somehow, maybe hit the hard-coded timer limit of 15
9922 end
9923 local msg = ""
9924 if self.type and not self.text then
9925 msg = pformat(self.mod:GetLocalizedTimerText(self.type, self.spellId), ...)
9926 else
9927 if type(self.text) == "number" then
9928 msg = pformat(self.mod:GetLocalizedTimerText(self.type, self.text), ...)
9929 else
9930 msg = pformat(self.text, ...)
9931 end
9932 end
9933 msg = msg:gsub(">.-<", stripServerName)
9934 bar:SetText(msg, self.inlineIcon)
9935 --ID: Internal DBM timer ID
9936 --msg: Timer Text
9937 --timer: Raw timer value (number).
9938 --Icon: Texture Path for Icon
9939 --type: Timer type (Cooldowns: cd, cdcount, nextcount, nextsource, cdspecial, nextspecial, phase, ai. Durations: target, active, fades, roleplay. Casting: cast)
9940 --spellId: Raw spellid if available (most timers will have spellId or EJ ID unless it's a specific timer not tied to ability such as pull or combat start or rez timers. EJ id will be in format ej%d
9941 --colorID: Type classification (1-Add, 2-Aoe, 3-targeted ability, 4-Interrupt, 5-Role, 6-Phase, 7-User(custom))
9942 --Mod ID: Encounter ID as string, or a generic string for mods that don't have encounter ID (such as trash, dummy/test mods)
9943 fireEvent("DBM_TimerStart", id, msg, timer, self.icon, self.type, self.spellId, colorId, self.mod.id)
9944 tinsert(self.startedTimers, id)
9945 self.mod:Unschedule(removeEntry, self.startedTimers, id)
9946 self.mod:Schedule(timer, removeEntry, self.startedTimers, id)
9947 return bar
9948 else
9949 return false, "disabled"
9950 end
9951 end
9952 timerPrototype.Show = timerPrototype.Start
9953
9954 function timerPrototype:DelayedStart(delay, ...)
9955 unschedule(self.Start, self.mod, self, ...)
9956 schedule(delay or 0.5, self.Start, self.mod, self, ...)
9957 end
9958 timerPrototype.DelayedShow = timerPrototype.DelayedStart
9959
9960 function timerPrototype:Schedule(t, ...)
9961 return schedule(t, self.Start, self.mod, self, ...)
9962 end
9963
9964 function timerPrototype:Unschedule(...)
9965 return unschedule(self.Start, self.mod, self, ...)
9966 end
9967
9968 function timerPrototype:Stop(...)
9969 if select("#", ...) == 0 then
9970 for i = #self.startedTimers, 1, -1 do
9971 --This callback sucks, it needs useful information for external mods to listen to it better, such as mod and spellid
9972 fireEvent("DBM_TimerStop", self.startedTimers[i])
9973 DBM.Bars:CancelBar(self.startedTimers[i])
9974 self.startedTimers[i] = nil
9975 end
9976 else
9977 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
9978 for i = #self.startedTimers, 1, -1 do
9979 if self.startedTimers[i] == id then
9980 --This callback sucks, it needs useful information for external mods to listen to it better, such as mod and spellid
9981 fireEvent("DBM_TimerStop", id)
9982 DBM.Bars:CancelBar(id)
9983 tremove(self.startedTimers, i)
9984 end
9985 end
9986 end
9987 end
9988
9989 function timerPrototype:Cancel(...)
9990 self:Stop(...)
9991 self:Unschedule(...)
9992 end
9993
9994 function timerPrototype:GetTime(...)
9995 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
9996 local bar = DBM.Bars:GetBar(id)
9997 return bar and (bar.totalTime - bar.timer) or 0, (bar and bar.totalTime) or 0
9998 end
9999
10000 function timerPrototype:GetRemaining(...)
10001 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
10002 local bar = DBM.Bars:GetBar(id)
10003 return bar and bar.timer or 0
10004 end
10005
10006 function timerPrototype:IsStarted(...)
10007 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
10008 local bar = DBM.Bars:GetBar(id)
10009 return bar and true
10010 end
10011
10012 function timerPrototype:SetTimer(timer)
10013 self.timer = timer
10014 end
10015
10016 function timerPrototype:Update(elapsed, totalTime, ...)
10017 if DBM.Options.DontShowBossTimers then return end
10018 if self:GetTime(...) == 0 then
10019 self:Start(totalTime, ...)
10020 end
10021 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
10022 return DBM.Bars:UpdateBar(id, elapsed, totalTime)
10023 end
10024
10025 function timerPrototype:UpdateIcon(icon, ...)
10026 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
10027 local bar = DBM.Bars:GetBar(id)
10028 if bar then
10029 return bar:SetIcon((type(icon) == "string" and icon:match("ej%d+") and select(4, EJ_GetSectionInfo(string.sub(icon, 3))) ~= "" and select(4, EJ_GetSectionInfo(string.sub(icon, 3)))) or (type(icon) == "number" and GetSpellTexture(icon)) or icon or "Interface\\Icons\\Spell_Nature_WispSplode")
10030 end
10031 end
10032
10033 function timerPrototype:UpdateName(name, ...)
10034 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
10035 local bar = DBM.Bars:GetBar(id)
10036 if bar then
10037 return bar:SetText(name, self.inlineIcon)
10038 end
10039 end
10040
10041 function timerPrototype:SetColor(c, ...)
10042 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
10043 local bar = DBM.Bars:GetBar(id)
10044 if bar then
10045 return bar:SetColor(c)
10046 end
10047 end
10048
10049 function timerPrototype:DisableEnlarge(...)
10050 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
10051 local bar = DBM.Bars:GetBar(id)
10052 if bar then
10053 bar.small = true
10054 end
10055 end
10056
10057 function timerPrototype:AddOption(optionDefault, optionName, colorType)
10058 if optionName ~= false then
10059 self.option = optionName or self.id
10060 self.mod:AddBoolOption(self.option, optionDefault, "timer", nil, colorType)
10061 end
10062 end
10063
10064 function bossModPrototype:NewTimer(timer, name, icon, optionDefault, optionName, colorType, inlineIcon, r, g, b)
10065 local icon = (type(icon) == "string" and icon:match("ej%d+") and select(4, EJ_GetSectionInfo(string.sub(icon, 3))) ~= "" and select(4, EJ_GetSectionInfo(string.sub(icon, 3)))) or (type(icon) == "number" and GetSpellTexture(icon)) or icon or "Interface\\Icons\\Spell_Nature_WispSplode"
10066 local obj = setmetatable(
10067 {
10068 text = self.localization.timers[name],
10069 timer = timer,
10070 id = name,
10071 icon = icon,
10072 colorType = colorType,
10073 inlineIcon = inlineIcon,
10074 r = r,
10075 g = g,
10076 b = b,
10077 startedTimers = {},
10078 mod = self,
10079 },
10080 mt
10081 )
10082 obj:AddOption(optionDefault, optionName, colorType)
10083 tinsert(self.timers, obj)
10084 return obj
10085 end
10086
10087 -- new constructor for the new auto-localized timer types
10088 -- note that the function might look unclear because it needs to handle different timer types, especially achievement timers need special treatment
10089 local function newTimer(self, timerType, timer, spellId, timerText, optionDefault, optionName, colorType, texture, inlineIcon, r, g, b)
10090 if type(timer) == "string" and timer:match("OptionVersion") then
10091 DBM:Debug("OptionVersion hack depricated, remove it from: "..spellId)
10092 return
10093 end
10094 if type(colorType) == "number" and colorType > 6 then
10095 DBM:Debug("texture is in the colorType arg for: "..spellId)
10096 end
10097 --Use option optionName for optionVersion as well, no reason to split.
10098 --This ensures that remaining arg positions match for auto generated and regular NewTimer
10099 local optionVersion
10100 if type(optionName) == "number" then
10101 optionVersion = optionName
10102 optionName = nil
10103 end
10104 local allowdouble
10105 if type(timer) == "string" and timer:match("d%d+") then
10106 allowdouble = true
10107 timer = tonumber(string.sub(timer, 2))
10108 end
10109 local spellName, icon
10110 local unparsedId = spellId
10111 if timerType == "achievement" then
10112 spellName = select(2, GetAchievementInfo(spellId))
10113 icon = type(texture) == "number" and select(10, GetAchievementInfo(texture)) or texture or spellId and select(10, GetAchievementInfo(spellId))
10114 elseif timerType == "cdspecial" or timerType == "nextspecial" or timerType == "phase" then
10115 icon = type(texture) == "number" and GetSpellTexture(texture) or texture or type(spellId) == "string" and select(4, EJ_GetSectionInfo(string.sub(spellId, 3))) ~= "" and select(4, EJ_GetSectionInfo(string.sub(spellId, 3))) or (type(spellId) == "number" and GetSpellTexture(spellId)) or "Interface\\Icons\\Spell_Nature_WispSplode"
10116 if timerType == "phase" then
10117 colorType = 6
10118 end
10119 elseif timerType == "roleplay" then
10120 icon = "Interface\\Icons\\Spell_Holy_BorrowedTime"
10121 colorType = 6
10122 else
10123 if type(spellId) == "string" and spellId:match("ej%d+") then
10124 spellName = EJ_GetSectionInfo(string.sub(spellId, 3)) or ""
10125 else
10126 spellName = GetSpellInfo(spellId or 0)
10127 end
10128 if spellName then
10129 icon = type(texture) == "number" and GetSpellTexture(texture) or texture or type(spellId) == "string" and select(4, EJ_GetSectionInfo(string.sub(spellId, 3))) ~= "" and select(4, EJ_GetSectionInfo(string.sub(spellId, 3))) or (type(spellId) == "number" and GetSpellTexture(spellId))
10130 else
10131 icon = nil
10132 end
10133 end
10134 spellName = spellName or tostring(spellId)
10135 local timerTextValue
10136 --If timertext is a number, accept it as a secondary auto translate spellid
10137 if timerText and type(timerText) == "number" and DBM.Options.ShortTimerText then
10138 timerTextValue = timerText
10139 else
10140 timerTextValue = self.localization.timers[timerText]
10141 end
10142 local id = "Timer"..(spellId or 0)..timerType..(optionVersion or "")
10143 local obj = setmetatable(
10144 {
10145 text = timerTextValue,
10146 type = timerType,
10147 spellId = spellId,
10148 timer = timer,
10149 id = id,
10150 icon = icon,
10151 colorType = colorType,
10152 inlineIcon = inlineIcon,
10153 r = r,
10154 g = g,
10155 b = b,
10156 allowdouble = allowdouble,
10157 startedTimers = {},
10158 mod = self,
10159 },
10160 mt
10161 )
10162 obj:AddOption(optionDefault, optionName, colorType)
10163 tinsert(self.timers, obj)
10164 -- todo: move the string creation to the GUI with SetFormattedString...
10165 if timerType == "achievement" then
10166 self.localization.options[id] = DBM_CORE_AUTO_TIMER_OPTIONS[timerType]:format(GetAchievementLink(spellId):gsub("%[(.+)%]", "%1"))
10167 elseif timerType == "cdspecial" or timerType == "nextspecial" or timerType == "phase" or timerType == "roleplay" then--Timers without spellid, generic
10168 self.localization.options[id] = DBM_CORE_AUTO_TIMER_OPTIONS[timerType]--Using more than 1 phase timer or more than 1 special timer will break this, fortunately you should NEVER use more than 1 of either in a mod
10169 else
10170 self.localization.options[id] = DBM_CORE_AUTO_TIMER_OPTIONS[timerType]:format(unparsedId)
10171 end
10172 return obj
10173 end
10174
10175 function bossModPrototype:NewTargetTimer(...)
10176 return newTimer(self, "target", ...)
10177 end
10178
10179 function bossModPrototype:NewBuffActiveTimer(...)
10180 return newTimer(self, "active", ...)
10181 end
10182
10183 function bossModPrototype:NewBuffFadesTimer(...)
10184 return newTimer(self, "fades", ...)
10185 end
10186
10187 function bossModPrototype:NewCastTimer(timer, ...)
10188 if tonumber(timer) and timer > 1000 then -- hehe :) best hack in DBM. This makes the first argument optional, so we can omit it to use the cast time from the spell id ;)
10189 local spellId = timer
10190 timer = select(4, GetSpellInfo(spellId)) or 1000 -- GetSpellInfo takes YOUR spell haste into account...WTF?
10191 local spellHaste = select(4, GetSpellInfo(53142)) / 10000 -- 53142 = Dalaran Portal, should have 10000 ms cast time
10192 timer = timer / spellHaste -- calculate the real cast time of the spell...
10193 return self:NewCastTimer(timer / 1000, spellId, ...)
10194 end
10195 return newTimer(self, "cast", timer, ...)
10196 end
10197
10198 function bossModPrototype:NewCastSourceTimer(timer, ...)
10199 if tonumber(timer) and timer > 1000 then -- hehe :) best hack in DBM. This makes the first argument optional, so we can omit it to use the cast time from the spell id ;)
10200 local spellId = timer
10201 timer = select(4, GetSpellInfo(spellId)) or 1000 -- GetSpellInfo takes YOUR spell haste into account...WTF?
10202 local spellHaste = select(4, GetSpellInfo(53142)) / 10000 -- 53142 = Dalaran Portal, should have 10000 ms cast time
10203 timer = timer / spellHaste -- calculate the real cast time of the spell...
10204 return self:NewCastSourceTimer(timer / 1000, spellId, ...)
10205 end
10206 return newTimer(self, "castsource", timer, ...)
10207 end
10208
10209 function bossModPrototype:NewCDTimer(...)
10210 return newTimer(self, "cd", ...)
10211 end
10212
10213 function bossModPrototype:NewCDCountTimer(...)
10214 return newTimer(self, "cdcount", ...)
10215 end
10216
10217 function bossModPrototype:NewCDSourceTimer(...)
10218 return newTimer(self, "cdsource", ...)
10219 end
10220
10221 function bossModPrototype:NewNextTimer(...)
10222 return newTimer(self, "next", ...)
10223 end
10224
10225 function bossModPrototype:NewNextCountTimer(...)
10226 return newTimer(self, "nextcount", ...)
10227 end
10228
10229 function bossModPrototype:NewNextSourceTimer(...)
10230 return newTimer(self, "nextsource", ...)
10231 end
10232
10233 function bossModPrototype:NewAchievementTimer(...)
10234 return newTimer(self, "achievement", ...)
10235 end
10236
10237 function bossModPrototype:NewCDSpecialTimer(...)
10238 return newTimer(self, "cdspecial", ...)
10239 end
10240
10241 function bossModPrototype:NewNextSpecialTimer(...)
10242 return newTimer(self, "nextspecial", ...)
10243 end
10244
10245 function bossModPrototype:NewPhaseTimer(...)
10246 return newTimer(self, "phase", ...)
10247 end
10248
10249 function bossModPrototype:NewRPTimer(...)
10250 return newTimer(self, "roleplay", ...)
10251 end
10252
10253 function bossModPrototype:NewAITimer(...)
10254 return newTimer(self, "ai", ...)
10255 end
10256
10257 function bossModPrototype:GetLocalizedTimerText(timerType, spellId)
10258 local spellName
10259 if timerType == "achievement" then
10260 spellName = select(2, GetAchievementInfo(spellId))
10261 elseif type(spellId) == "string" and spellId:match("ej%d+") then
10262 spellName = EJ_GetSectionInfo(string.sub(spellId, 3))
10263 else
10264 spellName = GetSpellInfo(spellId)
10265 end
10266 return pformat(DBM_CORE_AUTO_TIMER_TEXTS[timerType], spellName)
10267 end
10268end
10269
10270------------------------------
10271-- Berserk/Combat Objects --
10272------------------------------
10273do
10274 local enragePrototype = {}
10275 local mt = {__index = enragePrototype}
10276
10277 function enragePrototype:Start(timer)
10278 timer = timer or self.timer or 600
10279 timer = timer <= 0 and self.timer - timer or timer
10280 self.bar:SetTimer(timer)
10281 self.bar:Start()
10282 if self.warning1 then
10283 if timer > 660 then self.warning1:Schedule(timer - 600, 10, DBM_CORE_MIN) end
10284 if timer > 300 then self.warning1:Schedule(timer - 300, 5, DBM_CORE_MIN) end
10285 if timer > 180 then self.warning2:Schedule(timer - 180, 3, DBM_CORE_MIN) end
10286 end
10287 if self.warning2 then
10288 if timer > 60 then self.warning2:Schedule(timer - 60, 1, DBM_CORE_MIN) end
10289 if timer > 30 then self.warning2:Schedule(timer - 30, 30, DBM_CORE_SEC) end
10290 if timer > 10 then self.warning2:Schedule(timer - 10, 10, DBM_CORE_SEC) end
10291 end
10292 if self.countdown then
10293 if not DBM.Options.DontPlayPTCountdown then
10294 self.countdown:Start(timer)
10295 end
10296 if not DBM.Options.DontShowPTCountdownText then
10297 local threshold = DBM.Options.PTCountThreshold
10298 if timer > threshold then
10299 DBM:Schedule(timer-threshold, countDownTextDelay, threshold)
10300 else
10301 TimerTracker_OnEvent(TimerTracker, "START_TIMER", 2, timer, timer)
10302 end
10303 end
10304 end
10305 end
10306
10307 function enragePrototype:Schedule(t)
10308 return self.owner:Schedule(t, self.Start, self)
10309 end
10310
10311 function enragePrototype:Cancel()
10312 self.owner:Unschedule(self.Start, self)
10313 if self.warning1 then
10314 self.warning1:Cancel()
10315 end
10316 if self.warning2 then
10317 self.warning2:Cancel()
10318 end
10319 if self.countdown then
10320 DBM:Unschedule(countDownTextDelay)
10321 self.countdown:Cancel()
10322 TimerTracker_OnEvent(TimerTracker, "PLAYER_ENTERING_WORLD")
10323 end
10324 self.bar:Stop()
10325 end
10326 enragePrototype.Stop = enragePrototype.Cancel
10327
10328 function bossModPrototype:NewBerserkTimer(timer, text, barText, barIcon)
10329 timer = timer or 600
10330 local warning1 = self:NewAnnounce(text or DBM_CORE_GENERIC_WARNING_BERSERK, 1, nil, "warning_berserk", false)
10331 local warning2 = self:NewAnnounce(text or DBM_CORE_GENERIC_WARNING_BERSERK, 4, nil, "warning_berserk", false)
10332 local bar = self:NewTimer(timer, barText or DBM_CORE_GENERIC_TIMER_BERSERK, barIcon or 28131, nil, "timer_berserk")
10333 local obj = setmetatable(
10334 {
10335 warning1 = warning1,
10336 warning2 = warning2,
10337 bar = bar,
10338 timer = timer,
10339 owner = self
10340 },
10341 mt
10342 )
10343 return obj
10344 end
10345
10346 function bossModPrototype:NewCombatTimer(timer, text, barText, barIcon)
10347 timer = timer or 10
10348 local bar = self:NewTimer(timer, barText or DBM_CORE_GENERIC_TIMER_COMBAT, barIcon or "Interface\\Icons\\ability_warrior_offensivestance", nil, "timer_combat")
10349 local countdown = self:NewCountdown(0, 0, nil, false, nil, true)
10350 local obj = setmetatable(
10351 {
10352 bar = bar,
10353 timer = timer,
10354 countdown = countdown,
10355 owner = self
10356 },
10357 mt
10358 )
10359 return obj
10360 end
10361end
10362
10363---------------
10364-- Options --
10365---------------
10366function bossModPrototype:AddBoolOption(name, default, cat, func, extraOption)
10367 cat = cat or "misc"
10368 self.DefaultOptions[name] = (default == nil) or default
10369 if cat == "timer" then
10370 self.DefaultOptions[name.."TColor"] = extraOption or 0
10371 end
10372 if default and type(default) == "string" then
10373 default = self:GetRoleFlagValue(default)
10374 end
10375 self.Options[name] = (default == nil) or default
10376 if cat == "timer" then
10377 self.Options[name.."TColor"] = extraOption or 0
10378 end
10379 self:SetOptionCategory(name, cat)
10380 if func then
10381 self.optionFuncs = self.optionFuncs or {}
10382 self.optionFuncs[name] = func
10383 end
10384end
10385
10386function bossModPrototype:AddSpecialWarningOption(name, default, defaultSound, cat)
10387 cat = cat or "misc"
10388 self.DefaultOptions[name] = (default == nil) or default
10389 self.DefaultOptions[name.."SWSound"] = defaultSound or 1
10390 self.DefaultOptions[name.."SWNote"] = true
10391 if default and type(default) == "string" then
10392 default = self:GetRoleFlagValue(default)
10393 end
10394 self.Options[name] = (default == nil) or default
10395 self.Options[name.."SWSound"] = defaultSound or 1
10396 self.Options[name.."SWNote"] = true
10397 self:SetOptionCategory(name, cat)
10398end
10399
10400function bossModPrototype:AddSetIconOption(name, spellId, default, isHostile)
10401 self.DefaultOptions[name] = (default == nil) or default
10402 if default and type(default) == "string" then
10403 default = self:GetRoleFlagValue(default)
10404 end
10405 self.Options[name] = (default == nil) or default
10406 self:SetOptionCategory(name, "misc")
10407 if isHostile then
10408 if not self.findFastestComputer then
10409 self.findFastestComputer = {}
10410 end
10411 self.findFastestComputer[#self.findFastestComputer + 1] = name
10412 self.localization.options[name] = DBM_CORE_AUTO_ICONS_OPTION_TEXT2:format(spellId)
10413 else
10414 self.localization.options[name] = DBM_CORE_AUTO_ICONS_OPTION_TEXT:format(spellId)
10415 end
10416end
10417
10418function bossModPrototype:AddArrowOption(name, spellId, default, isRunTo)
10419 if isRunTo == true then isRunTo = 2 end--Support legacy
10420 self.DefaultOptions[name] = (default == nil) or default
10421 if default and type(default) == "string" then
10422 default = self:GetRoleFlagValue(default)
10423 end
10424 self.Options[name] = (default == nil) or default
10425 self:SetOptionCategory(name, "misc")
10426 if isRunTo == 2 then
10427 self.localization.options[name] = DBM_CORE_AUTO_ARROW_OPTION_TEXT:format(spellId)
10428 elseif isRunTo == 3 then
10429 self.localization.options[name] = DBM_CORE_AUTO_ARROW_OPTION_TEXT3:format(spellId)
10430 else
10431 self.localization.options[name] = DBM_CORE_AUTO_ARROW_OPTION_TEXT2:format(spellId)
10432 end
10433end
10434
10435function bossModPrototype:AddRangeFrameOption(range, spellId, default)
10436 self.DefaultOptions["RangeFrame"] = (default == nil) or default
10437 if default and type(default) == "string" then
10438 default = self:GetRoleFlagValue(default)
10439 end
10440 self.Options["RangeFrame"] = (default == nil) or default
10441 self:SetOptionCategory("RangeFrame", "misc")
10442 if spellId then
10443 self.localization.options["RangeFrame"] = DBM_CORE_AUTO_RANGE_OPTION_TEXT:format(range, spellId)
10444 else
10445 self.localization.options["RangeFrame"] = DBM_CORE_AUTO_RANGE_OPTION_TEXT_SHORT:format(range)
10446 end
10447end
10448
10449function bossModPrototype:AddHudMapOption(name, spellId, default)
10450 self.DefaultOptions[name] = (default == nil) or default
10451 if default and type(default) == "string" then
10452 default = self:GetRoleFlagValue(default)
10453 end
10454 self.Options[name] = (default == nil) or default
10455 self:SetOptionCategory(name, "misc")
10456 if spellId then
10457 self.localization.options[name] = DBM_CORE_AUTO_HUD_OPTION_TEXT:format(spellId)
10458 else
10459 self.localization.options[name] = DBM_CORE_AUTO_HUD_OPTION_TEXT_MULTI
10460 end
10461end
10462
10463function bossModPrototype:AddInfoFrameOption(spellId, default)
10464 self.DefaultOptions["InfoFrame"] = (default == nil) or default
10465 if default and type(default) == "string" then
10466 default = self:GetRoleFlagValue(default)
10467 end
10468 self.Options["InfoFrame"] = (default == nil) or default
10469 self:SetOptionCategory("InfoFrame", "misc")
10470 self.localization.options["InfoFrame"] = DBM_CORE_AUTO_INFO_FRAME_OPTION_TEXT:format(spellId)
10471end
10472
10473function bossModPrototype:AddReadyCheckOption(questId, default)
10474 self.readyCheckQuestId = questId
10475 self.DefaultOptions["ReadyCheck"] = (default == nil) or default
10476 if default and type(default) == "string" then
10477 default = self:GetRoleFlagValue(default)
10478 end
10479 self.Options["ReadyCheck"] = (default == nil) or default
10480 self.localization.options["ReadyCheck"] = DBM_CORE_AUTO_READY_CHECK_OPTION_TEXT
10481 self:SetOptionCategory("ReadyCheck", "misc")
10482end
10483
10484function bossModPrototype:AddSliderOption(name, minValue, maxValue, valueStep, default, cat, func)
10485 cat = cat or "misc"
10486 self.DefaultOptions[name] = {type = "slider", value = default or 0}
10487 self.Options[name] = default or 0
10488 self:SetOptionCategory(name, cat)
10489 self.sliders = self.sliders or {}
10490 self.sliders[name] = {
10491 minValue = minValue,
10492 maxValue = maxValue,
10493 valueStep = valueStep,
10494 }
10495 if func then
10496 self.optionFuncs = self.optionFuncs or {}
10497 self.optionFuncs[name] = func
10498 end
10499end
10500
10501function bossModPrototype:AddButton(name, onClick, cat, func)
10502 cat = cat or "misc"
10503 self:SetOptionCategory(name, cat)
10504 self.buttons = self.buttons or {}
10505 self.buttons[name] = onClick
10506 if func then
10507 self.optionFuncs = self.optionFuncs or {}
10508 self.optionFuncs[name] = func
10509 end
10510end
10511
10512-- FIXME: this function does not reset any settings to default if you remove an option in a later revision and a user has selected this option in an earlier revision were it still was available
10513-- this will be fixed as soon as it is necessary due to removed options ;-)
10514function bossModPrototype:AddDropdownOption(name, options, default, cat, func)
10515 cat = cat or "misc"
10516 self.DefaultOptions[name] = {type = "dropdown", value = default}
10517 self.Options[name] = default
10518 self:SetOptionCategory(name, cat)
10519 self.dropdowns = self.dropdowns or {}
10520 self.dropdowns[name] = options
10521 if func then
10522 self.optionFuncs = self.optionFuncs or {}
10523 self.optionFuncs[name] = func
10524 end
10525end
10526
10527function bossModPrototype:AddOptionSpacer(cat)
10528 cat = cat or "misc"
10529 if self.optionCategories[cat] then
10530 tinsert(self.optionCategories[cat], DBM_OPTION_SPACER)
10531 end
10532end
10533
10534function bossModPrototype:AddOptionLine(text, cat)
10535 cat = cat or "misc"
10536 if not self.optionCategories[cat] then
10537 self.optionCategories[cat] = {}
10538 end
10539 if self.optionCategories[cat] then
10540 tinsert(self.optionCategories[cat], {line = true, text = text})
10541 end
10542end
10543
10544function bossModPrototype:AddAnnounceSpacer()
10545 return self:AddOptionSpacer("announce")
10546end
10547
10548function bossModPrototype:AddTimerSpacer()
10549 return self:AddOptionSpacer("timer")
10550end
10551
10552function bossModPrototype:AddAnnounceLine(text)
10553 return self:AddOptionLine(text, "announce")
10554end
10555
10556function bossModPrototype:AddTimerLine(text)
10557 return self:AddOptionLine(text, "timer")
10558end
10559
10560function bossModPrototype:RemoveOption(name)
10561 self.Options[name] = nil
10562 for i, options in pairs(self.optionCategories) do
10563 removeEntry(options, name)
10564 if #options == 0 then
10565 self.optionCategories[i] = nil
10566 end
10567 end
10568 if self.optionFuncs then
10569 self.optionFuncs[name] = nil
10570 end
10571end
10572
10573function bossModPrototype:SetOptionCategory(name, cat)
10574 for _, options in pairs(self.optionCategories) do
10575 removeEntry(options, name)
10576 end
10577 if not self.optionCategories[cat] then
10578 self.optionCategories[cat] = {}
10579 end
10580 tinsert(self.optionCategories[cat], name)
10581end
10582
10583--------------
10584-- Combat --
10585--------------
10586function bossModPrototype:RegisterCombat(cType, ...)
10587 if cType then
10588 cType = cType:lower()
10589 end
10590 local info = {
10591 type = cType,
10592 mob = self.creatureId,
10593 eId = self.encounterId,
10594 name = self.localization.general.name or self.id,
10595 msgs = (cType ~= "combat") and {...},
10596 mod = self
10597 }
10598 if self.multiMobPullDetection then
10599 info.multiMobPullDetection = self.multiMobPullDetection
10600 end
10601 if self.multiEncounterPullDetection then
10602 info.multiEncounterPullDetection = self.multiEncounterPullDetection
10603 end
10604 if self.noESDetection then
10605 info.noESDetection = self.noESDetection
10606 end
10607 if self.noEEDetection then
10608 info.noEEDetection = self.noEEDetection
10609 end
10610 if self.noRegenDetection then
10611 info.noRegenDetection = self.noRegenDetection
10612 end
10613 if self.noWBEsync then
10614 info.noWBEsync = self.noWBEsync
10615 end
10616 -- use pull-mobs as kill mobs by default, can be overriden by RegisterKill
10617 if self.multiMobPullDetection then
10618 for i, v in ipairs(self.multiMobPullDetection) do
10619 info.killMobs = info.killMobs or {}
10620 info.killMobs[v] = true
10621 end
10622 end
10623 self.combatInfo = info
10624 if not self.zones then return end
10625 for v in pairs(self.zones) do
10626 combatInfo[v] = combatInfo[v] or {}
10627 tinsert(combatInfo[v], info)
10628 end
10629end
10630
10631-- needs to be called _AFTER_ RegisterCombat
10632function bossModPrototype:RegisterKill(msgType, ...)
10633 if not self.combatInfo then
10634 error("mod.combatInfo not yet initialized, use mod:RegisterCombat before using this method", 2)
10635 end
10636 if msgType == "kill" then
10637 if select("#", ...) > 0 then -- calling this method with 0 IDs means "use the values from SetCreatureID", this is already done by RegisterCombat as calling RegisterKill should be optional --> mod:RegisterKill("kill") with no IDs is never necessary
10638 self.combatInfo.killMobs = {}
10639 for i = 1, select("#", ...) do
10640 local v = select(i, ...)
10641 if type(v) == "number" then
10642 self.combatInfo.killMobs[v] = true
10643 end
10644 end
10645 end
10646 else
10647 self.combatInfo.killType = msgType
10648 self.combatInfo.killMsgs = {}
10649 for i = 1, select("#", ...) do
10650 local v = select(i, ...)
10651 self.combatInfo.killMsgs[v] = true
10652 end
10653 end
10654end
10655
10656function bossModPrototype:SetDetectCombatInVehicle(flag)
10657 if not self.combatInfo then
10658 error("mod.combatInfo not yet initialized, use mod:RegisterCombat before using this method", 2)
10659 end
10660 self.combatInfo.noCombatInVehicle = not flag
10661end
10662
10663function bossModPrototype:SetCreatureID(...)
10664 self.creatureId = ...
10665 if select("#", ...) > 1 then
10666 self.multiMobPullDetection = {...}
10667 if self.combatInfo then
10668 self.combatInfo.multiMobPullDetection = self.multiMobPullDetection
10669 end
10670 for i = 1, select("#", ...) do
10671 local cId = select(i, ...)
10672 bossIds[cId] = true
10673 end
10674 else
10675 local cId = ...
10676 bossIds[cId] = true
10677 end
10678end
10679
10680function bossModPrototype:SetEncounterID(...)
10681 self.encounterId = ...
10682 if select("#", ...) > 1 then
10683 self.multiEncounterPullDetection = {...}
10684 if self.combatInfo then
10685 self.combatInfo.multiEncounterPullDetection = self.multiEncounterPullDetection
10686 end
10687 end
10688end
10689
10690function bossModPrototype:DisableESCombatDetection()
10691 self.noESDetection = true
10692 if self.combatInfo then
10693 self.combatInfo.noESDetection = true
10694 end
10695end
10696
10697function bossModPrototype:DisableEEKillDetection()
10698 self.noEEDetection = true
10699 if self.combatInfo then
10700 self.combatInfo.noEEDetection = true
10701 end
10702end
10703
10704function bossModPrototype:DisableRegenDetection()
10705 self.noRegenDetection = true
10706 if self.combatInfo then
10707 self.combatInfo.noRegenDetection = true
10708 end
10709end
10710
10711function bossModPrototype:DisableWBEngageSync()
10712 self.noWBEsync = true
10713 if self.combatInfo then
10714 self.combatInfo.noWBEsync = true
10715 end
10716end
10717
10718function bossModPrototype:IsInCombat()
10719 return self.inCombat
10720end
10721
10722function bossModPrototype:IsAlive()
10723 return not UnitIsDeadOrGhost("player")
10724end
10725
10726function bossModPrototype:SetMinCombatTime(t)
10727 self.minCombatTime = t
10728end
10729
10730-- needs to be called after RegisterCombat
10731function bossModPrototype:SetWipeTime(t)
10732 if not self.combatInfo then
10733 error("mod.combatInfo not yet initialized, use mod:RegisterCombat before using this method", 2)
10734 end
10735 self.combatInfo.wipeTimer = t
10736end
10737
10738-- fix for LFR ToES Tsulong combat detection bug after killed.
10739function bossModPrototype:SetReCombatTime(t, t2)--T1, after kill. T2 after wipe
10740 self.reCombatTime = t
10741 self.reCombatTime2 = t2
10742end
10743
10744-----------------------
10745-- Synchronization --
10746-----------------------
10747function bossModPrototype:SendSync(event, ...)
10748 event = event or ""
10749 local arg = select("#", ...) > 0 and strjoin("\t", tostringall(...)) or ""
10750 local str = ("%s\t%s\t%s\t%s"):format(self.id, self.revision or 0, event, arg)
10751 local spamId = self.id .. event .. arg -- *not* the same as the sync string, as it doesn't use the revision information
10752 local time = GetTime()
10753 --Mod syncs are more strict and enforce latency threshold always.
10754 --Do not put latency check in main sendSync local function (line 313) though as we still want to get version information, etc from these users.
10755 if not modSyncSpam[spamId] or (time - modSyncSpam[spamId]) > 8 then
10756 self:ReceiveSync(event, nil, self.revision or 0, tostringall(...))
10757 sendSync("M", str)
10758 end
10759end
10760
10761function bossModPrototype:ReceiveSync(event, sender, revision, ...)
10762 local spamId = self.id .. event .. strjoin("\t", ...)
10763 local time = GetTime()
10764 if (not modSyncSpam[spamId] or (time - modSyncSpam[spamId]) > self.SyncThreshold) and self.OnSync and (not (self.blockSyncs and sender)) and (not sender or (not self.minSyncRevision or revision >= self.minSyncRevision)) then
10765 modSyncSpam[spamId] = time
10766 -- we have to use the sender as last argument for compatibility reasons (stupid old API...)
10767 -- avoid table allocations for frequently used number of arguments
10768 if select("#", ...) <= 1 then
10769 -- syncs with no arguments have an empty argument (also for compatibility reasons)
10770 self:OnSync(event, ... or "", sender)
10771 elseif select("#", ...) == 2 then
10772 self:OnSync(event, ..., select(2, ...), sender)
10773 else
10774 local tmp = { ... }
10775 tmp[#tmp + 1] = sender
10776 self:OnSync(event, unpack(tmp))
10777 end
10778 end
10779end
10780
10781function bossModPrototype:SetRevision(revision)
10782 revision = tonumber(revision or "")
10783 if not revision then
10784 -- bad revision: either forgot the svn keyword or using git svn
10785 revision = DBM.Revision
10786 end
10787 self.revision = revision
10788end
10789
10790function bossModPrototype:SetMinSyncRevision(revision)
10791 self.minSyncRevision = revision
10792end
10793
10794function bossModPrototype:SetHotfixNoticeRev(revision)
10795 self.hotfixNoticeRev = revision
10796end
10797
10798-----------------
10799-- Scheduler --
10800-----------------
10801function bossModPrototype:Schedule(t, f, ...)
10802 return schedule(t, f, self, ...)
10803end
10804
10805function bossModPrototype:Unschedule(f, ...)
10806 return unschedule(f, self, ...)
10807end
10808
10809function bossModPrototype:ScheduleMethod(t, method, ...)
10810 if not self[method] then
10811 error(("Method %s does not exist"):format(tostring(method)), 2)
10812 end
10813 return self:Schedule(t, self[method], self, ...)
10814end
10815bossModPrototype.ScheduleEvent = bossModPrototype.ScheduleMethod
10816
10817function bossModPrototype:UnscheduleMethod(method, ...)
10818 if not self[method] then
10819 error(("Method %s does not exist"):format(tostring(method)), 2)
10820 end
10821 return self:Unschedule(self[method], self, ...)
10822end
10823bossModPrototype.UnscheduleEvent = bossModPrototype.UnscheduleMethod
10824
10825-------------
10826-- Icons --
10827-------------
10828
10829do
10830 local scanExpires = {}
10831 local addsIcon = {}
10832 local addsIconSet = {}
10833
10834 function bossModPrototype:SetIcon(target, icon, timer)
10835 if not target then return end--Fix a rare bug where target becomes nil at last second (end combat fires and clears targets)
10836 if DBM.Options.DontSetIcons or not enableIcons or DBM:GetRaidRank(playerName) == 0 then
10837 return
10838 end
10839 self:UnscheduleMethod("SetIcon", target)
10840 if type(icon) ~= "number" or type(target) ~= "string" then--icon/target probably backwards.
10841 DBM:Debug("SetIcon is being used impropperly. Check icon/target order")
10842 return--Fail silently instead of spamming icon lua errors if we screw up
10843 end
10844 icon = icon and icon >= 0 and icon <= 8 and icon or 8
10845 local uId = DBM:GetRaidUnitId(target)
10846 if uId and UnitIsUnit(uId, "player") and DBM:GetNumRealGroupMembers() < 2 then return end--Solo raid, no reason to put icon on yourself.
10847 if uId or UnitExists(target) then--target accepts uid, unitname both.
10848 uId = uId or target
10849 --save previous icon into a table.
10850 local oldIcon = self:GetIcon(uId) or 0
10851 if not self.iconRestore[uId] then
10852 self.iconRestore[uId] = oldIcon
10853 end
10854 --set icon
10855 if oldIcon ~= icon then--Don't set icon if it's already set to what we're setting it to
10856 SetRaidTarget(uId, self.iconRestore[uId] and icon == 0 and self.iconRestore[uId] or icon)
10857 end
10858 --schedule restoring old icon if timer enabled.
10859 if timer then
10860 self:ScheduleMethod(timer, "SetIcon", target, 0)
10861 end
10862 end
10863 end
10864
10865 do
10866 local iconSortTable = {}
10867 local iconSet = 0
10868
10869 local function sort_by_group(v1, v2)
10870 return DBM:GetRaidSubgroup(DBM:GetUnitFullName(v1)) < DBM:GetRaidSubgroup(DBM:GetUnitFullName(v2))
10871 end
10872
10873 local function clearSortTable()
10874 twipe(iconSortTable)
10875 iconSet = 0
10876 end
10877
10878 function bossModPrototype:SetIconByAlphaTable(returnFunc)
10879 tsort(iconSortTable)--Sorted alphabetically
10880 for i = 1, #iconSortTable do
10881 local target = iconSortTable[i]
10882 if i > 8 then
10883 DBM:Debug("Too many players to set icons, reconsider where using icons", 2)
10884 return
10885 end
10886 if not self.iconRestore[target] then
10887 local oldIcon = self:GetIcon(target) or 0
10888 self.iconRestore[target] = oldIcon
10889 end
10890 SetRaidTarget(target, i)--Icons match number in table in alpha sort
10891 if returnFunc then
10892 self[returnFunc](self, target, i)--Send icon and target to returnFunc. (Generally used by announce icon targets to raid chat feature)
10893 end
10894 end
10895 C_TimerAfter(1.5, clearSortTable)--Table wipe delay so if icons go out too early do to low fps or bad latency, when they get new target on table, resort and reapplying should auto correct teh icon within .2-.4 seconds at most.
10896 end
10897
10898 function bossModPrototype:SetAlphaIcon(delay, target, maxIcon, returnFunc)
10899 if not target then return end
10900 if DBM.Options.DontSetIcons or not enableIcons or DBM:GetRaidRank(playerName) == 0 then
10901 return
10902 end
10903 local uId = DBM:GetRaidUnitId(target)
10904 if uId or UnitExists(target) then--target accepts uid, unitname both.
10905 uId = uId or target
10906 local foundDuplicate = false
10907 for i = #iconSortTable, 1, -1 do
10908 if iconSortTable[i] == uId then
10909 foundDuplicate = true
10910 break
10911 end
10912 end
10913 if not foundDuplicate then
10914 iconSet = iconSet + 1
10915 tinsert(iconSortTable, uId)
10916 end
10917 self:UnscheduleMethod("SetIconByAlphaTable")
10918 if maxIcon and iconSet == maxIcon then
10919 self:SetIconByAlphaTable(returnFunc)
10920 elseif self:LatencyCheck() then--lag can fail the icons so we check it before allowing.
10921 self:ScheduleMethod(delay or 0.5, "SetIconByAlphaTable", returnFunc)
10922 end
10923 end
10924 end
10925
10926 function bossModPrototype:SetIconBySortedTable(startIcon, reverseIcon, returnFunc)
10927 tsort(iconSortTable, sort_by_group)
10928 local icon = startIcon or 1
10929 for i, v in ipairs(iconSortTable) do
10930 if not self.iconRestore[v] then
10931 local oldIcon = self:GetIcon(v) or 0
10932 self.iconRestore[v] = oldIcon
10933 end
10934 SetRaidTarget(v, icon)--do not use SetIcon function again. It already checked in SetSortedIcon function.
10935 if reverseIcon then
10936 icon = icon - 1
10937 else
10938 icon = icon + 1
10939 end
10940 if returnFunc then
10941 self[returnFunc](self, v, icon)--Send icon and target to returnFunc. (Generally used by announce icon targets to raid chat feature)
10942 end
10943 end
10944 C_TimerAfter(1.5, clearSortTable)--Table wipe delay so if icons go out too early do to low fps or bad latency, when they get new target on table, resort and reapplying should auto correct teh icon within .2-.4 seconds at most.
10945 end
10946
10947 function bossModPrototype:SetSortedIcon(delay, target, startIcon, maxIcon, reverseIcon, returnFunc)
10948 if not target then return end
10949 if DBM.Options.DontSetIcons or not enableIcons or DBM:GetRaidRank(playerName) == 0 then
10950 return
10951 end
10952 if not startIcon then startIcon = 1 end
10953 startIcon = startIcon and startIcon >= 0 and startIcon <= 8 and startIcon or 8
10954 local uId = DBM:GetRaidUnitId(target)
10955 if uId or UnitExists(target) then--target accepts uid, unitname both.
10956 uId = uId or target
10957 local foundDuplicate = false
10958 for i = #iconSortTable, 1, -1 do
10959 if iconSortTable[i] == uId then
10960 foundDuplicate = true
10961 break
10962 end
10963 end
10964 if not foundDuplicate then
10965 iconSet = iconSet + 1
10966 tinsert(iconSortTable, uId)
10967 end
10968 self:UnscheduleMethod("SetIconBySortedTable")
10969 if maxIcon and iconSet == maxIcon then
10970 self:SetIconBySortedTable(startIcon, reverseIcon, returnFunc)
10971 elseif self:LatencyCheck() then--lag can fail the icons so we check it before allowing.
10972 self:ScheduleMethod(delay or 0.5, "SetIconBySortedTable", startIcon, reverseIcon, returnFunc)
10973 end
10974 end
10975 end
10976 end
10977
10978 function bossModPrototype:GetIcon(uId)
10979 return UnitExists(uId) and GetRaidTargetIndex(uId)
10980 end
10981
10982 function bossModPrototype:RemoveIcon(target)
10983 return self:SetIcon(target, 0)
10984 end
10985
10986 function bossModPrototype:ClearIcons()
10987 if IsInRaid() then
10988 for i = 1, GetNumGroupMembers() do
10989 if UnitExists("raid"..i) and GetRaidTargetIndex("raid"..i) then
10990 SetRaidTarget("raid"..i, 0)
10991 end
10992 end
10993 else
10994 for i = 1, GetNumSubgroupMembers() do
10995 if UnitExists("party"..i) and GetRaidTargetIndex("party"..i) then
10996 SetRaidTarget("party"..i, 0)
10997 end
10998 end
10999 end
11000 end
11001
11002 function bossModPrototype:CanSetIcon(optionName)
11003 if canSetIcons[optionName] then
11004 return true
11005 end
11006 return false
11007 end
11008
11009 local mobUids = {"mouseover", "target", "boss1", "boss2", "boss3", "boss4", "boss5"}
11010 function bossModPrototype:ScanForMobs(creatureID, iconSetMethod, mobIcon, maxIcon, scanInterval, scanningTime, optionName, isFriendly, secondCreatureID)
11011 if not optionName then optionName = self.findFastestComputer[1] end
11012 if canSetIcons[optionName] then
11013 --Declare variables.
11014 local timeNow = GetTime()
11015 local creatureID = creatureID--This function must not be used to boss, so remove self.creatureId. Accepts cid, guid and cid table
11016 local iconSetMethod = iconSetMethod or 0--Set IconSetMethod -- 0: Descending / 1:Ascending / 2: Force Set / 9:Force Stop
11017 --With different scanID, this function can support multi scanning same time. Required for Nazgrim.
11018 local scanID = 0
11019 if type(creatureID) == "number" then
11020 scanID = creatureID --guid and table no not supports multi scanning. only cid supports multi scanning
11021 end
11022 if iconSetMethod == 9 then--Force stop scanning
11023 --clear variables
11024 scanExpires[scanID] = nil
11025 addsIcon[scanID] = nil
11026 addsIconSet[scanID] = nil
11027 return
11028 end
11029 if not addsIcon[scanID] then addsIcon[scanID] = mobIcon or 8 end
11030 if not addsIconSet[scanID] then addsIconSet[scanID] = 0 end
11031 if not scanExpires[scanID] then scanExpires[scanID] = timeNow + scanningTime end
11032 local maxIcon = maxIcon or 8 --We only have 8 icons.
11033 local isFriendly = isFriendly or false
11034 local secondCreatureID = secondCreatureID or 0
11035 local scanInterval = scanInterval or 0.2
11036 local scanningTime = scanningTime or 8
11037 --DO SCAN NOW
11038 for uId in DBM:GetGroupMembers() do
11039 if not self.iconRestore[uId] then
11040 local oldIcon = self:GetIcon(uId) or 0
11041 self.iconRestore[uId] = oldIcon
11042 end
11043 local unitid = uId.."target"
11044 local guid = UnitGUID(unitid)
11045 local cid = self:GetCIDFromGUID(guid)
11046 local isEnemy = UnitIsEnemy("player", unitid) or true--If api returns nil, assume it's an enemy
11047 local isFiltered = false
11048 if not isFriendly and not isEnemy then
11049 isFiltered = true
11050 DBM:Debug("ScanForMobs aborting because friendly mob", 2)
11051 end
11052 if not isFiltered then
11053 if guid and type(creatureID) == "table" and creatureID[cid] and not addsGUIDs[guid] then
11054 if type(creatureID[cid]) == "number" then
11055 SetRaidTarget(unitid, creatureID[cid])
11056 else
11057 SetRaidTarget(unitid, addsIcon[scanID])
11058 if iconSetMethod == 1 then
11059 addsIcon[scanID] = addsIcon[scanID] + 1
11060 else
11061 addsIcon[scanID] = addsIcon[scanID] - 1
11062 end
11063 end
11064 addsGUIDs[guid] = true
11065 addsIconSet[scanID] = addsIconSet[scanID] + 1
11066 if addsIconSet[scanID] >= maxIcon then--stop scan immediately to save cpu
11067 --clear variables
11068 scanExpires[scanID] = nil
11069 addsIcon[scanID] = nil
11070 addsIconSet[scanID] = nil
11071 return
11072 end
11073 elseif guid and ((guid == creatureID) or (cid == creatureID or cid == secondCreatureID)) and not addsGUIDs[guid] then
11074 if iconSetMethod == 2 then
11075 SetRaidTarget(unitid, mobIcon)
11076 else
11077 SetRaidTarget(unitid, addsIcon[scanID])
11078 if iconSetMethod == 1 then
11079 addsIcon[scanID] = addsIcon[scanID] + 1
11080 else
11081 addsIcon[scanID] = addsIcon[scanID] - 1
11082 end
11083 end
11084 addsGUIDs[guid] = true
11085 addsIconSet[scanID] = addsIconSet[scanID] + 1
11086 if addsIconSet[scanID] >= maxIcon then--stop scan immediately to save cpu
11087 --clear variables
11088 scanExpires[scanID] = nil
11089 addsIcon[scanID] = nil
11090 addsIconSet[scanID] = nil
11091 return
11092 end
11093 end
11094 end
11095 end
11096 for _, unitid2 in ipairs(mobUids) do
11097 local guid2 = UnitGUID(unitid2)
11098 local cid2 = self:GetCIDFromGUID(guid2)
11099 local isEnemy = UnitIsEnemy("player", unitid2)
11100 local isFiltered = false
11101 if not isFriendly and not isEnemy then
11102 isFiltered = true
11103 end
11104 if not isFiltered then
11105 if guid2 and type(creatureID) == "table" and creatureID[cid2] and not addsGUIDs[guid2] then
11106 if type(creatureID[cid2]) == "number" then
11107 SetRaidTarget(unitid2, creatureID[cid2])
11108 else
11109 SetRaidTarget(unitid2, addsIcon[scanID])
11110 if iconSetMethod == 1 then
11111 addsIcon[scanID] = addsIcon[scanID] + 1
11112 else
11113 addsIcon[scanID] = addsIcon[scanID] - 1
11114 end
11115 end
11116 addsGUIDs[guid2] = true
11117 addsIconSet[scanID] = addsIconSet[scanID] + 1
11118 if addsIconSet[scanID] >= maxIcon then--stop scan immediately to save cpu
11119 --clear variables
11120 scanExpires[scanID] = nil
11121 addsIcon[scanID] = nil
11122 addsIconSet[scanID] = nil
11123 return
11124 end
11125 elseif guid2 and ((guid2 == creatureID) or (cid2 == creatureID or cid2 == secondCreatureID)) and not addsGUIDs[guid2] then
11126 if iconSetMethod == 2 then
11127 SetRaidTarget(unitid2, mobIcon)
11128 else
11129 SetRaidTarget(unitid2, addsIcon[scanID])
11130 if iconSetMethod == 1 then
11131 addsIcon[scanID] = addsIcon[scanID] + 1
11132 else
11133 addsIcon[scanID] = addsIcon[scanID] - 1
11134 end
11135 end
11136 addsGUIDs[guid2] = true
11137 addsIconSet[scanID] = addsIconSet[scanID] + 1
11138 if addsIconSet[scanID] >= maxIcon then--stop scan immediately to save cpu
11139 --clear variables
11140 scanExpires[scanID] = nil
11141 addsIcon[scanID] = nil
11142 addsIconSet[scanID] = nil
11143 return
11144 end
11145 end
11146 end
11147 end
11148 if timeNow < scanExpires[scanID] then--scan for limited times.
11149 self:ScheduleMethod(scanInterval, "ScanForMobs", creatureID, iconSetMethod, mobIcon, maxIcon, scanInterval, scanningTime, optionName, isFriendly, secondCreatureID)
11150 else
11151 DBM:Debug("Stopping ScanForMobs for: "..(optionName or "nil"), 2)
11152 --clear variables
11153 scanExpires[scanID] = nil
11154 addsIcon[scanID] = nil
11155 addsIconSet[scanID] = nil
11156 --Do not wipe adds GUID table here, it's wiped by :Stop() which is called by EndCombat
11157 end
11158 end
11159 end
11160end
11161
11162-----------------------
11163-- Model Functions --
11164-----------------------
11165function bossModPrototype:SetModelScale(scale)
11166 self.modelScale = scale
11167end
11168
11169function bossModPrototype:SetModelOffset(x, y, z)
11170 self.modelOffsetX = x
11171 self.modelOffsetY = y
11172 self.modelOffsetZ = z
11173end
11174
11175function bossModPrototype:SetModelRotation(r)
11176 self.modelRotation = r
11177end
11178
11179function bossModPrototype:SetModelMoveSpeed(v)
11180 self.modelMoveSpeed = v
11181end
11182
11183function bossModPrototype:SetModelID(id)
11184 self.modelId = id
11185end
11186
11187function bossModPrototype:SetModelSound(long, short)--PlaySoundFile prototype for model viewer, long is long sound, short is a short clip, configurable in UI, both sound paths defined in boss mods.
11188 self.modelSoundLong = long
11189 self.modelSoundShort = short
11190end
11191
11192function bossModPrototype:EnableModel()
11193 self.modelEnabled = true
11194end
11195
11196function bossModPrototype:DisableModel()
11197 self.modelEnabled = nil
11198end
11199
11200--------------------
11201-- Localization --
11202--------------------
11203function bossModPrototype:GetLocalizedStrings()
11204 self.localization.miscStrings.name = self.localization.general.name
11205 return self.localization.miscStrings
11206end
11207
11208-- Not really good, needs a few updates
11209do
11210 local modLocalizations = {}
11211 local modLocalizationPrototype = {}
11212 local mt = {__index = modLocalizationPrototype}
11213 local returnKey = {__index = function(t, k) return k end}
11214 local defaultCatLocalization = {
11215 __index = setmetatable({
11216 timer = DBM_CORE_OPTION_CATEGORY_TIMERS,
11217 announce = DBM_CORE_OPTION_CATEGORY_WARNINGS,
11218 announceother = DBM_CORE_OPTION_CATEGORY_WARNINGS_OTHER,
11219 announcepersonal = DBM_CORE_OPTION_CATEGORY_WARNINGS_YOU,
11220 announcerole = DBM_CORE_OPTION_CATEGORY_WARNINGS_ROLE,
11221 sound = DBM_CORE_OPTION_CATEGORY_SOUNDS,
11222 misc = MISCELLANEOUS
11223 }, returnKey)
11224 }
11225 local defaultTimerLocalization = {
11226 __index = setmetatable({
11227 timer_berserk = DBM_CORE_GENERIC_TIMER_BERSERK,
11228 timer_combat = DBM_CORE_GENERIC_TIMER_COMBAT,
11229 TimerSpeedKill = DBM_CORE_ACHIEVEMENT_TIMER_SPEED_KILL
11230 }, returnKey)
11231 }
11232 local defaultAnnounceLocalization = {
11233 __index = setmetatable({
11234 warning_berserk = DBM_CORE_GENERIC_WARNING_BERSERK
11235 }, returnKey)
11236 }
11237 local defaultOptionLocalization = {
11238 __index = setmetatable({
11239 timer_berserk = DBM_CORE_OPTION_TIMER_BERSERK,
11240 timer_combat = DBM_CORE_OPTION_TIMER_COMBAT,
11241 HealthFrame = DBM_CORE_OPTION_HEALTH_FRAME,
11242 }, returnKey)
11243 }
11244 local defaultMiscLocalization = {
11245 __index = {}
11246 }
11247
11248 function modLocalizationPrototype:SetGeneralLocalization(t)
11249 for i, v in pairs(t) do
11250 self.general[i] = v
11251 end
11252 end
11253
11254 function modLocalizationPrototype:SetWarningLocalization(t)
11255 for i, v in pairs(t) do
11256 self.warnings[i] = v
11257 end
11258 end
11259
11260 function modLocalizationPrototype:SetTimerLocalization(t)
11261 for i, v in pairs(t) do
11262 self.timers[i] = v
11263 end
11264 end
11265
11266 function modLocalizationPrototype:SetOptionLocalization(t)
11267 for i, v in pairs(t) do
11268 self.options[i] = v
11269 end
11270 end
11271
11272 function modLocalizationPrototype:SetOptionCatLocalization(t)
11273 for i, v in pairs(t) do
11274 self.cats[i] = v
11275 end
11276 end
11277
11278 function modLocalizationPrototype:SetMiscLocalization(t)
11279 for i, v in pairs(t) do
11280 self.miscStrings[i] = v
11281 end
11282 end
11283
11284 function DBM:CreateModLocalization(name)
11285 name = tostring(name)
11286 local obj = {
11287 general = setmetatable({}, returnKey),
11288 warnings = setmetatable({}, defaultAnnounceLocalization),
11289 options = setmetatable({}, defaultOptionLocalization),
11290 timers = setmetatable({}, defaultTimerLocalization),
11291 miscStrings = setmetatable({}, defaultMiscLocalization),
11292 cats = setmetatable({}, defaultCatLocalization),
11293 }
11294 setmetatable(obj, mt)
11295 modLocalizations[name] = obj
11296 return obj
11297 end
11298
11299 function DBM:GetModLocalization(name)
11300 name = tostring(name)
11301 return modLocalizations[name] or self:CreateModLocalization(name)
11302 end
11303end