· 7 years ago · Feb 13, 2019, 07:48 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: 17598 $"):sub(12, -3)),
45 DisplayVersion = "7.3.31", -- the string that is shown as version
46 ReleaseRevision = 17598 -- 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 wowVersionString, wowBuild, _, wowTOC = GetBuildInfo()
57local testBuild = false
58if IsTestBuild() then
59 testBuild = true
60end
61
62-- dual profile setup
63local _, playerClass = UnitClass("player")
64DBM_UseDualProfile = true
65if playerClass == "MAGE" or playerClass == "WARLOCK" and playerClass == "ROGUE" then
66 DBM_UseDualProfile = false
67end
68DBM_CharSavedRevision = 2
69
70--Hard code STANDARD_TEXT_FONT since skinning mods like to taint it (or worse, set it to nil, wtf?)
71local standardFont = STANDARD_TEXT_FONT
72if (LOCALE_koKR) then
73 standardFont = "Fonts\\2002.TTF"
74elseif (LOCALE_zhCN) then
75 standardFont = "Fonts\\ARKai_T.ttf"
76elseif (LOCALE_zhTW) then
77 standardFont = "Fonts\\blei00d.TTF"
78elseif (LOCALE_ruRU) then
79 standardFont = "Fonts\\FRIZQT___CYR.TTF"
80else
81 standardFont = "Fonts\\FRIZQT__.TTF"
82end
83
84DBM.DefaultOptions = {
85 WarningColors = {
86 {r = 0.41, g = 0.80, b = 0.94}, -- Color 1 - #69CCF0 - Turqoise
87 {r = 0.95, g = 0.95, b = 0.00}, -- Color 2 - #F2F200 - Yellow
88 {r = 1.00, g = 0.50, b = 0.00}, -- Color 3 - #FF8000 - Orange
89 {r = 1.00, g = 0.10, b = 0.10}, -- Color 4 - #FF1A1A - Red
90 },
91 RaidWarningSound = "Sound\\Doodad\\BellTollNightElf.ogg",
92 SpecialWarningSound = "Sound\\Spells\\PVPFlagTaken.ogg",
93 SpecialWarningSound2 = "Sound\\Creature\\AlgalonTheObserver\\UR_Algalon_BHole01.ogg",
94 SpecialWarningSound3 = "Interface\\AddOns\\DBM-Core\\sounds\\AirHorn.ogg",
95 SpecialWarningSound4 = "Sound\\Creature\\HoodWolf\\HoodWolfTransformPlayer01.ogg",
96 SpecialWarningSound5 = "Sound\\Creature\\Loathstare\\Loa_Naxx_Aggro02.ogg",
97 ModelSoundValue = "Short",
98 CountdownVoice = "Corsica",
99 CountdownVoice2 = "Kolt",
100 CountdownVoice3v2 = "Pewsey",
101 ChosenVoicePack = "None",
102 VoiceOverSpecW2 = "DefaultOnly",
103 AlwaysPlayVoice = false,
104 EventSoundVictory2 = "None",
105 EventSoundWipe = "None",
106 EventSoundEngage = "",
107 EventSoundMusic = "None",
108 EventSoundDungeonBGM = "None",
109 EventSoundMusicCombined = false,
110 EventDungMusicMythicFilter = true,
111 EventMusicMythicFilter = true,
112 Enabled = true,
113 ShowWarningsInChat = true,
114 ShowSWarningsInChat = true,
115 WarningIconLeft = true,
116 WarningIconRight = true,
117 WarningIconChat = true,
118 WarningAlphabetical = true,
119 StripServerName = true,
120 ShowAllVersions = true,
121 ShowPizzaMessage = true,
122 ShowEngageMessage = true,
123 ShowDefeatMessage = true,
124 ShowGuildMessages = true,
125 ShowGuildMessagesPlus = false,
126 AutoRespond = true,
127 StatusEnabled = true,
128 WhisperStats = false,
129 DisableStatusWhisper = false,
130 DisableGuildStatus = false,
131 HideBossEmoteFrame2 = true,
132 ShowMinimapButton = false,
133 ShowFlashFrame = true,
134 SWarningAlphabetical = true,
135 SWarnNameInNote = true,
136 CustomSounds = 0,
137 ShowBigBrotherOnCombatStart = false,
138 FilterTankSpec = true,
139 FilterInterrupt2 = "TandFandBossCooldown",
140 FilterInterruptNoteName = false,
141 FilterDispel = true,
142 --FilterSelfHud = true,
143 AutologBosses = false,
144 AdvancedAutologBosses = false,
145 LogOnlyRaidBosses = false,
146 UseSoundChannel = "Master",
147 LFDEnhance = true,
148 WorldBossNearAlert = false,
149 RLReadyCheckSound = true,
150 AFKHealthWarning = false,
151 AutoReplySound = true,
152 HideObjectivesFrame = true,
153 HideGarrisonToasts = true,
154 HideGuildChallengeUpdates = true,
155 HideQuestTooltips = true,
156 HideTooltips = false,
157 DisableSFX = false,
158 EnableModels = true,
159 RangeFrameFrames = "radar",
160 RangeFrameUpdates = "Average",
161 RangeFramePoint = "CENTER",
162 RangeFrameX = 50,
163 RangeFrameY = -50,
164 RangeFrameSound1 = "none",
165 RangeFrameSound2 = "none",
166 RangeFrameLocked = false,
167 RangeFrameRadarPoint = "CENTER",
168 RangeFrameRadarX = 100,
169 RangeFrameRadarY = -100,
170 InfoFramePoint = "CENTER",
171 InfoFrameX = 75,
172 InfoFrameY = -75,
173 InfoFrameShowSelf = false,
174 InfoFrameLines = 0,
175 WarningDuration2 = 1.5,
176 WarningPoint = "CENTER",
177 WarningX = 0,
178 WarningY = 260,
179 WarningFont = standardFont,
180 WarningFontSize = 20,
181 WarningFontStyle = "None",
182 WarningFontShadow = true,
183 SpecialWarningDuration2 = 1.5,
184 SpecialWarningPoint = "CENTER",
185 SpecialWarningX = 0,
186 SpecialWarningY = 75,
187 SpecialWarningFont = standardFont,
188 SpecialWarningFontSize2 = 35,
189 SpecialWarningFontStyle = "THICKOUTLINE",
190 SpecialWarningFontShadow = false,
191 SpecialWarningIcon = true,
192 SpecialWarningFontCol = {1.0, 0.7, 0.0},--Yellow, with a tint of orange
193 SpecialWarningFlashCol1 = {1.0, 1.0, 0.0},--Yellow
194 SpecialWarningFlashCol2 = {1.0, 0.5, 0.0},--Orange
195 SpecialWarningFlashCol3 = {1.0, 0.0, 0.0},--Red
196 SpecialWarningFlashCol4 = {1.0, 0.0, 1.0},--Purple
197 SpecialWarningFlashCol5 = {0.2, 1.0, 1.0},--Tealish
198 SpecialWarningFlashDura1 = 0.4,
199 SpecialWarningFlashDura2 = 0.4,
200 SpecialWarningFlashDura3 = 1,
201 SpecialWarningFlashDura4 = 0.7,
202 SpecialWarningFlashDura5 = 1,
203 SpecialWarningFlashAlph1 = 0.3,
204 SpecialWarningFlashAlph2 = 0.3,
205 SpecialWarningFlashAlph3 = 0.4,
206 SpecialWarningFlashAlph4 = 0.4,
207 SpecialWarningFlashAlph5 = 0.5,
208 SpecialWarningFlashRepeat1 = false,
209 SpecialWarningFlashRepeat2 = false,
210 SpecialWarningFlashRepeat3 = true,
211 SpecialWarningFlashRepeat4 = false,
212 SpecialWarningFlashRepeat5 = true,
213 SpecialWarningFlashRepeatAmount = 2,--Repeat 2 times, mean 3 flashes (first plus 2 repeat)
214 SWarnClassColor = true,
215 ArrowPosX = 0,
216 ArrowPosY = -150,
217 ArrowPoint = "TOP",
218 -- global boss mod settings (overrides mod-specific settings for some options)
219 DontShowBossAnnounces = false,
220 DontShowTargetAnnouncements = true,
221 DontShowSpecialWarnings = false,
222 DontShowSpecialWarningText = false,
223 DontShowBossTimers = false,
224 DontShowUserTimers = false,
225 DontShowFarWarnings = true,
226 DontSetIcons = false,
227 DontRestoreIcons = false,
228 DontShowRangeFrame = false,
229 DontRestoreRange = false,
230 DontShowInfoFrame = false,
231 DontShowHudMap2 = false,
232 DontShowNameplateIcons = false,
233 DontPlayCountdowns = false,
234 DontSendYells = false,
235 BlockNoteShare = false,
236 DontShowReminders = false,
237 DontShowPT2 = false,
238 DontShowPTCountdownText = false,
239 DontPlayPTCountdown = false,
240 DontShowPTText = false,
241 DontShowPTNoID = false,
242 PTCountThreshold = 5,
243 LatencyThreshold = 250,
244 BigBrotherAnnounceToRaid = false,
245 SettingsMessageShown = false,
246 ForumsMessageShown = false,
247 AlwaysShowSpeedKillTimer2 = false,
248 ShowRespawn = true,
249 ShowQueuePop = true,
250 HelpMessageVersion = 3,
251 MoviesSeen = {},
252 MovieFilter = "AfterFirst",
253 LastRevision = 0,
254 DebugMode = false,
255 DebugLevel = 1,
256 RoleSpecAlert = true,
257 CheckGear = true,
258 WorldBossAlert = false,
259 AutoAcceptFriendInvite = false,
260 AutoAcceptGuildInvite = false,
261 FakeBWVersion = false,
262 AITimer = true,
263 AutoCorrectTimer = false,
264 ShortTimerText = true,
265 ChatFrame = "DEFAULT_CHAT_FRAME",
266 CoreSavedRevision = 1,
267}
268
269DBM.Bars = DBT:New()
270DBM.Mods = {}
271DBM.ModLists = {}
272DBM.Counts = {
273 { text = "Moshne (Male)", value = "Mosh", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Mosh\\", max = 5},
274 { text = "Corsica (Female)",value = "Corsica", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Corsica\\", max = 10},
275 { text = "Koltrane (Male)",value = "Kolt", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Kolt\\", max = 10},
276 { text = "Pewsey (Male)",value = "Pewsey", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Pewsey\\", max = 10},
277 { text = "Bear (Male Child)",value = "Bear", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Bear\\", max = 10},
278 { text = "Anshlun (ptBR Male)",value = "Anshlun", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Anshlun\\", max = 10},
279 { text = "Neryssa (ptBR Female)",value = "Neryssa", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Neryssa\\", max = 10},
280}
281DBM.Victory = {
282 {text = "None",value = "None"},
283 {text = "Random",value = "Random"},
284 {text = "Blakbyrd: FF Fanfare",value = "Interface\\AddOns\\DBM-Core\\sounds\\Victory\\bbvictory.ogg", length=4},
285 {text = "SMG: FF Fanfare",value = "Interface\\AddOns\\DBM-Core\\sounds\\Victory\\SmoothMcGroove_Fanfare.ogg", length=4},
286}
287DBM.Defeat = {
288 {text = "None",value = "None"},
289 {text = "Random",value = "Random"},
290 {text = "Kologarn: You Fail",value = "Sound\\Creature\\Kologarn\\UR_Kologarn_Slay02.ogg", length=4},
291 {text = "Alizabal: Incompetent Raiders",value = "Sound\\Creature\\ALIZABAL\\VO_BH_ALIZABAL_RESET_01.ogg", length=4},
292 {text = "Hodir: Tragic",value = "Sound\\Creature\\Hodir\\UR_Hodir_Slay01.ogg", length=4},
293 {text = "Thorim: Failures",value = "Sound\\Creature\\Thorim\\UR_Thorim_P1Wipe01.ogg", length=4},
294 {text = "Valithria: Failures",value = "Sound\\Creature\\ValithriaDreamwalker\\IC_Valithria_Berserk01.ogg", length=4},
295}
296DBM.Music = {--Contains all music media, period
297 {text = "None",value = "None"},
298 {text = "Random",value = "Random"},
299 {text = "Anduin Part 1 B",value = "sound\\music\\Legion\\MUS_70_AnduinPt1_B.mp3", length=140},
300 {text = "Anduin Part 2 B",value = "sound\\music\\Legion\\MUS_70_AnduinPt2_B.mp3", length=111},
301 {text = "Bronze Jam",value = "Sound\\Music\\ZoneMusic\\IcecrownRaid\\IR_BronzeJam.mp3", length=116},
302 {text = "Invincible",value = "Sound\\Music\\Draenor\\MUS_Invincible.mp3", length=197},
303 {text = "Nightsong",value = "Sound\\Music\\cataclysm\\MUS_NightElves_GU01.mp3", length=160},
304 {text = "Ulduar: Titan Orchestra",value = "Sound\\Music\\ZoneMusic\\UlduarRaidInt\\UR_TitanOrchestraIntro.mp3", length=102},
305}
306DBM.DungeonMusic = {--Filtered list of media assigned to dungeon/raid background music catagory
307 {text = "None",value = "None"},
308 {text = "Random",value = "Random"},
309 {text = "Anduin Part 1 B",value = "sound\\music\\Legion\\MUS_70_AnduinPt1_B.mp3", length=140},
310 {text = "Nightsong",value = "Sound\\Music\\cataclysm\\MUS_NightElves_GU01.mp3", length=160},
311 {text = "Ulduar: Titan Orchestra",value = "Sound\\Music\\ZoneMusic\\UlduarRaidInt\\UR_TitanOrchestraIntro.mp3", length=102},
312}
313DBM.BattleMusic = {--Filtered list of media assigned to boss/encounter background music catagory
314 {text = "None",value = "None"},
315 {text = "Random",value = "Random"},
316 {text = "Anduin Part 2 B",value = "sound\\music\\Legion\\MUS_70_AnduinPt2_B.mp3", length=111},
317 {text = "Bronze Jam",value = "Sound\\Music\\ZoneMusic\\IcecrownRaid\\IR_BronzeJam.mp3", length=116},
318 {text = "Invincible",value = "Sound\\Music\\Draenor\\MUS_Invincible.mp3", length=197},
319}
320
321------------------------
322-- Global Identifiers --
323------------------------
324DBM_DISABLE_ZONE_DETECTION = newproxy(false)
325DBM_OPTION_SPACER = newproxy(false)
326
327--------------
328-- Locals --
329--------------
330local bossModPrototype = {}
331local usedProfile = "Default"
332local dbmIsEnabled = true
333local lastCombatStarted = GetTime()
334local loadcIds = {}
335local inCombat = {}
336local oocBWComms = {}
337local combatInfo = {}
338local bossIds = {}
339local updateFunctions = {}
340local raid = {}
341local modSyncSpam = {}
342local autoRespondSpam = {}
343local chatPrefix = "<Deadly Boss Mods> "
344local chatPrefixShort = "<DBM> "
345local ver = ("%s (r%d)"):format(DBM.DisplayVersion, DBM.Revision)
346local mainFrame = CreateFrame("Frame", "DBMMainFrame")
347local newerVersionPerson = {}
348local newerRevisionPerson = {}
349local combatInitialized = false
350local healthCombatInitialized = false
351local pformat
352local schedulerFrame = CreateFrame("Frame", "DBMScheduler")
353schedulerFrame:Hide()
354local startScheduler
355local schedule
356local unschedule
357local unscheduleAll
358local scheduleCountdown
359local loadOptions
360local checkWipe
361local checkBossHealth
362local checkCustomBossHealth
363local fireEvent
364local playerName = UnitName("player")
365local playerLevel = UnitLevel("player")
366local playerRealm = GetRealmName()
367local connectedServers = GetAutoCompleteRealms()
368local LastInstanceMapID = -1
369local LastGroupSize = 0
370local LastInstanceType = nil
371local queuedBattlefield = {}
372local noDelay = true
373local watchFrameRestore = false
374local bossHealth = {}
375local bossHealthuIdCache = {}
376local bossuIdCache = {}
377local savedDifficulty, difficultyText, difficultyIndex, difficultyModifier
378local lastBossEngage = {}
379local lastBossDefeat = {}
380local bossuIdFound = false
381local timerRequestInProgress = false
382local updateNotificationDisplayed = 0
383local showConstantReminder = 0
384local tooltipsHidden = false
385local SWFilterDisabed = 3
386local currentSpecID, currentSpecName, currentSpecGroup
387local cSyncSender = {}
388local cSyncReceived = 0
389local eeSyncSender = {}
390local eeSyncReceived = 0
391local canSetIcons = {}
392local iconSetRevision = {}
393local iconSetPerson = {}
394local addsGUIDs = {}
395local targetEventsRegistered = false
396local targetMonitor = nil
397local statusWhisperDisabled = false
398local statusGuildDisabled = false
399local dbmToc = 0
400local UpdateChestTimer
401local breakTimerStart
402local AddMsg
403local delayedFunction
404
405local fakeBWVersion, fakeBWHash = 97, "10064f7"
406local versionQueryString, versionResponseString = "Q^%d^%s", "V^%d^%s"
407
408local enableIcons = true -- set to false when a raid leader or a promoted player has a newer version of DBM
409
410local bannedMods = { -- a list of "banned" (meaning they are replaced by another mod or discontinued). These mods will not be loaded by DBM (and they wont show up in the GUI)
411 "DBM-Battlegrounds", --replaced by DBM-PvP
412 -- ZG and ZA are now part of the party mods for Cataclysm
413 "DBM-ZulAman",--Remove restriction in 8.0 classic wow
414 "DBM-ZG",--Remove restriction in 8.0 classic wow
415 "DBM-SiegeOfOrgrimmar",--Block legacy version. New version is "DBM-SiegeOfOrgrimmarV2"
416 "DBM-HighMail",
417 "DBM-ProvingGrounds-MoP",--Renamed to DBM-ProvingGrounds in 6.0 version since blizzard updated content for WoD
418 "DBM-ProvingGrounds",--Renamed to DBM-Challenges going forward to include proving grounds and any new single player challendges of similar design such as mage tower artifact quests
419 "DBM-VPKiwiBeta",--Renamed to DBM-VPKiwi in final version.
420 "DBM-Suramar",--Renamed to DBM-Nighthold
421 "DBM-KulTiras",--Merged to DBM-Azeroth-BfA
422 "DBM-Zandalar",--Merged to DBM-Azeroth-BfA
423-- "DBM-PvP",--Discontinued do to inability to maintain such large scale external projects with limitted time/resources
424}
425
426
427-----------------
428-- Libraries --
429-----------------
430local LL
431if LibStub("LibLatency", true) then
432 LL = LibStub("LibLatency")
433end
434local LD
435if LibStub("LibDurability", true) then
436 LD = LibStub("LibDurability")
437end
438
439
440--------------------------------------------------------
441-- Cache frequently used global variables in locals --
442--------------------------------------------------------
443local DBM = DBM
444-- these global functions are accessed all the time by the event handler
445-- so caching them is worth the effort
446local ipairs, pairs, next = ipairs, pairs, next
447local tinsert, tremove, twipe, tsort, tconcat = table.insert, table.remove, table.wipe, table.sort, table.concat
448local type, select = type, select
449local GetTime = GetTime
450local bband = bit.band
451local floor, mhuge, mmin, mmax = math.floor, math.huge, math.min, math.max
452local GetNumGroupMembers, GetRaidRosterInfo = GetNumGroupMembers, GetRaidRosterInfo
453local UnitName, GetUnitName = UnitName, GetUnitName
454local IsInRaid, IsInGroup, IsInInstance = IsInRaid, IsInGroup, IsInInstance
455local UnitAffectingCombat, InCombatLockdown, IsFalling, IsEncounterInProgress, UnitPlayerOrPetInRaid, UnitPlayerOrPetInParty = UnitAffectingCombat, InCombatLockdown, IsFalling, IsEncounterInProgress, UnitPlayerOrPetInRaid, UnitPlayerOrPetInParty
456local UnitGUID, UnitHealth, UnitHealthMax, UnitBuff, UnitDebuff = UnitGUID, UnitHealth, UnitHealthMax, UnitBuff, UnitDebuff
457local UnitExists, UnitIsDead, UnitIsFriend, UnitIsUnit = UnitExists, UnitIsDead, UnitIsFriend, UnitIsUnit
458local GetSpellInfo, EJ_GetSectionInfo, GetSectionIconFlags, GetSpellTexture, GetSpellCooldown = GetSpellInfo, C_EncounterJournal.GetSectionInfo, C_EncounterJournal.GetSectionIconFlags, GetSpellTexture, GetSpellCooldown
459local EJ_GetEncounterInfo, EJ_GetCreatureInfo, GetDungeonInfo = EJ_GetEncounterInfo, EJ_GetCreatureInfo, GetDungeonInfo
460local GetInstanceInfo = GetInstanceInfo
461local GetCurrentMapDungeonLevel, SetMapToCurrentZone, GetPlayerMapAreaID = GetCurrentMapDungeonLevel, SetMapToCurrentZone, GetPlayerMapAreaID
462local GetSpecialization, GetSpecializationInfo, GetSpecializationInfoByID = GetSpecialization, GetSpecializationInfo, GetSpecializationInfoByID
463local UnitDetailedThreatSituation = UnitDetailedThreatSituation
464local UnitIsGroupLeader, UnitIsGroupAssistant = UnitIsGroupLeader, UnitIsGroupAssistant
465local PlaySoundFile, PlaySound = PlaySoundFile, PlaySound
466local Ambiguate = Ambiguate
467local C_TimerNewTicker, C_TimerAfter = C_Timer.NewTicker, C_Timer.After
468
469--TODO, remove RegisterAddonMessagePrefix upvalue in 8.x, this is only a temp live/beta work around
470local RegisterAddonMessagePrefix, SendAddonMessage = RegisterAddonMessagePrefix or C_ChatInfo.RegisterAddonMessagePrefix, SendAddonMessage or C_ChatInfo.SendAddonMessage
471
472-- for Phanx' Class Colors
473local RAID_CLASS_COLORS = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
474
475---------------------------------
476-- General (local) functions --
477---------------------------------
478-- checks if a given value is in an array
479-- returns true if it finds the value, false otherwise
480local function checkEntry(t, val)
481 for i, v in ipairs(t) do
482 if v == val then
483 return true
484 end
485 end
486 return false
487end
488
489local function findEntry(t, val)
490 for i, v in ipairs(t) do
491 if v and val and val:find(v) then
492 return true
493 end
494 end
495 return false
496end
497
498-- removes all occurrences of a value in an array
499-- returns true if at least one occurrence was remove, false otherwise
500local function removeEntry(t, val)
501 local existed = false
502 for i = #t, 1, -1 do
503 if t[i] == val then
504 tremove(t, i)
505 existed = true
506 end
507 end
508 return existed
509end
510
511-- automatically sends an addon message to the appropriate channel (INSTANCE_CHAT, RAID or PARTY)
512local function sendSync(prefix, msg)
513 msg = msg or ""
514 if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and IsInInstance() 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)
515 SendAddonMessage("D4", prefix .. "\t" .. msg, "INSTANCE_CHAT")
516 else
517 if IsInRaid() then
518 SendAddonMessage("D4", prefix .. "\t" .. msg, "RAID")
519 elseif IsInGroup(LE_PARTY_CATEGORY_HOME) then
520 SendAddonMessage("D4", prefix .. "\t" .. msg, "PARTY")
521 else--for solo raid
522 SendAddonMessage("D4", prefix .. "\t" .. msg, "WHISPER", playerName)
523 end
524 end
525end
526
527--Custom sync function that should only be used for user generated sync messages
528local function sendLoggedSync(prefix, msg)
529 msg = msg or ""
530 if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and IsInInstance() 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)
531 C_ChatInfo.SendAddonMessageLogged("D4", prefix .. "\t" .. msg, "INSTANCE_CHAT")
532 else
533 if IsInRaid() then
534 C_ChatInfo.SendAddonMessageLogged("D4", prefix .. "\t" .. msg, "RAID")
535 elseif IsInGroup(LE_PARTY_CATEGORY_HOME) then
536 C_ChatInfo.SendAddonMessageLogged("D4", prefix .. "\t" .. msg, "PARTY")
537 else--for solo raid
538 C_ChatInfo.SendAddonMessageLogged("D4", prefix .. "\t" .. msg, "WHISPER", playerName)
539 end
540 end
541end
542
543local function strFromTime(time)
544 if type(time) ~= "number" then time = 0 end
545 time = floor(time*100)/100
546 if time < 60 then
547 return DBM_CORE_TIMER_FORMAT_SECS:format(time)
548 elseif time % 60 == 0 then
549 return DBM_CORE_TIMER_FORMAT_MINS:format(time/60)
550 else
551 return DBM_CORE_TIMER_FORMAT:format(time/60, time % 60)
552 end
553end
554
555do
556 -- fail-safe format, replaces missing arguments with unknown
557 -- note: doesn't handle cases like %%%s correctly at the moment (should become %unknown, but becomes %%s)
558 -- also, the end of the format directive is not detected in all cases, but handles everything that occurs in our boss mods ;)
559 --> 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)
560 local function replace(cap1, cap2)
561 return cap1 == "%" and DBM_CORE_UNKNOWN
562 end
563
564 function pformat(fstr, ...)
565 local ok, str = pcall(format, fstr, ...)
566 return ok and str or fstr:gsub("(%%+)([^%%%s<]+)", replace):gsub("%%%%", "%%")
567 end
568end
569
570-- sends a whisper to a player by his or her character name or BNet presence id
571-- returns true if the message was sent, nil otherwise
572local function sendWhisper(target, msg)
573 if type(target) == "number" then
574 if not BNIsSelf(target) then -- never send BNet whispers to ourselves
575 BNSendWhisper(target, msg)
576 return true
577 end
578 elseif type(target) == "string" then
579 -- whispering to ourselves here is okay and somewhat useful for whisper-warnings
580 SendChatMessage(msg, "WHISPER", nil, target)
581 return true
582 end
583end
584local BNSendWhisper = sendWhisper
585
586local function stripServerName(cap)
587 cap = cap:sub(2, -2)
588 if DBM.Options.StripServerName then
589 cap = Ambiguate(cap, "none")
590 end
591 return cap
592end
593
594--------------
595-- Events --
596--------------
597do
598 local registeredEvents = {}
599 local registeredSpellIds = {}
600 local unfilteredCLEUEvents = {}
601 local registeredUnitEventIds = {}
602 local argsMT = {__index = {}}
603 local args = setmetatable({}, argsMT)
604
605 function argsMT.__index:IsSpellID(a1, a2, a3, a4, a5)
606 local v = self.spellId
607 return v == a1 or v == a2 or v == a3 or v == a4 or v == a5
608 end
609
610 function argsMT.__index:IsPlayer()
611 return bband(args.destFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) ~= 0 and bband(args.destFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
612 end
613
614 function argsMT.__index:IsPlayerSource()
615 return bband(args.sourceFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) ~= 0 and bband(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
616 end
617
618 function argsMT.__index:IsNPC()
619 return bband(args.destFlags, COMBATLOG_OBJECT_TYPE_NPC) ~= 0
620 end
621
622 function argsMT.__index:IsPet()
623 return bband(args.destFlags, COMBATLOG_OBJECT_TYPE_PET) ~= 0
624 end
625
626 function argsMT.__index:IsPetSource()
627 return bband(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PET) ~= 0
628 end
629
630 function argsMT.__index:IsSrcTypePlayer()
631 return bband(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
632 end
633
634 function argsMT.__index:IsDestTypePlayer()
635 return bband(args.destFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
636 end
637
638 function argsMT.__index:IsSrcTypeHostile()
639 return bband(args.sourceFlags, COMBATLOG_OBJECT_REACTION_HOSTILE) ~= 0
640 end
641
642 function argsMT.__index:IsDestTypeHostile()
643 return bband(args.destFlags, COMBATLOG_OBJECT_REACTION_HOSTILE) ~= 0
644 end
645
646 function argsMT.__index:GetSrcCreatureID()
647 return DBM:GetCIDFromGUID(self.sourceGUID)
648 end
649
650 function argsMT.__index:GetDestCreatureID()
651 return DBM:GetCIDFromGUID(self.destGUID)
652 end
653
654 local function handleEvent(self, event, ...)
655 local isUnitEvent = event:sub(0, 5) == "UNIT_" and event ~= "UNIT_DIED" and event ~= "UNIT_DESTROYED"
656 if self == mainFrame and isUnitEvent then
657 -- UNIT_* events that come from mainFrame are _UNFILTERED variants and need their suffix
658 event = event .. "_UNFILTERED"
659 isUnitEvent = false -- not actually a real unit id for this function...
660 end
661 if not registeredEvents[event] or not dbmIsEnabled then return end
662 for i, v in ipairs(registeredEvents[event]) do
663 local zones = v.zones
664 local handler = v[event]
665 local modEvents = v.registeredUnitEvents
666 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
667 handler(v, ...)
668 end
669 end
670 end
671
672 local registerUnitEvent, unregisterUnitEvent, registerSpellId, unregisterSpellId, registerCLEUEvent, unregisterCLEUEvent
673 do
674 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)
675
676 function registerUnitEvent(mod, event, ...)
677 mod.registeredUnitEvents = mod.registeredUnitEvents or {}
678 for i = 1, select("#", ...) do
679 local uId = select(i, ...)
680 if not uId then break end
681 local frame = frames[uId]
682 if not frame then
683 frame = CreateFrame("Frame")
684 if uId == "mouseover" then
685 -- work-around for mouse-over events (broken!)
686 frame:SetScript("OnEvent", function(self, event, uId, ...)
687 -- we registered mouseover events, so we only want mouseover events, thanks.
688 handleEvent(self, event, "mouseover", ...)
689 end)
690 else
691 frame:SetScript("OnEvent", handleEvent)
692 end
693 frames[uId] = frame
694 end
695 registeredUnitEventIds[event .. uId] = (registeredUnitEventIds[event .. uId] or 0) + 1
696 mod.registeredUnitEvents[event .. uId] = true
697 frame:RegisterUnitEvent(event, uId)
698 end
699 end
700
701 function unregisterUnitEvent(mod, event, ...)
702 for i = 1, select("#", ...) do
703 local uId = select(i, ...)
704 if not uId then break end
705 local frame = frames[uId]
706 local refs = (registeredUnitEventIds[event .. uId] or 1) - 1
707 registeredUnitEventIds[event .. uId] = refs
708 if refs <= 0 then
709 registeredUnitEventIds[event .. uId] = nil
710 if frame then
711 frame:UnregisterEvent(event)
712 end
713 end
714 if mod.registeredUnitEvents and mod.registeredUnitEvents[event .. uId] then
715 mod.registeredUnitEvents[event .. uId] = nil
716 end
717 end
718 for i = #registeredEvents[event], 1, -1 do
719 if registeredEvents[event][i] == mod then
720 tremove(registeredEvents[event], i)
721 end
722 end
723 if #registeredEvents[event] == 0 then
724 registeredEvents[event] = nil
725 end
726 end
727
728 function registerSpellId(event, spellId)
729 if type(spellId) == "string" then--Something is screwed up, like SPELL_AURA_APPLIED DOSE
730 DBM:AddMsg("DBM RegisterEvents Error: "..spellId.." is not a number!")
731 return
732 end
733 if spellId and not DBM:GetSpellInfo(spellId) then
734 DBM:AddMsg("DBM RegisterEvents Error: "..spellId.." spell id does not exist!")
735 return
736 end
737 if not registeredSpellIds[event] then
738 registeredSpellIds[event] = {}
739 end
740 registeredSpellIds[event][spellId] = (registeredSpellIds[event][spellId] or 0) + 1
741 end
742
743 function unregisterSpellId(event, spellId)
744 if not registeredSpellIds[event] then return end
745 local refs = (registeredSpellIds[event][spellId] or 1) - 1
746 registeredSpellIds[event][spellId] = refs
747 if refs <= 0 then
748 registeredSpellIds[event][spellId] = nil
749 end
750 end
751
752 --There are 2 tables. unfilteredCLEUEvents and registeredSpellIds table.
753 --unfilteredCLEUEvents saves UNFILTERED cleu event count. this is count table to prevent bad unregister.
754 --registeredSpellIds tables filtered table. this saves event and spell ids. works smiliar with unfilteredCLEUEvents table.
755 function registerCLEUEvent(mod, event)
756 local argTable = {strsplit(" ", event)}
757 -- filtered cleu event. save information in registeredSpellIds table.
758 if #argTable > 1 then
759 event = argTable[1]
760 for i = 2, #argTable do
761 registerSpellId(event, tonumber(argTable[i]))
762 end
763 -- no args. works as unfiltered. save information in unfilteredCLEUEvents table.
764 else
765 unfilteredCLEUEvents[event] = (unfilteredCLEUEvents[event] or 0) + 1
766 end
767 registeredEvents[event] = registeredEvents[event] or {}
768 tinsert(registeredEvents[event], mod)
769 end
770
771 function unregisterCLEUEvent(mod, event)
772 local argTable = {strsplit(" ", event)}
773 local eventCleared = false
774 -- filtered cleu event. save information in registeredSpellIds table.
775 if #argTable > 1 then
776 event = argTable[1]
777 for i = 2, #argTable do
778 unregisterSpellId(event, tonumber(argTable[i]))
779 end
780 local remainingSpellIdCount = 0
781 if registeredSpellIds[event] then
782 for i, v in pairs(registeredSpellIds[event]) do
783 remainingSpellIdCount = remainingSpellIdCount + 1
784 end
785 end
786 if remainingSpellIdCount == 0 then
787 registeredSpellIds[event] = nil
788 -- if unfilteredCLEUEvents and registeredSpellIds do not exists, clear registeredEvents.
789 if not unfilteredCLEUEvents[event] then
790 eventCleared = true
791 end
792 end
793 -- no args. works as unfiltered. save information in unfilteredCLEUEvents table.
794 else
795 local refs = (unfilteredCLEUEvents[event] or 1) - 1
796 unfilteredCLEUEvents[event] = refs
797 if refs <= 0 then
798 unfilteredCLEUEvents[event] = nil
799 -- if unfilteredCLEUEvents and registeredSpellIds do not exists, clear registeredEvents.
800 if not registeredSpellIds[event] then
801 eventCleared = true
802 end
803 end
804 end
805 for i = #registeredEvents[event], 1, -1 do
806 if registeredEvents[event][i] == mod then
807 registeredEvents[event][i] = {}
808 break
809 end
810 end
811 if eventCleared then
812 registeredEvents[event] = nil
813 end
814 end
815 end
816
817 -- UNIT_* events are special: they can take 'parameters' like this: "UNIT_HEALTH boss1 boss2" which only trigger the event for the given unit ids
818 function DBM:RegisterEvents(...)
819 for i = 1, select("#", ...) do
820 local event = select(i, ...)
821 -- spell events with special care.
822 if event:sub(0, 6) == "SPELL_" and event ~= "SPELL_NAME_UPDATE" or event:sub(0, 6) == "RANGE_" or event == "UNIT_DIED" or event == "UNIT_DESTROYED" then
823 registerCLEUEvent(self, event)
824 else
825 local eventWithArgs = event
826 -- unit events need special care
827 if event:sub(0, 5) == "UNIT_" then
828 -- 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))
829 local arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8
830 event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 = strsplit(" ", event)
831 if not arg1 and event:sub(event:len() - 10) ~= "_UNFILTERED" then -- no arguments given, support for legacy mods
832 eventWithArgs = event .. " boss1 boss2 boss3 boss4 boss5 target focus"
833 event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 = strsplit(" ", eventWithArgs)
834 end
835 if event:sub(event:len() - 10) == "_UNFILTERED" then
836 -- we really want *all* unit ids
837 mainFrame:RegisterEvent(event:sub(0, -12))
838 else
839 registerUnitEvent(self, event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)
840 end
841 -- spell events with filter
842 else
843 -- normal events
844 mainFrame:RegisterEvent(event)
845 end
846 registeredEvents[eventWithArgs] = registeredEvents[eventWithArgs] or {}
847 tinsert(registeredEvents[eventWithArgs], self)
848 if event ~= eventWithArgs then
849 registeredEvents[event] = registeredEvents[event] or {}
850 tinsert(registeredEvents[event], self)
851 end
852 end
853 end
854 end
855
856 local function unregisterUEvent(mod, event)
857 if event:sub(0, 5) == "UNIT_" and event ~= "UNIT_DIED" and event ~= "UNIT_DESTROYED" then
858 local event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 = strsplit(" ", event)
859 if event:sub(event:len() - 10) == "_UNFILTERED" then
860 mainFrame:UnregisterEvent(event:sub(0, -12))
861 else
862 unregisterUnitEvent(mod, event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)
863 end
864 end
865 end
866
867 local function findRealEvent(t, val)
868 for i, v in ipairs(t) do
869 local event = strsplit(" ", v)
870 if event == val then
871 return v
872 end
873 end
874 end
875
876 function DBM:UnregisterInCombatEvents(srmOnly)
877 for event, mods in pairs(registeredEvents) do
878 if srmOnly then
879 local i = 1
880 while mods[i] do
881 if mods[i] == self and event == "SPELL_AURA_REMOVED" then
882 local findEvent = findRealEvent(self.inCombatOnlyEvents, "SPELL_AURA_REMOVED")
883 if findEvent then
884 unregisterCLEUEvent(self, findEvent)
885 break
886 end
887 end
888 i = i +1
889 end
890 elseif (event:sub(0, 6) == "SPELL_"and event ~= "SPELL_NAME_UPDATE" or event:sub(0, 6) == "RANGE_") then
891 local i = 1
892 while mods[i] do
893 if mods[i] == self and event ~= "SPELL_AURA_REMOVED" then
894 local findEvent = findRealEvent(self.inCombatOnlyEvents, event)
895 if findEvent then
896 unregisterCLEUEvent(self, findEvent)
897 break
898 end
899 end
900 i = i +1
901 end
902 else
903 local match = false
904 for i = #mods, 1, -1 do
905 if mods[i] == self and checkEntry(self.inCombatOnlyEvents, event) then
906 tremove(mods, i)
907 match = true
908 end
909 end
910 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
911 unregisterUEvent(self, event)
912 end
913 if #mods == 0 then
914 registeredEvents[event] = nil
915 end
916 end
917 end
918 end
919
920 function DBM:RegisterShortTermEvents(...)
921 if self.shortTermEventsRegistered then
922 return
923 end
924 self.shortTermRegisterEvents = {...}
925 for k, v in pairs(self.shortTermRegisterEvents) do
926 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
927 -- legacy event, oh noes
928 self.shortTermRegisterEvents[k] = v .. " boss1 boss2 boss3 boss4 boss5 target focus"
929 end
930 end
931 self.shortTermEventsRegistered = 1
932 self:RegisterEvents(unpack(self.shortTermRegisterEvents))
933 end
934
935 function DBM:UnregisterShortTermEvents()
936 if self.shortTermRegisterEvents then
937 for event, mods in pairs(registeredEvents) do
938 if event:sub(0, 6) == "SPELL_" or event:sub(0, 6) == "RANGE_" then
939 local i = 1
940 while mods[i] do
941 if mods[i] == self then
942 local findEvent = findRealEvent(self.shortTermRegisterEvents, event)
943 if findEvent then
944 unregisterCLEUEvent(self, findEvent)
945 break
946 end
947 end
948 i = i +1
949 end
950 else
951 local match = false
952 for i = #mods, 1, -1 do
953 if mods[i] == self and checkEntry(self.shortTermRegisterEvents, event) then
954 tremove(mods, i)
955 match = true
956 end
957 end
958 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
959 unregisterUEvent(self, event)
960 end
961 if #mods == 0 then
962 registeredEvents[event] = nil
963 end
964 end
965 end
966 self.shortTermEventsRegistered = nil
967 self.shortTermRegisterEvents = nil
968 end
969 end
970
971 DBM:RegisterEvents("ADDON_LOADED")
972
973 function DBM:FilterRaidBossEmote(msg, ...)
974 return handleEvent(nil, "CHAT_MSG_RAID_BOSS_EMOTE_FILTERED", msg:gsub("\124c%x+(.-)\124r", "%1"), ...)
975 end
976
977 local noArgTableEvents = {
978 SWING_DAMAGE = true,
979 SWING_MISSED = true,
980 RANGE_DAMAGE = true,
981 RANGE_MISSED = true,
982 SPELL_DAMAGE = true,
983 SPELL_BUILDING_DAMAGE = true,
984 SPELL_MISSED = true,
985 SPELL_ABSORBED = true,
986 SPELL_HEAL = true,
987 SPELL_ENERGIZE = true,
988 SPELL_PERIODIC_ENERGIZE = true,
989 SPELL_PERIODIC_MISSED = true,
990 SPELL_PERIODIC_DAMAGE = true,
991 SPELL_PERIODIC_DRAIN = true,
992 SPELL_PERIODIC_LEECH = true,
993 SPELL_PERIODIC_ENERGIZE = true,
994 SPELL_DRAIN = true,
995 SPELL_LEECH = true,
996 SPELL_CAST_FAILED = true
997 }
998 function DBM:COMBAT_LOG_EVENT_UNFILTERED(timestamp, event, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, extraArg1, extraArg2, extraArg3, extraArg4, extraArg5, extraArg6, extraArg7, extraArg8, extraArg9, extraArg10, ...)
999 if wowTOC == 80000 then--In 8.x+, CLEU is just an event with no args, all args must be requested via CombatLogGetCurrentEventInfo
1000 timestamp, event, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, extraArg1, extraArg2, extraArg3, extraArg4, extraArg5, extraArg6, extraArg7, extraArg8, extraArg9, extraArg10 = CombatLogGetCurrentEventInfo()
1001 end
1002 if not registeredEvents[event] then return end
1003 local eventSub6 = event:sub(0, 6)
1004 if (eventSub6 == "SPELL_" or eventSub6 == "RANGE_") and not unfilteredCLEUEvents[event] and registeredSpellIds[event] then
1005 if not registeredSpellIds[event][extraArg1] then return end
1006 end
1007 -- process some high volume events without building the whole table which is somewhat faster
1008 -- this prevents work-around with mods that used to have their own event handler to prevent this overhead
1009 if noArgTableEvents[event] then
1010 return handleEvent(nil, event, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, extraArg1, extraArg2, extraArg3, extraArg4, extraArg5, extraArg6, extraArg7, extraArg8, extraArg9, extraArg10, ...)
1011 else
1012 twipe(args)
1013 args.timestamp = timestamp
1014 args.event = event
1015 args.sourceGUID = sourceGUID
1016 args.sourceName = sourceName
1017 args.sourceFlags = sourceFlags
1018 args.sourceRaidFlags = sourceRaidFlags
1019 args.destGUID = destGUID
1020 args.destName = destName
1021 args.destFlags = destFlags
1022 args.destRaidFlags = destRaidFlags
1023 if eventSub6 == "SPELL_" then
1024 args.spellId, args.spellName = extraArg1, extraArg2
1025 if event == "SPELL_AURA_APPLIED" or event == "SPELL_AURA_REFRESH" or event == "SPELL_AURA_REMOVED" then
1026 if not args.sourceName then
1027 args.sourceName = args.destName
1028 args.sourceGUID = args.destGUID
1029 args.sourceFlags = args.destFlags
1030 end
1031 elseif event == "SPELL_AURA_APPLIED_DOSE" or event == "SPELL_AURA_REMOVED_DOSE" then
1032 args.amount = extraArg5
1033 if not args.sourceName then
1034 args.sourceName = args.destName
1035 args.sourceGUID = args.destGUID
1036 args.sourceFlags = args.destFlags
1037 end
1038 elseif event == "SPELL_INTERRUPT" or event == "SPELL_DISPEL" or event == "SPELL_DISPEL_FAILED" or event == "SPELL_AURA_STOLEN" then
1039 args.extraSpellId, args.extraSpellName = extraArg4, extraArg5
1040 end
1041 elseif event == "UNIT_DIED" or event == "UNIT_DESTROYED" then
1042 args.sourceName = args.destName
1043 args.sourceGUID = args.destGUID
1044 args.sourceFlags = args.destFlags
1045 elseif event == "ENVIRONMENTAL_DAMAGE" then
1046 args.environmentalType, args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = extraArg1, extraArg2, extraArg3, extraArg4, extraArg5, extraArg6, extraArg7, extraArg8, extraArg9, extraArg10
1047 end
1048 return handleEvent(nil, event, args)
1049 end
1050 end
1051 mainFrame:SetScript("OnEvent", handleEvent)
1052end
1053
1054--------------
1055-- OnLoad --
1056--------------
1057do
1058 local isLoaded = false
1059 local onLoadCallbacks = {}
1060 local disabledMods = {}
1061
1062 local function runDelayedFunctions(self)
1063 noDelay = false
1064 --Check if voice pack missing
1065 local activeVP = self.Options.ChosenVoicePack
1066 if activeVP ~= "None" then
1067 if not self.VoiceVersions[activeVP] or (self.VoiceVersions[activeVP] and self.VoiceVersions[activeVP] == 0) then--A voice pack is selected that does not belong
1068 self.Options.ChosenVoicePack = "None"--Set ChosenVoicePack back to None
1069 AddMsg(DBM, DBM_CORE_VOICE_MISSING)
1070 end
1071 else
1072 if not self.Options.DontShowReminders and #self.Voices > 1 then
1073 --At least one voice pack installed but activeVP set to "None"
1074 AddMsg(DBM, DBM_CORE_VOICE_DISABLED)
1075 end
1076 end
1077 --Check if any of countdown sounds are using missing voice pack
1078 local voice1 = self.Options.CountdownVoice
1079 local voice2 = self.Options.CountdownVoice2
1080 local voice3 = self.Options.CountdownVoice3v2
1081 if voice1 == "None" then--Migrate to new setting
1082 self.Options.CountdownVoice = self.DefaultOptions.CountdownVoice
1083 self.Options.DontPlayCountdowns = true
1084 end
1085 if voice3 == "HoTS_R" and select(4, GetAddOnInfo("DBM-CountPack-HoTS")) then
1086 --Heroes count pack already installed, migrate user setting instead of forcing pewsey voice
1087 self.Options.CountdownVoice3v2 = "HoTS_Ravenlord"
1088 end
1089 local found1, found2, found3 = false, false, false
1090 for i = 1, #self.Counts do
1091 local voice = self.Counts[i].value
1092 if voice == self.Options.CountdownVoice then
1093 found1 = true
1094 end
1095 if voice == self.Options.CountdownVoice2 then
1096 found2 = true
1097 end
1098 if voice == self.Options.CountdownVoice3v2 then
1099 found3 = true
1100 end
1101 end
1102 if not found1 then
1103 AddMsg(DBM, DBM_CORE_VOICE_COUNT_MISSING:format(1, self.DefaultOptions.CountdownVoice))
1104 self.Options.CountdownVoice = self.DefaultOptions.CountdownVoice
1105 end
1106 if not found2 then
1107 AddMsg(DBM, DBM_CORE_VOICE_COUNT_MISSING:format(2, self.DefaultOptions.CountdownVoice2))
1108 self.Options.CountdownVoice2 = self.DefaultOptions.CountdownVoice2
1109 end
1110 if not found3 then
1111 AddMsg(DBM, DBM_CORE_VOICE_COUNT_MISSING:format(3, self.DefaultOptions.CountdownVoice3v2))
1112 self.Options.CountdownVoice3v2 = self.DefaultOptions.CountdownVoice3v2
1113 end
1114 self:BuildVoiceCountdownCache()
1115 --Break timer recovery
1116 --Try local settings
1117 if self.Options.tempBreak2 then
1118 local timer, startTime = string.split("/", self.Options.tempBreak2)
1119 local elapsed = time() - tonumber(startTime)
1120 local remaining = timer - elapsed
1121 if remaining > 0 then
1122 breakTimerStart(DBM, remaining, playerName)
1123 else--It must have ended while we were offline, kill variable.
1124 self.Options.tempBreak2 = nil
1125 end
1126 end
1127 if IsInGuild() then
1128 SendAddonMessage("D4", "GH", "GUILD")
1129 end
1130 if not savedDifficulty or not difficultyText or not difficultyIndex then--prevent error if savedDifficulty or difficultyText is nil
1131 savedDifficulty, difficultyText, difficultyIndex, LastGroupSize, difficultyModifier = self:GetCurrentInstanceDifficulty()
1132 end
1133 end
1134
1135 -- register a callback that will be executed once the addon is fully loaded (ADDON_LOADED fired, saved vars are available)
1136 function DBM:RegisterOnLoadCallback(cb)
1137 if isLoaded then
1138 cb()
1139 else
1140 onLoadCallbacks[#onLoadCallbacks + 1] = cb
1141 end
1142 end
1143
1144 function DBM:ADDON_LOADED(modname)
1145 if modname == "DBM-Core" and not isLoaded then
1146 dbmToc = tonumber(GetAddOnMetadata("DBM-Core", "X-Min-Interface"))
1147 isLoaded = true
1148 for i, v in ipairs(onLoadCallbacks) do
1149 xpcall(v, geterrorhandler())
1150 end
1151 onLoadCallbacks = nil
1152 loadOptions(self)
1153 if GetAddOnEnableState(playerName, "VEM-Core") >= 1 then
1154 self:Disable(true)
1155 C_TimerAfter(15, function() AddMsg(self, DBM_CORE_VEM) end)
1156 return
1157 end
1158 if GetAddOnEnableState(playerName, "DBM-Profiles") >= 1 then
1159 self:Disable(true)
1160 C_TimerAfter(15, function() AddMsg(self, DBM_CORE_3RDPROFILES) end)
1161 return
1162 end
1163 if GetAddOnEnableState(playerName, "DPMCore") >= 1 then
1164 self:Disable(true)
1165 C_TimerAfter(15, function() AddMsg(self, DBM_CORE_DPMCORE) end)
1166 return
1167 end
1168 self.Bars:LoadOptions("DBM")
1169 self.Arrow:LoadPosition()
1170 if not self.Options.ShowMinimapButton then self:HideMinimapButton() end
1171 --[[local soundChannels = tonumber(GetCVar("Sound_NumChannels")) or 24--if set to 24, may return nil, Defaults usually do
1172 --If this messes with your fps, stop raiding with a toaster. It's only fix for addon sound ducking.
1173 if soundChannels < 64 then
1174 SetCVar("Sound_NumChannels", 64)
1175 end--]]
1176 self.AddOns = {}
1177 self.Voices = { {text = "None",value = "None"}, }--Create voice table, with default "None" value
1178 self.VoiceVersions = {}
1179 for i = 1, GetNumAddOns() do
1180 local addonName = GetAddOnInfo(i)
1181 local enabled = GetAddOnEnableState(playerName, i)
1182 local minToc = tonumber(GetAddOnMetadata(i, "X-Min-Interface"))
1183 if GetAddOnMetadata(i, "X-DBM-Mod") then
1184 if enabled ~= 0 then
1185 if checkEntry(bannedMods, addonName) then
1186 AddMsg(self, "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.")
1187 elseif not testBuild and minToc and minToc > wowTOC then
1188 self:Debug(i.." not loaded because mod requires minimum toc of "..minToc)
1189 else
1190 local mapIdTable = {strsplit(",", GetAddOnMetadata(i, "X-DBM-Mod-MapID") or "")}
1191 tinsert(self.AddOns, {
1192 sort = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Sort") or mhuge) or mhuge,
1193 type = GetAddOnMetadata(i, "X-DBM-Mod-Type") or "OTHER",
1194 category = GetAddOnMetadata(i, "X-DBM-Mod-Category") or "Other",
1195 name = GetAddOnMetadata(i, "X-DBM-Mod-Name") or GetRealZoneText(tonumber(mapIdTable[1])) or DBM_CORE_UNKNOWN,
1196 mapId = mapIdTable,
1197 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"))},
1198 oneFormat = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-Single-Format") or 0) == 1,
1199 hasLFR = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-LFR") or 0) == 1,
1200 hasChallenge = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-Challenge") or 0) == 1,
1201 noHeroic = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-No-Heroic") or 0) == 1,
1202 noStatistics = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-No-Statistics") or 0) == 1,
1203 hasMythic = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-Mythic") or 0) == 1,
1204 hasTimeWalker = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-TimeWalker") or 0) == 1,
1205 isWorldBoss = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-World-Boss") or 0) == 1,
1206 minRevision = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-MinCoreRevision") or 0),
1207 minExpansion = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-MinExpansion") or 0),
1208 modId = addonName,
1209 })
1210 for i = #self.AddOns[#self.AddOns].mapId, 1, -1 do
1211 local id = tonumber(self.AddOns[#self.AddOns].mapId[i])
1212 if id then
1213 self.AddOns[#self.AddOns].mapId[i] = id
1214 else
1215 tremove(self.AddOns[#self.AddOns].mapId, i)
1216 end
1217 end
1218 if self.AddOns[#self.AddOns].subTabs then
1219 for k, v in ipairs(self.AddOns[#self.AddOns].subTabs) do
1220 local id = tonumber(self.AddOns[#self.AddOns].subTabs[k])
1221 if id then
1222 self.AddOns[#self.AddOns].subTabs[k] = GetRealZoneText(id):trim() or id
1223 else
1224 self.AddOns[#self.AddOns].subTabs[k] = (self.AddOns[#self.AddOns].subTabs[k]):trim()
1225 end
1226 end
1227 end
1228 if GetAddOnMetadata(i, "X-DBM-Mod-LoadCID") then
1229 local idTable = {strsplit(",", GetAddOnMetadata(i, "X-DBM-Mod-LoadCID"))}
1230 for i = 1, #idTable do
1231 loadcIds[tonumber(idTable[i]) or ""] = addonName
1232 end
1233 end
1234 end
1235 else
1236 disabledMods[#disabledMods+1] = addonName
1237 end
1238 end
1239 if GetAddOnMetadata(i, "X-DBM-Voice") and enabled ~= 0 then
1240 if checkEntry(bannedMods, addonName) then
1241 AddMsg(self, "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.")
1242 else
1243 local voiceValue = GetAddOnMetadata(i, "X-DBM-Voice-ShortName")
1244 local voiceVersion = tonumber(GetAddOnMetadata(i, "X-DBM-Voice-Version") or 0)
1245 if voiceVersion > 0 then--Do not insert voice version 0 into THIS table. 0 should be used by voice packs that insert only countdown
1246 tinsert(self.Voices, { text = GetAddOnMetadata(i, "X-DBM-Voice-Name"), value = voiceValue })
1247 end
1248 self.VoiceVersions[voiceValue] = voiceVersion
1249 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
1250 if GetAddOnMetadata(i, "X-DBM-Voice-HasCount") then--Supports adding countdown options, insert new countdown into table
1251 tinsert(self.Counts, { text = GetAddOnMetadata(i, "X-DBM-Voice-Name"), value = "VP:"..voiceValue, path = "Interface\\AddOns\\DBM-VP"..voiceValue.."\\count\\", max = 10})
1252 end
1253 end
1254 end
1255 if GetAddOnMetadata(i, "X-DBM-CountPack") and enabled ~= 0 then
1256 if checkEntry(bannedMods, addonName) then
1257 AddMsg(self, "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.")
1258 else
1259 local loaded = LoadAddOn(addonName)
1260 local voiceGlobal = GetAddOnMetadata(i, "X-DBM-CountPack-GlobalName")
1261 local insertFunction = _G[voiceGlobal]
1262 if loaded and insertFunction then
1263 insertFunction()
1264 else
1265 DBM:Debug(addonName.." failed to load at time CountPack function ran", 2)
1266 end
1267 end
1268 end
1269 if GetAddOnMetadata(i, "X-DBM-VictoryPack") and enabled ~= 0 then
1270 if checkEntry(bannedMods, addonName) then
1271 AddMsg(self, "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.")
1272 else
1273 local loaded = LoadAddOn(addonName)
1274 local victoryGlobal = GetAddOnMetadata(i, "X-DBM-VictoryPack-GlobalName")
1275 local insertFunction = _G[victoryGlobal]
1276 if loaded and insertFunction then
1277 insertFunction()
1278 else
1279 DBM:Debug(addonName.." failed to load at time CountPack function ran", 2)
1280 end
1281 end
1282 end
1283 if GetAddOnMetadata(i, "X-DBM-DefeatPack") and enabled ~= 0 then
1284 if checkEntry(bannedMods, addonName) then
1285 AddMsg(self, "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.")
1286 else
1287 local loaded = LoadAddOn(addonName)
1288 local defeatGlobal = GetAddOnMetadata(i, "X-DBM-DefeatPack-GlobalName")
1289 local insertFunction = _G[defeatGlobal]
1290 if loaded and insertFunction then
1291 insertFunction()
1292 else
1293 DBM:Debug(addonName.." failed to load at time CountPack function ran", 2)
1294 end
1295 end
1296 end
1297 if GetAddOnMetadata(i, "X-DBM-MusicPack") and enabled ~= 0 then
1298 if checkEntry(bannedMods, addonName) then
1299 AddMsg(self, "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.")
1300 else
1301 local loaded = LoadAddOn(addonName)
1302 local musicGlobal = GetAddOnMetadata(i, "X-DBM-MusicPack-GlobalName")
1303 local insertFunction = _G[musicGlobal]
1304 if loaded and insertFunction then
1305 insertFunction()
1306 else
1307 DBM:Debug(addonName.." failed to load at time CountPack function ran", 2)
1308 end
1309 end
1310 end
1311 end
1312 tsort(self.AddOns, function(v1, v2) return v1.sort < v2.sort end)
1313 self:RegisterEvents(
1314 "COMBAT_LOG_EVENT_UNFILTERED",
1315 "GROUP_ROSTER_UPDATE",
1316 "INSTANCE_GROUP_SIZE_CHANGED",
1317 "CHAT_MSG_ADDON",
1318 --"CHAT_MSG_ADDON_LOGGED",--Enable in next Beta Build
1319 "BN_CHAT_MSG_ADDON",
1320 "PLAYER_REGEN_DISABLED",
1321 "PLAYER_REGEN_ENABLED",
1322 "INSTANCE_ENCOUNTER_ENGAGE_UNIT",
1323 "UNIT_TARGETABLE_CHANGED",
1324 "UNIT_SPELLCAST_SUCCEEDED boss1 boss2 boss3 boss4 boss5",
1325 "UNIT_TARGET_UNFILTERED",
1326 "ENCOUNTER_START",
1327 "ENCOUNTER_END",
1328 "BOSS_KILL",
1329 "UNIT_DIED",
1330 "UNIT_DESTROYED",
1331 "UNIT_HEALTH mouseover target focus player",
1332 "CHAT_MSG_WHISPER",
1333 "CHAT_MSG_BN_WHISPER",
1334 "CHAT_MSG_MONSTER_YELL",
1335 "CHAT_MSG_MONSTER_EMOTE",
1336 "CHAT_MSG_MONSTER_SAY",
1337 "CHAT_MSG_RAID_BOSS_EMOTE",
1338 "RAID_BOSS_EMOTE",
1339 "RAID_BOSS_WHISPER",
1340 "PLAYER_ENTERING_WORLD",
1341 "LFG_ROLE_CHECK_SHOW",
1342 "LFG_PROPOSAL_SHOW",
1343 "LFG_PROPOSAL_FAILED",
1344 "LFG_PROPOSAL_SUCCEEDED",
1345 "READY_CHECK",
1346 "UPDATE_BATTLEFIELD_STATUS",
1347 "PLAY_MOVIE",
1348 "CINEMATIC_START",
1349 --"PLAYER_LEVEL_UP",--PLAYER_LEVEL_CHANGED
1350 "PLAYER_SPECIALIZATION_CHANGED",
1351 "PARTY_INVITE_REQUEST",
1352 "LOADING_SCREEN_DISABLED",
1353 "SCENARIO_CRITERIA_UPDATE"
1354 )
1355 if RolePollPopup:IsEventRegistered("ROLE_POLL_BEGIN") then
1356 RolePollPopup:UnregisterEvent("ROLE_POLL_BEGIN")
1357 end
1358 self:GROUP_ROSTER_UPDATE()
1359 C_TimerAfter(1.5, function()
1360 combatInitialized = true
1361 end)
1362 C_TimerAfter(20, function()--Delay UNIT_HEALTH combat start for 20 sec. (not to break Timer Recovery stuff)
1363 healthCombatInitialized = true
1364 end)
1365 self:Schedule(10, runDelayedFunctions, self)
1366 end
1367 end
1368end
1369
1370
1371-----------------
1372-- Callbacks --
1373-----------------
1374do
1375 local callbacks = {}
1376
1377 function fireEvent(event, ...)
1378 if not callbacks[event] then return end
1379 for i, v in ipairs(callbacks[event]) do
1380 local ok, err = pcall(v, event, ...)
1381 if not ok then DBM:AddMsg(("Error while executing callback %s for event %s: %s"):format(tostring(v), tostring(event), err)) end
1382 end
1383 end
1384
1385 function DBM:FireEvent(event, ...)
1386 fireEvent(event, ...)
1387 end
1388
1389 function DBM:IsCallbackRegistered(event, f)
1390 if not event or type(f) ~= "function" then
1391 error("Usage: IsCallbackRegistered(event, callbackFunc)", 2)
1392 end
1393 if not callbacks[event] then return end
1394 for i = 1, #callbacks[event] do
1395 if callbacks[event][i] == f then return true end
1396 end
1397 return false
1398 end
1399
1400 function DBM:RegisterCallback(event, f)
1401 if not event or type(f) ~= "function" then
1402 error("Usage: DBM:RegisterCallback(event, callbackFunc)", 2)
1403 end
1404 callbacks[event] = callbacks[event] or {}
1405 tinsert(callbacks[event], f)
1406 return #callbacks[event]
1407 end
1408
1409 function DBM:UnregisterCallback(event, f)
1410 if not event or not callbacks[event] then return end
1411 if f then
1412 if type(f) ~= "function" then
1413 error("Usage: UnregisterCallback(event, callbackFunc)", 2)
1414 end
1415 --> checking from the end to start and not stoping after found one result in case of a func being twice registered.
1416 for i = #callbacks[event], 1, -1 do
1417 if callbacks[event][i] == f then tremove (callbacks[event], i) end
1418 end
1419 else
1420 callbacks[event] = nil
1421 end
1422 end
1423end
1424
1425
1426--------------------------
1427-- OnUpdate/Scheduler --
1428--------------------------
1429do
1430 -- stack that stores a few tables (up to 8) which will be recycled
1431 local popCachedTable, pushCachedTable
1432 local numChachedTables = 0
1433 do
1434 local tableCache = nil
1435
1436 -- gets a table from the stack, it will then be recycled.
1437 function popCachedTable()
1438 local t = tableCache
1439 if t then
1440 tableCache = t.next
1441 numChachedTables = numChachedTables - 1
1442 end
1443 return t
1444 end
1445
1446 -- tries to push a table on the stack
1447 -- 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...
1448 -- 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
1449 -- this is just to re-use all the tables of the small tasks that are scheduled all the time (like the wipe detection)
1450 -- 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
1451 function pushCachedTable(t)
1452 if numChachedTables < 8 and #t <= 4 then
1453 twipe(t)
1454 t.next = tableCache
1455 tableCache = t
1456 numChachedTables = numChachedTables + 1
1457 end
1458 end
1459 end
1460
1461 -- priority queue (min-heap) that stores all scheduled tasks.
1462 -- insert: O(log n)
1463 -- deleteMin: O(log n)
1464 -- getMin: O(1)
1465 -- removeAllMatching: O(n)
1466 local insert, removeAllMatching, getMin, deleteMin
1467 do
1468 local heap = {}
1469 local firstFree = 1
1470
1471 -- gets the next task
1472 function getMin()
1473 return heap[1]
1474 end
1475
1476 -- restores the heap invariant by moving an item up
1477 local function siftUp(n)
1478 local parent = floor(n / 2)
1479 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
1480 heap[n], heap[parent] = heap[parent], heap[n] -- swap the element with its parent
1481 n = parent
1482 parent = floor(n / 2)
1483 end
1484 end
1485
1486 -- restores the heap invariant by moving an item down
1487 local function siftDown(n)
1488 local m -- position of the smaller child
1489 while 2 * n < firstFree do -- #children >= 1
1490 -- swap the element with its smaller child
1491 if 2 * n + 1 == firstFree then -- n does not have a right child --> it only has a left child as #children >= 1
1492 m = 2 * n -- left child
1493 elseif heap[2 * n].time < heap[2 * n + 1].time then -- #children = 2 and left child < right child
1494 m = 2 * n -- left child
1495 else -- #children = 2 and right child is smaller than the left one
1496 m = 2 * n + 1 -- right
1497 end
1498 if heap[n].time <= heap[m].time then -- n is <= its smallest child --> heap invariant restored
1499 return
1500 end
1501 heap[n], heap[m] = heap[m], heap[n]
1502 n = m
1503 end
1504 end
1505
1506 -- inserts a new element into the heap
1507 function insert(ele)
1508 heap[firstFree] = ele
1509 siftUp(firstFree)
1510 firstFree = firstFree + 1
1511 end
1512
1513 -- deletes the min element
1514 function deleteMin()
1515 local min = heap[1]
1516 firstFree = firstFree - 1
1517 heap[1] = heap[firstFree]
1518 heap[firstFree] = nil
1519 siftDown(1)
1520 return min
1521 end
1522
1523 -- removes multiple scheduled tasks from the heap
1524 -- note that this function is comparatively slow by design as it has to check all tasks and allows partial matches
1525 function removeAllMatching(f, mod, ...)
1526 -- remove all elements that match the signature, this destroyes the heap and leaves a normal array
1527 local v, match
1528 local foundMatch = false
1529 for i = #heap, 1, -1 do -- iterate backwards over the array to allow usage of table.remove
1530 v = heap[i]
1531 if (not f or v.func == f) and (not mod or v.mod == mod) then
1532 match = true
1533 for i = 1, select("#", ...) do
1534 if select(i, ...) ~= v[i] then
1535 match = false
1536 break
1537 end
1538 end
1539 if match then
1540 tremove(heap, i)
1541 firstFree = firstFree - 1
1542 foundMatch = true
1543 end
1544 end
1545 end
1546 -- rebuild the heap from the array in O(n)
1547 if foundMatch then
1548 for i = floor((firstFree - 1) / 2), 1, -1 do
1549 siftDown(i)
1550 end
1551 end
1552 end
1553 end
1554
1555
1556 local wrappers = {}
1557 local function range(max, cur, ...)
1558 cur = cur or 1
1559 if cur > max then
1560 return ...
1561 end
1562 return cur, range(max, cur + 1, select(2, ...))
1563 end
1564 local function getWrapper(n)
1565 wrappers[n] = wrappers[n] or loadstring(([[
1566 return function(func, tbl)
1567 return func(]] .. ("tbl[%s], "):rep(n):sub(0, -3) .. [[)
1568 end
1569 ]]):format(range(n)))()
1570 return wrappers[n]
1571 end
1572
1573 local nextModSyncSpamUpdate = 0
1574 --mainFrame:SetScript("OnUpdate", function(self, elapsed)
1575 local function onUpdate(self, elapsed)
1576 local time = GetTime()
1577
1578 -- execute scheduled tasks
1579 local nextTask = getMin()
1580 while nextTask and nextTask.func and nextTask.time <= time do
1581 deleteMin()
1582 local n = nextTask.n
1583 if n == #nextTask then
1584 nextTask.func(unpack(nextTask))
1585 else
1586 -- too many nil values (or a trailing nil)
1587 -- this is bad because unpack will not work properly
1588 -- TODO: is there a better solution?
1589 getWrapper(n)(nextTask.func, nextTask)
1590 end
1591 pushCachedTable(nextTask)
1592 nextTask = getMin()
1593 end
1594
1595 -- execute OnUpdate handlers of all modules
1596 local foundModFunctions = 0
1597 for i, v in pairs(updateFunctions) do
1598 foundModFunctions = foundModFunctions + 1
1599 if i.Options.Enabled and (not i.zones or i.zones[LastInstanceMapID]) then
1600 i.elapsed = (i.elapsed or 0) + elapsed
1601 if i.elapsed >= (i.updateInterval or 0) then
1602 v(i, i.elapsed)
1603 i.elapsed = 0
1604 end
1605 end
1606 end
1607
1608 -- clean up sync spam timers and auto respond spam blockers
1609 if time > nextModSyncSpamUpdate then
1610 nextModSyncSpamUpdate = time + 20
1611 -- 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...
1612 -- we now do this just every 20 seconds since the earlier assumption about modSyncSpam isn't true any longer
1613 -- 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)
1614 local k, v = next(modSyncSpam, nil)
1615 if v and (time - v > 8) then
1616 modSyncSpam[k] = nil
1617 end
1618 end
1619 if not nextTask and foundModFunctions == 0 then--Nothing left, stop scheduler
1620 schedulerFrame:SetScript("OnUpdate", nil)
1621 schedulerFrame:Hide()
1622 end
1623 end
1624
1625 function startScheduler()
1626 if not schedulerFrame:IsShown() then
1627 schedulerFrame:Show()
1628 schedulerFrame:SetScript("OnUpdate", onUpdate)
1629 end
1630 end
1631
1632 function schedule(t, f, mod, ...)
1633 if type(f) ~= "function" then
1634 error("usage: schedule(time, func, [mod, args...])", 2)
1635 end
1636 startScheduler()
1637 local v
1638 if numChachedTables > 0 and select("#", ...) <= 4 then -- a cached table is available and all arguments fit into an array with four slots
1639 v = popCachedTable()
1640 v.time = GetTime() + t
1641 v.func = f
1642 v.mod = mod
1643 v.n = select("#", ...)
1644 for i = 1, v.n do
1645 v[i] = select(i, ...)
1646 end
1647 -- clear slots if necessary
1648 for i = v.n + 1, 4 do
1649 v[i] = nil
1650 end
1651 else -- create a new table
1652 v = {time = GetTime() + t, func = f, mod = mod, n = select("#", ...), ...}
1653 end
1654 insert(v)
1655 end
1656
1657 function scheduleCountdown(time, numAnnounces, func, mod, self, ...)
1658 time = time or 5
1659 numAnnounces = numAnnounces or 3
1660 for i = 1, numAnnounces do
1661 schedule(time - i, func, mod, self, i, ...)
1662 end
1663 end
1664
1665 function unschedule(f, mod, ...)
1666 if not f and not mod then
1667 -- you really want to kill the complete scheduler? call unscheduleAll
1668 error("cannot unschedule everything, pass a function and/or a mod")
1669 end
1670 return removeAllMatching(f, mod, ...)
1671 end
1672
1673 function unscheduleAll()
1674 return removeAllMatching()
1675 end
1676end
1677
1678function DBM:Schedule(t, f, ...)
1679 if type(f) ~= "function" then
1680 error("usage: DBM:Schedule(time, func, [args...])", 2)
1681 end
1682 return schedule(t, f, nil, ...)
1683end
1684
1685function DBM:Unschedule(f, ...)
1686 return unschedule(f, nil, ...)
1687end
1688
1689---------------
1690-- Profile --
1691---------------
1692function DBM:CreateProfile(name)
1693 if not name or name == "" or name:find(" ") then
1694 self:AddMsg(DBM_CORE_PROFILE_CREATE_ERROR)
1695 return
1696 end
1697 if DBM_AllSavedOptions[name] then
1698 self:AddMsg(DBM_CORE_PROFILE_CREATE_ERROR_D:format(name))
1699 return
1700 end
1701 -- create profile
1702 usedProfile = name
1703 DBM_UsedProfile = usedProfile
1704 DBM_AllSavedOptions[usedProfile] = DBM_AllSavedOptions[usedProfile] or {}
1705 self:AddDefaultOptions(DBM_AllSavedOptions[usedProfile], self.DefaultOptions)
1706 self.Options = DBM_AllSavedOptions[usedProfile]
1707 -- rearrange position
1708 self.Bars:CreateProfile("DBM")
1709 self:RepositionFrames()
1710 self:AddMsg(DBM_CORE_PROFILE_CREATED:format(name))
1711end
1712
1713function DBM:ApplyProfile(name)
1714 if not name or not DBM_AllSavedOptions[name] then
1715 self:AddMsg(DBM_CORE_PROFILE_APPLY_ERROR:format(name or DBM_CORE_UNKNOWN))
1716 return
1717 end
1718 usedProfile = name
1719 DBM_UsedProfile = usedProfile
1720 self:AddDefaultOptions(DBM_AllSavedOptions[usedProfile], self.DefaultOptions)
1721 self.Options = DBM_AllSavedOptions[usedProfile]
1722 -- rearrange position
1723 self.Bars:ApplyProfile("DBM")
1724 self:RepositionFrames()
1725 self:AddMsg(DBM_CORE_PROFILE_APPLIED:format(name))
1726end
1727
1728function DBM:CopyProfile(name)
1729 if not name or not DBM_AllSavedOptions[name] then
1730 self:AddMsg(DBM_CORE_PROFILE_COPY_ERROR:format(name or DBM_CORE_UNKNOWN))
1731 return
1732 elseif name == usedProfile then
1733 self:AddMsg(DBM_CORE_PROFILE_COPY_ERROR_SELF)
1734 return
1735 end
1736 DBM_AllSavedOptions[usedProfile] = DBM_AllSavedOptions[name]
1737 self:AddDefaultOptions(DBM_AllSavedOptions[usedProfile], self.DefaultOptions)
1738 self.Options = DBM_AllSavedOptions[usedProfile]
1739 -- rearrange position
1740 self.Bars:CopyProfile(name, "DBM")
1741 self:RepositionFrames()
1742 self:AddMsg(DBM_CORE_PROFILE_COPIED:format(name))
1743end
1744
1745function DBM:DeleteProfile(name)
1746 if not name or not DBM_AllSavedOptions[name] then
1747 self:AddMsg(DBM_CORE_PROFILE_DELETE_ERROR:format(name or DBM_CORE_UNKNOWN))
1748 return
1749 elseif name == "Default" then-- Default profile cannot be deleted.
1750 self:AddMsg(DBM_CORE_PROFILE_CANNOT_DELETE)
1751 return
1752 end
1753 --Delete
1754 DBM_AllSavedOptions[name] = nil
1755 usedProfile = "Default"--Restore to default
1756 DBM_UsedProfile = usedProfile
1757 self.Options = DBM_AllSavedOptions[usedProfile]
1758 if not self.Options then
1759 -- the default profile got lost somehow (maybe WoW crashed and the saved variables file got corrupted)
1760 self:CreateProfile("Default")
1761 end
1762 -- rearrange position
1763 self.Bars:DeleteProfile(name, "DBM")
1764 self:RepositionFrames()
1765 self:AddMsg(DBM_CORE_PROFILE_DELETED:format(name))
1766end
1767
1768function DBM:RepositionFrames()
1769 -- rearrange position
1770 self:UpdateWarningOptions()
1771 self:UpdateSpecialWarningOptions()
1772 self.Arrow:LoadPosition()
1773 if DBMRangeCheck then
1774 DBMRangeCheck:ClearAllPoints()
1775 DBMRangeCheck:SetPoint(self.Options.RangeFramePoint, UIParent, self.Options.RangeFramePoint, self.Options.RangeFrameX, self.Options.RangeFrameY)
1776 end
1777 if DBMRangeCheckRadar then
1778 DBMRangeCheckRadar:ClearAllPoints()
1779 DBMRangeCheckRadar:SetPoint(self.Options.RangeFrameRadarPoint, UIParent, self.Options.RangeFrameRadarPoint, self.Options.RangeFrameRadarX, self.Options.RangeFrameRadarY)
1780 end
1781 if DBMInfoFrame then
1782 DBMInfoFrame:ClearAllPoints()
1783 DBMInfoFrame:SetPoint(self.Options.InfoFramePoint, UIParent, self.Options.InfoFramePoint, self.Options.InfoFrameX, self.Options.InfoFrameY)
1784 end
1785end
1786
1787----------------------
1788-- Slash Commands --
1789----------------------
1790do
1791 local function Pull(timer)
1792 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.
1793 if (DBM:GetRaidRank(playerName) == 0 and IsInGroup() and not LFGTankException) or select(2, IsInInstance()) == "pvp" or IsEncounterInProgress() then
1794 return DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
1795 end
1796 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
1797 if targetName then
1798 sendSync("PT", timer.."\t"..LastInstanceMapID.."\t"..targetName)
1799 else
1800 sendSync("PT", timer.."\t"..LastInstanceMapID)
1801 end
1802 end
1803 local function Break(timer)
1804 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
1805 DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
1806 return
1807 end
1808 if timer > 60 then
1809 DBM:AddMsg(DBM_CORE_BREAK_USAGE)
1810 return
1811 end
1812 timer = timer * 60
1813 sendSync("BT", timer)
1814 end
1815
1816 SLASH_DEADLYBOSSMODS1 = "/dbm"
1817 SLASH_DEADLYBOSSMODSRPULL1 = "/rpull"
1818 SLASH_DEADLYBOSSMODSDWAY1 = "/dway"--/way not used because DBM would load before TomTom and can't check
1819 SlashCmdList["DEADLYBOSSMODSDWAY"] = function(msg)
1820 if DBM:HasMapRestrictions() then
1821 DBM:AddMsg(DBM_CORE_NO_ARROW)
1822 return
1823 end
1824 local x, y = string.split(" ", msg:sub(1):trim())
1825 local xNum, yNum = tonumber(x or ""), tonumber(y or "")
1826 local success
1827 if xNum and yNum then
1828 DBM.Arrow:ShowRunTo(xNum, yNum, 1, nil, true, true)
1829 success = true
1830 else--Check if they used , instead of space.
1831 x, y = string.split(",", msg:sub(1):trim())
1832 xNum, yNum = tonumber(x or ""), tonumber(y or "")
1833 if xNum and yNum then
1834 DBM.Arrow:ShowRunTo(xNum, yNum, 1, nil, true, true)
1835 success = true
1836 end
1837 end
1838 if not success then
1839 if DBM.Arrow:IsShown() then
1840 DBM.Arrow:Hide()--Hide
1841 else--error
1842 DBM:AddMsg(DBM_ARROW_WAY_USAGE)
1843 end
1844 end
1845 end
1846 if not BigWigs then
1847 --Register pull and break slash commands for BW converts, if BW isn't loaded
1848 --This shouldn't raise an issue since BW SHOULD load before DBM in any case they are both present.
1849 SLASH_DEADLYBOSSMODSPULL1 = "/pull"
1850 SLASH_DEADLYBOSSMODSBREAK1 = "/break"
1851 SlashCmdList["DEADLYBOSSMODSPULL"] = function(msg)
1852 Pull(tonumber(msg) or 10)
1853 end
1854 SlashCmdList["DEADLYBOSSMODSBREAK"] = function(msg)
1855 Break(tonumber(msg) or 10)
1856 end
1857 end
1858 SlashCmdList["DEADLYBOSSMODSRPULL"] = function(msg)
1859 Pull(30)
1860 end
1861 SlashCmdList["DEADLYBOSSMODS"] = function(msg)
1862 local cmd = msg:lower()
1863 if cmd == "ver" or cmd == "version" then
1864 DBM:ShowVersions(false)
1865 elseif cmd == "ver2" or cmd == "version2" then
1866 DBM:ShowVersions(true)
1867 elseif cmd == "unlock" or cmd == "move" then
1868 DBM.Bars:ShowMovableBar()
1869 elseif cmd == "help2" then
1870 for i, v in ipairs(DBM_CORE_SLASHCMD_HELP2) do DBM:AddMsg(v) end
1871 elseif cmd == "help" then
1872 for i, v in ipairs(DBM_CORE_SLASHCMD_HELP) do DBM:AddMsg(v) end
1873 elseif cmd:sub(1, 13) == "timer endloop" then
1874 DBM:CreatePizzaTimer(time, "", nil, nil, nil, nil, true)
1875 elseif cmd:sub(1, 5) == "timer" then
1876 local time, text = msg:match("^%w+ ([%d:]+) (.+)$")
1877 if not (time and text) then
1878 for i, v in ipairs(DBM_CORE_TIMER_USAGE) do DBM:AddMsg(v) end
1879 return
1880 end
1881 local min, sec = string.split(":", time)
1882 min = tonumber(min or "") or 0
1883 sec = tonumber(sec or "")
1884 if min and not sec then
1885 sec = min
1886 min = 0
1887 end
1888 time = min * 60 + sec
1889 DBM:CreatePizzaTimer(time, text)
1890 elseif cmd:sub(1, 6) == "ctimer" then
1891 local time, text = msg:match("^%w+ ([%d:]+) (.+)$")
1892 if not (time and text) then
1893 DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
1894 return
1895 end
1896 local min, sec = string.split(":", time)
1897 min = tonumber(min or "") or 0
1898 sec = tonumber(sec or "")
1899 if min and not sec then
1900 sec = min
1901 min = 0
1902 end
1903 time = min * 60 + sec
1904 DBM:CreatePizzaTimer(time, text, nil, nil, true)
1905 elseif cmd:sub(1, 6) == "ltimer" then
1906 local time, text = msg:match("^%w+ ([%d:]+) (.+)$")
1907 if not (time and text) then
1908 DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
1909 return
1910 end
1911 local min, sec = string.split(":", time)
1912 min = tonumber(min or "") or 0
1913 sec = tonumber(sec or "")
1914 if min and not sec then
1915 sec = min
1916 min = 0
1917 end
1918 time = min * 60 + sec
1919 DBM:CreatePizzaTimer(time, text, nil, nil, nil, true)
1920 elseif cmd:sub(1, 7) == "cltimer" then
1921 local time, text = msg:match("^%w+ ([%d:]+) (.+)$")
1922 if not (time and text) then
1923 DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
1924 return
1925 end
1926 local min, sec = string.split(":", time)
1927 min = tonumber(min or "") or 0
1928 sec = tonumber(sec or "")
1929 if min and not sec then
1930 sec = min
1931 min = 0
1932 end
1933 time = min * 60 + sec
1934 DBM:CreatePizzaTimer(time, text, nil, nil, true, true)
1935 elseif cmd:sub(1, 15) == "broadcast timer" then--Standard Timer
1936 local permission = true
1937 if DBM:GetRaidRank(playerName) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then
1938 DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
1939 permission = false
1940 end
1941 local time, text = msg:match("^%w+ %w+ ([%d:]+) (.+)$")
1942 if not (time and text) then
1943 DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
1944 return
1945 end
1946 local min, sec = string.split(":", time)
1947 min = tonumber(min or "") or 0
1948 sec = tonumber(sec or "")
1949 if min and not sec then
1950 sec = min
1951 min = 0
1952 end
1953 time = min * 60 + sec
1954 DBM:CreatePizzaTimer(time, text, permission)
1955 elseif cmd:sub(1, 16) == "broadcast ctimer" then
1956 local permission = true
1957 if DBM:GetRaidRank(playerName) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then
1958 DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
1959 permission = false
1960 end
1961 local time, text = msg:match("^%w+ %w+ ([%d:]+) (.+)$")
1962 if not (time and text) then
1963 DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
1964 return
1965 end
1966 local min, sec = string.split(":", time)
1967 min = tonumber(min or "") or 0
1968 sec = tonumber(sec or "")
1969 if min and not sec then
1970 sec = min
1971 min = 0
1972 end
1973 time = min * 60 + sec
1974 DBM:CreatePizzaTimer(time, text, permission, nil, true)
1975 elseif cmd:sub(1, 16) == "broadcast ltimer" then
1976 local permission = true
1977 if DBM:GetRaidRank(playerName) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then
1978 DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
1979 permission = false
1980 end
1981 local time, text = msg:match("^%w+ %w+ ([%d:]+) (.+)$")
1982 if not (time and text) then
1983 DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
1984 return
1985 end
1986 local min, sec = string.split(":", time)
1987 min = tonumber(min or "") or 0
1988 sec = tonumber(sec or "")
1989 if min and not sec then
1990 sec = min
1991 min = 0
1992 end
1993 time = min * 60 + sec
1994 DBM:CreatePizzaTimer(time, text, permission, nil, nil, true)
1995 elseif cmd:sub(1, 17) == "broadcast cltimer" then
1996 local permission = true
1997 if DBM:GetRaidRank(playerName) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then
1998 DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
1999 permission = false
2000 end
2001 local time, text = msg:match("^%w+ %w+ ([%d:]+) (.+)$")
2002 if not (time and text) then
2003 DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
2004 return
2005 end
2006 local min, sec = string.split(":", time)
2007 min = tonumber(min or "") or 0
2008 sec = tonumber(sec or "")
2009 if min and not sec then
2010 sec = min
2011 min = 0
2012 end
2013 time = min * 60 + sec
2014 DBM:CreatePizzaTimer(time, text, permission, nil, true, true)
2015 elseif cmd:sub(0,5) == "break" then
2016 local timer = tonumber(cmd:sub(6)) or 5
2017 Break(timer)
2018 elseif cmd:sub(1, 4) == "pull" then
2019 local timer = tonumber(cmd:sub(5)) or 10
2020 Pull(timer)
2021 elseif cmd:sub(1, 5) == "rpull" then
2022 Pull(30)
2023 elseif cmd:sub(1, 3) == "lag" then
2024 if not LL then
2025 DBM:AddMsg(DBM_CORE_UPDATE_REQUIRES_RELAUNCH)
2026 return
2027 end
2028 LL:RequestLatency()
2029 DBM:AddMsg(DBM_CORE_LAG_CHECKING)
2030 C_TimerAfter(5, function() DBM:ShowLag() end)
2031 elseif cmd:sub(1, 10) == "durability" then
2032 if not LD then
2033 DBM:AddMsg(DBM_CORE_UPDATE_REQUIRES_RELAUNCH)
2034 return
2035 end
2036 LD:RequestDurability()
2037 DBM:AddMsg(DBM_CORE_DUR_CHECKING)
2038 C_TimerAfter(5, function() DBM:ShowDurability() end)
2039 elseif cmd:sub(1, 3) == "hud" then
2040 if DBM:HasMapRestrictions() then
2041 DBM:AddMsg(DBM_CORE_NO_HUD)
2042 return
2043 end
2044 local hudType, target, duration = string.split(" ", msg:sub(4):trim())
2045 if hudType == "" then
2046 for i, v in ipairs(DBM_CORE_HUD_USAGE) do
2047 DBM:AddMsg(v)
2048 end
2049 return
2050 end
2051 local hudDuration = tonumber(duration) or 1200--if no duration defined. 20 minutes to cover even longest of fights
2052 local success = false
2053 if type(hudType) == "string" and hudType:trim() ~= "" then
2054 if hudType:upper() == "HIDE" then
2055 DBMHudMap:Disable()
2056 return
2057 end
2058 if not target then
2059 DBM:AddMsg(DBM_CORE_HUD_INVALID_TARGET)
2060 return
2061 end
2062 local uId
2063 if target:upper() == "TARGET" and UnitExists("target") then
2064 uId = "target"
2065 elseif target:upper() == "FOCUS" and UnitExists("focus") then
2066 uId = "focus"
2067 else--Try to use it as player name
2068 uId = DBM:GetRaidUnitId(target)
2069 end
2070 if not uId then
2071 DBM:AddMsg(DBM_CORE_HUD_INVALID_TARGET)
2072 return
2073 end
2074 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
2075 DBM:AddMsg(DBM_CORE_HUD_INVALID_SELF)
2076 return
2077 end
2078 if hudType:upper() == "ARROW" then
2079 local _, targetClass = UnitClass(uId)
2080 local color2 = RAID_CLASS_COLORS[targetClass]
2081 local m1 = DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "party", playerName, 0.1, hudDuration, 0, 1, 0, 1, nil, false):Appear()
2082 local m2 = DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "party", UnitName(uId), 0.75, hudDuration, color2.r, color2.g, color2.b, 1, nil, false):Appear()
2083 m2:EdgeTo(m1, nil, hudDuration, 0, 1, 0, 1)
2084 success = true
2085 elseif hudType:upper() == "DOT" then
2086 local _, targetClass = UnitClass(uId)
2087 local color2 = RAID_CLASS_COLORS[targetClass]
2088 DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "party", UnitName(uId), 0.75, hudDuration, color2.r, color2.g, color2.b, 1, nil, false):Appear()
2089 success = true
2090 elseif hudType:upper() == "GREEN" then
2091 DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "highlight", UnitName(uId), 3.5, hudDuration, 0, 1, 0, 0.5, nil, false):Pulse(0.5, 0.5)
2092 success = true
2093 elseif hudType:upper() == "RED" then
2094 DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "highlight", UnitName(uId), 3.5, hudDuration, 1, 0, 0, 0.5, nil, false):Pulse(0.5, 0.5)
2095 success = true
2096 elseif hudType:upper() == "YELLOW" then
2097 DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "highlight", UnitName(uId), 3.5, hudDuration, 1, 1, 0, 0.5, nil, false):Pulse(0.5, 0.5)
2098 success = true
2099 elseif hudType:upper() == "BLUE" then
2100 DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "highlight", UnitName(uId), 3.5, hudDuration, 0, 0, 1, 0.5, nil, false):Pulse(0.5, 0.5)
2101 success = true
2102 elseif hudType:upper() == "ICON" then
2103 local icon = GetRaidTargetIndex(uId)
2104 if not icon then
2105 DBM:AddMsg(DBM_CORE_HUD_INVALID_ICON)
2106 return
2107 end
2108 local iconString = DBM:IconNumToString(icon):lower()
2109 DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, iconString, UnitName(uId), 3.5, hudDuration, 1, 1, 1, 0.5, nil, false):Pulse(0.5, 0.5)
2110 success = true
2111 else
2112 DBM:AddMsg(DBM_CORE_HUD_INVALID_TYPE)
2113 end
2114 end
2115 if success then
2116 DBM:AddMsg(DBM_CORE_HUD_SUCCESS:format(strFromTime(hudDuration)))
2117 end
2118 elseif cmd:sub(1, 5) == "arrow" then
2119 if DBM:HasMapRestrictions() then
2120 DBM:AddMsg(DBM_CORE_NO_ARROW)
2121 return
2122 end
2123 local x, y, z = string.split(" ", msg:sub(6):trim())
2124 local xNum, yNum, zNum = tonumber(x or ""), tonumber(y or ""), tonumber(z or "")
2125 local success
2126 if xNum and yNum then
2127 DBM.Arrow:ShowRunTo(xNum, yNum, 0)
2128 success = true
2129 elseif type(x) == "string" and x:trim() ~= "" then
2130 local subCmd = x:trim()
2131 if subCmd:upper() == "HIDE" then
2132 DBM.Arrow:Hide()
2133 success = true
2134 elseif subCmd:upper() == "MOVE" then
2135 DBM.Arrow:Move()
2136 success = true
2137 elseif subCmd:upper() == "TARGET" then
2138 DBM.Arrow:ShowRunTo("target")
2139 success = true
2140 elseif subCmd:upper() == "FOCUS" then
2141 DBM.Arrow:ShowRunTo("focus")
2142 success = true
2143 elseif subCmd:upper() == "MAP" then
2144 DBM.Arrow:ShowRunTo(yNum, zNum, 0, nil, true)
2145 success = true
2146 elseif DBM:GetRaidUnitId(subCmd) then
2147 DBM.Arrow:ShowRunTo(subCmd)
2148 success = true
2149 end
2150 end
2151 if not success then
2152 for i, v in ipairs(DBM_ARROW_ERROR_USAGE) do
2153 DBM:AddMsg(v)
2154 end
2155 end
2156 elseif cmd:sub(1, 7) == "lockout" or cmd:sub(1, 3) == "ids" then
2157 if DBM:GetRaidRank(playerName) == 0 then
2158 return DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
2159 end
2160 if not IsInRaid() then
2161 return DBM:AddMsg(DBM_ERROR_NO_RAID)
2162 end
2163 DBM:RequestInstanceInfo()
2164 elseif cmd:sub(1, 10) == "debuglevel" then
2165 local level = tonumber(cmd:sub(11)) or 1
2166 if level < 1 or level > 3 then
2167 DBM:AddMsg("Invalid Value. Debug Level must be between 1 and 3.")
2168 return
2169 end
2170 DBM.Options.DebugLevel = level
2171 DBM:AddMsg("Debug Level is " .. level)
2172 elseif cmd:sub(1, 5) == "debug" then
2173 DBM.Options.DebugMode = DBM.Options.DebugMode == false and true or false
2174 DBM:AddMsg("Debug Message is " .. (DBM.Options.DebugMode and "ON" or "OFF"))
2175 elseif cmd:sub(1, 8) == "whereiam" or cmd:sub(1, 8) == "whereami" then
2176 if DBM:HasMapRestrictions() then
2177 local _, _, _, map = UnitPosition("player")
2178 local mapID = C_Map and C_Map.GetBestMapForUnit("player") or GetCurrentMapAreaID()
2179 DBM:AddMsg(("Location Information\nYou are at zone %u (%s).\nLocal Map ID %u (%s)"):format(map, GetRealZoneText(map), mapID, GetZoneText()))
2180 else
2181 local x, y, _, map = UnitPosition("player")
2182 local mapID, mapx, mapy
2183 if wowTOC == 80000 then
2184 mapID = C_Map.GetBestMapForUnit("player")
2185 local tempTable = C_Map.GetPlayerMapPosition(mapID, "player")
2186 mapx, mapy = tempTable.x, tempTable.y
2187 else
2188 SetMapToCurrentZone()
2189 mapID = GetCurrentMapAreaID()
2190 mapx, mapy = GetPlayerMapPosition("player")
2191 end
2192 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))
2193 end
2194 elseif cmd:sub(1, 7) == "request" then
2195 DBM:Unschedule(DBM.RequestTimers)
2196 DBM:RequestTimers(1)
2197 DBM:RequestTimers(2)
2198 DBM:RequestTimers(3)
2199 elseif cmd:sub(1, 6) == "silent" then
2200 DBM.Options.SilentMode = DBM.Options.SilentMode == false and true or false
2201 DBM:AddMsg("SilentMode is " .. (DBM.Options.SilentMode and "ON" or "OFF"))
2202 elseif cmd:sub(1, 10) == "musicstart" then
2203 DBM:TransitionToDungeonBGM(true)
2204 elseif cmd:sub(1, 9) == "musicstop" then
2205 DBM:TransitionToDungeonBGM(false, true)
2206 else
2207 DBM:LoadGUI()
2208 end
2209 end
2210end
2211
2212do
2213 local function updateRangeFrame(r, reverse)
2214 if DBM.RangeCheck:IsShown() then
2215 DBM.RangeCheck:Hide(true)
2216 else
2217 if DBM:HasMapRestrictions() then
2218 DBM:AddMsg(DBM_CORE_NO_RANGE)
2219 elseif IsInInstance() then
2220 DBM:AddMsg(DBM_CORE_NO_RANGE_SOON)
2221 end
2222 if r and (r < 201) then
2223 DBM.RangeCheck:Show(r, nil, true, nil, reverse)
2224 else
2225 DBM.RangeCheck:Show(10, nil, true, nil, reverse)
2226 end
2227 end
2228 end
2229 SLASH_DBMRANGE1 = "/range"
2230 SLASH_DBMRANGE2 = "/distance"
2231 SLASH_DBMHUDAR1 = "/hudar"
2232 SLASH_DBMRRANGE1 = "/rrange"
2233 SLASH_DBMRRANGE2 = "/rdistance"
2234 SlashCmdList["DBMRANGE"] = function(msg)
2235 local r = tonumber(msg) or 10
2236 updateRangeFrame(r, false)
2237 end
2238 SlashCmdList["DBMHUDAR"] = function(msg)
2239 local r = tonumber(msg) or 10
2240 DBMHudMap:ToggleHudar(r)
2241 end
2242 SlashCmdList["DBMRRANGE"] = function(msg)
2243 local r = tonumber(msg) or 10
2244 updateRangeFrame(r, true)
2245 end
2246end
2247
2248do
2249 local sortMe = {}
2250 local OutdatedUsers = {}
2251
2252 local function sort(v1, v2)
2253 if v1.revision and not v2.revision then
2254 return true
2255 elseif v2.revision and not v1.revision then
2256 return false
2257 elseif v1.revision and v2.revision then
2258 return v1.revision > v2.revision
2259 else
2260 return (v1.bwversion or 0) > (v2.bwversion or 0)
2261 end
2262 end
2263
2264 function DBM:ShowVersions(notify)
2265 for i, v in pairs(raid) do
2266 tinsert(sortMe, v)
2267 end
2268 tsort(sortMe, sort)
2269 twipe(OutdatedUsers)
2270 self:AddMsg(DBM_CORE_VERSIONCHECK_HEADER)
2271 for i, v in ipairs(sortMe) do
2272 local name = v.name
2273 local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(name)]
2274 if playerColor then
2275 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)
2276 end
2277 if v.displayVersion and not v.bwversion then--DBM, no BigWigs
2278 if self.Options.ShowAllVersions then
2279 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
2280 end
2281 if notify and v.revision < self.ReleaseRevision then
2282 SendChatMessage(chatPrefixShort..DBM_CORE_YOUR_VERSION_OUTDATED, "WHISPER", nil, v.name)
2283 end
2284 elseif self.Options.ShowAllVersions and v.displayVersion and v.bwversion then--DBM & BigWigs
2285 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)
2286 elseif self.Options.ShowAllVersions and not v.displayVersion and v.bwversion then--BigWigs, No DBM
2287 self:AddMsg(DBM_CORE_VERSIONCHECK_ENTRY:format(name, DBM_BIG_WIGS, versionResponseString:format(v.bwversion, v.bwhash), ""), false)
2288 else
2289 if self.Options.ShowAllVersions then
2290 self:AddMsg(DBM_CORE_VERSIONCHECK_ENTRY_NO_DBM:format(name), false)
2291 end
2292 end
2293 end
2294 local TotalUsers = #sortMe
2295 local NoDBM = 0
2296 local NoBigwigs = 0
2297 local OldMod = 0
2298 for i = #sortMe, 1, -1 do
2299 if not sortMe[i].revision then
2300 NoDBM = NoDBM + 1
2301 end
2302 if not (sortMe[i].bwversion) then
2303 NoBigwigs = NoBigwigs + 1
2304 end
2305 --Table sorting sorts dbm to top, bigwigs underneath. Highest version dbm always at top. so sortMe[1]
2306 --This check compares all dbm version to highest RELEASE version in raid.
2307 if sortMe[i].revision and (sortMe[i].revision < sortMe[1].version) or sortMe[i].bwversion and (sortMe[i].bwversion < fakeBWVersion) then
2308 OldMod = OldMod + 1
2309 local name = sortMe[i].name
2310 local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(name)]
2311 if playerColor then
2312 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)
2313 end
2314 tinsert(OutdatedUsers, name)
2315 end
2316 end
2317 local TotalDBM = TotalUsers - NoDBM
2318 local TotalBW = TotalUsers - NoBigwigs
2319 self:AddMsg("---", false)
2320 self:AddMsg(DBM_CORE_VERSIONCHECK_FOOTER:format(TotalDBM, TotalBW), false)
2321 self:AddMsg(DBM_CORE_VERSIONCHECK_OUTDATED:format(OldMod, #OutdatedUsers > 0 and tconcat(OutdatedUsers, ", ") or NONE), false)
2322 twipe(OutdatedUsers)
2323 twipe(sortMe)
2324 for i = #sortMe, 1, -1 do
2325 sortMe[i] = nil
2326 end
2327 end
2328end
2329
2330
2331-- Lag checking
2332do
2333 local sortLag = {}
2334 local nolagResponse = {}
2335 local function sortit(v1, v2)
2336 return (v1.worldlag or 0) < (v2.worldlag or 0)
2337 end
2338 function DBM:ShowLag()
2339 for i, v in pairs(raid) do
2340 tinsert(sortLag, v)
2341 end
2342 tsort(sortLag, sortit)
2343 self:AddMsg(DBM_CORE_LAG_HEADER)
2344 for i, v in ipairs(sortLag) do
2345 local name = v.name
2346 local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(name)]
2347 if playerColor then
2348 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)
2349 end
2350 if v.worldlag then
2351 self:AddMsg(DBM_CORE_LAG_ENTRY:format(name, v.worldlag, v.homelag), false)
2352 else
2353 tinsert(nolagResponse, v.name)
2354 end
2355 end
2356 if #nolagResponse > 0 then
2357 self:AddMsg(DBM_CORE_LAG_FOOTER:format(tconcat(nolagResponse, ", ")), false)
2358 for i = #nolagResponse, 1, -1 do
2359 nolagResponse[i] = nil
2360 end
2361 end
2362 for i = #sortLag, 1, -1 do
2363 sortLag[i] = nil
2364 end
2365 end
2366 if LL then
2367 LL:Register("DBM", function(homelag, worldlag, sender, channel)
2368 if sender and raid[sender] then
2369 raid[sender].homelag = homelag
2370 raid[sender].worldlag = worldlag
2371 end
2372 end)
2373 end
2374
2375end
2376
2377-- Durability checking
2378do
2379 local sortDur = {}
2380 local nodurResponse = {}
2381 local function sortit(v1, v2)
2382 return (v1.worldlag or 0) < (v2.worldlag or 0)
2383 end
2384 function DBM:ShowDurability()
2385 for i, v in pairs(raid) do
2386 tinsert(sortDur, v)
2387 end
2388 tsort(sortDur, sortit)
2389 self:AddMsg(DBM_CORE_DUR_HEADER)
2390 for i, v in ipairs(sortDur) do
2391 local name = v.name
2392 local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(name)]
2393 if playerColor then
2394 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)
2395 end
2396 if v.durpercent then
2397 self:AddMsg(DBM_CORE_DUR_ENTRY:format(name, v.durpercent, v.durbroken), false)
2398 else
2399 tinsert(nodurResponse, v.name)
2400 end
2401 end
2402 if #nodurResponse > 0 then
2403 self:AddMsg(DBM_CORE_LAG_FOOTER:format(tconcat(nodurResponse, ", ")), false)
2404 for i = #nodurResponse, 1, -1 do
2405 nodurResponse[i] = nil
2406 end
2407 end
2408 for i = #sortDur, 1, -1 do
2409 sortDur[i] = nil
2410 end
2411 end
2412 if LD then
2413 LD:Register("DBM", function(percent, broken, sender, channel)
2414 if sender and raid[sender] then
2415 raid[sender].durpercent = percent
2416 raid[sender].durbroken = broken
2417 end
2418 end)
2419 end
2420
2421end
2422
2423-------------------
2424-- Pizza Timer --
2425-------------------
2426do
2427
2428 local function loopTimer(time, text, broadcast, sender, count)
2429 DBM:CreatePizzaTimer(time, text, broadcast, sender, count, true)
2430 end
2431
2432 local ignore = {}
2433 local fakeMod -- dummy mod for the count sound effects
2434 --Standard Pizza Timer
2435 function DBM:CreatePizzaTimer(time, text, broadcast, sender, count, loop, terminate)
2436 if not fakeMod then
2437 fakeMod = self:NewMod("CreateCountTimerDummy")
2438 self:GetModLocalization("CreateCountTimerDummy"):SetGeneralLocalization{ name = DBM_CORE_MINIMAP_TOOLTIP_HEADER }
2439 fakeMod.countdown = fakeMod:NewCountdown(0, 0, nil, nil, nil, true)
2440 end
2441 if terminate or time == 0 then
2442 self:Unschedule(loopTimer)
2443 fakeMod.countdown:Cancel()
2444 self.Bars:CancelBar(text)
2445 fireEvent("DBM_TimerStop", "DBMPizzaTimer")
2446 return
2447 end
2448 if sender and ignore[sender] then return end
2449 text = text:sub(1, 16)
2450 text = text:gsub("%%t", UnitName("target") or "<no target>")
2451 if time < 3 then
2452 self:AddMsg(DBM_PIZZA_ERROR_USAGE)
2453 return
2454 end
2455 self.Bars:CreateBar(time, text, "Interface\\Icons\\Spell_Holy_BorrowedTime")
2456 fireEvent("DBM_TimerStart", "DBMPizzaTimer", text, time, "Interface\\Icons\\Spell_Holy_BorrowedTime", "pizzatimer", nil, 0)
2457 if broadcast then
2458 if count then
2459 if wowTOC == 80000 then
2460 sendLoggedSync("CU", ("%s\t%s"):format(time, text))
2461 else
2462 sendSync("CU", ("%s\t%s"):format(time, text))
2463 end
2464 else
2465 if wowTOC == 80000 then
2466 sendLoggedSync("U", ("%s\t%s"):format(time, text))
2467 else
2468 sendSync("U", ("%s\t%s"):format(time, text))
2469 end
2470 end
2471 end
2472 if sender then self:ShowPizzaInfo(text, sender) end
2473 if count then
2474 if not fakeMod then
2475 local threshold = self.Options.PTCountThreshold
2476 fakeMod = self:NewMod("CreateCountTimerDummy")
2477 self:GetModLocalization("CreateCountTimerDummy"):SetGeneralLocalization{ name = DBM_CORE_MINIMAP_TOOLTIP_HEADER }
2478 local adjustedThreshold = 5
2479 if threshold > 10 then
2480 adjustedThreshold = 10
2481 else
2482 adjustedThreshold = floor(threshold)
2483 end
2484 fakeMod.countdown = fakeMod:NewCountdown(0, 0, nil, nil, adjustedThreshold, true)
2485 end
2486 if not self.Options.DontPlayPTCountdown then
2487 fakeMod.countdown:Cancel()
2488 fakeMod.countdown:Start(time)
2489 end
2490 end
2491 if loop then
2492 self:Unschedule(loopTimer)--Only one loop timer supported at once doing this, but much cleaner this way
2493 self:Schedule(time, loopTimer, time, text, broadcast, sender, count)
2494 end
2495 end
2496
2497 function DBM:AddToPizzaIgnore(name)
2498 ignore[name] = true
2499 end
2500end
2501
2502function DBM:ShowPizzaInfo(id, sender)
2503 if self.Options.ShowPizzaMessage then
2504 self:AddMsg(DBM_PIZZA_SYNC_INFO:format(sender, id))
2505 end
2506end
2507
2508-----------------
2509-- GUI Stuff --
2510-----------------
2511do
2512 local callOnLoad = {}
2513 function DBM:LoadGUI()
2514 if GetAddOnEnableState(playerName, "VEM-Core") >= 1 then
2515 self:AddMsg(DBM_CORE_VEM)
2516 return
2517 end
2518 if GetAddOnEnableState(playerName, "DBM-Profiles") >= 1 then
2519 self:AddMsg(DBM_CORE_3RDPROFILES)
2520 return
2521 end
2522 if GetAddOnEnableState(playerName, "DPMCore") >= 1 then
2523 self:AddMsg(DBM_CORE_DPMCORE)
2524 return
2525 end
2526 if not dbmIsEnabled then
2527 DBM:AddMsg(DBM_CORE_UPDATEREMINDER_DISABLE)
2528 return
2529 end
2530 if not IsAddOnLoaded("DBM-GUI") then
2531 local enabled = GetAddOnEnableState(playerName, "DBM-GUI")
2532 if enabled == 0 then
2533 EnableAddOn("DBM-GUI")
2534 end
2535 local loaded, reason = LoadAddOn("DBM-GUI")
2536 if not loaded then
2537 if reason then
2538 self:AddMsg(DBM_CORE_LOAD_GUI_ERROR:format(tostring(_G["ADDON_"..reason or ""])))
2539 else
2540 self:AddMsg(DBM_CORE_LOAD_GUI_ERROR:format(DBM_CORE_UNKNOWN))
2541 end
2542 return false
2543 end
2544 tsort(callOnLoad, function(v1, v2) return v1[2] < v2[2] end)
2545 for i, v in ipairs(callOnLoad) do v[1]() end
2546 if not InCombatLockdown() and not UnitAffectingCombat("player") and not IsFalling() then--We loaded in combat but still need to avoid garbage collect in combat
2547 collectgarbage("collect")
2548 end
2549 end
2550 return DBM_GUI:ShowHide()
2551 end
2552
2553 function DBM:RegisterOnGuiLoadCallback(f, sort)
2554 tinsert(callOnLoad, {f, sort or mhuge})
2555 end
2556end
2557
2558
2559----------------------
2560-- Minimap Button --
2561----------------------
2562do
2563 local dragMode = nil
2564
2565 local function moveButton(self)
2566 if dragMode == "free" then
2567 local centerX, centerY = Minimap:GetCenter()
2568 local x, y = GetCursorPosition()
2569 x, y = x / self:GetEffectiveScale() - centerX, y / self:GetEffectiveScale() - centerY
2570 self:ClearAllPoints()
2571 self:SetPoint("CENTER", x, y)
2572 else
2573 local centerX, centerY = Minimap:GetCenter()
2574 local x, y = GetCursorPosition()
2575 x, y = x / self:GetEffectiveScale() - centerX, y / self:GetEffectiveScale() - centerY
2576 centerX, centerY = math.abs(x), math.abs(y)
2577 centerX, centerY = (centerX / math.sqrt(centerX^2 + centerY^2)) * 80, (centerY / sqrt(centerX^2 + centerY^2)) * 80
2578 centerX = x < 0 and -centerX or centerX
2579 centerY = y < 0 and -centerY or centerY
2580 self:ClearAllPoints()
2581 self:SetPoint("CENTER", centerX, centerY)
2582 end
2583 end
2584
2585 local button = CreateFrame("Button", "DBMMinimapButton", Minimap)
2586 button:SetHeight(32)
2587 button:SetWidth(32)
2588 button:SetFrameStrata("MEDIUM")
2589 button:SetPoint("CENTER", -65.35, -38.8)
2590 button:SetMovable(true)
2591 button:SetUserPlaced(true)
2592 button:SetNormalTexture("Interface\\AddOns\\DBM-Core\\textures\\Minimap-Button-Up")
2593 button:SetPushedTexture("Interface\\AddOns\\DBM-Core\\textures\\Minimap-Button-Down")
2594 button:SetHighlightTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")
2595
2596 button:SetScript("OnMouseDown", function(self, button)
2597 if IsShiftKeyDown() and IsAltKeyDown() then
2598 dragMode = "free"
2599 self:SetScript("OnUpdate", moveButton)
2600 elseif IsShiftKeyDown() or button == "RightButton" then
2601 dragMode = nil
2602 self:SetScript("OnUpdate", moveButton)
2603 end
2604 end)
2605 button:SetScript("OnMouseUp", function(self)
2606 self:SetScript("OnUpdate", nil)
2607 end)
2608 button:SetScript("OnClick", function(self, button)
2609 if IsShiftKeyDown() or button == "RightButton" then return end
2610 DBM:LoadGUI()
2611 end)
2612 button:SetScript("OnEnter", function(self)
2613 GameTooltip_SetDefaultAnchor(GameTooltip, self)
2614 GameTooltip:SetText(DBM_CORE_MINIMAP_TOOLTIP_HEADER, 1, 1, 1)
2615 GameTooltip:AddLine(ver, NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, 1)
2616 GameTooltip:AddLine(" ")
2617 GameTooltip:AddLine(DBM_CORE_MINIMAP_TOOLTIP_FOOTER, RAID_CLASS_COLORS.MAGE.r, RAID_CLASS_COLORS.MAGE.g, RAID_CLASS_COLORS.MAGE.b, 1)
2618 GameTooltip:Show()
2619 end)
2620 button:SetScript("OnLeave", function(self)
2621 GameTooltip:Hide()
2622 end)
2623
2624 function DBM:ToggleMinimapButton()
2625 self.Options.ShowMinimapButton = not self.Options.ShowMinimapButton
2626 if self.Options.ShowMinimapButton then
2627 button:Show()
2628 else
2629 button:Hide()
2630 end
2631 end
2632
2633 function DBM:HideMinimapButton()
2634 return button:Hide()
2635 end
2636end
2637
2638-------------------------------------------------
2639-- Raid/Party Handling and Unit ID Utilities --
2640-------------------------------------------------
2641do
2642 local inRaid = false
2643
2644 local raidGuids = {}
2645 local iconSeter = {}
2646
2647 -- save playerinfo into raid table on load. (for solo raid)
2648 DBM:RegisterOnLoadCallback(function()
2649 C_TimerAfter(6, function()
2650 if not raid[playerName] then
2651 raid[playerName] = {}
2652 raid[playerName].name = playerName
2653 raid[playerName].shortname = playerName
2654 raid[playerName].guid = UnitGUID("player")
2655 raid[playerName].rank = 0
2656 raid[playerName].class = playerClass
2657 raid[playerName].id = "player"
2658 raid[playerName].groupId = 0
2659 raid[playerName].revision = DBM.Revision
2660 raid[playerName].version = DBM.ReleaseRevision
2661 raid[playerName].displayVersion = DBM.DisplayVersion
2662 raid[playerName].locale = GetLocale()
2663 raid[playerName].enabledIcons = tostring(not DBM.Options.DontSetIcons)
2664 raidGuids[UnitGUID("player") or ""] = playerName
2665 end
2666 end)
2667 end)
2668
2669 local function updateAllRoster(self)
2670 if IsInRaid() then
2671 if not inRaid then
2672 inRaid = true
2673 sendSync("H")
2674 SendAddonMessage("BigWigs", versionQueryString:format(0, fakeBWHash), IsPartyLFG() and "INSTANCE_CHAT" or "RAID")
2675 self:Schedule(2, self.RoleCheck, false, self)
2676 fireEvent("raidJoin", playerName)
2677 if BigWigs and BigWigs.db.profile.raidicon and not self.Options.DontSetIcons and self:GetRaidRank() > 0 then--Both DBM and bigwigs have raid icon marking turned on.
2678 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 or turned off to stop this alert)
2679 end
2680 end
2681 for i = 1, GetNumGroupMembers() do
2682 local name, rank, subgroup, _, _, className = GetRaidRosterInfo(i)
2683 -- Maybe GetNumGroupMembers() bug? Seems that GetNumGroupMembers() rarely returns bad value, causing GetRaidRosterInfo() returns to nil.
2684 -- Filter name = nil to prevent nil table error.
2685 if name then
2686 local id = "raid" .. i
2687 local shortname = UnitName(id)
2688 if (not raid[name]) and inRaid then
2689 fireEvent("raidJoin", name)
2690 end
2691 raid[name] = raid[name] or {}
2692 raid[name].name = name
2693 raid[name].shortname = shortname
2694 raid[name].rank = rank
2695 raid[name].subgroup = subgroup
2696 raid[name].class = className
2697 raid[name].id = id
2698 raid[name].groupId = i
2699 raid[name].guid = UnitGUID(id) or ""
2700 raid[name].updated = true
2701 raidGuids[UnitGUID(id) or ""] = name
2702 end
2703 end
2704 enableIcons = false
2705 twipe(iconSeter)
2706 for i, v in pairs(raid) do
2707 if not v.updated then
2708 raidGuids[v.guid] = nil
2709 raid[i] = nil
2710 removeEntry(newerVersionPerson, i)
2711 fireEvent("raidLeave", i)
2712 else
2713 v.updated = nil
2714 if v.revision and v.rank > 0 and (v.enabledIcons or "") == "true" then
2715 iconSeter[#iconSeter + 1] = v.revision.." "..v.name
2716 end
2717 end
2718 end
2719 if #iconSeter > 0 then
2720 tsort(iconSeter, function(a, b) return a > b end)
2721 local elected = iconSeter[1]
2722 if playerName == elected:sub(elected:find(" ") + 1) then
2723 enableIcons = true
2724 end
2725 end
2726 elseif IsInGroup() then
2727 if not inRaid then
2728 -- joined a new party
2729 inRaid = true
2730 sendSync("H")
2731 SendAddonMessage("BigWigs", versionQueryString:format(0, fakeBWHash), IsPartyLFG() and "INSTANCE_CHAT" or "RAID")
2732 self:Schedule(2, self.RoleCheck, false, self)
2733 fireEvent("partyJoin", playerName)
2734 end
2735 for i = 0, GetNumSubgroupMembers() do
2736 local id
2737 if (i == 0) then
2738 id = "player"
2739 else
2740 id = "party"..i
2741 end
2742 local name = GetUnitName(id, true)
2743 local shortname = UnitName(id)
2744 local rank = UnitIsGroupLeader(id) and 2 or 0
2745 local _, className = UnitClass(id)
2746 if (not raid[name]) and inRaid then
2747 fireEvent("partyJoin", name)
2748 end
2749 raid[name] = raid[name] or {}
2750 raid[name].name = name
2751 raid[name].shortname = shortname
2752 raid[name].guid = UnitGUID(id) or ""
2753 raid[name].rank = rank
2754 raid[name].class = className
2755 raid[name].id = id
2756 raid[name].groupId = i
2757 raid[name].updated = true
2758 raidGuids[UnitGUID(id) or ""] = name
2759 end
2760 enableIcons = false
2761 twipe(iconSeter)
2762 for i, v in pairs(raid) do
2763 if not v.updated then
2764 raidGuids[v.guid] = nil
2765 raid[i] = nil
2766 removeEntry(newerVersionPerson, i)
2767 fireEvent("partyLeave", i)
2768 else
2769 v.updated = nil
2770 if v.revision and v.rank > 0 and (v.enabledIcons or "") == "true" then
2771 iconSeter[#iconSeter + 1] = v.revision.." "..v.name
2772 end
2773 end
2774 end
2775 if #iconSeter > 0 then
2776 tsort(iconSeter, function(a, b) return a > b end)
2777 local elected = iconSeter[1]
2778 if playerName == elected:sub(elected:find(" ") + 1) then
2779 enableIcons = true
2780 end
2781 end
2782 else
2783 -- left the current group/raid
2784 inRaid = false
2785 enableIcons = true
2786 fireEvent("raidLeave", playerName)
2787 twipe(raid)
2788 twipe(newerVersionPerson)
2789 -- restore playerinfo into raid table on raidleave. (for solo raid)
2790 raid[playerName] = {}
2791 raid[playerName].name = playerName
2792 raid[playerName].shortname = playerName
2793 raid[playerName].guid = UnitGUID("player")
2794 raid[playerName].rank = 0
2795 raid[playerName].class = playerClass
2796 raid[playerName].id = "player"
2797 raid[playerName].groupId = 0
2798 raid[playerName].revision = DBM.Revision
2799 raid[playerName].version = DBM.ReleaseRevision
2800 raid[playerName].displayVersion = DBM.DisplayVersion
2801 raid[playerName].locale = GetLocale()
2802 raidGuids[UnitGUID("player")] = playerName
2803 end
2804 end
2805
2806 function DBM:GROUP_ROSTER_UPDATE(force)
2807 self:Unschedule(updateAllRoster)
2808 if force then
2809 updateAllRoster(self)
2810 else
2811 self:Schedule(1.5, updateAllRoster, self)
2812 end
2813 end
2814
2815 function DBM:INSTANCE_GROUP_SIZE_CHANGED()
2816 local _, _, _, _, _, _, _, _, instanceGroupSize = GetInstanceInfo()
2817 LastGroupSize = instanceGroupSize
2818 end
2819
2820 --C_Map.GetMapGroupMembersInfo
2821 function DBM:GetNumRealPlayersInZone()
2822 if not IsInGroup() then return 1 end
2823 local total = 0
2824 local _, _, _, currentMapId = UnitPosition("player")
2825 if IsInRaid() then
2826 for i = 1, GetNumGroupMembers() do
2827 local _, _, _, targetMapId = UnitPosition("raid"..i)
2828 if targetMapId == currentMapId then
2829 total = total + 1
2830 end
2831 end
2832 else
2833 total = 1--add player/self for "party" count
2834 for i = 1, GetNumSubgroupMembers() do
2835 local _, _, _, targetMapId = UnitPosition("party"..i)
2836 if targetMapId == currentMapId then
2837 total = total + 1
2838 end
2839 end
2840 end
2841 return total
2842 end
2843
2844 function DBM:GetRaidRank(name)
2845 local name = name or playerName
2846 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.
2847 return UnitIsGroupLeader("player") and 2 or UnitIsGroupAssistant("player") and 1 or 0
2848 else
2849 return (raid[name] and raid[name].rank) or 0
2850 end
2851 end
2852
2853 function DBM:GetRaidSubgroup(name)
2854 return (raid[name] and raid[name].subgroup) or 0
2855 end
2856
2857 function DBM:GetRaidClass(name)
2858 return (raid[name] and raid[name].class) or "UNKNOWN"
2859 end
2860
2861 function DBM:GetRaidUnitId(name)
2862 return raid[name] and raid[name].id
2863 end
2864
2865 function DBM:GetEnemyUnitIdByGUID(guid)
2866 for i = 1, 5 do
2867 local unitId = "boss"..i
2868 local guid2 = UnitGUID(unitId)
2869 if guid == guid2 then
2870 return unitId
2871 end
2872 end
2873 local idType = (IsInRaid() and "raid") or "party"
2874 for i = 0, GetNumGroupMembers() do
2875 local unitId = ((i == 0) and "target") or idType..i.."target"
2876 local guid2 = UnitGUID(unitId)
2877 if guid == guid2 then
2878 return unitId
2879 end
2880 end
2881 return DBM_CORE_UNKNOWN
2882 end
2883
2884 function DBM:GetPlayerGUIDByName(name)
2885 return raid[name] and raid[name].guid
2886 end
2887
2888 function DBM:GetMyPlayerInfo()
2889 return playerName, playerLevel, playerRealm
2890 end
2891
2892 function DBM:GetUnitFullName(uId)
2893 if not uId then return nil end
2894 return GetUnitName(uId, true)
2895 end
2896
2897 function DBM:GetFullPlayerNameByGUID(guid)
2898 return raidGuids[guid]
2899 end
2900
2901 function DBM:GetPlayerNameByGUID(guid)
2902 return raidGuids[guid] and raidGuids[guid]:gsub("%-.*$", "")
2903 end
2904
2905 function DBM:GetGroupId(name)
2906 local raidMember = raid[name] or raid[GetUnitName(name, true) or ""]
2907 return raidMember and raidMember.groupId or 0
2908 end
2909end
2910
2911do
2912 -- yes, we still do avoid memory allocations during fights; so we don't use a closure around a counter here
2913 -- this seems to be the easiest way to write an iterator that returns the unit id *string* as first argument without a memory allocation
2914 local function raidIterator(groupMembers, uId)
2915 local a, b = uId:byte(-2, -1)
2916 local i = (a >= 0x30 and a <= 0x39 and (a - 0x30) * 10 or 0) + b - 0x30
2917 if i < groupMembers then
2918 return "raid" .. i + 1, i + 1
2919 end
2920 end
2921
2922 local function partyIterator(groupMembers, uId)
2923 if not uId then
2924 return "player", 0
2925 elseif uId == "player" then
2926 if groupMembers > 0 then
2927 return "party1", 1
2928 end
2929 else
2930 local i = uId:byte(-1) - 0x30
2931 if i < groupMembers then
2932 return "party" .. i + 1, i + 1
2933 end
2934 end
2935 end
2936
2937 local function soloIterator(_, state)
2938 if not state then -- no state == first call
2939 return "player", 0
2940 end
2941 end
2942
2943 -- returns the unit ids of all raid or party members, including the player's own id
2944 -- limitations: will break if there are ever raids with more than 99 players or partys with more than 10
2945 function DBM:GetGroupMembers()
2946 if IsInRaid() then
2947 return raidIterator, GetNumGroupMembers(), "raid0"
2948 elseif IsInGroup() then
2949 return partyIterator, GetNumSubgroupMembers(), nil
2950 else
2951 -- solo!
2952 return soloIterator, nil, nil
2953 end
2954 end
2955end
2956
2957function DBM:GetNumGroupMembers()
2958 return IsInGroup() and GetNumGroupMembers() or 1
2959end
2960
2961--For returning the number of players actually in zone with us for status functions
2962--This is very touchy though and will fail if everyone isn't in same SUB zone (ie same room/area)
2963--It should work for pretty much any case but outdoor
2964function DBM:GetNumRealGroupMembers()
2965 if not IsInInstance() then--Not accurate outside of instances (such as world bosses)
2966 return IsInGroup() and GetNumGroupMembers() or 1--So just return regular group members.
2967 end
2968 local _, _, _, currentMapId = UnitPosition("player")
2969 local realGroupMembers = 0
2970 if IsInGroup() then
2971 for uId in self:GetGroupMembers() do
2972 local _, _, _, targetMapId = UnitPosition(uId)
2973 if targetMapId == currentMapId then
2974 realGroupMembers = realGroupMembers + 1
2975 end
2976 end
2977 else
2978 return 1
2979 end
2980 return realGroupMembers
2981end
2982
2983function DBM:GetUnitCreatureId(uId)
2984 local guid = UnitGUID(uId)
2985 return self:GetCIDFromGUID(guid)
2986end
2987
2988--Creature/Vehicle/Pet
2989----<type>:<subtype>:<realmID>:<mapID>:<serverID>:<dbID>:<creationbits>
2990--Player/Item
2991----<type>:<realmID>:<dbID>
2992function DBM:GetCIDFromGUID(guid)
2993 local type, _, playerdbID, _, _, cid, creationbits = strsplit("-", guid or "")
2994 if type and (type == "Creature" or type == "Vehicle" or type == "Pet") then
2995 return tonumber(cid)
2996 elseif type and (type == "Player" or type == "Item") then
2997 return tonumber(playerdbID)
2998 end
2999 return 0
3000end
3001
3002function DBM:IsCreatureGUID(guid)
3003 local type = strsplit("-", guid or "")
3004 if type and (type == "Creature" or type == "Vehicle") then--To determine, add pet or not?
3005 return true
3006 end
3007 return false
3008end
3009
3010function DBM:GetBossUnitId(name, bossOnly)
3011 local returnUnitID
3012 for i = 1, 5 do
3013 if UnitName("boss" .. i) == name then
3014 returnUnitID = "boss"..i
3015 end
3016 end
3017 if not returnUnitID and not bossOnly then
3018 for uId in self:GetGroupMembers() do
3019 if UnitName(uId .. "target") == name and not UnitIsPlayer(uId .. "target") then
3020 returnUnitID = uId.."target"
3021 end
3022 end
3023 end
3024 return returnUnitID
3025end
3026
3027function DBM:GetUnitIdFromGUID(guid, bossOnly)
3028 local returnUnitID
3029 for i = 1, 5 do
3030 local unitId = "boss"..i
3031 local bossGUID = UnitGUID(unitId)
3032 if bossGUID == guid then
3033 returnUnitID = bossGUID
3034 end
3035 end
3036 --Didn't find valid unitID from boss units, scan raid targets
3037 if not returnUnitID and not bossOnly then
3038 for uId in self:GetGroupMembers() do
3039 if UnitGUID(uId .. "target") == guid then
3040 returnUnitID = uId.."target"
3041 end
3042 end
3043 end
3044 return returnUnitID
3045end
3046
3047---------------
3048-- Options --
3049---------------
3050function DBM:AddDefaultOptions(t1, t2)
3051 for i, v in pairs(t2) do
3052 if t1[i] == nil then
3053 t1[i] = v
3054 elseif type(v) == "table" and type(t1[i]) == "table" then
3055 self:AddDefaultOptions(t1[i], v)
3056 end
3057 end
3058end
3059
3060function DBM:LoadModOptions(modId, inCombat, first)
3061 local oldSavedVarsName = modId:gsub("-", "").."_SavedVars"
3062 local savedVarsName = modId:gsub("-", "").."_AllSavedVars"
3063 local savedStatsName = modId:gsub("-", "").."_SavedStats"
3064 local fullname = playerName.."-"..playerRealm
3065 if not currentSpecID or not currentSpecGroup then
3066 self:SetCurrentSpecInfo()
3067 end
3068 local profileNum = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
3069 if not _G[savedVarsName] then _G[savedVarsName] = {} end
3070 local savedOptions = _G[savedVarsName][fullname] or {}
3071 local savedStats = _G[savedStatsName] or {}
3072 local existId = {}
3073 for i, id in ipairs(DBM.ModLists[modId]) do
3074 existId[id] = true
3075 -- init
3076 if not savedOptions[id] then savedOptions[id] = {} end
3077 local mod = DBM:GetModByName(id)
3078 -- migrate old option
3079 if _G[oldSavedVarsName] and _G[oldSavedVarsName][id] then
3080 self:Debug("LoadModOptions: Found old options, importing", 2)
3081 local oldTable = _G[oldSavedVarsName][id]
3082 _G[oldSavedVarsName][id] = nil
3083 savedOptions[id][profileNum] = oldTable
3084 end
3085 if not savedOptions[id][profileNum] and not first then--previous profile not found. load defaults
3086 self:Debug("LoadModOptions: No saved options, creating defaults for profile "..profileNum, 2)
3087 local defaultOptions = {}
3088 for option, optionValue in pairs(mod.DefaultOptions) do
3089 if type(optionValue) == "table" then
3090 optionValue = optionValue.value
3091 elseif type(optionValue) == "string" then
3092 optionValue = mod:GetRoleFlagValue(optionValue)
3093 end
3094 defaultOptions[option] = optionValue
3095 end
3096 savedOptions[id][profileNum] = defaultOptions
3097 else
3098 savedOptions[id][profileNum] = savedOptions[id][profileNum] or mod.Options
3099 --check new option
3100 for option, optionValue in pairs(mod.DefaultOptions) do
3101 if savedOptions[id][profileNum][option] == nil then
3102 if type(optionValue) == "table" then
3103 optionValue = optionValue.value
3104 elseif type(optionValue) == "string" then
3105 optionValue = mod:GetRoleFlagValue(optionValue)
3106 end
3107 savedOptions[id][profileNum][option] = optionValue
3108 end
3109 end
3110 --clean unused saved variables (do not work on combat load)
3111 if not inCombat then
3112 for option, optionValue in pairs(savedOptions[id][profileNum]) do
3113 if mod.DefaultOptions[option] == nil then
3114 savedOptions[id][profileNum][option] = nil
3115 elseif mod.DefaultOptions[option] and (type(mod.DefaultOptions[option]) == "table") then--recover broken dropdown option
3116 if savedOptions[id][profileNum][option] and (type(savedOptions[id][profileNum][option]) == "boolean") then
3117 savedOptions[id][profileNum][option] = mod.DefaultOptions[option].value
3118 end
3119 --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.
3120 elseif option:find("TColor") then
3121 if savedOptions[id][profileNum][option] and savedOptions[id][profileNum][option] == 0 and mod.DefaultOptions[option] and mod.DefaultOptions[option] ~= 0 then
3122 savedOptions[id][profileNum][option] = mod.DefaultOptions[option]
3123 end
3124 end
3125 end
3126 end
3127 end
3128 --apply saved option to actual option table
3129 mod.Options = savedOptions[id][profileNum]
3130 --stats init (only first load)
3131 if first then
3132 savedStats[id] = savedStats[id] or {}
3133 local stats = savedStats[id]
3134 stats.normalKills = stats.normalKills or 0
3135 stats.normalPulls = stats.normalPulls or 0
3136 stats.heroicKills = stats.heroicKills or 0
3137 stats.heroicPulls = stats.heroicPulls or 0
3138 stats.challengeKills = stats.challengeKills or 0
3139 stats.challengePulls = stats.challengePulls or 0
3140 stats.challengeBestRank = stats.challengeBestRank or 0
3141 stats.mythicKills = stats.mythicKills or 0
3142 stats.mythicPulls = stats.mythicPulls or 0
3143 stats.normal25Kills = stats.normal25Kills or 0
3144 stats.normal25Kills = stats.normal25Kills or 0
3145 stats.normal25Pulls = stats.normal25Pulls or 0
3146 stats.heroic25Kills = stats.heroic25Kills or 0
3147 stats.heroic25Pulls = stats.heroic25Pulls or 0
3148 stats.lfr25Kills = stats.lfr25Kills or 0
3149 stats.lfr25Pulls = stats.lfr25Pulls or 0
3150 stats.timewalkerKills = stats.timewalkerKills or 0
3151 stats.timewalkerPulls = stats.timewalkerPulls or 0
3152 mod.stats = stats
3153 --run OnInitialize function
3154 if mod.OnInitialize then mod:OnInitialize(mod) end
3155 end
3156 end
3157 --clean unused saved variables (do not work on combat load)
3158 if not inCombat then
3159 for id, table in pairs(savedOptions) do
3160 if not existId[id] and not id:find("talent") then
3161 savedOptions[id] = nil
3162 end
3163 end
3164 for id, table in pairs(savedStats) do
3165 if not existId[id] then
3166 savedStats[id] = nil
3167 end
3168 end
3169 end
3170 _G[savedVarsName][fullname] = savedOptions
3171 if profileNum > 0 then
3172 _G[savedVarsName][fullname]["talent"..profileNum] = currentSpecName
3173 self:Debug("LoadModOptions: Finished loading "..(_G[savedVarsName][fullname]["talent"..profileNum] or DBM_CORE_UNKNOWN))
3174 end
3175 _G[savedStatsName] = savedStats
3176 if not first and DBM_GUI and DBM_GUI.currentViewing and DBM_GUI_OptionsFrame:IsShown() then
3177 DBM_GUI_OptionsFrame:DisplayFrame(DBM_GUI.currentViewing)
3178 end
3179end
3180
3181function DBM:SpecChanged(force)
3182 if not force and not DBM_UseDualProfile then return end
3183 --Load Options again.
3184 self:Debug("SpecChanged fired", 2)
3185 for modId, idTable in pairs(self.ModLists) do
3186 self:LoadModOptions(modId)
3187 end
3188end
3189
3190function DBM:PLAYER_LEVEL_UP()
3191 playerLevel = UnitLevel("player")
3192 if playerLevel < 15 and playerLevel > 9 then
3193 self:PLAYER_SPECIALIZATION_CHANGED()
3194 end
3195end
3196
3197function DBM:LoadAllModDefaultOption(modId)
3198 -- modId is string like "DBM-Highmaul"
3199 if not modId or not self.ModLists[modId] then return end
3200 -- prevent error
3201 if not currentSpecID or not currentSpecGroup then
3202 self:SetCurrentSpecInfo()
3203 end
3204 -- variable init
3205 local savedVarsName = modId:gsub("-", "").."_AllSavedVars"
3206 local fullname = playerName.."-"..playerRealm
3207 local profileNum = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
3208 -- prevent nil table error
3209 if not _G[savedVarsName] then _G[savedVarsName] = {} end
3210 for i, id in ipairs(self.ModLists[modId]) do
3211 -- prevent nil table error
3212 if not _G[savedVarsName][fullname][id] then _G[savedVarsName][fullname][id] = {} end
3213 -- actual do load default option
3214 local mod = self:GetModByName(id)
3215 local defaultOptions = {}
3216 for option, optionValue in pairs(mod.DefaultOptions) do
3217 if type(optionValue) == "table" then
3218 optionValue = optionValue.value
3219 elseif type(optionValue) == "string" then
3220 optionValue = mod:GetRoleFlagValue(optionValue)
3221 end
3222 defaultOptions[option] = optionValue
3223 end
3224 mod.Options = {}
3225 mod.Options = defaultOptions
3226 _G[savedVarsName][fullname][id][profileNum] = {}
3227 _G[savedVarsName][fullname][id][profileNum] = mod.Options
3228 end
3229 self:AddMsg(DBM_CORE_ALLMOD_DEFAULT_LOADED)
3230 -- update gui if showing
3231 if DBM_GUI and DBM_GUI.currentViewing and DBM_GUI_OptionsFrame:IsShown() then
3232 DBM_GUI_OptionsFrame:DisplayFrame(DBM_GUI.currentViewing)
3233 end
3234end
3235
3236function DBM:LoadModDefaultOption(mod)
3237 -- mod must be table
3238 if not mod then return end
3239 -- prevent error
3240 if not currentSpecID or not currentSpecGroup then
3241 self:SetCurrentSpecInfo()
3242 end
3243 -- variable init
3244 local savedVarsName = (mod.modId):gsub("-", "").."_AllSavedVars"
3245 local fullname = playerName.."-"..playerRealm
3246 local profileNum = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
3247 -- prevent nil table error
3248 if not _G[savedVarsName] then _G[savedVarsName] = {} end
3249 if not _G[savedVarsName][fullname] then _G[savedVarsName][fullname] = {} end
3250 if not _G[savedVarsName][fullname][mod.id] then _G[savedVarsName][fullname][mod.id] = {} end
3251 -- do load default
3252 local defaultOptions = {}
3253 for option, optionValue in pairs(mod.DefaultOptions) do
3254 if type(optionValue) == "table" then
3255 optionValue = optionValue.value
3256 elseif type(optionValue) == "string" then
3257 optionValue = mod:GetRoleFlagValue(optionValue)
3258 end
3259 defaultOptions[option] = optionValue
3260 end
3261 mod.Options = {}
3262 mod.Options = defaultOptions
3263 _G[savedVarsName][fullname][mod.id][profileNum] = {}
3264 _G[savedVarsName][fullname][mod.id][profileNum] = mod.Options
3265 self:AddMsg(DBM_CORE_MOD_DEFAULT_LOADED)
3266 -- update gui if showing
3267 if DBM_GUI and DBM_GUI.currentViewing and DBM_GUI_OptionsFrame:IsShown() then
3268 DBM_GUI_OptionsFrame:DisplayFrame(DBM_GUI.currentViewing)
3269 end
3270end
3271
3272function DBM:CopyAllModOption(modId, sourceName, sourceProfile)
3273 -- modId is string like "DBM-Highmaul"
3274 if not modId or not sourceName or not sourceProfile or not DBM.ModLists[modId] then return end
3275 -- prevent error
3276 if not currentSpecID or not currentSpecGroup then
3277 self:SetCurrentSpecInfo()
3278 end
3279 -- variable init
3280 local savedVarsName = modId:gsub("-", "").."_AllSavedVars"
3281 local targetName = playerName.."-"..playerRealm
3282 local targetProfile = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
3283 -- do not copy setting itself
3284 if targetName == sourceName and targetProfile == sourceProfile then
3285 self:AddMsg(DBM_CORE_MPROFILE_COPY_SELF_ERROR)
3286 return
3287 end
3288 -- prevent nil table error
3289 if not _G[savedVarsName] then _G[savedVarsName] = {} end
3290 -- check source is exist
3291 if not _G[savedVarsName][sourceName] then
3292 self:AddMsg(DBM_CORE_MPROFILE_COPY_S_ERROR)
3293 return
3294 end
3295 local targetOptions = _G[savedVarsName][targetName] or {}
3296 for i, id in ipairs(self.ModLists[modId]) do
3297 -- check source is exist
3298 if not _G[savedVarsName][sourceName][id] then
3299 self:AddMsg(DBM_CORE_MPROFILE_COPY_S_ERROR)
3300 return
3301 end
3302 if not _G[savedVarsName][sourceName][id][sourceProfile] then
3303 self:AddMsg(DBM_CORE_MPROFILE_COPY_S_ERROR)
3304 return
3305 end
3306 -- prevent nil table error
3307 if not _G[savedVarsName][targetName][id] then _G[savedVarsName][targetName][id] = {} end
3308 -- copy table
3309 _G[savedVarsName][targetName][id][targetProfile] = {}--clear before copy
3310 _G[savedVarsName][targetName][id][targetProfile] = _G[savedVarsName][sourceName][id][sourceProfile]
3311 --check new option
3312 local mod = self:GetModByName(id)
3313 for option, optionValue in pairs(mod.Options) do
3314 if _G[savedVarsName][targetName][id][targetProfile][option] == nil then
3315 _G[savedVarsName][targetName][id][targetProfile][option] = optionValue
3316 end
3317 end
3318 -- apply to options table
3319 mod.Options = {}
3320 mod.Options = _G[savedVarsName][targetName][id][targetProfile]
3321 end
3322 if targetProfile > 0 then
3323 _G[savedVarsName][targetName]["talent"..targetProfile] = currentSpecName
3324 end
3325 self:AddMsg(DBM_CORE_MPROFILE_COPY_SUCCESS:format(sourceName, sourceProfile))
3326 -- update gui if showing
3327 if DBM_GUI and DBM_GUI.currentViewing and DBM_GUI_OptionsFrame:IsShown() then
3328 DBM_GUI_OptionsFrame:DisplayFrame(DBM_GUI.currentViewing)
3329 end
3330end
3331
3332function DBM:CopyAllModTypeOption(modId, sourceName, sourceProfile, Type)
3333 -- modId is string like "DBM-Highmaul"
3334 if not modId or not sourceName or not sourceProfile or not self.ModLists[modId] or not Type then return end
3335 -- prevent error
3336 if not currentSpecID or not currentSpecGroup then
3337 self:SetCurrentSpecInfo()
3338 end
3339 -- variable init
3340 local savedVarsName = modId:gsub("-", "").."_AllSavedVars"
3341 local targetName = playerName.."-"..playerRealm
3342 local targetProfile = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
3343 -- do not copy setting itself
3344 if targetName == sourceName and targetProfile == sourceProfile then
3345 self:AddMsg(DBM_CORE_MPROFILE_COPYS_SELF_ERROR)
3346 return
3347 end
3348 -- prevent nil table error
3349 if not _G[savedVarsName] then _G[savedVarsName] = {} end
3350 -- check source is exist
3351 if not _G[savedVarsName][sourceName] then
3352 self:AddMsg(DBM_CORE_MPROFILE_COPYS_S_ERROR)
3353 return
3354 end
3355 local targetOptions = _G[savedVarsName][targetName] or {}
3356 for i, id in ipairs(self.ModLists[modId]) do
3357 -- check source is exist
3358 if not _G[savedVarsName][sourceName][id] then
3359 self:AddMsg(DBM_CORE_MPROFILE_COPYS_S_ERROR)
3360 return
3361 end
3362 if not _G[savedVarsName][sourceName][id][sourceProfile] then
3363 self:AddMsg(DBM_CORE_MPROFILE_COPYS_S_ERROR)
3364 return
3365 end
3366 -- prevent nil table error
3367 if not _G[savedVarsName][targetName][id] then _G[savedVarsName][targetName][id] = {} end
3368 -- copy table
3369 for option, optionValue in pairs(_G[savedVarsName][sourceName][id][sourceProfile]) do
3370 if option:find(Type) then
3371 _G[savedVarsName][targetName][id][targetProfile][option] = optionValue
3372 end
3373 end
3374 -- apply to options table
3375 local mod = self:GetModByName(id)
3376 mod.Options = {}
3377 mod.Options = _G[savedVarsName][targetName][id][targetProfile]
3378 end
3379 if targetProfile > 0 then
3380 _G[savedVarsName][targetName]["talent"..targetProfile] = currentSpecName
3381 end
3382 self:AddMsg(DBM_CORE_MPROFILE_COPYS_SUCCESS:format(sourceName, sourceProfile))
3383 -- update gui if showing
3384 if DBM_GUI and DBM_GUI.currentViewing and DBM_GUI_OptionsFrame:IsShown() then
3385 DBM_GUI_OptionsFrame:DisplayFrame(DBM_GUI.currentViewing)
3386 end
3387end
3388
3389function DBM:DeleteAllModOption(modId, name, profile)
3390 -- modId is string like "DBM-Highmaul"
3391 if not modId or not name or not profile or not self.ModLists[modId] then return end
3392 -- prevent error
3393 if not currentSpecID or not currentSpecGroup then
3394 self:SetCurrentSpecInfo()
3395 end
3396 -- variable init
3397 local savedVarsName = modId:gsub("-", "").."_AllSavedVars"
3398 local fullname = playerName.."-"..playerRealm
3399 local profileNum = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
3400 -- cannot delete current profile.
3401 if fullname == name and profileNum == profile then
3402 self:AddMsg(DBM_CORE_MPROFILE_DELETE_SELF_ERROR)
3403 return
3404 end
3405 -- prevent nil table error
3406 if not _G[savedVarsName] then _G[savedVarsName] = {} end
3407 if not _G[savedVarsName][name] then
3408 self:AddMsg(DBM_CORE_MPROFILE_DELETE_S_ERROR)
3409 return
3410 end
3411 for i, id in ipairs(self.ModLists[modId]) do
3412 -- prevent nil table error
3413 if not _G[savedVarsName][name][id] then
3414 self:AddMsg(DBM_CORE_MPROFILE_DELETE_S_ERROR)
3415 return
3416 end
3417 -- delete
3418 _G[savedVarsName][name][id][profile] = nil
3419 end
3420 _G[savedVarsName][name]["talent"..profile] = nil
3421 self:AddMsg(DBM_CORE_MPROFILE_DELETE_SUCCESS:format(name, profile))
3422end
3423
3424function DBM:ClearAllStats(modId)
3425 -- modId is string like "DBM-Highmaul"
3426 if not modId or not self.ModLists[modId] then return end
3427 -- variable init
3428 local savedStatsName = modId:gsub("-", "").."_SavedStats"
3429 -- prevent nil table error
3430 if not _G[savedStatsName] then _G[savedStatsName] = {} end
3431 for i, id in ipairs(self.ModLists[modId]) do
3432 local mod = self:GetModByName(id)
3433 -- prevent nil table error
3434 local defaultStats = {}
3435 defaultStats.normalKills = 0
3436 defaultStats.normalPulls = 0
3437 defaultStats.heroicKills = 0
3438 defaultStats.heroicPulls = 0
3439 defaultStats.challengeKills = 0
3440 defaultStats.challengePulls = 0
3441 defaultStats.challengeBestRank = 0
3442 defaultStats.mythicKills = 0
3443 defaultStats.mythicPulls = 0
3444 defaultStats.normal25Kills = 0
3445 defaultStats.normal25Kills = 0
3446 defaultStats.normal25Pulls = 0
3447 defaultStats.heroic25Kills = 0
3448 defaultStats.heroic25Pulls = 0
3449 defaultStats.lfr25Kills = 0
3450 defaultStats.lfr25Pulls = 0
3451 defaultStats.timewalkerKills = 0
3452 defaultStats.timewalkerPulls = 0
3453 mod.stats = {}
3454 mod.stats = defaultStats
3455 _G[savedStatsName][id] = {}
3456 _G[savedStatsName][id] = defaultStats
3457 end
3458 self:AddMsg(DBM_CORE_ALLMOD_STATS_RESETED)
3459 DBM_GUI:UpdateModList()
3460end
3461
3462do
3463 function loadOptions(self)
3464 --init
3465 if not DBM_AllSavedOptions then DBM_AllSavedOptions = {} end
3466 usedProfile = DBM_UsedProfile or usedProfile
3467 if not usedProfile or (usedProfile ~= "Default" and not DBM_AllSavedOptions[usedProfile]) then
3468 -- DBM.Option is not loaded. so use print function
3469 print(DBM_CORE_PROFILE_NOT_FOUND)
3470 usedProfile = "Default"
3471 end
3472 DBM_UsedProfile = usedProfile
3473 --migrate old options
3474 if DBM_SavedOptions and not DBM_AllSavedOptions[usedProfile] then
3475 DBM_AllSavedOptions[usedProfile] = DBM_SavedOptions
3476 end
3477 self.Options = DBM_AllSavedOptions[usedProfile] or {}
3478 dbmIsEnabled = true
3479 self:AddDefaultOptions(self.Options, self.DefaultOptions)
3480 DBM_AllSavedOptions[usedProfile] = self.Options
3481
3482 -- force enable dual profile (change default)
3483 if DBM_CharSavedRevision < 12976 then
3484 if playerClass ~= "MAGE" and playerClass ~= "WARLOCK" and playerClass ~= "ROGUE" then
3485 DBM_UseDualProfile = true
3486 end
3487 end
3488 DBM_CharSavedRevision = self.Revision
3489
3490 -- load special warning options
3491 self:UpdateWarningOptions()
3492 self:UpdateSpecialWarningOptions()
3493 if self.Options.CoreSavedRevision < 16970 then
3494 if self.Options.SpecialWarningSound3 == "Sound\\Creature\\KilJaeden\\KILJAEDEN02.ogg" then self.Options.SpecialWarningSound3 = self.DefaultOptions.SpecialWarningSound3 end
3495 end
3496 self.Options.CoreSavedRevision = self.Revision
3497 end
3498end
3499
3500do
3501 local lastLFGAlert = 0
3502 function DBM:LFG_ROLE_CHECK_SHOW()
3503 if not UnitIsGroupLeader("player") and self.Options.LFDEnhance and GetTime() - lastLFGAlert > 5 then
3504 self:FlashClientIcon()
3505 self:PlaySoundFile("Sound\\interface\\levelup2.ogg", true)--Because regular sound uses SFX channel which is too low of volume most of time
3506 lastLFGAlert = GetTime()
3507 end
3508 end
3509end
3510
3511function DBM:LFG_PROPOSAL_SHOW()
3512 if self.Options.ShowQueuePop and not self.Options.DontShowBossTimers then
3513 self.Bars:CreateBar(40, DBM_LFG_INVITE, "Interface\\Icons\\Spell_Holy_BorrowedTime")
3514 fireEvent("DBM_TimerStart", "DBMLFGTimer", DBM_LFG_INVITE, 40, "Interface\\Icons\\Spell_Holy_BorrowedTime", "extratimer", nil, 0)
3515 end
3516 if self.Options.LFDEnhance then
3517 self:FlashClientIcon()
3518 self:PlaySoundFile("Sound\\interface\\levelup2.ogg", true)--Because regular sound uses SFX channel which is too low of volume most of time
3519 end
3520end
3521
3522function DBM:LFG_PROPOSAL_FAILED()
3523 self.Bars:CancelBar(DBM_LFG_INVITE)
3524 fireEvent("DBM_TimerStop", "DBMLFGTimer")
3525end
3526
3527function DBM:LFG_PROPOSAL_SUCCEEDED()
3528 self.Bars:CancelBar(DBM_LFG_INVITE)
3529 fireEvent("DBM_TimerStop", "DBMLFGTimer")
3530end
3531
3532function DBM:READY_CHECK()
3533 if self.Options.RLReadyCheckSound then--readycheck sound, if ora3 not installed (bad to have 2 mods do it)
3534 self:FlashClientIcon()
3535 if not BINDING_HEADER_oRA3 then
3536 self:PlaySoundFile("Sound\\interface\\levelup2.ogg", true)--Because regular sound uses SFX channel which is too low of volume most of time
3537 end
3538 end
3539 self:TransitionToDungeonBGM(false, true)
3540 self:Schedule(4, self.TransitionToDungeonBGM, self)
3541end
3542
3543function DBM:PLAYER_SPECIALIZATION_CHANGED()
3544 local lastSpecID = currentSpecID
3545 self:SetCurrentSpecInfo()
3546 if currentSpecID ~= lastSpecID then--Don't fire specchanged unless spec actually has changed.
3547 self:SpecChanged()
3548 if IsInGroup() then
3549 self:RoleCheck(false)
3550 end
3551 end
3552end
3553
3554do
3555 local function AcceptPartyInvite()
3556 AcceptGroup()
3557 for i=1, STATICPOPUP_NUMDIALOGS do
3558 local whichDialog = _G["StaticPopup"..i].which
3559 if whichDialog == "PARTY_INVITE" or whichDialog == "PARTY_INVITE_XREALM" then
3560 _G["StaticPopup"..i].inviteAccepted = 1
3561 StaticPopup_Hide(whichDialog)
3562 break
3563 end
3564 end
3565 end
3566
3567 function DBM:PARTY_INVITE_REQUEST(sender)
3568 --First off, if you are in queue for something, lets not allow guildies or friends boot you from it.
3569 if IsInInstance() or GetLFGMode(1) or GetLFGMode(2) or GetLFGMode(3) or GetLFGMode(4) or GetLFGMode(5) then return end
3570 --First check realID
3571 if self.Options.AutoAcceptFriendInvite then
3572 local _, numBNetOnline = BNGetNumFriends()
3573 for i = 1, numBNetOnline do
3574 local presenceID, _, _, _, _, _, _, isOnline = BNGetFriendInfo(i)
3575 local friendIndex = BNGetFriendIndex(presenceID)--Check if they are on more than one client at once (very likely with new launcher)
3576 for i=1, BNGetNumFriendGameAccounts(friendIndex) do
3577 local _, toonName, client = BNGetFriendGameAccountInfo(friendIndex, i)
3578 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
3579 if toonName == sender then--Now simply see if this is sender
3580 AcceptPartyInvite()
3581 return
3582 end
3583 end
3584 end
3585 end
3586 -- Check regular non-BNet friends
3587 local nf = GetNumFriends()
3588 for i = 1, nf do
3589 local toonName = GetFriendInfo(i)
3590 if toonName == sender then
3591 AcceptPartyInvite()
3592 return
3593 end
3594 end
3595 end
3596 --Second check guildies
3597 if self.Options.AutoAcceptGuildInvite then
3598 local totalMembers, numOnlineGuildMembers, numOnlineAndMobileMembers = GetNumGuildMembers()
3599 local scanTotal = GetGuildRosterShowOffline() and totalMembers or numOnlineAndMobileMembers
3600 for i=1, scanTotal do
3601 --At this time, it's not easy to tell an officer from a non officer
3602 --since a guild might have ranks 1-3 or even 1-4 be officers/leader while another might only be 1-2
3603 --therefor, this feature is just a "yes/no" for if sender is a guildy
3604 local name, rank, rankIndex = GetGuildRosterInfo(i)
3605 if not name then break end
3606 name = Ambiguate(name, "none")
3607 if sender == name then
3608 AcceptPartyInvite()
3609 return
3610 end
3611 end
3612 end
3613 end
3614end
3615
3616function DBM:UPDATE_BATTLEFIELD_STATUS()
3617 for i = 1, 2 do
3618 if GetBattlefieldStatus(i) == "confirm" then
3619 if self.Options.ShowQueuePop and not self.Options.DontShowBossTimers then
3620 queuedBattlefield[i] = select(2, GetBattlefieldStatus(i))
3621 self.Bars:CreateBar(85, queuedBattlefield[i], "Interface\\Icons\\Spell_Holy_BorrowedTime") -- need to confirm the timer
3622 fireEvent("DBM_TimerStart", "DBMBFSTimer", queuedBattlefield[i], 85, "Interface\\Icons\\Spell_Holy_BorrowedTime", "extratimer", nil, 0)
3623 end
3624 if self.Options.LFDEnhance then
3625 self:PlaySoundFile("Sound\\interface\\levelup2.ogg", true)--Because regular sound uses SFX channel which is too low of volume most of time
3626 end
3627 elseif queuedBattlefield[i] then
3628 self.Bars:CancelBar(queuedBattlefield[i])
3629 fireEvent("DBM_TimerStop", "DBMBFSTimer")
3630 queuedBattlefield[i] = nil
3631 end
3632 end
3633end
3634
3635function DBM:SCENARIO_CRITERIA_UPDATE()
3636 local _, currentStage, numStages = C_Scenario.GetInfo()
3637 if #inCombat > 0 and currentStage > numStages and C_Scenario.IsInScenario() then
3638 for i = #inCombat, 1, -1 do
3639 local v = inCombat[i]
3640 if v.inScenario then
3641 self:EndCombat(v)
3642 end
3643 end
3644 end
3645end
3646
3647--------------------------------
3648-- Load Boss Mods on Demand --
3649--------------------------------
3650do
3651 local classicZones = {[509]=true,[531]=true,[469]=true,[409]=true,}
3652 local bcZones = {[564]=true,[534]=true,[532]=true,[565]=true,[540]=true,[558]=true,[556]=true,[555]=true,[542]=true,[546]=true,[545]=true,[547]=true,[553]=true,[554]=true,[552]=true,[557]=true,[269]=true,[560]=true,[543]=true,[585]=true,[548]=true,[580]=true,[550]=true}
3653 local wrathZones = {[615]=true,[724]=true,[649]=true,[616]=true,[631]=true,[533]=true,[249]=true,[619]=true,[601]=true,[595]=true,[600]=true,[604]=true,[602]=true,[599]=true,[576]=true,[578]=true,[574]=true,[575]=true,[608]=true,[658]=true,[632]=true,[668]=true,[650]=true,[603]=true,[624]=true}
3654 local cataZones = {[757]=true,[671]=true,[669]=true,[967]=true,[720]=true,[951]=true,[755]=true,[645]=true,[36]=true,[670]=true,[644]=true,[33]=true,[643]=true,[725]=true,[657]=true,[309]=true,[859]=true,[568]=true,[938]=true,[940]=true,[939]=true,[646]=true,[754]=true}
3655 local mopZones = {[1009]=true,[1008]=true,[960]=true,[961]=true,[959]=true,[962]=true,[994]=true,[1011]=true,[1007]=true,[1001]=true,[1004]=true,[1136]=true,[996]=true,[1098]=true}
3656 local wodZones = {[1205]=true,[1448]=true,[1182]=true,[1175]=true,[1208]=true,[1195]=true,[1279]=true,[1176]=true,[1209]=true,[1358]=true}
3657 local challengeScenarios = {[1148]=true,[1698]=true,[1710]=true,[1703]=true,[1702]=true,[1684]=true,[1673]=true,[1616]=true}
3658 function DBM:CheckAvailableMods()
3659 if BigWigs then return end--If they are running two boss mods at once, lets assume they are only using DBM for a specific feature and not nag
3660 local timeWalking = difficultyIndex == 24 or difficultyIndex == 33 or false
3661 if (classicZones[LastInstanceMapID] or bcZones[LastInstanceMapID]) and (timeWalking or playerLevel < 71) and not GetAddOnInfo("DBM-BlackTemple") then
3662 AddMsg(self, DBM_CORE_MOD_AVAILABLE:format("DBM BC/Vanilla mods"))
3663 elseif wrathZones[LastInstanceMapID] and (timeWalking or playerLevel < 81) and not GetAddOnInfo("DBM-Ulduar") then
3664 AddMsg(self, DBM_CORE_MOD_AVAILABLE:format("DBM Wrath of the Lich King mods"))
3665 elseif cataZones[LastInstanceMapID] and (timeWalking or playerLevel < 86) and not GetAddOnInfo("DBM-Party-Cataclysm") then
3666 AddMsg(self, DBM_CORE_MOD_AVAILABLE:format("DBM Cataclysm mods"))
3667 elseif mopZones[LastInstanceMapID] and (timeWalking or playerLevel < 91) and not GetAddOnInfo("DBM-Party-MoP") then
3668 AddMsg(self, DBM_CORE_MOD_AVAILABLE:format("DBM Mists of Pandaria mods"))
3669 elseif wodZones[LastInstanceMapID] and (timeWalking or playerLevel < 101) and not GetAddOnInfo("DBM-MC") then
3670 AddMsg(self, DBM_CORE_MOD_AVAILABLE:format("DBM Warlords of Draenor mods"))
3671 elseif challengeScenarios[LastInstanceMapID] and not GetAddOnInfo("DBM-Challenges") then
3672 AddMsg(self, DBM_CORE_MOD_AVAILABLE:format("DBM-Challenges"))
3673 end
3674 end
3675 function DBM:TransitionToDungeonBGM(force, cleanup)
3676 if cleanup then--Runs on zone change (first load delay) and combat end
3677 self:Unschedule(self.TransitionToDungeonBGM)
3678 if self.Options.tempMusicSetting then
3679 SetCVar("Sound_EnableMusic", self.Options.tempMusicSetting)
3680 self.Options.tempMusicSetting = nil
3681 DBM:Debug("Restoring Sound_EnableMusic CVAR")
3682 end
3683 if self.Options.musicPlaying then--Primarily so DBM doesn't call StopMusic unless DBM is one that started it. We don't want to screw with other addons
3684 StopMusic()
3685 self.Options.musicPlaying = nil
3686 DBM:Debug("Stopping music")
3687 end
3688 fireEvent("DBM_MusicStop", "ZoneOrCombatEndTransition")
3689 return
3690 end
3691 if LastInstanceType ~= "raid" and LastInstanceType ~= "party" and not force then return end
3692 fireEvent("DBM_MusicStart", "RaidOrDungeon")
3693 if self.Options.EventSoundDungeonBGM and self.Options.EventSoundDungeonBGM ~= "None" and self.Options.EventSoundDungeonBGM ~= "" and not (self.Options.EventDungMusicMythicFilter and (savedDifficulty == "mythic" or savedDifficulty == "challenge")) then
3694 if not self.Options.tempMusicSetting then
3695 self.Options.tempMusicSetting = tonumber(GetCVar("Sound_EnableMusic"))
3696 if self.Options.tempMusicSetting == 0 then
3697 SetCVar("Sound_EnableMusic", 1)
3698 else
3699 self.Options.tempMusicSetting = nil--Don't actually need it
3700 end
3701 end
3702 local path = "MISSING"
3703 if self.Options.EventSoundDungeonBGM == "Random" then
3704 local usedTable = self.Options.EventSoundMusicCombined and DBM.Music or DBM.DungeonMusic
3705 local random = fastrandom(3, #usedTable)
3706 path = usedTable[random].value
3707 else
3708 path = self.Options.EventSoundDungeonBGM
3709 end
3710 PlayMusic(path)
3711 self.Options.musicPlaying = true
3712 DBM:Debug("Starting Dungeon music with file: "..path)
3713 end
3714 end
3715 local function SecondaryLoadCheck(self)
3716 local _, instanceType, difficulty, _, _, _, _, mapID, instanceGroupSize = GetInstanceInfo()
3717 if not savedDifficulty then
3718 savedDifficulty, difficultyText = self:GetCurrentInstanceDifficulty()
3719 end
3720 self:Debug("Instance Check fired with mapID "..mapID.." and difficulty "..difficulty, 2)
3721 if LastInstanceMapID == mapID then
3722 self:TransitionToDungeonBGM()
3723 self:Debug("No action taken because mapID hasn't changed since last check", 2)
3724 return
3725 end--ID hasn't changed, don't waste cpu doing anything else (example situation, porting into garrosh stage 4 is a loading screen)
3726 LastInstanceMapID = mapID
3727 LastGroupSize = instanceGroupSize
3728 difficultyIndex = difficulty
3729 if instanceType == "none" or C_Garrison:IsOnGarrisonMap() then
3730 LastInstanceType = "none"
3731 if not targetEventsRegistered then
3732 self:RegisterShortTermEvents("UPDATE_MOUSEOVER_UNIT")
3733 targetEventsRegistered = true
3734 end
3735 else
3736 LastInstanceType = instanceType
3737 if targetEventsRegistered then
3738 self:UnregisterShortTermEvents()
3739 targetEventsRegistered = false
3740 end
3741 if savedDifficulty == "worldboss" then
3742 for i = #inCombat, 1, -1 do
3743 self:EndCombat(inCombat[i], true)
3744 end
3745 end
3746 end
3747 -- LoadMod
3748 self:LoadModsOnDemand("mapId", mapID)
3749 if not self.Options.DontShowReminders then
3750 self:CheckAvailableMods()
3751 end
3752 if DBM:HasMapRestrictions() then
3753 DBM.Arrow:Hide()
3754 DBMHudMap:Disable()
3755 if DBM.RangeCheck:IsRadarShown() then
3756 DBM.RangeCheck:Hide(true)
3757 end
3758 end
3759 end
3760 --Faster and more accurate loading for instances, but useless outside of them
3761 function DBM:LOADING_SCREEN_DISABLED()
3762 self.Bars:CancelBar(DBM_LFG_INVITE)--Disable bar here since LFG_PROPOSAL_SUCCEEDED seems broken right now
3763 fireEvent("DBM_TimerStop", "DBMLFGTimer")
3764 timerRequestInProgress = false
3765 self:Debug("LOADING_SCREEN_DISABLED fired")
3766 self:Unschedule(SecondaryLoadCheck)
3767 --SecondaryLoadCheck(self)
3768 self:Schedule(1, SecondaryLoadCheck, self)--Now delayed by one second to work around an issue on beta where spec info isn't available yet on reloadui
3769 self:TransitionToDungeonBGM(false, true)
3770 self:Schedule(5, SecondaryLoadCheck, self)
3771 if DBM:HasMapRestrictions() then
3772 DBM.Arrow:Hide()
3773 DBMHudMap:Disable()
3774 if DBM.RangeCheck:IsRadarShown() then
3775 DBM.RangeCheck:Hide(true)
3776 end
3777 end
3778 end
3779
3780 function DBM:LoadModsOnDemand(checkTable, checkValue)
3781 self:Debug("LoadModsOnDemand fired")
3782 for i, v in ipairs(self.AddOns) do
3783 local modTable = v[checkTable]
3784 local enabled = GetAddOnEnableState(playerName, v.modId)
3785 --self:Debug(v.modId.." is "..enabled, 2)
3786 if not IsAddOnLoaded(v.modId) and modTable and checkEntry(modTable, checkValue) then
3787 if enabled ~= 0 then
3788 self:LoadMod(v)
3789 else
3790 if not self.Options.DontShowReminders then
3791 self:AddMsg(DBM_CORE_LOAD_MOD_DISABLED:format(v.name))
3792 end
3793 end
3794 end
3795 end
3796 self:ScenarioCheck()--Do not filter. Because ScenarioCheck function includes filter.
3797 end
3798end
3799
3800--Scenario mods
3801function DBM:ScenarioCheck()
3802 if dbmIsEnabled and combatInfo[LastInstanceMapID] then
3803 for i, v in ipairs(combatInfo[LastInstanceMapID]) do
3804 if (v.type == "scenario") and checkEntry(v.msgs, LastInstanceMapID) then
3805 self:StartCombat(v.mod, 0, "LOADING_SCREEN_DISABLED")
3806 end
3807 end
3808 end
3809end
3810
3811function DBM:LoadMod(mod, force)
3812 if type(mod) ~= "table" then
3813 self:Debug("LoadMod failed because mod table not valid")
3814 return false
3815 end
3816 if mod.isWorldBoss and not IsInInstance() and not force then
3817 return
3818 end--Don't load world boss mod this way.
3819 if mod.minRevision > self.Revision then
3820 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
3821 self:AddMsg(DBM_CORE_LOAD_MOD_VER_MISMATCH:format(mod.name))
3822 end
3823 return
3824 end
3825 if mod.minExpansion > GetExpansionLevel() then
3826 self:AddMsg(DBM_CORE_LOAD_MOD_EXP_MISMATCH:format(mod.name))
3827 return
3828 end
3829 if not currentSpecID then
3830 self:SetCurrentSpecInfo()
3831 end
3832 if not difficultyIndex then -- prevent error in EJ_SetDifficulty if not yet set
3833 savedDifficulty, difficultyText, difficultyIndex, LastGroupSize, difficultyModifier = DBM:GetCurrentInstanceDifficulty()
3834 end
3835 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.
3836 self:Debug("LoadAddOn should have fired for "..mod.name, 2)
3837 local loaded, reason = LoadAddOn(mod.modId)
3838 if not loaded then
3839 if reason then
3840 self:AddMsg(DBM_CORE_LOAD_MOD_ERROR:format(tostring(mod.name), tostring(_G["ADDON_"..reason or ""])))
3841 else
3842 self:Debug("LoadAddOn failed and did not give reason")
3843 end
3844 return false
3845 else
3846 self:Debug("LoadAddOn should have succeeded for "..mod.name, 2)
3847 self:AddMsg(DBM_CORE_LOAD_MOD_SUCCESS:format(tostring(mod.name)))
3848 self:LoadModOptions(mod.modId, InCombatLockdown(), true)
3849 if DBM_GUI then
3850 DBM_GUI:UpdateModList()
3851 end
3852 if LastInstanceType ~= "pvp" and #inCombat == 0 and IsInGroup() then--do timer recovery only mod load
3853 if not timerRequestInProgress then
3854 timerRequestInProgress = true
3855 -- Request timer to 3 person to prevent failure.
3856 self:Unschedule(self.RequestTimers)
3857 self:Schedule(7, self.RequestTimers, self, 1)
3858 self:Schedule(10, self.RequestTimers, self, 2)
3859 self:Schedule(13, self.RequestTimers, self, 3)
3860 C_TimerAfter(15, function() timerRequestInProgress = false end)
3861 self:GROUP_ROSTER_UPDATE(true)
3862 end
3863 end
3864 if not InCombatLockdown() and not UnitAffectingCombat("player") and not IsFalling() then--We loaded in combat but still need to avoid garbage collect in combat
3865 collectgarbage("collect")
3866 end
3867 return true
3868 end
3869end
3870
3871do
3872 local function loadModByUnit(uId)
3873 if not uId then
3874 uId = "mouseover"
3875 else
3876 uId = uId.."target"
3877 end
3878 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.
3879 local guid = UnitGUID(uId)
3880 if guid and DBM:IsCreatureGUID(guid) then
3881 local cId = DBM:GetCIDFromGUID(guid)
3882 for bosscId, addon in pairs(loadcIds) do
3883 local enabled = GetAddOnEnableState(playerName, addon)
3884 if cId and bosscId and cId == bosscId and not IsAddOnLoaded(addon) and enabled ~= 0 then
3885 for i, v in ipairs(DBM.AddOns) do
3886 if v.modId == addon then
3887 DBM:LoadMod(v, true)
3888 break
3889 end
3890 end
3891 end
3892 end
3893 end
3894 end
3895
3896 --Loading routeens hacks for world bosses based on target or mouseover.
3897 function DBM:UPDATE_MOUSEOVER_UNIT()
3898 loadModByUnit()
3899 end
3900
3901 function DBM:UNIT_TARGET_UNFILTERED(uId)
3902 if targetEventsRegistered then--Allow outdoor mod loading
3903 loadModByUnit(uId)
3904 end
3905 --Debug options for seeing where BossUnitTargetScanner can be used.
3906 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
3907 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"
3908 self:Debug(uId.." changed targets to "..targetName)
3909 end
3910 --Active BossUnitTargetScanner
3911 if targetMonitor and UnitExists(uId.."target") and UnitPlayerOrPetInRaid(uId.."target") then
3912 self:Debug("targetMonitor exists, target exists", 2)
3913 local modId, unitId, returnFunc = string.split("\t", targetMonitor)
3914 self:Debug("targetMonitor: "..modId..", "..unitId..", "..returnFunc, 2)
3915 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
3916 if tanking or (status == 3) then
3917 self:Debug("targetMonitor ending, it's a tank", 2)
3918 return
3919 end--It's a tank/highest threat, this method ignores tanks
3920 local mod = self:GetModByName(modId)
3921 self:Debug("targetMonitor success, a valid target that's not a tank", 2)
3922 mod[returnFunc](mod, self:GetUnitFullName(unitId.."target"), unitId.."target", unitId)--Return results to warning function with all variables.
3923 targetMonitor = nil
3924 end
3925 end
3926end
3927
3928-----------------------------
3929-- Handle Incoming Syncs --
3930-----------------------------
3931
3932do
3933 local function checkForActualPull()
3934 if #inCombat == 0 then
3935 DBM:StopLogging()
3936 end
3937 end
3938
3939 local syncHandlers = {}
3940 local whisperSyncHandlers = {}
3941
3942 -- 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
3943 -- M = Mod
3944 -- C = Combat start
3945 -- GC = Guild Combat Start
3946 -- IS = Icon set info
3947 -- K = Kill
3948 -- H = Hi!
3949 -- V = Incoming version information
3950 -- U = User Timer
3951 -- PT = Pull Timer (for sound effects, the timer itself is still sent as a normal timer)
3952 -- RT = Request Timers
3953 -- CI = Combat Info
3954 -- TI = Timer Info
3955 -- IR = Instance Info Request
3956 -- IRE = Instance Info Requested Ended/Canceled
3957 -- II = Instance Info
3958 -- WBE = World Boss engage info
3959 -- WBD = World Boss defeat info
3960 -- DSW = Disable Send Whisper
3961 -- NS = Note Share
3962
3963 syncHandlers["M"] = function(sender, mod, revision, event, ...)
3964 mod = DBM:GetModByName(mod or "")
3965 if mod and event and revision then
3966 revision = tonumber(revision) or 0
3967 mod:ReceiveSync(event, sender, revision, ...)
3968 end
3969 end
3970
3971 syncHandlers["NS"] = function(sender, modid, modvar, text, abilityName)
3972 if sender == playerName then return end
3973 if DBM.Options.BlockNoteShare or InCombatLockdown() or UnitAffectingCombat("player") or IsFalling() or DBM:GetRaidRank(sender) == 0 then return end
3974 if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and IsInInstance() then return end
3975 --^^You are in LFR, BG, or LFG. Block note syncs. They shouldn't be sendable, but in case someone edits DBM^^
3976 local mod = DBM:GetModByName(modid or "")
3977 local ability = abilityName or DBM_CORE_UNKNOWN
3978 if mod and modvar and text and text ~= "" then
3979 if DBM:AntiSpam(5, modvar) then--Don't allow calling same note more than once per 5 seconds
3980 DBM:AddMsg(DBM_CORE_NOTE_SHARE_SUCCESS:format(sender, abilityName))
3981 DBM:ShowNoteEditor(mod, modvar, ability, text, sender)
3982 else
3983 DBM:Debug(sender.." is attempting to send too many notes so notes are being throttled")
3984 end
3985 else
3986 DBM:AddMsg(DBM_CORE_NOTE_SHARE_FAIL:format(sender, ability))
3987 end
3988 end
3989
3990 syncHandlers["C"] = function(sender, delay, mod, modRevision, startHp, dbmRevision, modHFRevision)
3991 if not dbmIsEnabled or sender == playerName then return end
3992 if LastInstanceType == "pvp" then return end
3993 if LastInstanceType == "none" and (not UnitAffectingCombat("player") or #inCombat > 0) then--world boss
3994 local senderuId = DBM:GetRaidUnitId(sender)
3995 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.
3996 local _, _, _, playerZone = UnitPosition("player")
3997 local _, _, _, senderZone = UnitPosition(senderuId)
3998 if playerZone ~= senderZone then return end--not same zone
3999 local range = DBM.RangeCheck:GetDistance("player", senderuId)--Same zone, so check range
4000 if not range or range > 120 then return end
4001 end
4002 if not cSyncSender[sender] then
4003 cSyncSender[sender] = true
4004 cSyncReceived = cSyncReceived + 1
4005 if cSyncReceived > 2 then -- need at least 3 sync to combat start. (for security)
4006 local lag = select(4, GetNetStats()) / 1000
4007 delay = tonumber(delay or 0) or 0
4008 mod = DBM:GetModByName(mod or "")
4009 modRevision = tonumber(modRevision or 0) or 0
4010 dbmRevision = tonumber(dbmRevision or 0) or 0
4011 modHFRevision = tonumber(modHFRevision or 0) or 0
4012 startHp = tonumber(startHp or -1) or -1
4013 if dbmRevision < 10481 then return end
4014 if mod and delay and (not mod.zones or mod.zones[LastInstanceMapID]) and (not mod.minSyncRevision or modRevision >= mod.minSyncRevision) then
4015 DBM:StartCombat(mod, delay + lag, "SYNC from - "..sender, true, startHp)
4016 if (mod.revision < modHFRevision) and (mod.revision > 1000) then--mod.revision because we want to compare to OUR revision not senders
4017 if DBM:AntiSpam(3, "HOTFIX") and not DBM.Options.DontShowReminders then
4018 if DBM.HighestRelease < modHFRevision then--There is a newer RELEASE version of DBM out that has this mods fixes
4019 showConstantReminder = 2
4020 DBM:AddMsg(DBM_CORE_UPDATEREMINDER_HOTFIX)
4021 else--This mods fixes are in an alpha version
4022 DBM:AddMsg(DBM_CORE_UPDATEREMINDER_HOTFIX_ALPHA)
4023 end
4024 end
4025 end
4026 end
4027 end
4028 end
4029 end
4030
4031 syncHandlers["DSW"] = function(sender)
4032 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
4033 statusWhisperDisabled = true
4034 DBM:Debug("Raid leader has disabled status whispers")
4035 end
4036
4037 syncHandlers["DGP"] = function(sender)
4038 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
4039 statusGuildDisabled = true
4040 DBM:Debug("Raid leader has disabled guild progress messages")
4041 end
4042
4043 syncHandlers["IS"] = function(sender, guid, ver, optionName)
4044 ver = tonumber(ver)
4045 if ver > (iconSetRevision[optionName] or 0) then--Save first synced version and person, ignore same version. refresh occurs only above version (fastest person)
4046 iconSetRevision[optionName] = ver
4047 iconSetPerson[optionName] = guid
4048 end
4049 if iconSetPerson[optionName] == UnitGUID("player") then--Check if that highest version was from ourself
4050 canSetIcons[optionName] = true
4051 else--Not from self, it means someone with a higher version than us probably sent it
4052 canSetIcons[optionName] = false
4053 end
4054 local name = DBM:GetFullPlayerNameByGUID(iconSetPerson[optionName]) or DBM_CORE_UNKNOWN
4055 DBM:Debug(name.." was elected icon setter for "..optionName, 2)
4056 end
4057
4058 syncHandlers["K"] = function(sender, cId)
4059 if select(2, IsInInstance()) == "pvp" or select(2, IsInInstance()) == "none" then return end
4060 cId = tonumber(cId or "")
4061 if cId then DBM:OnMobKill(cId, true) end
4062 end
4063
4064 syncHandlers["EE"] = function(sender, eId, success, mod, modRevision)
4065 if select(2, IsInInstance()) == "pvp" then return end
4066 eId = tonumber(eId or "")
4067 success = tonumber(success)
4068 mod = DBM:GetModByName(mod or "")
4069 modRevision = tonumber(modRevision or 0) or 0
4070 if mod and eId and success and (not mod.minSyncRevision or modRevision >= mod.minSyncRevision) and not eeSyncSender[sender] then
4071 eeSyncSender[sender] = true
4072 eeSyncReceived = eeSyncReceived + 1
4073 if eeSyncReceived > 2 then -- need at least 3 person to combat end. (for security)
4074 DBM:EndCombat(mod, success == 0)
4075 end
4076 end
4077 end
4078
4079 local dummyMod -- dummy mod for the pull timer
4080 syncHandlers["PT"] = function(sender, timer, lastMapID, target)
4081 if DBM.Options.DontShowUserTimers then return end
4082 local LFGTankException = IsPartyLFG() and UnitGroupRolesAssigned(sender) == "TANK"
4083 if (DBM:GetRaidRank(sender) == 0 and IsInGroup() and not LFGTankException) or select(2, IsInInstance()) == "pvp" or IsEncounterInProgress() then
4084 return
4085 end
4086 if (lastMapID and tonumber(lastMapID) ~= LastInstanceMapID) or (not lastMapID and DBM.Options.DontShowPTNoID) then return end
4087 timer = tonumber(timer or 0)
4088 if timer > 60 then
4089 return
4090 end
4091 if not dummyMod then
4092 local threshold = DBM.Options.PTCountThreshold
4093 local adjustedThreshold = 5
4094 if threshold > 10 then
4095 adjustedThreshold = 10
4096 else
4097 adjustedThreshold = floor(threshold)
4098 end
4099 dummyMod = DBM:NewMod("PullTimerCountdownDummy")
4100 DBM:GetModLocalization("PullTimerCountdownDummy"):SetGeneralLocalization{ name = DBM_CORE_MINIMAP_TOOLTIP_HEADER }
4101 dummyMod.countdown = dummyMod:NewCountdown(0, 0, nil, nil, adjustedThreshold, true)
4102 dummyMod.text = dummyMod:NewAnnounce("%s", 1, "Interface\\Icons\\ability_warrior_offensivestance")
4103 dummyMod.geartext = dummyMod:NewSpecialWarning(" %s ", nil, nil, nil, 3)
4104 end
4105 --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)
4106 if not DBM.Options.DontShowPT2 and DBM.Bars:GetBar(DBM_CORE_TIMER_PULL) then
4107 DBM.Bars:CancelBar(DBM_CORE_TIMER_PULL)
4108 fireEvent("DBM_TimerStop", "pull")
4109 end
4110 if not DBM.Options.DontPlayPTCountdown then
4111 dummyMod.countdown:Cancel()
4112 end
4113 if not DBM.Options.DontShowPTCountdownText then
4114 TimerTracker_OnEvent(TimerTracker, "PLAYER_ENTERING_WORLD")--easiest way to nil out timers on TimerTracker frame. This frame just has no actual star/stop functions
4115 end
4116 dummyMod.text:Cancel()
4117 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)
4118 DBM:FlashClientIcon()
4119 if not DBM.Options.DontShowPT2 then
4120 DBM.Bars:CreateBar(timer, DBM_CORE_TIMER_PULL, "Interface\\Icons\\Spell_Holy_BorrowedTime")
4121 fireEvent("DBM_TimerStart", "pull", DBM_CORE_TIMER_PULL, timer, "Interface\\Icons\\Spell_Holy_BorrowedTime", "utilitytimer", nil, 0)
4122 end
4123 if not DBM.Options.DontPlayPTCountdown then
4124 dummyMod.countdown:Start(timer)
4125 end
4126 if not DBM.Options.DontShowPTCountdownText then
4127 TimerTracker_OnEvent(TimerTracker, "START_TIMER", 2, timer, timer)
4128 end
4129 if not DBM.Options.DontShowPTText then
4130 if target then
4131 dummyMod.text:Show(DBM_CORE_ANNOUNCE_PULL_TARGET:format(target, timer, sender))
4132 dummyMod.text:Schedule(timer, DBM_CORE_ANNOUNCE_PULL_NOW_TARGET:format(target))
4133 else
4134 dummyMod.text:Show(DBM_CORE_ANNOUNCE_PULL:format(timer, sender))
4135 dummyMod.text:Schedule(timer, DBM_CORE_ANNOUNCE_PULL_NOW)
4136 end
4137 end
4138 DBM:StartLogging(timer, checkForActualPull)
4139 if DBM.Options.CheckGear then
4140 local bagilvl, equippedilvl = GetAverageItemLevel()
4141 local difference = bagilvl - equippedilvl
4142 local weapon = GetInventoryItemLink("player", 16)
4143 local fishingPole = false
4144 if weapon then
4145 local _, _, _, _, _, _, type = GetItemInfo(weapon)
4146 if type and type == DBM_CORE_GEAR_FISHING_POLE then
4147 fishingPole = true
4148 end
4149 end
4150 if IsInRaid() and difference >= 40 then
4151 dummyMod.geartext:Show(DBM_CORE_GEAR_WARNING:format(floor(difference)))
4152 elseif IsInRaid() and (not weapon or fishingPole) then
4153 dummyMod.geartext:Show(DBM_CORE_GEAR_WARNING_WEAPON)
4154 end
4155 end
4156 end
4157
4158 do
4159 local dummyMod2 -- dummy mod for the break timer
4160 function breakTimerStart(self, timer, sender)
4161 if not dummyMod2 then
4162 local threshold = DBM.Options.PTCountThreshold
4163 local adjustedThreshold = 5
4164 if threshold > 10 then
4165 adjustedThreshold = 10
4166 else
4167 adjustedThreshold = floor(threshold)
4168 end
4169 dummyMod2 = DBM:NewMod("BreakTimerCountdownDummy")
4170 DBM:GetModLocalization("BreakTimerCountdownDummy"):SetGeneralLocalization{ name = DBM_CORE_MINIMAP_TOOLTIP_HEADER }
4171 dummyMod2.countdown = dummyMod2:NewCountdown(0, 0, nil, nil, adjustedThreshold, true)
4172 dummyMod2.text = dummyMod2:NewAnnounce("%s", 1, "Interface\\Icons\\Spell_Holy_BorrowedTime")
4173 end
4174 --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)
4175 if not DBM.Options.DontShowPT2 and DBM.Bars:GetBar(DBM_CORE_TIMER_BREAK) then
4176 DBM.Bars:CancelBar(DBM_CORE_TIMER_BREAK)
4177 fireEvent("DBM_TimerStop", "break")
4178 end
4179 if not DBM.Options.DontPlayPTCountdown then
4180 dummyMod2.countdown:Cancel()
4181 end
4182 dummyMod2.text:Cancel()
4183 DBM.Options.tempBreak2 = nil
4184 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)
4185 self.Options.tempBreak2 = timer.."/"..time()
4186 if not self.Options.DontShowPT2 then
4187 self.Bars:CreateBar(timer, DBM_CORE_TIMER_BREAK, "Interface\\Icons\\Spell_Holy_BorrowedTime")
4188 fireEvent("DBM_TimerStart", "break", DBM_CORE_TIMER_BREAK, timer, "Interface\\Icons\\Spell_Holy_BorrowedTime", "utilitytimer", nil, 0)
4189 end
4190 if not self.Options.DontPlayPTCountdown then
4191 dummyMod2.countdown:Start(timer)
4192 end
4193 if not self.Options.DontShowPTText then
4194 local hour, minute = GetGameTime()
4195 minute = minute+(timer/60)
4196 if minute >= 60 then
4197 hour = hour + 1
4198 minute = minute - 60
4199 end
4200 minute = floor(minute)
4201 if minute < 10 then
4202 minute = tostring(0 .. minute)
4203 end
4204 dummyMod2.text:Show(DBM_CORE_BREAK_START:format(strFromTime(timer).." ("..hour..":"..minute..")", sender))
4205 if timer/60 > 10 then dummyMod2.text:Schedule(timer - 10*60, DBM_CORE_BREAK_MIN:format(10)) end
4206 if timer/60 > 5 then dummyMod2.text:Schedule(timer - 5*60, DBM_CORE_BREAK_MIN:format(5)) end
4207 if timer/60 > 2 then dummyMod2.text:Schedule(timer - 2*60, DBM_CORE_BREAK_MIN:format(2)) end
4208 if timer/60 > 1 then dummyMod2.text:Schedule(timer - 1*60, DBM_CORE_BREAK_MIN:format(1)) end
4209 dummyMod2.text:Schedule(timer, DBM_CORE_ANNOUNCE_BREAK_OVER:format(hour..":"..minute))
4210 end
4211 C_TimerAfter(timer, function() self.Options.tempBreak2 = nil end)
4212 end
4213 end
4214
4215 syncHandlers["BT"] = function(sender, timer)
4216 if DBM.Options.DontShowUserTimers then return end
4217 timer = tonumber(timer or 0)
4218 if timer > 3600 then return end
4219 if (DBM:GetRaidRank(sender) == 0 and IsInGroup()) or select(2, IsInInstance()) == "pvp" or IsEncounterInProgress() then
4220 return
4221 end
4222 breakTimerStart(DBM, timer, sender)
4223 end
4224
4225 whisperSyncHandlers["BTR3"] = function(sender, timer)
4226 if DBM.Options.DontShowUserTimers or not DBM:GetRaidUnitId(sender) then return end
4227 timer = tonumber(timer or 0)
4228 if timer > 3600 then return end
4229 DBM:Unschedule(DBM.RequestTimers)--IF we got BTR3 sync, then we know immediately RequestTimers was successful, so abort others
4230 if #inCombat >= 1 then return end
4231 if DBM.Bars:GetBar(DBM_CORE_TIMER_BREAK) then return end--Already recovered. Prevent duplicate recovery
4232 breakTimerStart(DBM, timer, sender)
4233 end
4234
4235 local function SendVersion(guild)
4236 if guild then
4237 local message = ("%d\t%s\t%s"):format(DBM.Revision, tostring(DBM.ReleaseRevision), DBM.DisplayVersion)
4238 SendAddonMessage("D4", "GV\t" .. message, "GUILD")
4239 return
4240 end
4241 if DBM.Options.FakeBWVersion then
4242 SendAddonMessage("BigWigs", versionResponseString:format(fakeBWVersion, fakeBWHash), IsInGroup(2) and "INSTANCE_CHAT" or "RAID")
4243 return
4244 end
4245 --(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)
4246 local VPVersion
4247 local VoicePack = DBM.Options.ChosenVoicePack
4248 if VoicePack ~= "None" then
4249 VPVersion = "/ VP"..VoicePack..": v"..DBM.VoiceVersions[VoicePack]
4250 end
4251 if VPVersion then
4252 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))
4253 else
4254 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)))
4255 end
4256 end
4257
4258 local function HandleVersion(revision, version, displayVersion, sender, noRaid)
4259 if version > DBM.Revision then -- Update reminder
4260 if #newerVersionPerson < 4 then
4261 if not checkEntry(newerVersionPerson, sender) then
4262 newerVersionPerson[#newerVersionPerson + 1] = sender
4263 DBM:Debug("Newer version detected from "..sender.." : Rev - "..revision..", Ver - "..version..", Rev Diff - "..(revision - DBM.Revision), 3)
4264 end
4265 if #newerVersionPerson == 2 and updateNotificationDisplayed < 2 then--Only requires 2 for update notification.
4266 if DBM.HighestRelease < version then
4267 DBM.HighestRelease = version
4268 end
4269 DBM.NewerVersion = displayVersion
4270 --UGLY hack to get release version number instead of alpha one
4271 if DBM.NewerVersion:find("alpha") then
4272 local temp1, temp2 = string.split(" ", DBM.NewerVersion)--Strip down to just version, no alpha
4273 if temp1 then
4274 local temp3, temp4, temp5 = string.split(".", temp1)--Strip version down to 3 numbers
4275 if temp3 and temp4 and temp5 and tonumber(temp5) then
4276 temp5 = tonumber(temp5)
4277 temp5 = temp5 - 1
4278 temp5 = tostring(temp5)
4279 DBM.NewerVersion = temp3.."."..temp4.."."..temp5
4280 end
4281 end
4282 end
4283 --Find min revision.
4284 updateNotificationDisplayed = 2
4285 AddMsg(DBM, DBM_CORE_UPDATEREMINDER_HEADER:match("([^\n]*)"))
4286 AddMsg(DBM, DBM_CORE_UPDATEREMINDER_HEADER:match("\n(.*)"):format(displayVersion, version))
4287 showConstantReminder = 1
4288 elseif not noRaid and #newerVersionPerson == 3 and updateNotificationDisplayed < 3 then--The following code requires at least THREE people to send that higher revision. That should be more than adaquate
4289 --Disable if revision grossly out of date even if not major patch.
4290 if raid[newerVersionPerson[1]] and raid[newerVersionPerson[2]] and raid[newerVersionPerson[3]] then
4291 local revDifference = mmin((raid[newerVersionPerson[1]].revision - DBM.Revision), (raid[newerVersionPerson[2]].revision - DBM.Revision), (raid[newerVersionPerson[3]].revision - DBM.Revision))
4292 if revDifference > 100 then
4293 if updateNotificationDisplayed < 3 then
4294 updateNotificationDisplayed = 3
4295 AddMsg(DBM, DBM_CORE_UPDATEREMINDER_DISABLE)
4296 DBM:Disable(true)
4297 end
4298 end
4299 --Disable if out of date and it's a major patch.
4300 elseif not testBuild and dbmToc < wowTOC then
4301 updateNotificationDisplayed = 3
4302 AddMsg(DBM, DBM_CORE_UPDATEREMINDER_MAJORPATCH)
4303 DBM:Disable(true)
4304 end
4305 end
4306 end
4307 end
4308 if DBM.DisplayVersion:find("alpha") and #newerRevisionPerson < 3 and updateNotificationDisplayed < 2 and (revision - DBM.Revision) > 20 then
4309 if not checkEntry(newerRevisionPerson, sender) then
4310 newerRevisionPerson[#newerRevisionPerson + 1] = sender
4311 DBM:Debug("Newer revision detected from "..sender.." : Rev - "..revision..", Ver - "..version..", Rev Diff - "..(revision - DBM.Revision))
4312 end
4313 if #newerRevisionPerson == 2 and raid[newerRevisionPerson[1]] and raid[newerRevisionPerson[2]] then
4314 local revDifference = mmin((raid[newerRevisionPerson[1]].revision - DBM.Revision), (raid[newerRevisionPerson[2]].revision - DBM.Revision))
4315 if testBuild and revDifference > 5 then
4316 updateNotificationDisplayed = 3
4317 AddMsg(DBM, DBM_CORE_UPDATEREMINDER_DISABLE)
4318 DBM:Disable(true)
4319 else
4320 updateNotificationDisplayed = 2
4321 AddMsg(DBM, DBM_CORE_UPDATEREMINDER_HEADER_ALPHA:format(revDifference))
4322 end
4323 end
4324 end
4325 end
4326
4327 -- TODO: is there a good reason that version information is broadcasted and not unicasted?
4328 syncHandlers["H"] = function(sender)
4329 DBM:Unschedule(SendVersion)--Throttle so we don't needlessly send tons of comms during initial raid invites
4330 DBM:Schedule(3, SendVersion)--Send version if 3 seconds have past since last "Hi" sync
4331 end
4332
4333 syncHandlers["GH"] = function(sender)
4334 DBM:Unschedule(SendVersion, true)--Throttle so we don't needlessly send tons of comms during initial raid invites
4335 DBM:Schedule(3, SendVersion, true)--Send version if 3 seconds have past since last "Hi" sync
4336 end
4337
4338 syncHandlers["BV"] = function(sender, version, hash)--Parsed from bigwigs V7+
4339 if version and raid[sender] then
4340 raid[sender].bwversion = version
4341 raid[sender].bwhash = hash or ""
4342 end
4343 end
4344
4345 syncHandlers["V"] = function(sender, revision, version, displayVersion, locale, iconEnabled, VPVersion)
4346 revision, version = tonumber(revision), tonumber(version)
4347 if revision and version and displayVersion and raid[sender] then
4348 raid[sender].revision = revision
4349 raid[sender].version = version
4350 raid[sender].displayVersion = displayVersion
4351 raid[sender].VPVersion = VPVersion
4352 raid[sender].locale = locale
4353 raid[sender].enabledIcons = iconEnabled or "false"
4354 DBM:Debug("Received version info from "..sender.." : Rev - "..revision..", Ver - "..version..", Rev Diff - "..(revision - DBM.Revision), 3)
4355 if not DBM.Options.DontShowReminders then
4356 --HandleVersion(revision, version, displayVersion, sender)
4357 end
4358 end
4359 DBM:GROUP_ROSTER_UPDATE()
4360 end
4361
4362 syncHandlers["GV"] = function(sender, revision, version, displayVersion)
4363 revision, version = tonumber(revision), tonumber(version)
4364 if revision and version and displayVersion then
4365 DBM:Debug("Received G version info from "..sender.." : Rev - "..revision..", Ver - "..version..", Rev Diff - "..(revision - DBM.Revision), 3)
4366 if not DBM.Options.DontShowReminders then
4367 --HandleVersion(revision, version, displayVersion, sender, true)
4368 end
4369 end
4370 end
4371
4372 syncHandlers["U"] = function(sender, time, text)
4373 if select(2, IsInInstance()) == "pvp" then return end -- no pizza timers in battlegrounds
4374 if DBM.Options.DontShowUserTimers then return end
4375 if DBM:GetRaidRank(sender) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then return end
4376 if sender == playerName then return end
4377 time = tonumber(time or 0)
4378 text = tostring(text)
4379 if time and text then
4380 DBM:CreatePizzaTimer(time, text, nil, sender)
4381 end
4382 end
4383
4384 syncHandlers["CU"] = function(sender, time, text)
4385 if select(2, IsInInstance()) == "pvp" then return end -- no pizza timers in battlegrounds
4386 if DBM.Options.DontShowUserTimers then return end
4387 if DBM:GetRaidRank(sender) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then return end
4388 if sender == playerName then return end
4389 time = tonumber(time or 0)
4390 text = tostring(text)
4391 if time and text then
4392 DBM:CreatePizzaTimer(time, text, nil, sender, true)
4393 end
4394 end
4395
4396 -- beware, ugly and missplaced code ahead
4397 -- todo: move this somewhere else
4398 do
4399 local accessList = {}
4400 local savedSender
4401
4402 local inspopup = CreateFrame("Frame", "DBMPopupLockout", UIParent)
4403 inspopup:SetBackdrop({bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
4404 edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
4405 tile = true, tileSize = 16, edgeSize = 16,
4406 insets = {left = 1, right = 1, top = 1, bottom = 1}}
4407 )
4408 inspopup:SetSize(500, 120)
4409 inspopup:SetPoint("TOP", UIParent, "TOP", 0, -200)
4410 inspopup:SetFrameStrata("DIALOG")
4411
4412 local inspopuptext = inspopup:CreateFontString()
4413 inspopuptext:SetFontObject(ChatFontNormal)
4414 inspopuptext:SetWidth(470)
4415 inspopuptext:SetWordWrap(true)
4416 inspopuptext:SetPoint("TOP", inspopup, "TOP", 0, -15)
4417
4418 local buttonaccept = CreateFrame("Button", nil, inspopup)
4419 buttonaccept:SetNormalTexture("Interface\\Buttons\\UI-DialogBox-Button-Up")
4420 buttonaccept:SetPushedTexture("Interface\\Buttons\\UI-DialogBox-Button-Down")
4421 buttonaccept:SetHighlightTexture("Interface\\Buttons\\UI-DialogBox-Button-Highlight", "ADD")
4422 buttonaccept:SetSize(128, 35)
4423 buttonaccept:SetPoint("BOTTOM", inspopup, "BOTTOM", -75, 0)
4424
4425 local buttonatext = buttonaccept:CreateFontString()
4426 buttonatext:SetFontObject(ChatFontNormal)
4427 buttonatext:SetPoint("CENTER", buttonaccept, "CENTER", 0, 5)
4428 buttonatext:SetText(YES)
4429
4430 local buttondecline = CreateFrame("Button", nil, inspopup)
4431 buttondecline:SetNormalTexture("Interface\\Buttons\\UI-DialogBox-Button-Up")
4432 buttondecline:SetPushedTexture("Interface\\Buttons\\UI-DialogBox-Button-Down")
4433 buttondecline:SetHighlightTexture("Interface\\Buttons\\UI-DialogBox-Button-Highlight", "ADD")
4434 buttondecline:SetSize(128, 35)
4435 buttondecline:SetPoint("BOTTOM", inspopup, "BOTTOM", 75, 0)
4436
4437 local buttondtext = buttondecline:CreateFontString()
4438 buttondtext:SetFontObject(ChatFontNormal)
4439 buttondtext:SetPoint("CENTER", buttondecline, "CENTER", 0, 5)
4440 buttondtext:SetText(NO)
4441
4442 inspopup:Hide()
4443
4444 local function autoDecline(sender, force)
4445 inspopup:Hide()
4446 savedSender = nil
4447 if force then
4448 SendAddonMessage("D4", "II\t" .. "denied", "WHISPER", sender)
4449 else
4450 SendAddonMessage("D4", "II\t" .. "timeout", "WHISPER", sender)
4451 end
4452 end
4453
4454 local function showPopupInstanceIdPermission(sender)
4455 DBM:Unschedule(autoDecline)
4456 DBM:Schedule(59, autoDecline, sender)
4457 inspopup:Hide()
4458 if savedSender ~= sender then
4459 if savedSender then
4460 autoDecline(savedSender, 1) -- Do not allow multiple popups, so auto decline to previous sender.
4461 end
4462 savedSender = sender
4463 end
4464 inspopuptext:SetText(DBM_REQ_INSTANCE_ID_PERMISSION:format(sender, sender))
4465 buttonaccept:SetScript("OnClick", function(f) savedSender = nil DBM:Unschedule(autoDecline) accessList[sender] = true syncHandlers["IR"](sender) f:GetParent():Hide() end)
4466 buttondecline:SetScript("OnClick", function(f) autoDecline(sender, 1) end)
4467 PlaySound(850)
4468 inspopup:Show()
4469 end
4470
4471 syncHandlers["IR"] = function(sender)
4472 if DBM:GetRaidRank(sender) == 0 or sender == playerName then
4473 return
4474 end
4475 accessList = accessList or {}
4476 if not accessList[sender] then
4477 -- ask for permission
4478 showPopupInstanceIdPermission(sender)
4479 return
4480 end
4481 -- okay, send data
4482 local sentData = false
4483 for i = 1, GetNumSavedInstances() do
4484 local name, id, _, difficulty, locked, extended, instanceIDMostSig, isRaid, maxPlayers, textDiff, _, progress = GetSavedInstanceInfo(i)
4485 if (locked or extended) and isRaid then -- only report locked raid instances
4486 SendAddonMessage("D4", "II\tData\t" .. name .. "\t" .. id .. "\t" .. difficulty .. "\t" .. maxPlayers .. "\t" .. (progress or 0) .. "\t" .. textDiff, "WHISPER", sender)
4487 sentData = true
4488 end
4489 end
4490 if not sentData then
4491 -- 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...
4492 SendAddonMessage("D4", "II\tNoData", "WHISPER", sender)
4493 end
4494 end
4495
4496 syncHandlers["IRE"] = function(sender)
4497 local popup = inspopup:IsShown()
4498 if popup and savedSender == sender then -- found the popup with the correct data
4499 savedSender = nil
4500 DBM:Unschedule(autoDecline)
4501 inspopup:Hide()
4502 end
4503 end
4504
4505 syncHandlers["GCB"] = function(sender, modId, ver, difficulty, difficultyModifier)
4506 if not DBM.Options.ShowGuildMessages or not difficulty then return end
4507 if not ver or not (ver == "2") then return end--Ignore old versions
4508 if DBM:AntiSpam(10, "GCB") then
4509 if IsInInstance() then return end--Simple filter, if you are inside an instance, just filter it, if not in instance, good to go.
4510 difficulty = tonumber(difficulty)
4511 if not DBM.Options.ShowGuildMessagesPlus and difficulty == 8 then return end
4512 local bossName = EJ_GetEncounterInfo(modId) or DBM_CORE_UNKNOWN
4513 local difficultyName = DBM_CORE_UNKNOWN
4514 if difficulty == 8 then
4515 if difficultyModifier and difficultyModifier ~= 0 then
4516 difficultyName = PLAYER_DIFFICULTY6.."+ ("..difficultyModifier..")"
4517 else
4518 difficultyName = PLAYER_DIFFICULTY6.."+"
4519 end
4520 elseif difficulty == 16 then
4521 difficultyName = PLAYER_DIFFICULTY6
4522 elseif difficulty == 15 then
4523 difficultyName = PLAYER_DIFFICULTY2
4524 else
4525 difficultyName = PLAYER_DIFFICULTY1
4526 end
4527 DBM:AddMsg(DBM_CORE_GUILD_COMBAT_STARTED:format(difficultyName.."-"..bossName))
4528 end
4529 end
4530
4531 syncHandlers["GCE"] = function(sender, modId, ver, wipe, time, difficulty, difficultyModifier, wipeHP)
4532 if not DBM.Options.ShowGuildMessages or not difficulty then return end
4533 if not ver or not (ver == "4") then return end--Ignore old versions
4534 if DBM:AntiSpam(5, "GCE") then
4535 if IsInInstance() then return end--Simple filter, if you are inside an instance, just filter it, if not in instance, good to go.
4536 difficulty = tonumber(difficulty)
4537 if not DBM.Options.ShowGuildMessagesPlus and difficulty == 8 then return end
4538 local bossName = EJ_GetEncounterInfo(modId) or DBM_CORE_UNKNOWN
4539 local difficultyName = DBM_CORE_UNKNOWN
4540 if difficulty == 8 then
4541 if difficultyModifier and difficultyModifier ~= 0 then
4542 difficultyName = PLAYER_DIFFICULTY6.."+ ("..difficultyModifier..")"
4543 else
4544 difficultyName = PLAYER_DIFFICULTY6.."+"
4545 end
4546 elseif difficulty == 16 then
4547 difficultyName = PLAYER_DIFFICULTY6
4548 elseif difficulty == 15 then
4549 difficultyName = PLAYER_DIFFICULTY2
4550 else
4551 difficultyName = PLAYER_DIFFICULTY1
4552 end
4553 if wipe == "1" then
4554 DBM:AddMsg(DBM_CORE_GUILD_COMBAT_ENDED_AT:format(difficultyName.."-"..bossName, wipeHP, time))
4555 else
4556 DBM:AddMsg(DBM_CORE_GUILD_BOSS_DOWN:format(difficultyName.."-"..bossName, time))
4557 end
4558 end
4559 end
4560
4561 syncHandlers["WBE"] = function(sender, modId, realm, health, ver, name)
4562 if not ver or not (ver == "8") then return end--Ignore old versions
4563 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.
4564 lastBossEngage[modId..realm] = GetTime()
4565 if realm == playerRealm and DBM.Options.WorldBossAlert and not IsEncounterInProgress() then
4566 modId = tonumber(modId)--If it fails to convert into number, this makes it nil
4567 local bossName = modId and EJ_GetEncounterInfo(modId) or name or DBM_CORE_UNKNOWN
4568 DBM:AddMsg(DBM_CORE_WORLDBOSS_ENGAGED:format(bossName, floor(health), sender))
4569 end
4570 end
4571
4572 syncHandlers["WBD"] = function(sender, modId, realm, ver, name)
4573 if not ver or not (ver == "8") then return end--Ignore old versions
4574 if lastBossDefeat[modId..realm] and (GetTime() - lastBossDefeat[modId..realm] < 30) then return end
4575 lastBossDefeat[modId..realm] = GetTime()
4576 if realm == playerRealm and DBM.Options.WorldBossAlert and not IsEncounterInProgress() then
4577 modId = tonumber(modId)--If it fails to convert into number, this makes it nil
4578 local bossName = modId and EJ_GetEncounterInfo(modId) or name or DBM_CORE_UNKNOWN
4579 DBM:AddMsg(DBM_CORE_WORLDBOSS_DEFEATED:format(bossName, sender))
4580 end
4581 end
4582
4583 whisperSyncHandlers["WBE"] = function(sender, modId, realm, health, ver, name)
4584 if not ver or not (ver == "8") then return end--Ignore old versions
4585 if lastBossEngage[modId..realm] and (GetTime() - lastBossEngage[modId..realm] < 30) then return end
4586 lastBossEngage[modId..realm] = GetTime()
4587 if realm == playerRealm and DBM.Options.WorldBossAlert and not IsEncounterInProgress() then
4588 local _, toonName = BNGetGameAccountInfo(sender)
4589 modId = tonumber(modId)--If it fails to convert into number, this makes it nil
4590 local bossName = modId and EJ_GetEncounterInfo(modId) or name or DBM_CORE_UNKNOWN
4591 DBM:AddMsg(DBM_CORE_WORLDBOSS_ENGAGED:format(bossName, floor(health), toonName))
4592 end
4593 end
4594
4595 whisperSyncHandlers["WBD"] = function(sender, modId, realm, ver, name)
4596 if not ver or not (ver == "8") then return end--Ignore old versions
4597 if lastBossDefeat[modId..realm] and (GetTime() - lastBossDefeat[modId..realm] < 30) then return end
4598 lastBossDefeat[modId..realm] = GetTime()
4599 if realm == playerRealm and DBM.Options.WorldBossAlert and not IsEncounterInProgress() then
4600 local _, toonName = BNGetGameAccountInfo(sender)
4601 modId = tonumber(modId)--If it fails to convert into number, this makes it nil
4602 local bossName = modId and EJ_GetEncounterInfo(modId) or name or DBM_CORE_UNKNOWN
4603 DBM:AddMsg(DBM_CORE_WORLDBOSS_DEFEATED:format(bossName, toonName))
4604 end
4605 end
4606
4607 local lastRequest = 0
4608 local numResponses = 0
4609 local expectedResponses = 0
4610 local allResponded = false
4611 local results
4612
4613 local updateInstanceInfo, showResults
4614
4615 whisperSyncHandlers["II"] = function(sender, result, name, id, diff, maxPlayers, progress, textDiff)
4616 if not DBM:GetRaidUnitId(sender) then return end
4617 if GetTime() - lastRequest > 62 or not results then
4618 return
4619 end
4620 if not result then
4621 return
4622 end
4623 name = name or DBM_CORE_UNKNOWN
4624 id = id or ""
4625 diff = tonumber(diff or 0) or 0
4626 maxPlayers = tonumber(maxPlayers or 0) or 0
4627 progress = tonumber(progress or 0) or 0
4628 textDiff = textDiff or ""
4629
4630 -- count responses
4631 if not results.responses[sender] then
4632 results.responses[sender] = result
4633 numResponses = numResponses + 1
4634 end
4635
4636 -- get localized difficulty text
4637 if textDiff ~= "" then
4638 results.difftext[diff] = textDiff
4639 end
4640
4641 if result == "Data" then
4642 -- got data in that response and not just a "no" or "i'm away"
4643 local instanceId = name.." "..maxPlayers.." "..diff -- locale-dependant dungeon ID
4644 results.data[instanceId] = results.data[instanceId] or {
4645 ids = {}, -- array of all ids of all raid members
4646 name = name,
4647 diff = diff,
4648 maxPlayers = maxPlayers,
4649 }
4650 if diff == 5 or diff == 6 or diff == 16 then
4651 results.data[instanceId].ids[id] = results.data[instanceId].ids[id] or { progress = progress, haveid = true }
4652 tinsert(results.data[instanceId].ids[id], sender)
4653 else
4654 results.data[instanceId].ids[progress] = results.data[instanceId].ids[progress] or { progress = progress }
4655 tinsert(results.data[instanceId].ids[progress], sender)
4656 end
4657 end
4658
4659 if numResponses >= expectedResponses then -- unlikely, lol
4660 DBM:Unschedule(updateInstanceInfo)
4661 DBM:Unschedule(showResults)
4662 if not allResponded then --Only display message once in case we get for example 4 syncs the last sender
4663 DBM:Schedule(0.99, DBM.AddMsg, DBM, DBM_INSTANCE_INFO_ALL_RESPONSES)
4664 allResponded = true
4665 end
4666 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.
4667 end
4668 end
4669
4670 function showResults()
4671 local resultCount = 0
4672 -- 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)
4673 DBM:AddMsg(DBM_INSTANCE_INFO_RESULTS, false)
4674 DBM:AddMsg("---", false)
4675 for i, v in pairs(results.data) do
4676 resultCount = resultCount + 1
4677 DBM:AddMsg(DBM_INSTANCE_INFO_DETAIL_HEADER:format(v.name, (results.difftext[v.diff] or v.diff)), false)
4678 for id, v in pairs(v.ids) do
4679 if v.haveid then
4680 DBM:AddMsg(DBM_INSTANCE_INFO_DETAIL_INSTANCE:format(id, v.progress, tconcat(v, ", ")), false)
4681 else
4682 DBM:AddMsg(DBM_INSTANCE_INFO_DETAIL_INSTANCE2:format(v.progress, tconcat(v, ", ")), false)
4683 end
4684 end
4685 DBM:AddMsg("---", false)
4686 end
4687 if resultCount == 0 then
4688 DBM:AddMsg(DBM_INSTANCE_INFO_NOLOCKOUT, false)
4689 end
4690 local denied = {}
4691 local away = {}
4692 local noResponse = {}
4693 for i = 1, GetNumGroupMembers() do
4694 if not UnitIsUnit("raid"..i, "player") then
4695 tinsert(noResponse, (GetRaidRosterInfo(i)))
4696 end
4697 end
4698 for i, v in pairs(results.responses) do
4699 if v == "Data" or v == "NoData" then
4700 elseif v == "timeout" then
4701 tinsert(away, i)
4702 else -- could be "clicked" or "override", in both cases we don't get the data because the dialog requesting it was dismissed
4703 tinsert(denied, i)
4704 end
4705 removeEntry(noResponse, i)
4706 end
4707 if #denied > 0 then
4708 DBM:AddMsg(DBM_INSTANCE_INFO_STATS_DENIED:format(tconcat(denied, ", ")), false)
4709 end
4710 if #away > 0 then
4711 DBM:AddMsg(DBM_INSTANCE_INFO_STATS_AWAY:format(tconcat(away, ", ")), false)
4712 end
4713 if #noResponse > 0 then
4714 DBM:AddMsg(DBM_INSTANCE_INFO_STATS_NO_RESPONSE:format(tconcat(noResponse, ", ")), false)
4715 end
4716 results = nil
4717 end
4718
4719 -- called when the chat link is clicked
4720 function DBM:ShowRaidIDRequestResults()
4721 if not results then -- check if we are currently querying raid IDs, results will be nil if we don't
4722 return
4723 end
4724 self:Unschedule(updateInstanceInfo)
4725 self:Unschedule(showResults)
4726 showResults() -- sets results to nil after the results are displayed, ending the current id request; future incoming data will be discarded
4727 sendSync("IRE")
4728 end
4729
4730 local function getResponseStats()
4731 local numResponses = 0
4732 local sent = 0
4733 local denied = 0
4734 local away = 0
4735 for k, v in pairs(results.responses) do
4736 numResponses = numResponses + 1
4737 if v == "Data" or v == "NoData" then
4738 sent = sent + 1
4739 elseif v == "timeout" then
4740 away = away + 1
4741 else -- could be "clicked" or "override", in both cases we don't get the data because the dialog requesting it was dismissed
4742 denied = denied + 1
4743 end
4744 end
4745 return numResponses, sent, denied, away
4746 end
4747
4748 local function getNumDBMUsers() -- without ourselves
4749 local r = 0
4750 for i, v in pairs(raid) do
4751 if v.revision and v.name ~= playerName and UnitIsConnected(v.id) then
4752 r = r + 1
4753 end
4754 end
4755 return r
4756 end
4757
4758 function updateInstanceInfo(timeRemaining, dontAddShowResultNowButton)
4759 local numResponses, sent, denied, away = getResponseStats()
4760 local dbmUsers = getNumDBMUsers()
4761 DBM:AddMsg(DBM_INSTANCE_INFO_STATUS_UPDATE:format(numResponses, dbmUsers, sent, denied, timeRemaining), false)
4762 if not dontAddShowResultNowButton then
4763 if dbmUsers - numResponses <= 7 then -- waiting for 7 or less players, show their names and the early result option
4764 -- copied from above, todo: implement a smarter way of keeping track of stuff like this
4765 local noResponse = {}
4766 for i = 1, GetNumGroupMembers() do
4767 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)
4768 tinsert(noResponse, (GetRaidRosterInfo(i)))
4769 end
4770 end
4771 for i, v in pairs(results.responses) do
4772 removeEntry(noResponse, i)
4773 end
4774
4775 --[[
4776 -- this looked like the easiest way (for some reason?) to create the player string when writing this code -.-
4777 local function dup(...) if select("#", ...) == 0 then return else return ..., ..., dup(select(2, ...)) end end
4778 DBM:AddMsg(DBM_INSTANCE_INFO_SHOW_RESULTS:format(("|Hplayer:%s|h[%s]|h| "):rep(#noResponse):format(dup(unpack(noResponse)))), false)
4779 ]]
4780 -- code that one can actually read
4781 for i, v in ipairs(noResponse) do
4782 noResponse[i] = ("|Hplayer:%s|h[%s]|h|"):format(v, v)
4783 end
4784 DBM:AddMsg(DBM_INSTANCE_INFO_SHOW_RESULTS:format(tconcat(noResponse, ", ")), false)
4785 end
4786 end
4787 end
4788
4789 function DBM:RequestInstanceInfo()
4790 self:AddMsg(DBM_INSTANCE_INFO_REQUESTED)
4791 lastRequest = GetTime()
4792 allResponded = false
4793 results = {
4794 responses = { -- who responded to our request?
4795 },
4796 data = { -- the actual data
4797 },
4798 difftext = {
4799 }
4800 }
4801 numResponses = 0
4802 expectedResponses = getNumDBMUsers()
4803 sendSync("IR")
4804 self:Unschedule(updateInstanceInfo)
4805 self:Unschedule(showResults)
4806 self:Schedule(17, updateInstanceInfo, 45, true)
4807 self:Schedule(32, updateInstanceInfo, 30)
4808 self:Schedule(48, updateInstanceInfo, 15)
4809 C_TimerAfter(62, showResults)
4810 end
4811 end
4812
4813 whisperSyncHandlers["RT"] = function(sender)
4814 if not DBM:GetRaidUnitId(sender) then
4815 DBM:Debug(sender.." attempted to request timers but isn't in your group")
4816 return
4817 end
4818 DBM:SendTimers(sender)
4819 end
4820
4821 whisperSyncHandlers["CI"] = function(sender, mod, time)
4822 if not DBM:GetRaidUnitId(sender) then
4823 DBM:Debug(sender.." attempted to send you combat info but isn't in your group")
4824 return
4825 end
4826 mod = DBM:GetModByName(mod or "")
4827 time = tonumber(time or 0)
4828 if mod and time then
4829 DBM:ReceiveCombatInfo(sender, mod, time)
4830 end
4831 end
4832
4833 whisperSyncHandlers["TI"] = function(sender, mod, timeLeft, totalTime, id, ...)
4834 if not DBM:GetRaidUnitId(sender) then return end--This can't be checked fast enough on timer recovery, so it causes it to fail
4835 mod = DBM:GetModByName(mod or "")
4836 timeLeft = tonumber(timeLeft or 0)
4837 totalTime = tonumber(totalTime or 0)
4838 if mod and timeLeft and timeLeft > 0 and totalTime and totalTime > 0 and id then
4839 DBM:ReceiveTimerInfo(sender, mod, timeLeft, totalTime, id, ...)
4840 end
4841 end
4842
4843 whisperSyncHandlers["VI"] = function(sender, mod, name, value)
4844 if not DBM:GetRaidUnitId(sender) then return end--This can't be checked fast enough on timer recovery, so it causes it to fail
4845 mod = DBM:GetModByName(mod or "")
4846 value = tonumber(value) or value
4847 if mod and name and value then
4848 DBM:ReceiveVariableInfo(sender, mod, name, value)
4849 end
4850 end
4851
4852 local function handleSync(channel, sender, prefix, ...)
4853 if not prefix then
4854 return
4855 end
4856 local handler
4857 if channel == "WHISPER" and sender ~= playerName then -- separate between broadcast and unicast, broadcast must not be sent as unicast or vice-versa
4858 handler = whisperSyncHandlers[prefix]
4859 else
4860 handler = syncHandlers[prefix]
4861 end
4862 if handler then
4863 return handler(sender, ...)
4864 end
4865 end
4866
4867 function DBM:CHAT_MSG_ADDON(prefix, msg, channel, sender)
4868 if prefix == "D4" and msg and (channel == "PARTY" or channel == "RAID" or channel == "INSTANCE_CHAT" or channel == "WHISPER" or channel == "GUILD") then
4869 sender = Ambiguate(sender, "none")
4870 handleSync(channel, sender, strsplit("\t", msg))
4871 elseif prefix == "BigWigs" and msg and (channel == "PARTY" or channel == "RAID" or channel == "INSTANCE_CHAT") then
4872 local bwPrefix, bwMsg, extra = strsplit("^", msg)
4873 if bwPrefix and bwMsg then
4874 if bwPrefix == "V" and extra then--Nil check "extra" to avoid error from older version
4875 local verString, hash = bwMsg, extra
4876 local version = tonumber(verString) or 0
4877 if version == 0 then return end--Just a query
4878 sender = Ambiguate(sender, "none")
4879 handleSync(channel, sender, "BV", version, hash)--Prefix changed, so it's not handled by DBMs "V" handler
4880 if version > fakeBWVersion then--Newer revision found, upgrade!
4881 fakeBWVersion = version
4882 fakeBWHash = hash
4883 end
4884 elseif bwPrefix == "Q" then--Version request prefix
4885 self:Unschedule(SendVersion)
4886 self:Schedule(3, SendVersion)
4887 elseif bwPrefix == "B" then--Boss Mod Sync
4888 for i = 1, #inCombat do
4889 local mod = inCombat[i]
4890 if mod and mod.OnBWSync then
4891 mod:OnBWSync(bwMsg, extra, sender)
4892 end
4893 end
4894 for i = 1, #oocBWComms do
4895 local mod = oocBWComms[i]
4896 if mod and mod.OnBWSync then
4897 mod:OnBWSync(bwMsg, extra, sender)
4898 end
4899 end
4900 end
4901 end
4902 elseif prefix == "Transcriptor" and msg then
4903 for i = 1, #inCombat do
4904 local mod = inCombat[i]
4905 if mod and mod.OnTranscriptorSync then
4906 mod:OnTranscriptorSync(msg, sender)
4907 end
4908 end
4909 if msg:find("spell:") and (DBM.Options.DebugLevel > 2 or (Transcriptor and Transcriptor:IsLogging())) then
4910 local spellId = string.match(msg, "spell:(%d+)") or DBM_CORE_UNKNOWN
4911 local spellName = string.match(msg, "h%[(.-)%]|h") or DBM_CORE_UNKNOWN
4912 local message = "RAID_BOSS_WHISPER on "..sender.." with spell of "..spellName.." ("..spellId..")"
4913 self:Debug(message)
4914 end
4915 end
4916 end
4917 --DBM.CHAT_MSG_ADDON_LOGGED = DBM.CHAT_MSG_ADDON
4918
4919 function DBM:BN_CHAT_MSG_ADDON(prefix, msg, channel, sender)
4920 if prefix == "D4" and msg then
4921 handleSync(channel, sender, strsplit("\t", msg))
4922 end
4923 end
4924end
4925
4926-----------------------
4927-- Update Reminder --
4928-----------------------
4929do
4930 local frame, fontstring, fontstringFooter, editBox, urlText
4931
4932 local function createFrame()
4933 frame = CreateFrame("Frame", "DBMUpdateReminder", UIParent)
4934 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...)
4935 frame:SetWidth(430)
4936 frame:SetHeight(140)
4937 frame:SetPoint("TOP", 0, -230)
4938 frame:SetBackdrop({
4939 bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
4940 edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", tile = true, tileSize = 32, edgeSize = 32,
4941 insets = {left = 11, right = 12, top = 12, bottom = 11},
4942 })
4943 fontstring = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
4944 fontstring:SetWidth(410)
4945 fontstring:SetHeight(0)
4946 fontstring:SetPoint("TOP", 0, -16)
4947 editBox = CreateFrame("EditBox", nil, frame)
4948 do
4949 local editBoxLeft = editBox:CreateTexture(nil, "BACKGROUND")
4950 local editBoxRight = editBox:CreateTexture(nil, "BACKGROUND")
4951 local editBoxMiddle = editBox:CreateTexture(nil, "BACKGROUND")
4952 editBoxLeft:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Left")
4953 editBoxLeft:SetHeight(32)
4954 editBoxLeft:SetWidth(32)
4955 editBoxLeft:SetPoint("LEFT", -14, 0)
4956 editBoxLeft:SetTexCoord(0, 0.125, 0, 1)
4957 editBoxRight:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Right")
4958 editBoxRight:SetHeight(32)
4959 editBoxRight:SetWidth(32)
4960 editBoxRight:SetPoint("RIGHT", 6, 0)
4961 editBoxRight:SetTexCoord(0.875, 1, 0, 1)
4962 editBoxMiddle:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Right")
4963 editBoxMiddle:SetHeight(32)
4964 editBoxMiddle:SetWidth(1)
4965 editBoxMiddle:SetPoint("LEFT", editBoxLeft, "RIGHT")
4966 editBoxMiddle:SetPoint("RIGHT", editBoxRight, "LEFT")
4967 editBoxMiddle:SetTexCoord(0, 0.9375, 0, 1)
4968 end
4969 editBox:SetHeight(32)
4970 editBox:SetWidth(250)
4971 editBox:SetPoint("TOP", fontstring, "BOTTOM", 0, -4)
4972 editBox:SetFontObject("GameFontHighlight")
4973 editBox:SetTextInsets(0, 0, 0, 1)
4974 editBox:SetFocus()
4975 editBox:SetText(urlText)
4976 editBox:HighlightText()
4977 editBox:SetScript("OnTextChanged", function(self)
4978 editBox:SetText(urlText)
4979 editBox:HighlightText()
4980 end)
4981 fontstringFooter = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
4982 fontstringFooter:SetWidth(410)
4983 fontstringFooter:SetHeight(0)
4984 fontstringFooter:SetPoint("TOP", editBox, "BOTTOM", 0, 0)
4985 local button = CreateFrame("Button", nil, frame)
4986 button:SetHeight(24)
4987 button:SetWidth(75)
4988 button:SetPoint("BOTTOM", 0, 13)
4989 button:SetNormalFontObject("GameFontNormal")
4990 button:SetHighlightFontObject("GameFontHighlight")
4991 button:SetNormalTexture(button:CreateTexture(nil, nil, "UIPanelButtonUpTexture"))
4992 button:SetPushedTexture(button:CreateTexture(nil, nil, "UIPanelButtonDownTexture"))
4993 button:SetHighlightTexture(button:CreateTexture(nil, nil, "UIPanelButtonHighlightTexture"))
4994 button:SetText(OKAY)
4995 button:SetScript("OnClick", function(self)
4996 frame:Hide()
4997 end)
4998
4999 end
5000
5001 function DBM:ShowUpdateReminder(newVersion, newRevision, text, url)
5002 urlText = url or DBM_CORE_UPDATEREMINDER_URL or "http://www.deadlybossmods.com"
5003 if not frame then
5004 createFrame()
5005 else
5006 editBox:SetText(urlText)
5007 editBox:HighlightText()
5008 end
5009 frame:Show()
5010 if newVersion then
5011 fontstring:SetText(DBM_CORE_UPDATEREMINDER_HEADER:format(newVersion, newRevision))
5012 fontstringFooter:SetText(DBM_CORE_UPDATEREMINDER_FOOTER)
5013 elseif text then
5014 fontstring:SetText(text)
5015 fontstringFooter:SetText(DBM_CORE_UPDATEREMINDER_FOOTER_GENERIC)
5016 end
5017 end
5018end
5019
5020--------------------
5021-- Notes Editor --
5022--------------------
5023do
5024 local frame, fontstring, fontstringFooter, editBox, button3
5025
5026 local function createFrame()
5027 frame = CreateFrame("Frame", "DBMNotesEditor", UIParent)
5028 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...)
5029 frame:SetWidth(430)
5030 frame:SetHeight(140)
5031 frame:SetPoint("TOP", 0, -230)
5032 frame:SetBackdrop({
5033 bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
5034 edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", tile = true, tileSize = 32, edgeSize = 32,
5035 insets = {left = 11, right = 12, top = 12, bottom = 11},
5036 })
5037 fontstring = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
5038 fontstring:SetWidth(410)
5039 fontstring:SetHeight(0)
5040 fontstring:SetPoint("TOP", 0, -16)
5041 editBox = CreateFrame("EditBox", nil, frame)
5042 do
5043 local editBoxLeft = editBox:CreateTexture(nil, "BACKGROUND")
5044 local editBoxRight = editBox:CreateTexture(nil, "BACKGROUND")
5045 local editBoxMiddle = editBox:CreateTexture(nil, "BACKGROUND")
5046 editBoxLeft:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Left")
5047 editBoxLeft:SetHeight(32)
5048 editBoxLeft:SetWidth(32)
5049 editBoxLeft:SetPoint("LEFT", -14, 0)
5050 editBoxLeft:SetTexCoord(0, 0.125, 0, 1)
5051 editBoxRight:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Right")
5052 editBoxRight:SetHeight(32)
5053 editBoxRight:SetWidth(32)
5054 editBoxRight:SetPoint("RIGHT", 6, 0)
5055 editBoxRight:SetTexCoord(0.875, 1, 0, 1)
5056 editBoxMiddle:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Right")
5057 editBoxMiddle:SetHeight(32)
5058 editBoxMiddle:SetWidth(1)
5059 editBoxMiddle:SetPoint("LEFT", editBoxLeft, "RIGHT")
5060 editBoxMiddle:SetPoint("RIGHT", editBoxRight, "LEFT")
5061 editBoxMiddle:SetTexCoord(0, 0.9375, 0, 1)
5062 end
5063 editBox:SetHeight(32)
5064 editBox:SetWidth(250)
5065 editBox:SetPoint("TOP", fontstring, "BOTTOM", 0, -4)
5066 editBox:SetFontObject("GameFontHighlight")
5067 editBox:SetTextInsets(0, 0, 0, 1)
5068 editBox:SetFocus()
5069 editBox:SetText("")
5070 fontstringFooter = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
5071 fontstringFooter:SetWidth(410)
5072 fontstringFooter:SetHeight(0)
5073 fontstringFooter:SetPoint("TOP", editBox, "BOTTOM", 0, 0)
5074 local button = CreateFrame("Button", nil, frame)
5075 button:SetHeight(24)
5076 button:SetWidth(75)
5077 button:SetPoint("BOTTOM", 80, 13)
5078 button:SetNormalFontObject("GameFontNormal")
5079 button:SetHighlightFontObject("GameFontHighlight")
5080 button:SetNormalTexture(button:CreateTexture(nil, nil, "UIPanelButtonUpTexture"))
5081 button:SetPushedTexture(button:CreateTexture(nil, nil, "UIPanelButtonDownTexture"))
5082 button:SetHighlightTexture(button:CreateTexture(nil, nil, "UIPanelButtonHighlightTexture"))
5083 button:SetText(OKAY)
5084 button:SetScript("OnClick", function(self)
5085 local mod = DBM.Noteframe.mod
5086 local modvar = DBM.Noteframe.modvar
5087 mod.Options[modvar .. "SWNote"] = editBox:GetText() or ""
5088 DBM.Noteframe.mod = nil
5089 DBM.Noteframe.modvar = nil
5090 DBM.Noteframe.abilityName = nil
5091 frame:Hide()
5092 end)
5093 local button2 = CreateFrame("Button", nil, frame)
5094 button2:SetHeight(24)
5095 button2:SetWidth(75)
5096 button2:SetPoint("BOTTOM", 0, 13)
5097 button2:SetNormalFontObject("GameFontNormal")
5098 button2:SetHighlightFontObject("GameFontHighlight")
5099 button2:SetNormalTexture(button2:CreateTexture(nil, nil, "UIPanelButtonUpTexture"))
5100 button2:SetPushedTexture(button2:CreateTexture(nil, nil, "UIPanelButtonDownTexture"))
5101 button2:SetHighlightTexture(button2:CreateTexture(nil, nil, "UIPanelButtonHighlightTexture"))
5102 button2:SetText(CANCEL)
5103 button2:SetScript("OnClick", function(self)
5104 DBM.Noteframe.mod = nil
5105 DBM.Noteframe.modvar = nil
5106 DBM.Noteframe.abilityName = nil
5107 frame:Hide()
5108 end)
5109 button3 = CreateFrame("Button", nil, frame)
5110 button3:SetHeight(24)
5111 button3:SetWidth(75)
5112 button3:SetPoint("BOTTOM", -80, 13)
5113 button3:SetNormalFontObject("GameFontNormal")
5114 button3:SetHighlightFontObject("GameFontHighlight")
5115 button3:SetNormalTexture(button3:CreateTexture(nil, nil, "UIPanelButtonUpTexture"))
5116 button3:SetPushedTexture(button3:CreateTexture(nil, nil, "UIPanelButtonDownTexture"))
5117 button3:SetHighlightTexture(button3:CreateTexture(nil, nil, "UIPanelButtonHighlightTexture"))
5118 button3:SetText(SHARE_QUEST_ABBREV)
5119 button3:SetScript("OnClick", function(self)
5120 local modid = DBM.Noteframe.mod.id
5121 local modvar = DBM.Noteframe.modvar
5122 local abilityName = DBM.Noteframe.abilityName
5123 local syncText = editBox:GetText() or ""
5124 if syncText == "" then
5125 DBM:AddMsg(DBM_CORE_NOTESHAREERRORBLANK)
5126 elseif IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and IsInInstance() 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)
5127 DBM:AddMsg(DBM_CORE_NOTESHAREERRORGROUPFINDER)
5128 else
5129 local msg = modid.."\t"..modvar.."\t"..syncText.."\t"..abilityName
5130 if IsInRaid() then
5131 if DBM:GetRaidRank(playerName) == 0 then
5132 DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
5133 else
5134 SendAddonMessage("D4", "NS\t" .. msg, "RAID")
5135 DBM:AddMsg(DBM_CORE_NOTESHARED)
5136 end
5137 elseif IsInGroup(LE_PARTY_CATEGORY_HOME) then
5138 if DBM:GetRaidRank(playerName) == 0 then
5139 DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
5140 else
5141 SendAddonMessage("D4", "NS\t" .. msg, "PARTY")
5142 DBM:AddMsg(DBM_CORE_NOTESHARED)
5143 end
5144 else--Solo
5145 DBM:AddMsg(DBM_CORE_NOTESHAREERRORSOLO)
5146 end
5147 end
5148 end)
5149 end
5150
5151 function DBM:ShowNoteEditor(mod, modvar, abilityName, syncText, sender)
5152 if not frame then
5153 createFrame()
5154 self.Noteframe = frame
5155 else
5156 if frame:IsShown() and syncText then
5157 self:AddMsg(DBM_CORE_NOTESHAREERRORALREADYOPEN)
5158 return
5159 end
5160 end
5161 frame:Show()
5162 fontstringFooter:SetText(DBM_CORE_NOTEFOOTER)
5163 self.Noteframe.mod = mod
5164 self.Noteframe.modvar = modvar
5165 self.Noteframe.abilityName = abilityName
5166 if syncText then
5167 button3:Hide()--Don't show share button in shared notes
5168 fontstring:SetText(DBM_CORE_NOTESHAREDHEADER:format(sender, abilityName))
5169 editBox:SetText(syncText)
5170 else
5171 button3:Show()
5172 fontstring:SetText(DBM_CORE_NOTEHEADER:format(abilityName))
5173 if type(mod.Options[modvar .. "SWNote"]) == "string" then
5174 editBox:SetText(mod.Options[modvar .. "SWNote"])
5175 else
5176 editBox:SetText("")
5177 end
5178 end
5179 end
5180end
5181
5182----------------------
5183-- Pull Detection --
5184----------------------
5185do
5186 local targetList = {}
5187 local function buildTargetList()
5188 local uId = (IsInRaid() and "raid") or "party"
5189 for i = 0, GetNumGroupMembers() do
5190 local id = (i == 0 and "target") or uId..i.."target"
5191 local guid = UnitGUID(id)
5192 if guid and DBM:IsCreatureGUID(guid) then
5193 local cId = DBM:GetCIDFromGUID(guid)
5194 targetList[cId] = id
5195 end
5196 end
5197 end
5198
5199 local function clearTargetList()
5200 twipe(targetList)
5201 end
5202
5203 local function scanForCombat(mod, mob, delay)
5204 if not checkEntry(inCombat, mob) then
5205 buildTargetList()
5206 if targetList[mob] then
5207 if delay > 0 and UnitAffectingCombat(targetList[mob]) and not (UnitPlayerOrPetInRaid(targetList[mob]) or UnitPlayerOrPetInParty(targetList[mob])) then
5208 DBM:StartCombat(mod, delay, "PLAYER_REGEN_DISABLED")
5209 elseif (delay == 0) then
5210 DBM:StartCombat(mod, 0, "PLAYER_REGEN_DISABLED_AND_MESSAGE")
5211 end
5212 end
5213 clearTargetList()
5214 end
5215 end
5216
5217 local function checkForPull(mob, combatInfo)
5218 healthCombatInitialized = false
5219 --This just can't be avoided, tryig to save cpu by using C_TimerAfter broke this
5220 --This needs the redundancy and ability to pass args.
5221 DBM:Schedule(0.5, scanForCombat, combatInfo.mod, mob, 0.5)
5222 DBM:Schedule(2, scanForCombat, combatInfo.mod, mob, 2)
5223 C_TimerAfter(2.1, function()
5224 healthCombatInitialized = true
5225 end)
5226 end
5227
5228 -- TODO: fix the duplicate code that was added for quick & dirty support of zone IDs
5229
5230 -- 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
5231 function DBM:PLAYER_REGEN_DISABLED()
5232 lastCombatStarted = GetTime()
5233 if not combatInitialized then return end
5234 if dbmIsEnabled and combatInfo[LastInstanceMapID] then
5235 for i, v in ipairs(combatInfo[LastInstanceMapID]) do
5236 if v.type:find("combat") and not v.noRegenDetection then
5237 if v.multiMobPullDetection then
5238 for _, mob in ipairs(v.multiMobPullDetection) do
5239 if checkForPull(mob, v) then
5240 break
5241 end
5242 end
5243 else
5244 checkForPull(v.mob, v)
5245 end
5246 end
5247 end
5248 end
5249 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.
5250 self:FlashClientIcon()
5251 local voice = DBM.Options.ChosenVoicePack
5252 local path = "Sound\\Creature\\CThun\\CThunYouWillDIe.ogg"
5253 if voice ~= "None" then
5254 path = "Interface\\AddOns\\DBM-VP"..voice.."\\checkhp.ogg"
5255 end
5256 self:PlaySoundFile(path)
5257 if UnitHealthMax("player") ~= 0 then
5258 local health = UnitHealth("player") / UnitHealthMax("player") * 100
5259 self:AddMsg(DBM_CORE_AFK_WARNING:format(health))
5260 end
5261 end
5262 end
5263
5264 function DBM:PLAYER_REGEN_ENABLED()
5265 if delayedFunction then--Will throw error if not a function, purposely not doing and type(delayedFunction) == "function" for now to make sure code works though cause it always should be function
5266 delayedFunction()
5267 delayedFunction = nil
5268 end
5269 end
5270
5271 local function isBossEngaged(cId)
5272 -- note that this is designed to work with any number of bosses, but it might be sufficient to check the first 5 unit ids
5273 local i = 1
5274 repeat
5275 local bossUnitId = "boss"..i
5276 local bossGUID = not UnitIsDead(bossUnitId) and UnitGUID(bossUnitId) -- check for UnitIsVisible maybe?
5277 local bossCId = bossGUID and DBM:GetCIDFromGUID(bossGUID)
5278 if bossCId and (type(cId) == "number" and cId == bossCId or type(cId) == "table" and checkEntry(cId, bossCId)) then
5279 return true
5280 end
5281 i = i + 1
5282 until not bossGUID
5283 end
5284
5285 function DBM:INSTANCE_ENCOUNTER_ENGAGE_UNIT()
5286 if timerRequestInProgress then return end--do not start ieeu combat if timer request is progressing. (not to break Timer Recovery stuff)
5287 if dbmIsEnabled and combatInfo[LastInstanceMapID] then
5288 self:Debug("INSTANCE_ENCOUNTER_ENGAGE_UNIT event fired for zoneId"..LastInstanceMapID, 3)
5289 for i, v in ipairs(combatInfo[LastInstanceMapID]) do
5290 if v.type:find("combat") and isBossEngaged(v.multiMobPullDetection or v.mob) then
5291 self:StartCombat(v.mod, 0, "IEEU")
5292 end
5293 end
5294 end
5295 end
5296
5297 function DBM:UNIT_TARGETABLE_CHANGED(uId)
5298 if self.Options.DebugLevel > 2 or (Transcriptor and Transcriptor:IsLogging()) then
5299 local active = UnitExists(uId) and "true" or "false"
5300 self:Debug("UNIT_TARGETABLE_CHANGED event fired for "..UnitName(uId)..". Active: "..active)
5301 end
5302 end
5303
5304 function DBM:UNIT_SPELLCAST_SUCCEEDED(uId, _, bfaSpellId, _, legacySpellId)
5305 local spellId = legacySpellId or bfaSpellId
5306 local spellName = self:GetSpellInfo(spellId)
5307 self:Debug("UNIT_SPELLCAST_SUCCEEDED fired: "..UnitName(uId).."'s "..spellName.."("..spellId..")", 3)
5308 end
5309
5310 function DBM:ENCOUNTER_START(encounterID, name, difficulty, size)
5311 self:Debug("ENCOUNTER_START event fired: "..encounterID.." "..name.." "..difficulty.." "..size)
5312 if dbmIsEnabled then
5313 if not self.Options.DontShowReminders then
5314 self:CheckAvailableMods()
5315 end
5316 if combatInfo[LastInstanceMapID] then
5317 for i, v in ipairs(combatInfo[LastInstanceMapID]) do
5318 if not v.noESDetection then
5319 if v.multiEncounterPullDetection then
5320 for _, eId in ipairs(v.multiEncounterPullDetection) do
5321 if encounterID == eId then
5322 self:StartCombat(v.mod, 0, "ENCOUNTER_START")
5323 return
5324 end
5325 end
5326 elseif encounterID == v.eId then
5327 self:StartCombat(v.mod, 0, "ENCOUNTER_START")
5328 return
5329 end
5330 end
5331 end
5332 end
5333 end
5334 end
5335
5336 function DBM:ENCOUNTER_END(encounterID, name, difficulty, size, success)
5337 self:Debug("ENCOUNTER_END event fired: "..encounterID.." "..name.." "..difficulty.." "..size.." "..success)
5338 for i = #inCombat, 1, -1 do
5339 local v = inCombat[i]
5340 if not v.combatInfo then return end
5341 if v.noEEDetection then return end
5342 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.
5343 local name = string.split(",", name)
5344 self.Bars:CreateBar(v.respawnTime, DBM_CORE_TIMER_RESPAWN:format(name), "Interface\\Icons\\Spell_Holy_BorrowedTime")
5345 fireEvent("DBM_TimerStart", "DBMRespawnTimer", DBM_CORE_TIMER_RESPAWN:format(name), v.respawnTime, "Interface\\Icons\\Spell_Holy_BorrowedTime", "extratimer", nil, 0, v.id)
5346 end
5347 if v.multiEncounterPullDetection then
5348 for _, eId in ipairs(v.multiEncounterPullDetection) do
5349 if encounterID == eId then
5350 self:EndCombat(v, success == 0)
5351 sendSync("EE", encounterID.."\t"..success.."\t"..v.id.."\t"..(v.revision or 0))
5352 return
5353 end
5354 end
5355 elseif encounterID == v.combatInfo.eId then
5356 self:EndCombat(v, success == 0)
5357 sendSync("EE", encounterID.."\t"..success.."\t"..v.id.."\t"..(v.revision or 0))
5358 return
5359 end
5360 end
5361 end
5362
5363 function DBM:BOSS_KILL(encounterID, name)
5364 self:Debug("BOSS_KILL event fired: "..encounterID.." "..name)
5365 for i = #inCombat, 1, -1 do
5366 local v = inCombat[i]
5367 if not v.combatInfo then return end
5368 if v.multiEncounterPullDetection then
5369 for _, eId in ipairs(v.multiEncounterPullDetection) do
5370 if encounterID == eId then
5371 self:EndCombat(v)
5372 sendSync("EE", encounterID.."\t1\t"..v.id.."\t"..(v.revision or 0))
5373 return
5374 end
5375 end
5376 elseif encounterID == v.combatInfo.eId then
5377 self:EndCombat(v)
5378 sendSync("EE", encounterID.."\t1\t"..v.id.."\t"..(v.revision or 0))
5379 return
5380 end
5381 end
5382 end
5383
5384 local function checkExpressionList(exp, str)
5385 for i, v in ipairs(exp) do
5386 if str:match(v) then
5387 return true
5388 end
5389 end
5390 return false
5391 end
5392
5393 -- called for all mob chat events
5394 local function onMonsterMessage(self, type, msg)
5395 -- pull detection
5396 if dbmIsEnabled and combatInfo[LastInstanceMapID] then
5397 for i, v in ipairs(combatInfo[LastInstanceMapID]) do
5398 if v.type == type and checkEntry(v.msgs, msg) or v.type == type .. "_regex" and checkExpressionList(v.msgs, msg) then
5399 self:StartCombat(v.mod, 0, "MONSTER_MESSAGE")
5400 elseif v.type == "combat_" .. type .. "find" and findEntry(v.msgs, msg) or v.type == "combat_" .. type and checkEntry(v.msgs, msg) then
5401 if IsInInstance() then--Indoor boss that uses both combat and message for combat, so in other words (such as hodir), don't require "target" of boss for yell like scanForCombat does for World Bosses
5402 self:StartCombat(v.mod, 0, "MONSTER_MESSAGE")
5403 else--World Boss
5404 scanForCombat(v.mod, v.mob, 0)
5405 if v.mod.readyCheckQuestId and (self.Options.WorldBossNearAlert or v.mod.Options.ReadyCheck) and not IsQuestFlaggedCompleted(v.mod.readyCheckQuestId) then
5406 self:FlashClientIcon()
5407 self:PlaySoundFile("Sound\\interface\\levelup2.ogg", true)
5408 end
5409 end
5410 end
5411 end
5412 end
5413 -- kill detection (wipe detection would also be nice to have)
5414 -- todo: add sync
5415 for i = #inCombat, 1, -1 do
5416 local v = inCombat[i]
5417 if not v.combatInfo then return end
5418 if v.combatInfo.killType == type and v.combatInfo.killMsgs[msg] then
5419 self:EndCombat(v)
5420 end
5421 end
5422 end
5423
5424 function DBM:CHAT_MSG_MONSTER_YELL(msg, npc, _, _, target)
5425 if IsEncounterInProgress() or (IsInInstance() and InCombatLockdown()) then--Too many 5 mans/old raids don't properly return encounterinprogress
5426 local targetName = target or "nil"
5427 self:Debug("CHAT_MSG_MONSTER_YELL from "..npc.." while looking at "..targetName, 2)
5428 end
5429 return onMonsterMessage(self, "yell", msg)
5430 end
5431
5432 function DBM:CHAT_MSG_MONSTER_EMOTE(msg)
5433 return onMonsterMessage(self, "emote", msg)
5434 end
5435
5436 function DBM:CHAT_MSG_RAID_BOSS_EMOTE(msg, ...)
5437 onMonsterMessage(self, "emote", msg)
5438 return self:FilterRaidBossEmote(msg, ...)
5439 end
5440
5441 function DBM:RAID_BOSS_EMOTE(msg, ...)--This is a mirror of above prototype only it has less args, both still exist for some reason.
5442 onMonsterMessage(self, "emote", msg)
5443 return self:FilterRaidBossEmote(msg, ...)
5444 end
5445
5446 function DBM:RAID_BOSS_WHISPER(msg)
5447 --Make it easier for devs to detect whispers they are unable to see
5448 --TINTERFACE\\ICONS\\ability_socererking_arcanewrath.blp:20|t You have been branded by |cFFF00000|Hspell:156238|h[Arcane Wrath]|h|r!"
5449 if IsInGroup() and not BigWigs then
5450 SendAddonMessage("Transcriptor", msg, IsInGroup(2) and "INSTANCE_CHAT" or IsInRaid() and "RAID" or "PARTY")--Send any emote to transcriptor, even if no spellid
5451 end
5452 end
5453
5454 function DBM:CHAT_MSG_MONSTER_SAY(msg)
5455 return onMonsterMessage(self, "say", msg)
5456 end
5457end
5458
5459---------------------------
5460-- Kill/Wipe Detection --
5461---------------------------
5462
5463function checkWipe(self, confirm)
5464 if #inCombat > 0 then
5465 if not savedDifficulty or not difficultyText or not difficultyIndex then--prevent error if savedDifficulty or difficultyText is nil
5466 savedDifficulty, difficultyText, difficultyIndex, LastGroupSize, difficultyModifier = self:GetCurrentInstanceDifficulty()
5467 end
5468 --hack for no iEEU information is provided.
5469 if not bossuIdFound then
5470 for i = 1, 5 do
5471 if UnitExists("boss"..i) then
5472 bossuIdFound = true
5473 break
5474 end
5475 end
5476 end
5477 local wipe = 1 -- 0: no wipe, 1: normal wipe, 2: wipe by UnitExists check.
5478 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.
5479 wipe = 0
5480 elseif IsEncounterInProgress() then -- Encounter Progress marked, you obviously in combat with boss. So do not Wipe
5481 wipe = 0
5482 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.
5483 wipe = 0
5484 elseif bossuIdFound and LastInstanceType == "raid" then -- Combat started by IEEU and no boss exist and no EncounterProgress marked, that means wipe
5485 wipe = 2
5486 for i = 1, 5 do
5487 if UnitExists("boss"..i) then
5488 wipe = 0 -- Boss found. No wipe
5489 break
5490 end
5491 end
5492 else -- Unit combat status detection. No combat unit in your party and no EncounterProgress marked, that means wipe
5493 wipe = 1
5494 local uId = (IsInRaid() and "raid") or "party"
5495 for i = 0, GetNumGroupMembers() do
5496 local id = (i == 0 and "player") or uId..i
5497 if UnitAffectingCombat(id) and not UnitIsDeadOrGhost(id) then
5498 wipe = 0 -- Someone still in combat. No wipe
5499 break
5500 end
5501 end
5502 end
5503 if wipe == 0 then
5504 self:Schedule(3, checkWipe, self)
5505 elseif confirm then
5506 for i = #inCombat, 1, -1 do
5507 local reason = (wipe == 1 and "No combat unit found in your party." or "No boss found : "..(wipe or "nil"))
5508 self:Debug("You wiped. Reason : "..reason)
5509 self:EndCombat(inCombat[i], true)
5510 end
5511 else
5512 local maxDelayTime = (savedDifficulty == "worldboss" and 15) or 5 --wait 10s more on worldboss do actual wipe.
5513 for i, v in ipairs(inCombat) do
5514 maxDelayTime = v.combatInfo and v.combatInfo.wipeTimer and v.combatInfo.wipeTimer > maxDelayTime and v.combatInfo.wipeTimer or maxDelayTime
5515 end
5516 self:Schedule(maxDelayTime, checkWipe, self, true)
5517 end
5518 end
5519end
5520
5521function checkBossHealth(self)
5522 if #inCombat > 0 then
5523 for i, v in ipairs(inCombat) do
5524 if not v.multiMobPullDetection or v.mainBoss then
5525 self:GetBossHP(v.mainBoss or v.combatInfo.mob or -1)
5526 else
5527 for _, mob in ipairs(v.multiMobPullDetection) do
5528 self:GetBossHP(mob)
5529 end
5530 end
5531 end
5532 self:Schedule(1, checkBossHealth, self)
5533 end
5534end
5535
5536function checkCustomBossHealth(self, mod)
5537 mod:CustomHealthUpdate()
5538 self:Schedule(1, checkCustomBossHealth, self, mod)
5539end
5540
5541do
5542 local statVarTable = {
5543 --6.0+
5544 ["event5"] = "normal",
5545 ["event20"] = "lfr25",
5546 ["event40"] = "lfr25",
5547 ["normal5"] = "normal",
5548 ["heroic5"] = "heroic",
5549 ["challenge5"] = "challenge",
5550 ["lfr"] = "lfr25",
5551 ["normal"] = "normal",
5552 ["heroic"] = "heroic",
5553 ["mythic"] = "mythic",
5554 ["worldboss"] = "normal",
5555 ["timewalker"] = "timewalker",
5556 --Legacy
5557 ["lfr25"] = "lfr25",
5558 ["normal10"] = "normal",
5559 ["normal25"] = "normal25",
5560 ["heroic10"] = "heroic",
5561 ["heroic25"] = "heroic25",
5562 }
5563
5564 function DBM:StartCombat(mod, delay, event, synced, syncedStartHp)
5565 cSyncSender = {}
5566 cSyncReceived = 0
5567 if not checkEntry(inCombat, mod) then
5568 if not mod.Options.Enabled then return end
5569 if not mod.combatInfo then return end
5570 if mod.combatInfo.noCombatInVehicle and UnitInVehicle("player") then -- HACK
5571 return
5572 end
5573 --HACK: makes sure that we don't detect a false pull if the event fires again when the boss dies...
5574 if mod.lastKillTime and GetTime() - mod.lastKillTime < (mod.reCombatTime or 120) and event ~= "LOADING_SCREEN_DISABLED" then return end
5575 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
5576 if event then
5577 self:Debug("StartCombat called by : "..event..". LastInstanceMapID is "..LastInstanceMapID)
5578 if event ~= "ENCOUNTER_START" then
5579 self:Debug("This event is started by"..event..". Review ENCOUNTER_START event to ensure if this is still needed", 2)
5580 end
5581 else
5582 self:Debug("StartCombat called by individual mod or unknown reason. LastInstanceMapID is "..LastInstanceMapID)
5583 end
5584 --check completed. starting combat
5585 if self.Options.DisableGuildStatus and UnitIsGroupLeader("player") then
5586 sendSync("DGP")
5587 end
5588 tinsert(inCombat, mod)
5589 if mod.inCombatOnlyEvents and not mod.inCombatOnlyEventsRegistered then
5590 mod.inCombatOnlyEventsRegistered = 1
5591 mod:RegisterEvents(unpack(mod.inCombatOnlyEvents))
5592 end
5593 --Fix for "attempt to perform arithmetic on field 'stats' (a nil value)"
5594 if not mod.stats then
5595 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
5596 mod.ignoreBestkill = true--Force this to true so we don't check any more occurances of "stats"
5597 elseif event == "TIMER_RECOVERY" then --add a lag time to delay when TIMER_RECOVERY
5598 delay = delay + select(4, GetNetStats()) / 1000
5599 end
5600 --set mod default info
5601 savedDifficulty, difficultyText, difficultyIndex, LastGroupSize, difficultyModifier = self:GetCurrentInstanceDifficulty()
5602 local name = mod.combatInfo.name
5603 local modId = mod.id
5604 if C_Scenario.IsInScenario() and (mod.addon.type == "SCENARIO") then
5605 mod.inScenario = true
5606 end
5607 mod.inCombat = true
5608 mod.blockSyncs = nil
5609 mod.combatInfo.pull = GetTime() - (delay or 0)
5610 bossuIdFound = (event or "") == "IEEU"
5611 if mod.minCombatTime then
5612 self:Schedule(mmax((mod.minCombatTime - delay), 3), checkWipe, self)
5613 else
5614 self:Schedule(3, checkWipe, self)
5615 end
5616 --get boss hp at pull
5617 if syncedStartHp and syncedStartHp < 1 then
5618 syncedStartHp = syncedStartHp * 100
5619 end
5620 local startHp = syncedStartHp or mod:GetBossHP(mod.mainBoss or mod.combatInfo.mob or -1) or 100
5621 --check boss engaged first?
5622 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
5623 mod.ignoreBestkill = true
5624 elseif mod.inScenario then
5625 local _, currentStage, numStages = C_Scenario.GetInfo()
5626 if currentStage > 1 and numStages > 1 then
5627 mod.ignoreBestkill = true
5628 end
5629 else--Reset ignoreBestkill after wipe
5630 mod.ignoreBestkill = false
5631 --It was a clean pull, so cancel any RequestTimers which might fire after boss was pulled if boss was pulled right after mod load
5632 --Only want timer recovery on in progress bosses, not clean pulls
5633 if startHp > 98 and (savedDifficulty == "worldboss" or event == "IEEU") or event == "ENCOUNTER_START" then
5634 self:Unschedule(self.RequestTimers)
5635 end
5636 end
5637 if not mod.inScenario then
5638 if self.Options.HideTooltips then
5639 --Better or cleaner way?
5640 tooltipsHidden = true
5641 GameTooltip.Temphide = function() GameTooltip:Hide() end; GameTooltip:SetScript("OnShow", GameTooltip.Temphide)
5642 end
5643 if self.Options.DisableSFX and GetCVar("Sound_EnableSFX") == "1" then
5644 self.Options.sfxDisabled = true
5645 SetCVar("Sound_EnableSFX", 0)
5646 end
5647 --boss health info scheduler
5648 if not mod.CustomHealthUpdate then
5649 self:Schedule(1, checkBossHealth, self)
5650 else
5651 self:Schedule(1, checkCustomBossHealth, self, mod)
5652 end
5653 end
5654 --process global options
5655 self:HideBlizzardEvents(1)
5656 self:StartLogging(0, nil)
5657 if self.Options.HideObjectivesFrame and mod.addon.type ~= "SCENARIO" and GetNumTrackedAchievements() == 0 and difficultyIndex ~= 8 then
5658 if ObjectiveTrackerFrame:IsVisible() then
5659 ObjectiveTrackerFrame:Hide()
5660 watchFrameRestore = true
5661 end
5662 end
5663 fireEvent("pull", mod, delay, synced, startHp)
5664 self:FlashClientIcon()
5665 --serperate timer recovery and normal start.
5666 if event ~= "TIMER_RECOVERY" then
5667 --add pull count
5668 if mod.stats then
5669 if not mod.stats[statVarTable[savedDifficulty].."Pulls"] then mod.stats[statVarTable[savedDifficulty].."Pulls"] = 0 end
5670 mod.stats[statVarTable[savedDifficulty].."Pulls"] = mod.stats[statVarTable[savedDifficulty].."Pulls"] + 1
5671 end
5672 --show speed timer
5673 if self.Options.AlwaysShowSpeedKillTimer2 and mod.stats and not mod.ignoreBestkill then
5674 local bestTime
5675 if difficultyIndex == 8 then--Mythic+/Challenge Mode
5676 local bestMPRank = mod.stats.challengeBestRank or 0
5677 if bestMPRank == difficultyModifier then
5678 --Don't show speed kill timer if not our highest rank. DBM only stores highest rank
5679 bestTime = mod.stats[statVarTable[savedDifficulty].."BestTime"]
5680 end
5681 else
5682 bestTime = mod.stats[statVarTable[savedDifficulty].."BestTime"]
5683 end
5684 if bestTime and bestTime > 0 then
5685 local speedTimer = mod:NewTimer(bestTime, DBM_SPEED_KILL_TIMER_TEXT, "Interface\\Icons\\Spell_Holy_BorrowedTime", nil, false)
5686 speedTimer:Start()
5687 end
5688 end
5689 --update boss left
5690 if mod.numBoss then
5691 mod.vb.bossLeft = mod.numBoss
5692 end
5693 --elect icon person
5694 if mod.findFastestComputer and not self.Options.DontSetIcons then
5695 if self:GetRaidRank() > 0 then
5696 for i = 1, #mod.findFastestComputer do
5697 local option = mod.findFastestComputer[i]
5698 if mod.Options[option] then
5699 sendSync("IS", UnitGUID("player").."\t"..DBM.Revision.."\t"..option)
5700 end
5701 end
5702 elseif not IsInGroup() then
5703 for i = 1, #mod.findFastestComputer do
5704 local option = mod.findFastestComputer[i]
5705 if mod.Options[option] then
5706 canSetIcons[option] = true
5707 end
5708 end
5709 end
5710 end
5711 --call OnCombatStart
5712 if mod.OnCombatStart then
5713 mod:OnCombatStart(delay or 0, event == "PLAYER_REGEN_DISABLED_AND_MESSAGE" or event == "SPELL_CAST_SUCCESS")
5714 end
5715 --send "C" sync
5716 if not synced then
5717 sendSync("C", (delay or 0).."\t"..modId.."\t"..(mod.revision or 0).."\t"..startHp.."\t"..DBM.Revision.."\t"..(mod.hotfixNoticeRev or 0))
5718 end
5719 if self.Options.DisableStatusWhisper and UnitIsGroupLeader("player") and (difficultyIndex == 8 or difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16) then
5720 sendSync("DSW")
5721 end
5722 --show bigbrother check
5723 if self.Options.ShowBigBrotherOnCombatStart and BigBrother and type(BigBrother.ConsumableCheck) == "function" then
5724 if self.Options.BigBrotherAnnounceToRaid then
5725 BigBrother:ConsumableCheck("RAID")
5726 else
5727 BigBrother:ConsumableCheck("SELF")
5728 end
5729 end
5730 --show enage message
5731 if self.Options.ShowEngageMessage then
5732 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.
5733 self:AddMsg(DBM_CORE_COMBAT_STARTED_IN_PROGRESS:format(difficultyText..name))
5734 elseif mod.ignoreBestkill and mod.inScenario then
5735 self:AddMsg(DBM_CORE_SCENARIO_STARTED_IN_PROGRESS:format(difficultyText..name))
5736 else
5737 if mod.addon.type == "SCENARIO" then
5738 self:AddMsg(DBM_CORE_SCENARIO_STARTED:format(difficultyText..name))
5739 else
5740 self:AddMsg(DBM_CORE_COMBAT_STARTED:format(difficultyText..name))
5741 if (difficultyIndex == 8 or difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16) and InGuildParty() and not statusGuildDisabled and not self.Options.DisableGuildStatus then--Only send relevant content, not guild beating down lich king or LFR.
5742 SendAddonMessage("D4", "GCB\t"..modId.."\t2\t"..difficultyIndex.."\t"..difficultyModifier, "GUILD")
5743 end
5744 end
5745 end
5746 end
5747 --stop pull count
5748 local dummyMod = self:GetModByName("PullTimerCountdownDummy")
5749 if dummyMod then--stop pull timer, warning, countdowns
5750 dummyMod.countdown:Cancel()
5751 dummyMod.text:Cancel()
5752 self.Bars:CancelBar(DBM_CORE_TIMER_PULL)
5753 fireEvent("DBM_TimerStop", "pull")
5754 TimerTracker_OnEvent(TimerTracker, "PLAYER_ENTERING_WORLD")
5755 end
5756 if BigWigs and BigWigs.db.profile.raidicon and not self.Options.DontSetIcons and self:GetRaidRank() > 0 then--Both DBM and bigwigs have raid icon marking turned on.
5757 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 or turned off to stop this alert)
5758 end
5759 if self.Options.EventSoundEngage and self.Options.EventSoundEngage ~= "" and self.Options.EventSoundEngage ~= "None" then
5760 self:PlaySoundFile(self.Options.EventSoundEngage)
5761 end
5762 fireEvent("DBM_MusicStart", "BossEncounter")
5763 if self.Options.EventSoundMusic and self.Options.EventSoundMusic ~= "None" and self.Options.EventSoundMusic ~= "" and not (self.Options.EventMusicMythicFilter and (savedDifficulty == "mythic" or savedDifficulty == "challenge")) then
5764 if not self.Options.tempMusicSetting then
5765 self.Options.tempMusicSetting = tonumber(GetCVar("Sound_EnableMusic"))
5766 if self.Options.tempMusicSetting == 0 then
5767 SetCVar("Sound_EnableMusic", 1)
5768 else
5769 self.Options.tempMusicSetting = nil--Don't actually need it
5770 end
5771 end
5772 local path = "MISSING"
5773 if self.Options.EventSoundMusic == "Random" then
5774 local usedTable = self.Options.EventSoundMusicCombined and DBM.Music or DBM.BattleMusic
5775 local random = fastrandom(3, #usedTable)
5776 path = usedTable[random].value
5777 else
5778 path = self.Options.EventSoundMusic
5779 end
5780 PlayMusic(path)
5781 self.Options.musicPlaying = true
5782 DBM:Debug("Starting combat music with file: "..path)
5783 end
5784 else
5785 self:AddMsg(DBM_CORE_COMBAT_STATE_RECOVERED:format(difficultyText..name, strFromTime(delay)))
5786 if mod.OnTimerRecovery then
5787 mod:OnTimerRecovery()
5788 end
5789 end
5790 if savedDifficulty == "worldboss" and not mod.noWBEsync then
5791 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.
5792 lastBossEngage[modId..playerRealm] = GetTime()--Update last engage time, that way we ignore our own sync
5793 if IsInGuild() then
5794 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.
5795 end
5796 local _, numBNetOnline = BNGetNumFriends()
5797 for i = 1, numBNetOnline do
5798 local sameRealm = false
5799 local presenceID, _, _, _, _, _, client, isOnline = BNGetFriendInfo(i)
5800 if isOnline and client == BNET_CLIENT_WOW then
5801 local _, _, _, userRealm = BNGetGameAccountInfo(presenceID)
5802 if connectedServers then
5803 for i = 1, #connectedServers do
5804 if userRealm == connectedServers[i] then
5805 sameRealm = true
5806 break
5807 end
5808 end
5809 else
5810 if userRealm == playerRealm then
5811 sameRealm = true
5812 end
5813 end
5814 if sameRealm then
5815 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
5816 end
5817 end
5818 end
5819 end
5820 end
5821 end
5822
5823 function DBM:UNIT_HEALTH(uId)
5824 local cId = self:GetCIDFromGUID(UnitGUID(uId))
5825 local health
5826 if UnitHealthMax(uId) ~= 0 then
5827 health = UnitHealth(uId) / UnitHealthMax(uId) * 100
5828 end
5829 if not health or health < 2 then return end -- no worthy of combat start if health is below 2%
5830 if dbmIsEnabled and InCombatLockdown() then
5831 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.
5832 if combatInfo[LastInstanceMapID] then
5833 for i, v in ipairs(combatInfo[LastInstanceMapID]) do
5834 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
5835 -- Delay set, > 97% = 0.5 (consider as normal pulling), max dealy limited to 20s.
5836 self:StartCombat(v.mod, health > 97 and 0.5 or mmin(GetTime() - lastCombatStarted, 20), "UNIT_HEALTH", nil, health)
5837 end
5838 end
5839 end
5840 end
5841 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.
5842 self:PlaySoundFile("Sound\\Creature\\CThun\\CThunYouWillDIe.ogg")--So fire an alert sound to save yourself from this person's behavior.
5843 self:AddMsg(DBM_CORE_AFK_WARNING:format(health))
5844 end
5845 end
5846 end
5847
5848 function DBM:EndCombat(mod, wipe)
5849 if removeEntry(inCombat, mod) then
5850 local scenario = mod.addon.type == "SCENARIO" and not mod.soloChallenge
5851 if mod.inCombatOnlyEvents and mod.inCombatOnlyEventsRegistered then
5852 -- unregister all events except for SPELL_AURA_REMOVED events (might still be needed to remove icons etc...)
5853 mod:UnregisterInCombatEvents()
5854 self:Schedule(2, mod.UnregisterInCombatEvents, mod, true) -- 2 seconds should be enough for all auras to fade
5855 self:Schedule(3, mod.Stop, mod) -- Remove accident started timers.
5856 mod.inCombatOnlyEventsRegistered = nil
5857 end
5858 if mod.updateInterval then
5859 mod:UnregisterOnUpdateHandler()
5860 end
5861 mod:Stop()
5862 if enableIcons and not self.Options.DontSetIcons and not self.Options.DontRestoreIcons then
5863 -- restore saved previous icon
5864 for uId, icon in pairs(mod.iconRestore) do
5865 SetRaidTarget(uId, icon)
5866 end
5867 twipe(mod.iconRestore)
5868 end
5869 mod.inCombat = false
5870 mod.blockSyncs = true
5871 if mod.combatInfo.killMobs then
5872 for i, v in pairs(mod.combatInfo.killMobs) do
5873 mod.combatInfo.killMobs[i] = true
5874 end
5875 end
5876 if not savedDifficulty or not difficultyText or not difficultyIndex then--prevent error if savedDifficulty or difficultyText is nil
5877 savedDifficulty, difficultyText, difficultyIndex, LastGroupSize, difficultyModifier = DBM:GetCurrentInstanceDifficulty()
5878 end
5879 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)
5880 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
5881 return--Don't run any further, stats are nil on a bad load so rest of this code will also error out.
5882 end
5883 local name = mod.combatInfo.name
5884 local modId = mod.id
5885 if wipe then
5886 mod.lastWipeTime = GetTime()
5887 --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)
5888 local thisTime = GetTime() - mod.combatInfo.pull
5889 local hp = mod.highesthealth and mod:GetHighestBossHealth() or mod:GetLowestBossHealth()
5890 local wipeHP = mod.CustomHealthUpdate and mod:CustomHealthUpdate() or hp and ("%d%%"):format(hp) or DBM_CORE_UNKNOWN
5891 if mod.vb.phase then
5892 wipeHP = wipeHP.." ("..SCENARIO_STAGE:format(mod.vb.phase)..")"
5893 end
5894 if mod.numBoss then
5895 local bossesKilled = mod.numBoss - mod.vb.bossLeft
5896 wipeHP = wipeHP.." ("..BOSSES_KILLED:format(bossesKilled, mod.numBoss)..")"
5897 end
5898 local totalPulls = mod.stats[statVarTable[savedDifficulty].."Pulls"]
5899 local totalKills = mod.stats[statVarTable[savedDifficulty].."Kills"]
5900 if thisTime < 30 then -- Normally, one attempt will last at least 30 sec.
5901 totalPulls = totalPulls - 1
5902 mod.stats[statVarTable[savedDifficulty].."Pulls"] = totalPulls
5903 if self.Options.ShowDefeatMessage then
5904 if scenario then
5905 self:AddMsg(DBM_CORE_SCENARIO_ENDED_AT:format(difficultyText..name, strFromTime(thisTime)))
5906 else
5907 self:AddMsg(DBM_CORE_COMBAT_ENDED_AT:format(difficultyText..name, wipeHP, strFromTime(thisTime)))
5908 --No reason to GCE it here, so omited on purpose.
5909 end
5910 end
5911 else
5912 if self.Options.ShowDefeatMessage then
5913 if scenario then
5914 self:AddMsg(DBM_CORE_SCENARIO_ENDED_AT_LONG:format(difficultyText..name, strFromTime(thisTime), totalPulls - totalKills))
5915 else
5916 self:AddMsg(DBM_CORE_COMBAT_ENDED_AT_LONG:format(difficultyText..name, wipeHP, strFromTime(thisTime), totalPulls - totalKills))
5917 if (difficultyIndex == 8 or difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16) and InGuildParty() and not statusGuildDisabled and not self.Options.DisableGuildStatus then--Maybe add mythic plus/CM?
5918 SendAddonMessage("D4", "GCE\t"..modId.."\t4\t1\t"..strFromTime(thisTime).."\t"..difficultyIndex.."\t"..difficultyModifier.."\t"..wipeHP, "GUILD")
5919 end
5920 end
5921 end
5922 end
5923 if not self.Options.DontShowReminders and showConstantReminder == 2 and IsInGroup() and savedDifficulty ~= "lfr" and savedDifficulty ~= "lfr25" then
5924 showConstantReminder = 1
5925 --Show message any time this is a mod that has a newer hotfix revision
5926 --These people need to know the wipe could very well be their fault.
5927 self:AddMsg(DBM_CORE_OUT_OF_DATE_NAG)
5928 end
5929 local msg
5930 for k, v in pairs(autoRespondSpam) do
5931 if self.Options.WhisperStats then
5932 if scenario then
5933 msg = msg or chatPrefixShort..DBM_CORE_WHISPER_SCENARIO_END_WIPE_STATS:format(playerName, difficultyText..(name or ""), totalPulls - totalKills)
5934 else
5935 msg = msg or chatPrefixShort..DBM_CORE_WHISPER_COMBAT_END_WIPE_STATS_AT:format(playerName, difficultyText..(name or ""), wipeHP, totalPulls - totalKills)
5936 end
5937 else
5938 if scenario then
5939 msg = msg or chatPrefixShort..DBM_CORE_WHISPER_SCENARIO_END_WIPE:format(playerName, difficultyText..(name or ""))
5940 else
5941 msg = msg or chatPrefixShort..DBM_CORE_WHISPER_COMBAT_END_WIPE_AT:format(playerName, difficultyText..(name or ""), wipeHP)
5942 end
5943 end
5944 sendWhisper(k, msg)
5945 end
5946 fireEvent("wipe", mod)
5947 if self.Options.EventSoundWipe and self.Options.EventSoundWipe ~= "None" and self.Options.EventSoundWipe ~= "" then
5948 if self.Options.EventSoundWipe == "Random" then
5949 local random = fastrandom(3, #DBM.Defeat)
5950 self:PlaySoundFile(DBM.Defeat[random].value)
5951 else
5952 self:PlaySoundFile(self.Options.EventSoundWipe)
5953 end
5954 end
5955 else
5956 mod.lastKillTime = GetTime()
5957 local thisTime = GetTime() - (mod.combatInfo.pull or 0)
5958 local lastTime = mod.stats[statVarTable[savedDifficulty].."LastTime"]
5959 local bestTime = mod.stats[statVarTable[savedDifficulty].."BestTime"]
5960 if not mod.stats[statVarTable[savedDifficulty].."Kills"] or mod.stats[statVarTable[savedDifficulty].."Kills"] < 0 then mod.stats[statVarTable[savedDifficulty].."Kills"] = 0 end
5961 --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.
5962 mod.stats[statVarTable[savedDifficulty].."Kills"] = mod.stats[statVarTable[savedDifficulty].."Kills"] + 1
5963 if mod.stats[statVarTable[savedDifficulty].."Kills"] > mod.stats[statVarTable[savedDifficulty].."Pulls"] then mod.stats[statVarTable[savedDifficulty].."Kills"] = mod.stats[statVarTable[savedDifficulty].."Pulls"] end
5964 if not mod.ignoreBestkill and mod.combatInfo.pull then
5965 mod.stats[statVarTable[savedDifficulty].."LastTime"] = thisTime
5966 --Just to prevent pre mature end combat calls from broken mods from saving bad time stats.
5967 if bestTime and bestTime > 0 and bestTime < 1.5 then
5968 mod.stats[statVarTable[savedDifficulty].."BestTime"] = thisTime
5969 else
5970 if difficultyIndex == 8 then--Mythic+/Challenge Mode
5971 local bestMPRank = mod.stats.challengeBestRank or 0
5972 if mod.stats.challengeBestRank > difficultyModifier then--Don't save time stats at all
5973 --DO nothing
5974 elseif mod.stats.challengeBestRank < difficultyModifier then--Update best time and best rank, even if best time is lower (for a lower rank)
5975 mod.stats.challengeBestRank = difficultyModifier--Update best rank
5976 mod.stats[statVarTable[savedDifficulty].."BestTime"] = thisTime--Write this time no matter what.
5977 else--Best rank must match current rank, so update time normally
5978 mod.stats[statVarTable[savedDifficulty].."BestTime"] = mmin(bestTime or mhuge, thisTime)
5979 end
5980 else
5981 mod.stats[statVarTable[savedDifficulty].."BestTime"] = mmin(bestTime or mhuge, thisTime)
5982 end
5983 end
5984 end
5985 local totalKills = mod.stats[statVarTable[savedDifficulty].."Kills"]
5986 if self.Options.ShowDefeatMessage then
5987 local msg = ""
5988 if not mod.combatInfo.pull then--was a bad pull so we ignored thisTime, should never happen
5989 if scenario then
5990 msg = DBM_CORE_SCENARIO_COMPLETE:format(difficultyText..name, DBM_CORE_UNKNOWN)
5991 else
5992 msg = DBM_CORE_BOSS_DOWN:format(difficultyText..name, DBM_CORE_UNKNOWN)
5993 end
5994 elseif mod.ignoreBestkill then--Should never happen in a scenario so no need for scenario check.
5995 if scenario then
5996 msg = DBM_CORE_SCENARIO_COMPLETE_I:format(difficultyText..name, totalKills)
5997 else
5998 msg = DBM_CORE_BOSS_DOWN_I:format(difficultyText..name, totalKills)
5999 end
6000 elseif not lastTime then
6001 if scenario then
6002 msg = DBM_CORE_SCENARIO_COMPLETE:format(difficultyText..name, strFromTime(thisTime))
6003 else
6004 msg = DBM_CORE_BOSS_DOWN:format(difficultyText..name, strFromTime(thisTime))
6005 if (difficultyIndex == 8 or difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16) and InGuildParty() and not statusGuildDisabled and not self.Options.DisableGuildStatus then
6006 SendAddonMessage("D4", "GCE\t"..modId.."\t4\t0\t"..strFromTime(thisTime).."\t"..difficultyIndex.."\t"..difficultyModifier, "GUILD")
6007 end
6008 end
6009 elseif thisTime < (bestTime or mhuge) then
6010 if scenario then
6011 msg = DBM_CORE_SCENARIO_COMPLETE_NR:format(difficultyText..name, strFromTime(thisTime), strFromTime(bestTime), totalKills)
6012 else
6013 msg = DBM_CORE_BOSS_DOWN_NR:format(difficultyText..name, strFromTime(thisTime), strFromTime(bestTime), totalKills)
6014 if (difficultyIndex == 8 or difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16) and InGuildParty() and not statusGuildDisabled and not self.Options.DisableGuildStatus then
6015 SendAddonMessage("D4", "GCE\t"..modId.."\t4\t0\t"..strFromTime(thisTime).."\t"..difficultyIndex.."\t"..difficultyModifier, "GUILD")
6016 end
6017 end
6018 else
6019 if scenario then
6020 msg = DBM_CORE_SCENARIO_COMPLETE_L:format(difficultyText..name, strFromTime(thisTime), strFromTime(lastTime), strFromTime(bestTime), totalKills)
6021 else
6022 msg = DBM_CORE_BOSS_DOWN_L:format(difficultyText..name, strFromTime(thisTime), strFromTime(lastTime), strFromTime(bestTime), totalKills)
6023 if (difficultyIndex == 8 or difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16) and InGuildParty() and not statusGuildDisabled and not self.Options.DisableGuildStatus then
6024 SendAddonMessage("D4", "GCE\t"..modId.."\t4\t0\t"..strFromTime(thisTime).."\t"..difficultyIndex.."\t"..difficultyModifier, "GUILD")
6025 end
6026 end
6027 end
6028 self:Schedule(1, self.AddMsg, self, msg)
6029 end
6030 local msg
6031 for k, v in pairs(autoRespondSpam) do
6032 if self.Options.WhisperStats then
6033 if scenario then
6034 msg = msg or chatPrefixShort..DBM_CORE_WHISPER_SCENARIO_END_KILL_STATS:format(playerName, difficultyText..(name or ""), totalKills)
6035 else
6036 msg = msg or chatPrefixShort..DBM_CORE_WHISPER_COMBAT_END_KILL_STATS:format(playerName, difficultyText..(name or ""), totalKills)
6037 end
6038 else
6039 if scenario then
6040 msg = msg or chatPrefixShort..DBM_CORE_WHISPER_SCENARIO_END_KILL:format(playerName, difficultyText..(name or ""))
6041 else
6042 msg = msg or chatPrefixShort..DBM_CORE_WHISPER_COMBAT_END_KILL:format(playerName, difficultyText..(name or ""))
6043 end
6044 end
6045 sendWhisper(k, msg)
6046 end
6047 fireEvent("kill", mod)
6048 if savedDifficulty == "worldboss" and not mod.noWBEsync then
6049 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.
6050 lastBossDefeat[modId..playerRealm] = GetTime()--Update last defeat time before we send it, so we don't handle our own sync
6051 if IsInGuild() then
6052 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.
6053 end
6054 local _, numBNetOnline = BNGetNumFriends()
6055 for i = 1, numBNetOnline do
6056 local sameRealm = false
6057 local presenceID, _, _, _, _, _, client, isOnline = BNGetFriendInfo(i)
6058 if isOnline and client == BNET_CLIENT_WOW then
6059 local _, _, _, userRealm = BNGetGameAccountInfo(presenceID)
6060 if connectedServers then
6061 for i = 1, #connectedServers do
6062 if userRealm == connectedServers[i] then
6063 sameRealm = true
6064 break
6065 end
6066 end
6067 else
6068 if userRealm == playerRealm then
6069 sameRealm = true
6070 end
6071 end
6072 if sameRealm then
6073 BNSendGameData(presenceID, "D4", "WBD\t"..modId.."\t"..userRealm.."\t8\t"..name)
6074 end
6075 end
6076 end
6077 end
6078 if self.Options.EventSoundVictory2 and self.Options.EventSoundVictory2 ~= "" then
6079 if self.Options.EventSoundVictory2 == "Random" then
6080 local random = fastrandom(3, #DBM.Victory)
6081 self:PlaySoundFile(DBM.Victory[random].value)
6082 else
6083 self:PlaySoundFile(self.Options.EventSoundVictory2)
6084 end
6085 end
6086 end
6087 if mod.OnCombatEnd then mod:OnCombatEnd(wipe) end
6088 if mod.OnLeavingCombat then delayedFunction = mod.OnLeavingCombat end
6089 if #inCombat == 0 then--prevent error if you pulled multiple boss. (Earth, Wind and Fire)
6090 statusWhisperDisabled = false
6091 statusGuildDisabled = false
6092 self:Schedule(10, self.StopLogging, self)--small delay to catch kill/died combatlog events
6093 self:HideBlizzardEvents(0)
6094 self:Unschedule(checkBossHealth)
6095 self:Unschedule(checkCustomBossHealth)
6096 self.Arrow:Hide(true)
6097 if watchFrameRestore then
6098 ObjectiveTrackerFrame:Show()
6099 watchFrameRestore = false
6100 end
6101 if tooltipsHidden then
6102 --Better or cleaner way?
6103 tooltipsHidden = false
6104 GameTooltip:SetScript("OnShow", GameTooltip.Show)
6105 end
6106 if self.Options.sfxDisabled then
6107 self.Options.sfxDisabled = nil
6108 SetCVar("Sound_EnableSFX", 1)
6109 end
6110 --cache table
6111 twipe(autoRespondSpam)
6112 twipe(bossHealth)
6113 twipe(bossHealthuIdCache)
6114 twipe(bossuIdCache)
6115 --sync table
6116 twipe(canSetIcons)
6117 twipe(iconSetRevision)
6118 twipe(iconSetPerson)
6119 twipe(addsGUIDs)
6120 bossuIdFound = false
6121 eeSyncSender = {}
6122 eeSyncReceived = 0
6123 targetMonitor = nil
6124 self:CreatePizzaTimer(time, "", nil, nil, nil, nil, true)--Auto Terminate infinite loop timers on combat end
6125 self:TransitionToDungeonBGM(false, true)
6126 self:Schedule(22, self.TransitionToDungeonBGM, self)--
6127 end
6128 end
6129 end
6130end
6131
6132function DBM:OnMobKill(cId, synced)
6133 for i = #inCombat, 1, -1 do
6134 local v = inCombat[i]
6135 if not v.combatInfo then
6136 return
6137 end
6138 if v.combatInfo.killMobs and v.combatInfo.killMobs[cId] then
6139 if not synced then
6140 sendSync("K", cId)
6141 end
6142 v.combatInfo.killMobs[cId] = false
6143 if v.numBoss then
6144 v.vb.bossLeft = (v.vb.bossLeft or v.numBoss) - 1
6145 self:Debug("Boss left - "..v.vb.bossLeft.."/"..v.numBoss, 2)
6146 end
6147 local allMobsDown = true
6148 for i, v in pairs(v.combatInfo.killMobs) do
6149 if v then
6150 allMobsDown = false
6151 break
6152 end
6153 end
6154 if allMobsDown then
6155 self:EndCombat(v)
6156 end
6157 elseif cId == v.combatInfo.mob and not v.combatInfo.killMobs and not v.combatInfo.multiMobPullDetection then
6158 if not synced then
6159 sendSync("K", cId)
6160 end
6161 self:EndCombat(v)
6162 end
6163 end
6164end
6165
6166do
6167 local autoLog = false
6168 local autoTLog = false
6169
6170 local function isCurrentContent()
6171 if LastInstanceMapID == 1520 or LastInstanceMapID == 1530 or LastInstanceMapID == 1220 or LastInstanceMapID == 1648 or LastInstanceMapID == 1676 or LastInstanceMapID == 1712 then--Legion
6172 return true
6173 end
6174 return false
6175 end
6176
6177 function DBM:StartLogging(timer, checkFunc)
6178 self:Unschedule(DBM.StopLogging)
6179 if self.Options.LogOnlyRaidBosses and ((LastInstanceType ~= "raid") or IsPartyLFG() or not isCurrentContent()) then return end
6180 if self.Options.AutologBosses then--Start logging here to catch pre pots.
6181 if not LoggingCombat() then
6182 autoLog = true
6183 self:AddMsg("|cffffff00"..COMBATLOGENABLED.."|r")
6184 LoggingCombat(true)
6185 if checkFunc then
6186 self:Unschedule(checkFunc)
6187 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
6188 end
6189 end
6190 end
6191 if self.Options.AdvancedAutologBosses and Transcriptor then
6192 if not Transcriptor:IsLogging() then
6193 autoTLog = true
6194 self:AddMsg("|cffffff00"..DBM_CORE_TRANSCRIPTOR_LOG_START.."|r")
6195 Transcriptor:StartLog(1)
6196 end
6197 if checkFunc then
6198 self:Unschedule(checkFunc)
6199 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
6200 end
6201 end
6202 end
6203
6204 function DBM:StopLogging()
6205 if self.Options.AutologBosses and LoggingCombat() and autoLog then
6206 autoLog = false
6207 self:AddMsg("|cffffff00"..COMBATLOGDISABLED.."|r")
6208 LoggingCombat(false)
6209 end
6210 if self.Options.AdvancedAutologBosses and Transcriptor and autoTLog then
6211 if Transcriptor:IsLogging() then
6212 autoTLog = false
6213 self:AddMsg("|cffffff00"..DBM_CORE_TRANSCRIPTOR_LOG_END.."|r")
6214 Transcriptor:StopLog(1)
6215 end
6216 end
6217 end
6218end
6219
6220function DBM:SetCurrentSpecInfo()
6221 currentSpecGroup = GetSpecialization() or 1
6222 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.
6223 currentSpecID = tonumber(currentSpecID)
6224end
6225
6226function DBM:GetCurrentInstanceDifficulty()
6227 local _, instanceType, difficulty, difficultyName, _, _, _, _, instanceGroupSize = GetInstanceInfo()
6228 local keystoneLevel = C_ChallengeMode.GetActiveKeystoneInfo() or 0
6229 if difficulty == 0 or (difficulty == 1 and instanceType == "none") or C_Garrison:IsOnGarrisonMap() then--draenor field returns 1, causing world boss mod bug.
6230 return "worldboss", RAID_INFO_WORLD_BOSS.." - ", difficulty, instanceGroupSize, keystoneLevel
6231 elseif difficulty == 1 then
6232 return "normal5", difficultyName.." - ", difficulty, instanceGroupSize, keystoneLevel
6233 elseif difficulty == 2 then
6234 return "heroic5", difficultyName.." - ", difficulty, instanceGroupSize, keystoneLevel
6235 elseif difficulty == 3 then
6236 return "normal10", difficultyName.." - ", difficulty, instanceGroupSize, keystoneLevel
6237 elseif difficulty == 4 then
6238 return "normal25", difficultyName.." - ", difficulty, instanceGroupSize, keystoneLevel
6239 elseif difficulty == 5 then
6240 return "heroic10", difficultyName.." - ", difficulty, instanceGroupSize, keystoneLevel
6241 elseif difficulty == 6 then
6242 return "heroic25", difficultyName.." - ", difficulty, instanceGroupSize, keystoneLevel
6243 elseif difficulty == 7 then--Fixed LFR (ie pre WoD zones)
6244 return "lfr25", difficultyName.." - ", difficulty, instanceGroupSize, keystoneLevel
6245 elseif difficulty == 8 then
6246 return "challenge5", PLAYER_DIFFICULTY6.."+ ("..keystoneLevel..") - ", difficulty, instanceGroupSize, keystoneLevel
6247 elseif difficulty == 9 then--40 man raids have their own difficulty now, no longer returned as normal 10man raids
6248 return "normal10", difficultyName.." - ",difficulty, instanceGroupSize, keystoneLevel--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.
6249 elseif difficulty == 11 then
6250 return "heroic5", difficultyName.." - ", difficulty, instanceGroupSize, keystoneLevel
6251 elseif difficulty == 12 then
6252 return "normal5", difficultyName.." - ", difficulty, instanceGroupSize, keystoneLevel
6253 elseif difficulty == 14 then
6254 return "normal", difficultyName.." - ", difficulty, instanceGroupSize, keystoneLevel
6255 elseif difficulty == 15 then
6256 return "heroic", difficultyName.." - ", difficulty, instanceGroupSize, keystoneLevel
6257 elseif difficulty == 16 then
6258 return "mythic", difficultyName.." - ", difficulty, instanceGroupSize, keystoneLevel
6259 elseif difficulty == 17 then--Variable LFR (ie post WoD zones)
6260 return "lfr", difficultyName.." - ", difficulty, instanceGroupSize, keystoneLevel
6261 elseif difficulty == 18 then
6262 return "event40", difficultyName.." - ", difficulty, instanceGroupSize, keystoneLevel
6263 elseif difficulty == 19 then
6264 return "event5", difficultyName.." - ", difficulty, instanceGroupSize, keystoneLevel
6265 elseif difficulty == 20 then
6266 return "event20", difficultyName.." - ", difficulty, instanceGroupSize, keystoneLevel
6267 elseif difficulty == 23 then
6268 return "mythic", difficultyName.." - ", difficulty, instanceGroupSize, keystoneLevel
6269 elseif difficulty == 24 or difficulty == 33 then
6270 return "timewalker", difficultyName.." - ", difficulty, instanceGroupSize, keystoneLevel
6271-- elseif difficulty == 25 then--Used by Ashran in 7.x.
6272-- return "pvpscenario", difficultyName.." - ", difficulty, instanceGroupSize, keystoneLevel
6273 else--failsafe
6274 return "normal5", "", difficulty, instanceGroupSize, keystoneLevel
6275 end
6276end
6277
6278function DBM:GetCurrentArea()
6279 return LastInstanceMapID
6280end
6281
6282function DBM:GetGroupSize()
6283 return LastGroupSize
6284end
6285
6286function DBM:GetKeyStoneLevel()
6287 return difficultyModifier
6288end
6289
6290function DBM:HasMapRestrictions()
6291 --Check playerX and playerY. if they are nil restrictions are active
6292 --Restrictions active in all party, raid, pvp, arena maps. No restrictions in "none" or "scenario"
6293 local playerX, playerY = UnitPosition("player")
6294 if not playerX or not playerY then
6295 return true
6296 end
6297 return false
6298end
6299
6300function DBM:PlaySoundFile(path, ignoreSFX)
6301 if self.Options.SilentMode then return end
6302 local soundSetting = self.Options.UseSoundChannel
6303 if soundSetting == "Dialog" then
6304 PlaySoundFile(path, "Dialog")
6305 elseif ignoreSFX or soundSetting == "Master" then
6306 PlaySoundFile(path, "Master")
6307 else
6308 PlaySoundFile(path)
6309 end
6310end
6311
6312--Future proofing EJ_GetSectionInfo compat layer to make it easier updatable. EJ_GetSectionInfo won't be depricated functions forever.
6313function DBM:EJ_GetSectionInfo(sectionID)
6314 local info = EJ_GetSectionInfo(sectionID);
6315 local flag1, flag2, flag3, flag4;
6316 local flags = GetSectionIconFlags(sectionID);
6317 if flags then
6318 flag1, flag2, flag3, flag4 = unpack(flags);
6319 end
6320 return info.title, info.description, info.headerType, info.abilityIcon, info.creatureDisplayID, info.siblingSectionID, info.firstChildSectionID, info.filteredByDifficulty, info.link, info.startsOpen, flag1, flag2, flag3, flag4
6321end
6322
6323--Handle new spell name requesting with wrapper, to make api changes easier to handle
6324function DBM:GetSpellInfo(spellId)
6325 local name, rank, icon, castingTime, minRange, maxRange, returnedSpellId = GetSpellInfo(spellId)
6326 if not returnedSpellId then--Bad request all together
6327 DBM:Debug("|cffff0000Invalid call to GetSpellInfo for spellID: |r"..spellId)
6328 return nil
6329 else--Good request, return now
6330 return name, rank, icon, castingTime, minRange, maxRange, returnedSpellId
6331 end
6332end
6333
6334function DBM:UnitDebuff(uId, spellInput, spellInput2, spellInput3)
6335 if wowTOC == 80000 then
6336 for i = 1, 60 do
6337 local spellName, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitDebuff(uId, i)
6338 if not spellName then return end
6339 if spellInput == spellName or spellInput == spellId or spellInput2 == spellName or spellInput2 == spellId or spellInput3 == spellName or spellInput3 == spellId then
6340 return spellName, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3
6341 end
6342 end
6343 else
6344 if type(spellInput) == "number" then
6345 for i = 1, 60 do
6346 local spellName, rank, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitDebuff(uId, i)
6347 if not spellName then return end
6348 if spellInput == spellName or spellInput == spellId or spellInput2 == spellName or spellInput2 == spellId or spellInput3 == spellName or spellInput3 == spellId then
6349 return spellName, rank, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3
6350 end
6351 end
6352 else
6353 return UnitDebuff(uId, spellInput)
6354 end
6355 end
6356end
6357
6358function DBM:UnitBuff(uId, spellInput, spellInput2, spellInput3)
6359 if wowTOC == 80000 then
6360 for i = 1, 60 do
6361 local spellName, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff(uId, i)
6362 if not spellName then return end
6363 if spellInput == spellName or spellInput == spellId or spellInput2 == spellName or spellInput2 == spellId or spellInput3 == spellName or spellInput3 == spellId then
6364 return spellName, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3
6365 end
6366 end
6367 else
6368 if type(spellInput) == "number" then
6369 for i = 1, 60 do
6370 local spellName, rank, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3 = UnitBuff(uId, i)
6371 if not spellName then return end
6372 if spellInput == spellName or spellInput == spellId or spellInput2 == spellName or spellInput2 == spellId or spellInput3 == spellName or spellInput3 == spellId then
6373 return spellName, rank, icon, count, debuffType, duration, expirationTime, unitCaster, isStealable, nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, nameplateShowAll, timeMod, value1, value2, value3
6374 end
6375 end
6376 else
6377 return UnitBuff(uId, spellInput)
6378 end
6379 end
6380end
6381
6382function DBM:PlaySound(path)
6383 if self.Options.SilentMode then return end
6384 local soundSetting = self.Options.UseSoundChannel
6385 if soundSetting == "Master" then
6386 PlaySound(path, "Master")
6387 elseif soundSetting == "Dialog" then
6388 PlaySound(path, "Dialog")
6389 else
6390 PlaySound(path)
6391 end
6392end
6393
6394function DBM:UNIT_DIED(args)
6395 local GUID = args.destGUID
6396 if self:IsCreatureGUID(GUID) then
6397 self:OnMobKill(self:GetCIDFromGUID(GUID))
6398 end
6399 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.
6400 self:FlashClientIcon()
6401 self:PlaySoundFile("Sound\\Creature\\CThun\\CThunYouWillDIe.ogg")--So fire an alert sound to save yourself from this person's behavior.
6402 self:AddMsg(DBM_CORE_AFK_WARNING:format(0))
6403 end
6404end
6405DBM.UNIT_DESTROYED = DBM.UNIT_DIED
6406
6407----------------------
6408-- Timer recovery --
6409----------------------
6410do
6411 local requestedFrom = {}
6412 local requestTime = 0
6413 local clientUsed = {}
6414 local sortMe = {}
6415
6416 local function sort(v1, v2)
6417 if v1.revision and not v2.revision then
6418 return true
6419 elseif v2.revision and not v1.revision then
6420 return false
6421 elseif v1.revision and v2.revision then
6422 return v1.revision > v2.revision
6423 else
6424 return (v1.bwversion or 0) > (v2.bwversion or 0)
6425 end
6426 end
6427
6428 function DBM:RequestTimers(requestNum)
6429 twipe(sortMe)
6430 for i, v in pairs(raid) do
6431 tinsert(sortMe, v)
6432 end
6433 tsort(sortMe, sort)
6434 self:Debug("RequestTimers Running", 2)
6435 local selectedClient
6436 local listNum = 0
6437 for i, v in ipairs(sortMe) do
6438 -- If selectedClient player's realm is not same with your's, timer recovery by selectedClient not works at all.
6439 -- 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?)
6440 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
6441 listNum = listNum + 1
6442 if listNum == requestNum then
6443 selectedClient = v
6444 clientUsed[v.name] = GetTime()
6445 break
6446 end
6447 end
6448 end
6449 if not selectedClient then return end
6450 self:Debug("Requesting timer recovery to "..selectedClient.name)
6451 requestedFrom[selectedClient.name] = true
6452 requestTime = GetTime()
6453 SendAddonMessage("D4", "RT", "WHISPER", selectedClient.name)
6454 end
6455
6456 function DBM:ReceiveCombatInfo(sender, mod, time)
6457 if dbmIsEnabled and requestedFrom[sender] and (GetTime() - requestTime) < 5 and #inCombat == 0 then
6458 self:StartCombat(mod, time, "TIMER_RECOVERY")
6459 --Recovery successful, someone sent info, abort other recovery requests
6460 self:Unschedule(self.RequestTimers)
6461 twipe(requestedFrom)
6462 end
6463 end
6464
6465 function DBM:ReceiveTimerInfo(sender, mod, timeLeft, totalTime, id, ...)
6466 if requestedFrom[sender] and (GetTime() - requestTime) < 5 then
6467 local lag = select(4, GetNetStats()) / 1000
6468 for i, v in ipairs(mod.timers) do
6469 if v.id == id then
6470 v:Start(totalTime, ...)
6471 v:Update(totalTime - timeLeft + lag, totalTime, ...)
6472 end
6473 end
6474 end
6475 end
6476
6477 function DBM:ReceiveVariableInfo(sender, mod, name, value)
6478 if requestedFrom[sender] and (GetTime() - requestTime) < 5 then
6479 if value == "true" then
6480 mod.vb[name] = true
6481 elseif value == "false" then
6482 mod.vb[name] = false
6483 else
6484 mod.vb[name] = value
6485 end
6486 end
6487 end
6488end
6489
6490do
6491 local spamProtection = {}
6492 function DBM:SendTimers(target)
6493 self:Debug("SendTimers requested by "..target, 2)
6494 local spamForTarget = spamProtection[target] or 0
6495 -- just try to clean up the table, that should keep the hash table at max. 4 entries or something :)
6496 for k, v in pairs(spamProtection) do
6497 if GetTime() - v >= 1 then
6498 spamProtection[k] = nil
6499 end
6500 end
6501 if GetTime() - spamForTarget < 1 then -- just to prevent players from flooding this on purpose
6502 return
6503 end
6504 spamProtection[target] = GetTime()
6505 if UnitInBattleground("player") then
6506 self:SendBGTimers(target)
6507 return
6508 end
6509 if #inCombat < 1 then
6510 --Break timer is up, so send that
6511 --But only if we are not in combat with a boss
6512 if self.Bars:GetBar(DBM_CORE_TIMER_BREAK) then
6513 local remaining = self.Bars:GetBar(DBM_CORE_TIMER_BREAK).timer
6514 SendAddonMessage("D4", "BTR3\t"..remaining, "WHISPER", target)
6515 end
6516 return
6517 end
6518 local mod
6519 for i, v in ipairs(inCombat) do
6520 mod = not v.isCustomMod and v
6521 end
6522 mod = mod or inCombat[1]
6523 self:SendCombatInfo(mod, target)
6524 self:SendVariableInfo(mod, target)
6525 self:SendTimerInfo(mod, target)
6526 end
6527end
6528
6529function DBM:SendBGTimers(target)
6530 local mod
6531 if IsActiveBattlefieldArena() then
6532 mod = self:GetModByName("Arenas")
6533 else
6534 -- FIXME: this doesn't work for non-english clients
6535 local zone = GetRealZoneText():gsub(" ", "")--Does this need updating to mapid arta?
6536 mod = self:GetModByName(zone)
6537 end
6538 if mod and mod.timers then
6539 self:SendTimerInfo(mod, target)
6540 end
6541end
6542
6543function DBM:SendCombatInfo(mod, target)
6544 return SendAddonMessage("D4", ("CI\t%s\t%s"):format(mod.id, GetTime() - mod.combatInfo.pull), "WHISPER", target)
6545end
6546
6547function DBM:SendTimerInfo(mod, target)
6548 for i, v in ipairs(mod.timers) do
6549 for _, uId in ipairs(v.startedTimers) do
6550 local elapsed, totalTime, timeLeft
6551 if select("#", string.split("\t", uId)) > 1 then
6552 elapsed, totalTime = v:GetTime(select(2, string.split("\t", uId)))
6553 else
6554 elapsed, totalTime = v:GetTime()
6555 end
6556 timeLeft = totalTime - elapsed
6557 if timeLeft > 0 and totalTime > 0 then
6558 SendAddonMessage("D4", ("TI\t%s\t%s\t%s\t%s"):format(mod.id, timeLeft, totalTime, uId), "WHISPER", target)
6559 end
6560 end
6561 end
6562end
6563
6564function DBM:SendVariableInfo(mod, target)
6565 for vname, v in pairs(mod.vb) do
6566 local v2 = tostring(v)
6567 if v2 then
6568 SendAddonMessage("D4", ("VI\t%s\t%s\t%s"):format(mod.id, vname, v2), "WHISPER", target)
6569 end
6570 end
6571end
6572
6573do
6574 function DBM:PLAYER_ENTERING_WORLD()
6575 if not self.Options.DontShowReminders then
6576 C_TimerAfter(25, function() if self.Options.SilentMode then self:AddMsg(DBM_SILENT_REMINDER) end end)
6577 C_TimerAfter(30, function() if not self.Options.SettingsMessageShown then self.Options.SettingsMessageShown = true self:AddMsg(DBM_HOW_TO_USE_MOD) end end)
6578 end
6579 if type(RegisterAddonMessagePrefix) == "function" then
6580 if not RegisterAddonMessagePrefix("D4") then -- main prefix for DBM4
6581 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
6582 end
6583 if not RegisterAddonMessagePrefix("BigWigs") then
6584 self:AddMsg("Error: unable to register BigWigs addon message prefix (reached client side addon message filter limit), BigWigs version checks will be unavailable")
6585 end
6586 if not RegisterAddonMessagePrefix("Transcriptor") then
6587 self:AddMsg("Error: unable to register Transcriptor addon message prefix (reached client side addon message filter limit)")
6588 end
6589 end
6590 if self.Options.sfxDisabled then--Check if sound was disabled by previous session and not re-enabled.
6591 self.Options.sfxDisabled = nil
6592 SetCVar("Sound_EnableSFX", 1)
6593 end
6594 if self.Options.RestoreRange then self.Options.RestoreRange = nil end--User DCed while this was true, clear it
6595 end
6596end
6597
6598------------------------------------
6599-- Auto-respond/Status whispers --
6600------------------------------------
6601do
6602 local function getNumAlivePlayers()
6603 local alive = 0
6604 if IsInRaid() then
6605 for i = 1, GetNumGroupMembers() do
6606 alive = alive + ((UnitIsDeadOrGhost("raid"..i) and 0) or 1)
6607 end
6608 else
6609 alive = (UnitIsDeadOrGhost("player") and 0) or 1
6610 for i = 1, GetNumSubgroupMembers() do
6611 alive = alive + ((UnitIsDeadOrGhost("party"..i) and 0) or 1)
6612 end
6613 end
6614 return alive
6615 end
6616
6617 --Cleanup in 8.x with C_Map.GetMapGroupMembersInfo
6618 local function getNumRealAlivePlayers()
6619 local alive = 0
6620 local isInInstance = IsInInstance() or false
6621 local currentMapId = isInInstance and select(4, UnitPosition("player")) or C_Map and C_Map.GetBestMapForUnit("player") or GetPlayerMapAreaID("player")
6622 if not currentMapId then--REMOVE in 8.x, C_Map should never fail
6623 SetMapToCurrentZone()
6624 currentMapId = C_Map and C_Map.GetCurrentMapID("player") or GetCurrentMapAreaID()
6625 end
6626 local currentMapName = C_Map and C_Map.GetMapInfo(currentMapId) or GetMapNameByID(currentMapId)
6627 if IsInRaid() then
6628 for i = 1, GetNumGroupMembers() do
6629 if isInInstance and select(4, UnitPosition("raid"..i)) == currentMapId or select(7, GetRaidRosterInfo(i)) == currentMapName then
6630 alive = alive + ((UnitIsDeadOrGhost("raid"..i) and 0) or 1)
6631 end
6632 end
6633 else
6634 alive = (UnitIsDeadOrGhost("player") and 0) or 1
6635 for i = 1, GetNumSubgroupMembers() do
6636 if isInInstance and select(4, UnitPosition("party"..i)) == currentMapId or select(7, GetRaidRosterInfo(i)) == currentMapName then
6637 alive = alive + ((UnitIsDeadOrGhost("party"..i) and 0) or 1)
6638 end
6639 end
6640 end
6641 return alive
6642 end
6643
6644 local function isOnSameServer(presenceId)
6645 local toonID, client = select(6, BNGetFriendInfoByID(presenceId))
6646 if client ~= "WoW" then
6647 return false
6648 end
6649 return GetRealmName() == select(4, BNGetGameAccountInfo(toonID))
6650 end
6651
6652 -- sender is a presenceId for real id messages, a character name otherwise
6653 local function onWhisper(msg, sender, isRealIdMessage)
6654 if statusWhisperDisabled then return end--RL has disabled status whispers for entire raid.
6655 if msg:find(chatPrefix) and not InCombatLockdown() and DBM:AntiSpam(60, "Ogron") and DBM.Options.AutoReplySound then
6656 --Might need more validation if people figure out they can just whisper people with chatPrefix to trigger it.
6657 --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
6658 DBM:PlaySoundFile("sound\\creature\\aggron1\\VO_60_HIGHMAUL_AGGRON_1_AGGRO_1.ogg")
6659 elseif msg == "status" and #inCombat > 0 and DBM.Options.StatusEnabled then
6660 if not difficultyText then -- prevent error when timer recovery function worked and etc (StartCombat not called)
6661 savedDifficulty, difficultyText, difficultyIndex, LastGroupSize, difficultyModifier = DBM:GetCurrentInstanceDifficulty()
6662 end
6663 local mod
6664 for i, v in ipairs(inCombat) do
6665 mod = not v.isCustomMod and v
6666 end
6667 mod = mod or inCombat[1]
6668 if IsInScenarioGroup() and not mod.soloChallenge 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.
6669 local hp = mod.highesthealth and mod:GetHighestBossHealth() or mod:GetLowestBossHealth()
6670 local hpText = mod.CustomHealthUpdate and mod:CustomHealthUpdate() or hp and ("%d%%"):format(hp) or DBM_CORE_UNKNOWN
6671 if mod.vb.phase then
6672 hpText = hpText.." ("..SCENARIO_STAGE:format(mod.vb.phase)..")"
6673 end
6674 if mod.numBoss then
6675 local bossesKilled = mod.numBoss - mod.vb.bossLeft
6676 hpText = hpText.." ("..BOSSES_KILLED:format(bossesKilled, mod.numBoss)..")"
6677 end
6678 sendWhisper(sender, chatPrefix..DBM_CORE_STATUS_WHISPER:format(difficultyText..(mod.combatInfo.name or ""), hpText, IsInInstance() and getNumRealAlivePlayers() or getNumAlivePlayers(), DBM:GetNumRealGroupMembers()))
6679 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
6680 if not difficultyText then -- prevent error when timer recovery function worked and etc (StartCombat not called)
6681 savedDifficulty, difficultyText, difficultyIndex, LastGroupSize, difficultyModifier = DBM:GetCurrentInstanceDifficulty()
6682 end
6683 local mod
6684 for i, v in ipairs(inCombat) do
6685 mod = not v.isCustomMod and v
6686 end
6687 mod = mod or inCombat[1]
6688 local hp = mod.highesthealth and mod:GetHighestBossHealth() or mod:GetLowestBossHealth()
6689 local hpText = mod.CustomHealthUpdate and mod:CustomHealthUpdate() or hp and ("%d%%"):format(hp) or DBM_CORE_UNKNOWN
6690 if mod.vb.phase then
6691 hpText = hpText.." ("..SCENARIO_STAGE:format(mod.vb.phase)..")"
6692 end
6693 if mod.numBoss then
6694 local bossesKilled = mod.numBoss - mod.vb.bossLeft
6695 hpText = hpText.." ("..BOSSES_KILLED:format(bossesKilled, mod.numBoss)..")"
6696 end
6697 if not autoRespondSpam[sender] then
6698 if IsInScenarioGroup() and not mod.soloChallenge then
6699 sendWhisper(sender, chatPrefix..DBM_CORE_AUTO_RESPOND_WHISPER_SCENARIO:format(playerName, difficultyText..(mod.combatInfo.name or ""), getNumAlivePlayers(), DBM:GetNumGroupMembers()))
6700 else
6701 sendWhisper(sender, chatPrefix..DBM_CORE_AUTO_RESPOND_WHISPER:format(playerName, difficultyText..(mod.combatInfo.name or ""), hpText, IsInInstance() and getNumRealAlivePlayers() or getNumAlivePlayers(), DBM:GetNumRealGroupMembers()))
6702 end
6703 DBM:AddMsg(DBM_CORE_AUTO_RESPONDED)
6704 end
6705 autoRespondSpam[sender] = true
6706 end
6707 end
6708
6709 function DBM:CHAT_MSG_WHISPER(msg, name, _, _, _, status)
6710 if status ~= "GM" then
6711 name = Ambiguate(name, "none")
6712 return onWhisper(msg, name, false)
6713 end
6714 end
6715
6716 function DBM:CHAT_MSG_BN_WHISPER(msg, ...)
6717 local presenceId = select(12, ...) -- srsly?
6718 return onWhisper(msg, presenceId, true)
6719 end
6720end
6721
6722--This completely unregisteres or registers distruptive events so they don't obstruct combat
6723--Toggle is for if we are turning off or on.
6724--Custom is for external mods to call function without duplication and allowing pvp mods custom toggle.
6725do
6726 local unregisteredEvents = {}
6727 local function DisableEvent(frameName, eventName)
6728 if frameName:IsEventRegistered(eventName) then
6729 frameName:UnregisterEvent(eventName)
6730 unregisteredEvents[eventName] = true
6731 end
6732 end
6733 local function EnableEvent(frameName, eventName)
6734 if unregisteredEvents[eventName] then
6735 frameName:RegisterEvent(eventName)
6736 unregisteredEvents[eventName] = nil
6737 end
6738 end
6739 function DBM:HideBlizzardEvents(toggle, custom)
6740 if toggle == 1 then
6741 if self.Options.HideQuestTooltips then
6742 SetCVar("showQuestTrackingTooltips", 0)
6743 end
6744 if (self.Options.HideBossEmoteFrame2 or custom) and not testBuild then
6745 DisableEvent(RaidBossEmoteFrame, "RAID_BOSS_EMOTE")
6746 DisableEvent(RaidBossEmoteFrame, "RAID_BOSS_WHISPER")
6747 DisableEvent(RaidBossEmoteFrame, "CLEAR_BOSS_EMOTES")
6748 SOUNDKIT.UI_RAID_BOSS_WHISPER_WARNING = 999999--Since blizzard can still play the sound via RaidBossEmoteFrame_OnEvent (line 148) via encounter scripts in certain cases despite the frame having no registered events
6749 end
6750 if self.Options.HideGarrisonToasts or custom then
6751 DisableEvent(AlertFrame, "GARRISON_MISSION_FINISHED")
6752 DisableEvent(AlertFrame, "GARRISON_BUILDING_ACTIVATABLE")
6753 end
6754 if self.Options.HideGuildChallengeUpdates or custom then
6755 DisableEvent(AlertFrame, "GUILD_CHALLENGE_COMPLETED")
6756 end
6757 elseif toggle == 0 then
6758 if self.Options.HideQuestTooltips then
6759 SetCVar("showQuestTrackingTooltips", 1)
6760 end
6761 if (self.Options.HideBossEmoteFrame2 or custom) and not testBuild then
6762 EnableEvent(RaidBossEmoteFrame, "RAID_BOSS_EMOTE")
6763 EnableEvent(RaidBossEmoteFrame, "RAID_BOSS_WHISPER")
6764 EnableEvent(RaidBossEmoteFrame, "CLEAR_BOSS_EMOTES")
6765 SOUNDKIT.UI_RAID_BOSS_WHISPER_WARNING = 37666--restore it
6766 end
6767 if self.Options.HideGarrisonToasts then
6768 EnableEvent(AlertFrame, "GARRISON_MISSION_FINISHED")
6769 EnableEvent(AlertFrame, "GARRISON_BUILDING_ACTIVATABLE")
6770 end
6771 if self.Options.HideGuildChallengeUpdates then
6772 EnableEvent(AlertFrame, "GUILD_CHALLENGE_COMPLETED")
6773 end
6774 end
6775 end
6776end
6777
6778--------------------------
6779-- Enable/Disable DBM --
6780--------------------------
6781do
6782 local forceDisabled = false
6783 function DBM:Disable(forceDisable)
6784 unscheduleAll()
6785 dbmIsEnabled = false
6786 forceDisabled = forceDisable
6787 end
6788
6789 function DBM:Enable()
6790 if not forceDisabled then
6791 dbmIsEnabled = true
6792 end
6793 end
6794
6795 function DBM:IsEnabled()
6796 return dbmIsEnabled
6797 end
6798end
6799
6800-----------------------
6801-- Misc. Functions --
6802-----------------------
6803function DBM:AddMsg(text, prefix)
6804 local tag = prefix or (self.localization and self.localization.general.name) or "DBM"
6805 local frame = _G[tostring(DBM.Options.ChatFrame)]
6806 frame = frame and frame:IsShown() and frame or DEFAULT_CHAT_FRAME
6807 if prefix ~= false then
6808 frame:AddMessage(("|cffff7d0a<|r|cffffd200%s|r|cffff7d0a>|r %s"):format(tostring(tag), tostring(text)), 0.41, 0.8, 0.94)
6809 else
6810 frame:AddMessage(text, 0.41, 0.8, 0.94)
6811 end
6812end
6813AddMsg = DBM.AddMsg
6814
6815function DBM:Debug(text, level)
6816 if not self.Options or not self.Options.DebugMode then return end
6817 if (level or 1) <= DBM.Options.DebugLevel then
6818 local frame = _G[tostring(DBM.Options.ChatFrame)]
6819 frame = frame and frame:IsShown() and frame or DEFAULT_CHAT_FRAME
6820 frame:AddMessage("|cffff7d0aDBM Debug:|r "..text, 1, 1, 1)
6821 end
6822end
6823
6824do
6825 local testMod
6826 local testWarning1, testWarning2, testWarning3
6827 local testTimer1, testTimer2, testTimer3, testTimer4, testTimer5, testTimer6, testTimer7, testTimer8
6828 local testCount1, testCount2
6829 local testSpecialWarning1, testSpecialWarning2, testSpecialWarning3
6830 function DBM:DemoMode()
6831 if not testMod then
6832 testMod = self:NewMod("TestMod")
6833 self:GetModLocalization("TestMod"):SetGeneralLocalization{ name = "Test Mod" }
6834 testWarning1 = testMod:NewAnnounce("%s", 1, "Interface\\Icons\\Spell_Nature_WispSplode")
6835 testWarning2 = testMod:NewAnnounce("%s", 2, "Interface\\Icons\\Spell_Shadow_ShadesOfDarkness")
6836 testWarning3 = testMod:NewAnnounce("%s", 3, "Interface\\Icons\\Spell_Fire_SelfDestruct")
6837 testTimer1 = testMod:NewTimer(20, "%s", "Interface\\Icons\\Spell_Nature_WispSplode", nil, nil)
6838 testTimer2 = testMod:NewTimer(20, "%s ", "Interface\\ICONS\\INV_Misc_Head_Orc_01.blp", nil, nil, 1)
6839 testTimer3 = testMod:NewTimer(20, "%s ", "Interface\\Icons\\Spell_Shadow_ShadesOfDarkness", nil, nil, 3, DBM_CORE_MAGIC_ICON)
6840 testTimer4 = testMod:NewTimer(20, "%s ", "Interface\\Icons\\Spell_Nature_WispSplode", nil, nil, 4, DBM_CORE_INTERRUPT_ICON)
6841 testTimer5 = testMod:NewTimer(20, "%s ", "Interface\\Icons\\Spell_Fire_SelfDestruct", nil, nil, 2, DBM_CORE_HEALER_ICON)
6842 testTimer6 = testMod:NewTimer(20, "%s ", "Interface\\Icons\\Spell_Nature_WispSplode", nil, nil, 5, DBM_CORE_TANK_ICON)
6843 testTimer7 = testMod:NewTimer(20, "%s ", "Interface\\Icons\\Spell_Nature_WispSplode", nil, nil, 6)
6844 testTimer8 = testMod:NewTimer(20, "%s ", "Interface\\Icons\\Spell_Nature_WispSplode", nil, nil, 7)
6845 testCount1 = testMod:NewCountdown(0, 0, nil, nil, nil, true)
6846 testCount2 = testMod:NewCountdown(0, 0, nil, nil, nil, true, true)
6847 testSpecialWarning1 = testMod:NewSpecialWarning("%s", nil, nil, nil, 1, 2)
6848 testSpecialWarning2 = testMod:NewSpecialWarning(" %s ", nil, nil, nil, 2, 2)
6849 testSpecialWarning3 = testMod:NewSpecialWarning(" %s ", nil, nil, nil, 3, 2) -- 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)
6850 end
6851 testTimer1:Start(10, "Test Bar")
6852 testTimer2:Start(30, "Adds")
6853 testTimer3:Start(43, "Evil Debuff")
6854 testTimer4:Start(20, "Important Interrupt")
6855 testTimer5:Start(60, "Boom!")
6856 testTimer6:Start(35, "Handle your Role")
6857 testTimer7:Start(50, "Next Stage")
6858 testTimer8:Start(55, "Custom User Bar")
6859 testCount1:Cancel()
6860 testCount1:Start(43)
6861 testCount2:Cancel()
6862 testCount2:Start(60)
6863 testWarning1:Cancel()
6864 testWarning2:Cancel()
6865 testWarning3:Cancel()
6866 testSpecialWarning1:Cancel()
6867 testSpecialWarning1:CancelVoice()
6868 testSpecialWarning2:Cancel()
6869 testSpecialWarning2:CancelVoice()
6870 testSpecialWarning3:Cancel()
6871 testSpecialWarning3:CancelVoice()
6872 testWarning1:Show("Test-mode started...")
6873 testWarning1:Schedule(62, "Test-mode finished!")
6874 testWarning3:Schedule(50, "Boom in 10 sec!")
6875 testWarning3:Schedule(20, "Pew Pew Laser Owl!")
6876 testWarning2:Schedule(38, "Evil Spell in 5 sec!")
6877 testWarning2:Schedule(43, "Evil Spell!")
6878 testWarning1:Schedule(10, "Test bar expired!")
6879 testSpecialWarning1:Schedule(20, "Pew Pew Laser Owl")
6880 testSpecialWarning1:ScheduleVoice(20, "runaway")
6881 testSpecialWarning2:Schedule(43, "Fear!")
6882 testSpecialWarning2:ScheduleVoice(43, "fearsoon")
6883 testSpecialWarning3:Schedule(60, "Boom!")
6884 testSpecialWarning3:ScheduleVoice(60, "defensive")
6885 end
6886end
6887
6888DBM.Bars:SetAnnounceHook(function(bar)
6889 local prefix
6890 if bar.color and bar.color.r == 1 and bar.color.g == 0 and bar.color.b == 0 then
6891 prefix = DBM_CORE_HORDE or FACTION_HORDE
6892 elseif bar.color and bar.color.r == 0 and bar.color.g == 0 and bar.color.b == 1 then
6893 prefix = DBM_CORE_ALLIANCE or FACTION_ALLIANCE
6894 end
6895 if prefix then
6896 return ("%s: %s %d:%02d"):format(prefix, _G[bar.frame:GetName().."BarName"]:GetText(), floor(bar.timer / 60), bar.timer % 60)
6897 end
6898end)
6899
6900function DBM:Capitalize(str)
6901 local firstByte = str:byte(1, 1)
6902 local numBytes = 1
6903 if firstByte >= 0xF0 then -- firstByte & 0b11110000
6904 numBytes = 4
6905 elseif firstByte >= 0xE0 then -- firstByte & 0b11100000
6906 numBytes = 3
6907 elseif firstByte >= 0xC0 then -- firstByte & 0b11000000
6908 numBytes = 2
6909 end
6910 return str:sub(1, numBytes):upper()..str:sub(numBytes + 1):lower()
6911end
6912
6913--copied from big wigs with permission from funkydude. Modified by MysticalOS
6914function DBM:RoleCheck(ignoreLoot)
6915 local spec = GetSpecialization()
6916 if not spec then return end
6917 local role = GetSpecializationRole(spec)
6918 if not role then return end
6919 local specID = GetLootSpecialization()
6920 local _, _, _, _, lootrole = GetSpecializationInfoByID(specID)
6921 if not InCombatLockdown() and not IsFalling() and ((IsPartyLFG() and (difficultyIndex == 14 or difficultyIndex == 15)) or not IsPartyLFG()) then
6922 if UnitGroupRolesAssigned("player") ~= role then
6923 UnitSetRole("player", role)
6924 end
6925 end
6926 --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.
6927 if not ignoreLoot and lootrole and (role ~= lootrole) and self.Options.RoleSpecAlert then
6928 self:AddMsg(DBM_CORE_LOOT_SPEC_REMINDER:format(_G[role] or DBM_CORE_UNKNOWN, _G[lootrole]))
6929 end
6930end
6931
6932-- An anti spam function to throttle spammy events (e.g. SPELL_AURA_APPLIED on all group members)
6933-- @param time the time to wait between two events (optional, default 2.5 seconds)
6934-- @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)
6935function DBM:AntiSpam(time, id)
6936 if GetTime() - (id and (self["lastAntiSpam" .. tostring(id)] or 0) or self.lastAntiSpam or 0) > (time or 2.5) then
6937 if id then
6938 self["lastAntiSpam" .. tostring(id)] = GetTime()
6939 else
6940 self.lastAntiSpam = GetTime()
6941 end
6942 return true
6943 else
6944 return false
6945 end
6946end
6947
6948function DBM:GetTOC()
6949 return wowTOC, testBuild, wowVersionString, wowBuild
6950end
6951
6952function DBM:InCombat()
6953 if #inCombat > 0 then
6954 return true
6955 end
6956 return false
6957end
6958
6959function DBM:FlashClientIcon()
6960 if self:AntiSpam(5, "FLASH") then
6961 FlashClientIcon()
6962 end
6963end
6964
6965do
6966 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,}
6967 function DBM:IconNumToString(number)
6968 return iconStrings[number] or number
6969 end
6970 function DBM:IconNumToTexture(number)
6971 return "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_"..number..".blp:12:12|t" or number
6972 end
6973end
6974
6975--To speed up creating new mods.
6976function DBM:FindDungeonIDs()
6977 for i=1, 3000 do
6978 local dungeon = GetRealZoneText(i)
6979 if dungeon and dungeon ~= "" then
6980 self:AddMsg(i..": "..dungeon)
6981 end
6982 end
6983end
6984
6985function DBM:FindInstanceIDs()
6986 for i=1, 3000 do
6987 local instance = EJ_GetInstanceInfo(i)
6988 if instance then
6989 self:AddMsg(i..": "..instance)
6990 end
6991 end
6992end
6993
6994--/run DBM:FindEncounterIDs(1028)--Azeroth (BfA)
6995--/run DBM:FindEncounterIDs(1031)--Uldir
6996--/run DBM:FindEncounterIDs(1001, 23)--Dungeon Template (mythic difficulty)
6997function DBM:FindEncounterIDs(instanceID, diff)
6998 if not instanceID then
6999 self:AddMsg("Error: Function requires instanceID be provided")
7000 end
7001 if not diff then diff = 14 end--Default to "normal" in 6.0+ if diff arg not given.
7002 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
7003 for i=1, 25 do
7004 local name, _, encounterID = EJ_GetEncounterInfoByIndex(i, instanceID)
7005 if name then
7006 self:AddMsg(encounterID..": "..name)
7007 end
7008 end
7009end
7010
7011--Taint the script that disables /run /dump, etc
7012--ScriptsDisallowedForBeta = function() return false end
7013
7014-------------------
7015-- Movie Filter --
7016-------------------
7017do
7018 local neverFilter = {
7019 [486] = true, -- Tomb of Sarg Intro
7020 [487] = true, -- Alliance Broken Shore cut-scene
7021 [488] = true, -- Horde Broken Shore cut-scene
7022 [489] = true, -- Unknown, currently encrypted
7023 [490] = true, -- Unknown, currently encrypted
7024 }
7025 function DBM:PLAY_MOVIE(id)
7026 if id and not neverFilter[id] then
7027 DBM:Debug("PLAY_MOVIE fired for ID: "..id, 2)
7028 local isInstance, instanceType = IsInInstance()
7029 if not isInstance or C_Garrison:IsOnGarrisonMap() or instanceType == "scenario" or DBM.Options.MovieFilter == "Never" then return end
7030 if DBM.Options.MovieFilter == "Block" or DBM.Options.MovieFilter == "AfterFirst" and DBM.Options.MoviesSeen[id] then
7031 MovieFrame:Hide()--can only just hide movie frame safely now, which means can't stop audio anymore :\
7032 DBM:AddMsg(DBM_CORE_MOVIE_SKIPPED)
7033 else
7034 DBM.Options.MoviesSeen[id] = true
7035 end
7036 end
7037 end
7038
7039 function DBM:CINEMATIC_START()
7040 self:Debug("CINEMATIC_START fired", 2)
7041 local isInstance, instanceType = IsInInstance()
7042 if not isInstance or C_Garrison:IsOnGarrisonMap() or instanceType == "scenario" or self.Options.MovieFilter == "Never" then return end
7043 SetMapToCurrentZone()
7044 local currentMapID
7045 if C_Map then
7046 currentMapID = C_Map.GetBestMapForUnit("player")
7047 else
7048 currentMapID = GetCurrentMapAreaID()
7049 end
7050 if not currentMapID then return end--Protection from map failures in zones that have no maps yet
7051 local currentFloor = GetCurrentMapDungeonLevel and GetCurrentMapDungeonLevel() or 0--REMOVE In 8.x
7052 if self.Options.MovieFilter == "Block" or self.Options.MovieFilter == "AfterFirst" and self.Options.MoviesSeen[currentMapID..currentFloor] then
7053 CinematicFrame_CancelCinematic()
7054 self:AddMsg(DBM_CORE_MOVIE_SKIPPED)
7055 else
7056 self.Options.MoviesSeen[currentMapID..currentFloor] = true
7057 end
7058 end
7059end
7060
7061----------------------------
7062-- Boss Mod Constructor --
7063----------------------------
7064do
7065 local modsById = setmetatable({}, {__mode = "v"})
7066 local mt = {__index = bossModPrototype}
7067
7068 function DBM:NewMod(name, modId, modSubTab, instanceId, nameModifier)
7069 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
7070 if name == "DBM-ProfilesDummy" then return end
7071 if modsById[name] then error("DBM:NewMod(): Mod names are used as IDs and must therefore be unique.", 2) end
7072 local obj = setmetatable(
7073 {
7074 Options = {
7075 Enabled = true,
7076 },
7077 DefaultOptions = {
7078 Enabled = true,
7079 },
7080 subTab = modSubTab,
7081 optionCategories = {
7082 },
7083 categorySort = {"announce", "announceother", "announcepersonal", "announcerole", "timer", "sound", "misc"},
7084 id = name,
7085 announces = {},
7086 specwarns = {},
7087 timers = {},
7088 countdowns = {},
7089 vb = {},
7090 iconRestore = {},
7091 modId = modId,
7092 instanceId = instanceId,
7093 revision = 0,
7094 SyncThreshold = 8,
7095 localization = self:GetModLocalization(name)
7096 },
7097 mt
7098 )
7099 for i, v in ipairs(self.AddOns) do
7100 if v.modId == modId then
7101 obj.addon = v
7102 break
7103 end
7104 end
7105
7106 if tonumber(name) then
7107 local t = EJ_GetEncounterInfo(tonumber(name))
7108 if type(nameModifier) == "number" then--Get name form EJ_GetCreatureInfo
7109 t = select(2, EJ_GetCreatureInfo(nameModifier, tonumber(name)))
7110 elseif type(nameModifier) == "function" then--custom name modify function
7111 t = nameModifier(t or name)
7112 else--default name modify
7113 t = tostring(t)
7114 t = string.split(",", t or name)
7115 end
7116 obj.localization.general.name = t or name
7117 obj.modelId = select(4, EJ_GetCreatureInfo(1, tonumber(name)))
7118 elseif name:match("z%d+") then
7119 local t = GetRealZoneText(string.sub(name, 2))
7120 if type(nameModifier) == "number" then--do nothing
7121 elseif type(nameModifier) == "function" then--custom name modify function
7122 t = nameModifier(t or name)
7123 else--default name modify
7124 t = string.split(",", t or name)
7125 end
7126 obj.localization.general.name = t or name
7127 elseif name:match("d%d+") then
7128 local t = GetDungeonInfo(string.sub(name, 2))
7129 if type(nameModifier) == "number" then--do nothing
7130 elseif type(nameModifier) == "function" then--custom name modify function
7131 t = nameModifier(t or name)
7132 else--default name modify
7133 t = string.split(",", t or name)
7134 end
7135 obj.localization.general.name = t or name
7136 else
7137 obj.localization.general.name = obj.localization.general.name or name
7138 end
7139 tinsert(self.Mods, obj)
7140 if modId then
7141 self.ModLists[modId] = self.ModLists[modId] or {}
7142 tinsert(self.ModLists[modId], name)
7143 end
7144 modsById[name] = obj
7145 obj:SetZone()
7146 return obj
7147 end
7148
7149 function DBM:GetModByName(name)
7150 return modsById[tostring(name)]
7151 end
7152end
7153
7154-----------------------
7155-- General Methods --
7156-----------------------
7157bossModPrototype.RegisterEvents = DBM.RegisterEvents
7158bossModPrototype.UnregisterInCombatEvents = DBM.UnregisterInCombatEvents
7159bossModPrototype.AddMsg = DBM.AddMsg
7160bossModPrototype.RegisterShortTermEvents = DBM.RegisterShortTermEvents
7161bossModPrototype.UnregisterShortTermEvents = DBM.UnregisterShortTermEvents
7162
7163function bossModPrototype:SetZone(...)
7164 if select("#", ...) == 0 then
7165 self.zones = {}
7166 if self.addon and self.addon.mapId then
7167 for i, v in ipairs(self.addon.mapId) do
7168 self.zones[v] = true
7169 end
7170 end
7171 elseif select(1, ...) ~= DBM_DISABLE_ZONE_DETECTION then
7172 self.zones = {}
7173 for i = 1, select("#", ...) do
7174 self.zones[select(i, ...)] = true
7175 end
7176 else -- disable zone detection
7177 self.zones = nil
7178 end
7179end
7180
7181function bossModPrototype:Toggle()
7182 if self.Options.Enabled then
7183 self:DisableMod()
7184 else
7185 self:EnableMod()
7186 end
7187end
7188
7189function bossModPrototype:EnableMod()
7190 self.Options.Enabled = true
7191end
7192
7193function bossModPrototype:DisableMod()
7194 self:Stop()
7195 self.Options.Enabled = false
7196end
7197
7198function bossModPrototype:Stop()
7199 for i, v in ipairs(self.timers) do
7200 v:Stop()
7201 end
7202 for i, v in ipairs(self.countdowns) do
7203 v:Stop()
7204 end
7205 self:Unschedule()
7206end
7207
7208function bossModPrototype:SetUsedIcons(...)
7209 self.usedIcons = {}
7210 for i = 1, select("#", ...) do
7211 self.usedIcons[select(i, ...)] = true
7212 end
7213end
7214
7215function bossModPrototype:RegisterOnUpdateHandler(func, interval)
7216 startScheduler()
7217 if type(func) ~= "function" then return end
7218 self.elapsed = 0
7219 self.updateInterval = interval or 0
7220 updateFunctions[self] = func
7221end
7222
7223function bossModPrototype:UnregisterOnUpdateHandler()
7224 self.elapsed = nil
7225 self.updateInterval = nil
7226 twipe(updateFunctions)
7227end
7228
7229--------------
7230-- Events --
7231--------------
7232function bossModPrototype:RegisterEventsInCombat(...)
7233 if self.inCombatOnlyEvents then
7234 geterrorhandler()("combat events already set")
7235 end
7236 self.inCombatOnlyEvents = {...}
7237 for k, v in pairs(self.inCombatOnlyEvents) do
7238 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
7239 -- legacy event, oh noes
7240 self.inCombatOnlyEvents[k] = v .. " boss1 boss2 boss3 boss4 boss5 target focus"
7241 end
7242 end
7243end
7244
7245-----------------------
7246-- Utility Methods --
7247-----------------------
7248
7249function bossModPrototype:IsDifficulty(...)
7250 local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
7251 for i = 1, select("#", ...) do
7252 if diff == select(i, ...) then
7253 return true
7254 end
7255 end
7256 return false
7257end
7258
7259function bossModPrototype:IsLFR()
7260 local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
7261 if diff == "lfr" or diff == "lfr25" then
7262 return true
7263 end
7264 return false
7265end
7266
7267--Dungeons: normal, heroic. Raids: LFR, normal
7268function bossModPrototype:IsEasy()
7269 local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
7270 if diff == "normal" or diff == "lfr" or diff == "lfr25" or diff == "heroic5" or diff == "normal5" then
7271 return true
7272 end
7273 return false
7274end
7275
7276--Dungeons, mythic, mythic+. Raids: heroic, mythic
7277function bossModPrototype:IsHard()
7278 local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
7279 if diff == "mythic" or diff == "challenge5" or diff == "heroic" then
7280 return true
7281 end
7282 return false
7283end
7284
7285function bossModPrototype:IsNormal()
7286 local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
7287 if diff == "normal" or diff == "normal5" or diff == "normal10" or diff == "normal25" then
7288 return true
7289 end
7290 return false
7291end
7292
7293function bossModPrototype:IsHeroic()
7294 local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
7295 if diff == "heroic" or diff == "heroic5" or diff == "heroic10" or diff == "heroic25" then
7296 return true
7297 end
7298 return false
7299end
7300
7301function bossModPrototype:IsMythic()
7302 local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
7303 if diff == "mythic" then
7304 return true
7305 end
7306 return false
7307end
7308
7309function bossModPrototype:IsEvent()
7310 local diff = savedDifficulty or DBM:GetCurrentInstanceDifficulty()
7311 if diff == "event5" or diff == "event20" or diff == "event40" then
7312 return true
7313 end
7314 return false
7315end
7316
7317function bossModPrototype:IsTrivial(level)
7318 if difficultyIndex == 24 then return false end--Timewalker dungeon, ignore level and return false for trivial
7319 if playerLevel >= level then
7320 return true
7321 end
7322 return false
7323end
7324
7325--Skip param is used when CheckInterruptFilter is actually being used for a simpe target/focus check and nothing more.
7326--checkCooldown should never be passed with skip or COUNT interrupt warnings. It should be passed with any other interrupt filter
7327function bossModPrototype:CheckInterruptFilter(sourceGUID, skip, checkCooldown)
7328 if DBM.Options.FilterInterrupt2 == "None" and not skip then return true end--use doesn't want to use interrupt filter, always return true
7329 --Pummel, Mind Freeze, Counterspell, Kick, Skull Bash, Rebuke, Silence, Wind Shear
7330 local InterruptAvailable = true
7331 local requireCooldown = checkCooldown
7332 if (DBM.Options.FilterInterrupt2 == "onlyTandF") or self.isTrashMod and (DBM.Options.FilterInterrupt2 == "TandFandBossCooldown") then
7333 requireCooldown = false
7334 end
7335 if requireCooldown and ((GetSpellCooldown(6552)) ~= 0 or (GetSpellCooldown(47528)) ~= 0 or (GetSpellCooldown(2139)) ~= 0 or (GetSpellCooldown(1766)) ~= 0 or (GetSpellCooldown(106839)) ~= 0 or (GetSpellCooldown(96231)) ~= 0 or (GetSpellCooldown(15487)) ~= 0 or (GetSpellCooldown(57994)) ~= 0) then
7336 InterruptAvailable = false--checkCooldown check requested and player has no spell that can interrupt available
7337 end
7338 if InterruptAvailable and (UnitGUID("target") == sourceGUID or UnitGUID("focus") == sourceGUID) then
7339 return true
7340 end
7341 return false
7342end
7343
7344function bossModPrototype:CheckDispelFilter()
7345 if not DBM.Options.FilterDispel then return true end
7346 --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)
7347 --start, duration, enable = GetSpellCooldown
7348 --start & duration == 0 if spell not on cd
7349 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
7350 return false
7351 end
7352 return true
7353end
7354
7355function bossModPrototype:IsCriteriaCompleted(criteriaIDToCheck)
7356 if not criteriaIDToCheck then
7357 geterrorhandler()("usage: mod:IsCriteriaComplected(criteriaId)")
7358 return false
7359 end
7360 local _, _, numCriteria = C_Scenario.GetStepInfo()
7361 for i = 1, numCriteria do
7362 local _, _, criteriaCompleted, _, _, _, _, _, criteriaID = C_Scenario.GetCriteriaInfo(i)
7363 if criteriaID == criteriaIDToCheck and criteriaCompleted then
7364 return true
7365 end
7366 end
7367 return false
7368end
7369
7370function bossModPrototype:LatencyCheck()
7371 return select(4, GetNetStats()) < DBM.Options.LatencyThreshold
7372end
7373
7374function bossModPrototype:CheckBigWigs(name)
7375 if raid[name] and raid[name].bwversion then
7376 return raid[name].bwversion
7377 else
7378 return false
7379 end
7380end
7381
7382bossModPrototype.IconNumToString = DBM.IconNumToString
7383bossModPrototype.IconNumToTexture = DBM.IconNumToTexture
7384bossModPrototype.AntiSpam = DBM.AntiSpam
7385bossModPrototype.HasMapRestrictions = DBM.HasMapRestrictions
7386bossModPrototype.GetUnitCreatureId = DBM.GetUnitCreatureId
7387bossModPrototype.GetCIDFromGUID = DBM.GetCIDFromGUID
7388bossModPrototype.IsCreatureGUID = DBM.IsCreatureGUID
7389bossModPrototype.GetUnitIdFromGUID = DBM.GetUnitIdFromGUID
7390
7391do
7392 local bossTargetuIds = {
7393 "boss1", "boss2", "boss3", "boss4", "boss5", "focus", "target"
7394 }
7395 local targetScanCount = {}
7396 local repeatedScanEnabled = {}
7397
7398 local function getBossTarget(guid, scanOnlyBoss)
7399 local name, uid, bossuid
7400 local cacheuid = bossuIdCache[guid] or "boss1"
7401 if UnitGUID(cacheuid) == guid then
7402 bossuid = cacheuid
7403 name = DBM:GetUnitFullName(cacheuid.."target")
7404 uid = cacheuid.."target"
7405 bossuIdCache[guid] = bossuid
7406 end
7407 if name then return name, uid, bossuid end
7408 for i, uId in ipairs(bossTargetuIds) do
7409 if UnitGUID(uId) == guid then
7410 bossuid = uId
7411 name = DBM:GetUnitFullName(uId.."target")
7412 uid = uId.."target"
7413 bossuIdCache[guid] = bossuid
7414 break
7415 end
7416 end
7417 if name or scanOnlyBoss then return name, uid, bossuid end
7418 -- Now lets check nameplates
7419 for i = 1, 40 do
7420 if UnitGUID("nameplate"..i) == guid then
7421 bossuid = "nameplate"..i
7422 name = DBM:GetUnitFullName("nameplate"..i.."target")
7423 uid = "nameplate"..i.."target"
7424 bossuIdCache[guid] = bossuid
7425 break
7426 end
7427 end
7428 if name then return name, uid, bossuid end
7429 -- failed to detect from default uIds, scan all group members's target.
7430 if IsInRaid() then
7431 for i = 1, GetNumGroupMembers() do
7432 if UnitGUID("raid"..i.."target") == guid then
7433 bossuid = "raid"..i.."target"
7434 name = DBM:GetUnitFullName("raid"..i.."targettarget")
7435 uid = "raid"..i.."targettarget"
7436 bossuIdCache[guid] = bossuid
7437 break
7438 end
7439 end
7440 elseif IsInGroup() then
7441 for i = 1, GetNumSubgroupMembers() do
7442 if UnitGUID("party"..i.."target") == guid then
7443 bossuid = "party"..i.."target"
7444 name = DBM:GetUnitFullName("party"..i.."targettarget")
7445 uid = "party"..i.."targettarget"
7446 bossuIdCache[guid] = bossuid
7447 break
7448 end
7449 end
7450 end
7451 return name, uid, bossuid
7452 end
7453
7454 function bossModPrototype:GetBossTarget(cidOrGuid, scanOnlyBoss)
7455 local name, uid, bossuid
7456 if type(cidOrGuid) == "number" then
7457 local cidOrGuid = cidOrGuid or self.creatureId
7458 local cacheuid = bossuIdCache[cidOrGuid] or "boss1"
7459 if self:GetUnitCreatureId(cacheuid) == cidOrGuid then
7460 bossuIdCache[cidOrGuid] = cacheuid
7461 bossuIdCache[UnitGUID(cacheuid)] = cacheuid
7462 name, uid, bossuid = getBossTarget(UnitGUID(cacheuid), scanOnlyBoss)
7463 else
7464 local found = false
7465 for i, uId in ipairs(bossTargetuIds) do
7466 if self:GetUnitCreatureId(uId) == cidOrGuid then
7467 found = true
7468 bossuIdCache[cidOrGuid] = uId
7469 bossuIdCache[UnitGUID(uId)] = uId
7470 name, uid, bossuid = getBossTarget(UnitGUID(uId), scanOnlyBoss)
7471 break
7472 end
7473 end
7474 if not found and not scanOnlyBoss then
7475 if IsInRaid() then
7476 for i = 1, GetNumGroupMembers() do
7477 if self:GetUnitCreatureId("raid"..i.."target") == cidOrGuid then
7478 bossuIdCache[cidOrGuid] = "raid"..i.."target"
7479 bossuIdCache[UnitGUID("raid"..i.."target")] = "raid"..i.."target"
7480 name, uid, bossuid = getBossTarget(UnitGUID("raid"..i.."target"))
7481 break
7482 end
7483 end
7484 elseif IsInGroup() then
7485 for i = 1, GetNumSubgroupMembers() do
7486 if self:GetUnitCreatureId("party"..i.."target") == cidOrGuid then
7487 bossuIdCache[cidOrGuid] = "party"..i.."target"
7488 bossuIdCache[UnitGUID("party"..i.."target")] = "party"..i.."target"
7489 name, uid, bossuid = getBossTarget(UnitGUID("party"..i.."target"))
7490 break
7491 end
7492 end
7493 end
7494 end
7495 end
7496 else
7497 name, uid, bossuid = getBossTarget(cidOrGuid, scanOnlyBoss)
7498 end
7499 if uid then
7500 local cid = DBM:GetUnitCreatureId(uid)
7501 if cid == 24207 or cid == 80258 or cid == 87519 then--filter army of the dead/Garrison Footman (basically same thing as army)
7502 return nil, nil, nil
7503 end
7504 end
7505 return name, uid, bossuid
7506 end
7507
7508 function bossModPrototype:BossTargetScannerAbort(cidOrGuid, returnFunc)
7509 targetScanCount[cidOrGuid] = nil--Reset count for later use.
7510 self:UnscheduleMethod("BossTargetScanner", cidOrGuid, returnFunc)
7511 DBM:Debug("Boss target scan for "..cidOrGuid.." should be aborting.", 3)
7512 end
7513
7514 function bossModPrototype:BossUnitTargetScannerAbort()
7515 targetMonitor = nil
7516 DBM:Debug("Boss unit target scan should be aborting.", 3)
7517 end
7518
7519 function bossModPrototype:BossUnitTargetScanner(unitId, returnFunc, scanTime)
7520 --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.
7521 --This is fastest and most accurate method for getting the target and probably should be used where it does work 100% of time.
7522 --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
7523 --In most cases, using BossTargetScanner is probably still better, especially if boss is expected to look at target before or immediately on cast start
7524 --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?
7525 --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.
7526 local modId = self.id
7527 local scanDuration = scanTime or 1.5
7528 targetMonitor = modId.."\t"..unitId.."\t"..returnFunc
7529 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
7530 end
7531
7532 function bossModPrototype:BossTargetScanner(cidOrGuid, returnFunc, scanInterval, scanTimes, scanOnlyBoss, isEnemyScan, isFinalScan, targetFilter, tankFilter)
7533 --Increase scan count
7534 local cidOrGuid = cidOrGuid or self.creatureId
7535 if not cidOrGuid then return end
7536 if not targetScanCount[cidOrGuid] then targetScanCount[cidOrGuid] = 0 end
7537 targetScanCount[cidOrGuid] = targetScanCount[cidOrGuid] + 1
7538 --Set default values
7539 local scanInterval = scanInterval or 0.05
7540 local scanTimes = scanTimes or 16
7541 local targetname, targetuid, bossuid = self:GetBossTarget(cidOrGuid, scanOnlyBoss)
7542 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
7543 --Do scan
7544 if targetname and targetname ~= DBM_CORE_UNKNOWN and (not targetFilter or (targetFilter and targetFilter ~= targetname)) then
7545 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
7546 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.
7547 if targetScanCount[cidOrGuid] < scanTimes then--Make sure no infinite loop.
7548 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).
7549 else--Go final scan.
7550 self:BossTargetScanner(cidOrGuid, returnFunc, scanInterval, scanTimes, scanOnlyBoss, isEnemyScan, true, targetFilter, tankFilter)
7551 end
7552 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)
7553 targetScanCount[cidOrGuid] = nil--Reset count for later use.
7554 self:UnscheduleMethod("BossTargetScanner", cidOrGuid, returnFunc)--Unschedule all checks just to be sure none are running, we are done.
7555 if (tankFilter and self:IsTanking(targetuid, bossuid)) or (isFinalScan and isEnemyScan) then return end--If enemyScan and playerDetected, return nothing
7556 self[returnFunc](self, targetname, targetuid, bossuid)--Return results to warning function with all variables.
7557 end
7558 else--target was nil, lets schedule a rescan here too.
7559 if targetScanCount[cidOrGuid] < scanTimes then--Make sure not to infinite loop here as well.
7560 self:ScheduleMethod(scanInterval, "BossTargetScanner", cidOrGuid, returnFunc, scanInterval, scanTimes, scanOnlyBoss, isEnemyScan, nil, targetFilter, tankFilter)
7561 else
7562 targetScanCount[cidOrGuid] = nil--Reset count for later use.
7563 self:UnscheduleMethod("BossTargetScanner", cidOrGuid, returnFunc)--Unschedule all checks just to be sure none are running, we are done.
7564 end
7565 end
7566 end
7567
7568 --infinite scanner. so use this carefully.
7569 local function repeatedScanner(cidOrGuid, returnFunc, scanInterval, scanOnlyBoss, includeTank, mod)
7570 if repeatedScanEnabled[returnFunc] then
7571 local cidOrGuid = cidOrGuid or mod.creatureId
7572 local scanInterval = scanInterval or 0.1
7573 local targetname, targetuid, bossuid = mod:GetBossTarget(cidOrGuid, scanOnlyBoss)
7574 if targetname and (includeTank or not mod:IsTanking(targetuid, bossuid)) then
7575 mod[returnFunc](mod, targetname, targetuid, bossuid)
7576 end
7577 DBM:Schedule(scanInterval, repeatedScanner, cidOrGuid, returnFunc, scanInterval, scanOnlyBoss, includeTank, mod)
7578 end
7579 end
7580
7581 function bossModPrototype:StartRepeatedScan(cidOrGuid, returnFunc, scanInterval, scanOnlyBoss, includeTank)
7582 repeatedScanEnabled[returnFunc] = true
7583 repeatedScanner(cidOrGuid, returnFunc, scanInterval, scanOnlyBoss, includeTank, self)
7584 end
7585
7586 function bossModPrototype:StopRepeatedScan(returnFunc)
7587 repeatedScanEnabled[returnFunc] = nil
7588 end
7589end
7590
7591function bossModPrototype:CheckNearby(range, targetname)
7592 if not targetname and DBM.RangeCheck:GetDistanceAll(range) then
7593 return true--No target name means check if anyone is near self, period
7594 else
7595 local uId = DBM:GetRaidUnitId(targetname)
7596 if uId and not UnitIsUnit("player", uId) then
7597 local inRange = DBM.RangeCheck:GetDistance(uId)
7598 if inRange and inRange < range+0.5 then
7599 return true
7600 end
7601 end
7602 end
7603 return false
7604end
7605
7606do
7607 local bossCache = {}
7608 local lastTank = nil
7609
7610 function bossModPrototype:GetCurrentTank(cidOrGuid)
7611 if lastTank and GetTime() - (bossCache[cidOrGuid] or 0) < 2 then -- return last tank within 2 seconds of call
7612 return lastTank
7613 else
7614 local cidOrGuid = cidOrGuid or self.creatureId--GetBossTarget supports GUID or CID and it will automatically return correct values with EITHER ONE
7615 local uId
7616 local _, fallbackuId, mobuId = self:GetBossTarget(cidOrGuid)
7617 if mobuId then--Have a valid mob unit ID
7618 --First, use trust threat more than fallbackuId and see what we pull from it first.
7619 --This is because for GetCurrentTank we want to know who is tanking it, not who it's targeting.
7620 local unitId = (IsInRaid() and "raid") or "party"
7621 for i = 0, GetNumGroupMembers() do
7622 local id = (i == 0 and "target") or unitId..i
7623 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
7624 if tanking or (status == 3) then uId = id end--Found highest threat target, make them uId
7625 if uId then break end
7626 end
7627 --Did not get anything useful from threat, so use who the boss was looking at, at time of cast (ie fallbackuId)
7628 if fallbackuId and not uId then
7629 uId = fallbackuId
7630 end
7631 end
7632 if uId then--Now we have a valid uId
7633 bossCache[cidOrGuid] = GetTime()
7634 lastTank = UnitName(uId)
7635 return UnitName(lastTank), uId
7636 end
7637 return false
7638 end
7639 end
7640end
7641
7642--Now this function works perfectly. But have some limitation due to DBM.RangeCheck:GetDistance() function.
7643--Unfortunely, DBM.RangeCheck:GetDistance() function cannot reflects altitude difference. This makes range unreliable.
7644--So, we need to cafefully check range in difference altitude (Especially, tower top and bottom)
7645do
7646 local rangeCache = {}
7647 local rangeUpdated = {}
7648
7649 function bossModPrototype:CheckTankDistance(cidOrGuid, distance, defaultReturn)
7650 if not DBM.Options.DontShowFarWarnings then return true end--Global disable.
7651 if rangeCache[cidOrGuid] and (GetTime() - (rangeUpdated[cidOrGuid] or 0)) < 2 then -- return same range within 2 sec call
7652 if rangeCache[cidOrGuid] > distance then
7653 return false
7654 else
7655 return true
7656 end
7657 else
7658 local cidOrGuid = cidOrGuid or self.creatureId--GetBossTarget supports GUID or CID and it will automatically return correct values with EITHER ONE
7659 local distance = distance or 40
7660 local uId
7661 local _, fallbackuId, mobuId = self:GetBossTarget(cidOrGuid)
7662 if mobuId then--Have a valid mob unit ID
7663 --First, use trust threat more than fallbackuId and see what we pull from it first.
7664 --This is because for CheckTankDistance we want to know who is tanking it, not who it's targeting.
7665 local unitId = (IsInRaid() and "raid") or "party"
7666 for i = 0, GetNumGroupMembers() do
7667 local id = (i == 0 and "target") or unitId..i
7668 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
7669 if tanking or (status == 3) then uId = id end--Found highest threat target, make them uId
7670 if uId then break end
7671 end
7672 --Did not get anything useful from threat, so use who the boss was looking at, at time of cast (ie fallbackuId)
7673 if fallbackuId and not uId then
7674 uId = fallbackuId
7675 end
7676 end
7677 if uId then--Now we have a valid uId
7678 if UnitIsUnit(uId, "player") then return true end--If "player" is target, avoid doing any complicated stuff
7679 local inRange = 0
7680 if not UnitIsPlayer(uId) then
7681 local inRange2, checkedRange = UnitInRange(uId)
7682 if checkedRange then--checkedRange only returns true if api worked, so if we get false, true then we are not near npc
7683 return inRange2 and true or false
7684 else--Its probably a totem or just something we can't assess. Fall back to no filtering
7685 return true
7686 end
7687 else
7688 inRange = DBM.RangeCheck:GetDistance("player", uId)--We check how far we are from the tank who has that boss
7689 end
7690 rangeCache[cidOrGuid] = inRange
7691 rangeUpdated[cidOrGuid] = GetTime()
7692 if inRange and (inRange > distance) then--You are not near the person tanking boss
7693 return false
7694 end
7695 --Tank in range, return true.
7696 return true
7697 end
7698 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.
7699 end
7700 end
7701end
7702
7703---------------------
7704-- Class Methods --
7705---------------------
7706do
7707 --[[local specFlags ={
7708 ["Tank"] = true,
7709 ["Dps"] = true,
7710 ["Healer"] = true,
7711 ["Melee"] = true,--ANY melee, including tanks or healers that are 100% excempt from healer/ranged mechanics (like mistweaver monks)
7712 ["MeleeDps"] = true,
7713 ["Physical"] = true,
7714 ["Ranged"] = true,--ANY ranged, healer and dps included
7715 ["RangedDps"] = true,--Only ranged dps
7716 ["ManaUser"] = true,--Affected by things like mana drains, or mana detonation, etc
7717 ["SpellCaster"] = true,--Has channeled casts, can be interrupted/spell locked by roars, etc, include healers. Use CasterDps if dealing with reflect
7718 ["CasterDps"] = true,--Ranged dps that uses spells, relevant for spell reflect type abilities that only reflect spells but not ranged physical such as hunters
7719 ["RaidCooldown"] = true,
7720 ["RemovePoison"] = true,--from ally
7721 ["RemoveDisease"] = true,--from ally
7722 ["RemoveEnrage"] = true,--Unused, no one can remove enrage anymore, returning in classic/8.x!
7723 ["RemoveCurse"] = true,--from ally
7724 ["MagicDispeller"] = true--from ENEMY, not debuffs on players. use "Healer" for ally magic dispels. ALL healers can do that.
7725 ["HasInterrupt"] = true,--Has an interrupt that is 24 seconds or less CD that is BASELINE (not a talent)
7726 ["HasImmunity"] = true,--Has an immunity that can prevent or remove a spell effect (not just one that reduces damage like turtle or dispursion)
7727 }]]
7728
7729 local specRoleTable = {
7730 [62] = { --Arcane Mage
7731 ["Dps"] = true,
7732 ["Ranged"] = true,
7733 ["RangedDps"] = true,
7734 ["ManaUser"] = true,
7735 ["SpellCaster"] = true,
7736 ["CasterDps"] = true,
7737 ["MagicDispeller"] = true,
7738 ["HasInterrupt"] = true,
7739 ["HasImmunity"] = true,
7740 },
7741 [65] = { --Holy Paladin
7742 ["Healer"] = true,
7743 ["Ranged"] = true,
7744 ["ManaUser"] = true,
7745 ["SpellCaster"] = true,
7746 ["RaidCooldown"] = true,--Devotion Aura
7747 ["RemovePoison"] = true,
7748 ["RemoveDisease"] = true,
7749 ["HasImmunity"] = true,
7750 },
7751 [66] = { --Protection Paladin
7752 ["Tank"] = true,
7753 ["Melee"] = true,
7754 ["ManaUser"] = true,
7755 ["Physical"] = true,
7756 ["RemovePoison"] = true,
7757 ["RemoveDisease"] = true,
7758 ["HasInterrupt"] = true,
7759 ["HasImmunity"] = true,
7760 },
7761 [70] = { --Retribution Paladin
7762 ["Dps"] = true,
7763 ["Melee"] = true,
7764 ["MeleeDps"] = true,
7765 ["ManaUser"] = true,
7766 ["Physical"] = true,
7767 ["RemovePoison"] = true,
7768 ["RemoveDisease"] = true,
7769 ["HasInterrupt"] = true,
7770 ["HasImmunity"] = true,
7771 },
7772 [71] = { --Arms Warrior
7773 ["Dps"] = true,
7774 ["Melee"] = true,
7775 ["MeleeDps"] = true,
7776 ["RaidCooldown"] = true,--Rallying Cry
7777 ["Physical"] = true,
7778 ["HasInterrupt"] = true,
7779 },
7780 [73] = { --Protection Warrior
7781 ["Tank"] = true,
7782 ["Melee"] = true,
7783 ["Physical"] = true,
7784 ["HasInterrupt"] = true,
7785 --["RaidCooldown"] = true,--Rallying Cry (in 8.x)
7786 },
7787 [102] = { --Balance Druid
7788 ["Dps"] = true,
7789 ["Ranged"] = true,
7790 ["RangedDps"] = true,
7791 ["ManaUser"] = true,
7792 ["SpellCaster"] = true,
7793 ["CasterDps"] = true,
7794 ["RemoveCurse"] = true,
7795 ["RemovePoison"] = true,
7796 },
7797 [103] = { --Feral Druid
7798 ["Dps"] = true,
7799 ["Melee"] = true,
7800 ["MeleeDps"] = true,
7801 ["Physical"] = true,
7802 ["RemoveCurse"] = true,
7803 ["RemovePoison"] = true,
7804 ["HasInterrupt"] = true,
7805 },
7806 [104] = { --Guardian Druid
7807 ["Tank"] = true,
7808 ["Melee"] = true,
7809 ["Physical"] = true,
7810 ["RemoveCurse"] = true,
7811 ["RemovePoison"] = true,
7812 ["HasInterrupt"] = true,
7813 },
7814 [105] = { -- Restoration Druid
7815 ["Healer"] = true,
7816 ["Ranged"] = true,
7817 ["ManaUser"] = true,
7818 ["SpellCaster"] = true,
7819 ["RaidCooldown"] = true,--Tranquility
7820 ["RemoveCurse"] = true,
7821 ["RemovePoison"] = true,
7822 },
7823 [250] = { --Blood DK
7824 ["Tank"] = true,
7825 ["Melee"] = true,
7826 ["Physical"] = true,
7827 ["HasInterrupt"] = true,
7828 },
7829 [251] = { --Frost DK
7830 ["Dps"] = true,
7831 ["Melee"] = true,
7832 ["MeleeDps"] = true,
7833 ["Physical"] = true,
7834 ["HasInterrupt"] = true,
7835 },
7836 [253] = { --Beastmaster Hunter
7837 ["Dps"] = true,
7838 ["Ranged"] = true,
7839 ["RangedDps"] = true,
7840 ["Physical"] = true,
7841 ["HasInterrupt"] = true,
7842 },
7843 [255] = { --Survival Hunter (Legion)
7844 ["Dps"] = true,
7845 ["Melee"] = true,
7846 ["MeleeDps"] = true,
7847 ["Physical"] = true,
7848 ["HasInterrupt"] = true,
7849 },
7850 [256] = { --Discipline Priest
7851 ["Healer"] = true,
7852 ["Ranged"] = true,
7853 ["ManaUser"] = true,
7854 ["SpellCaster"] = true,
7855 ["CasterDps"] = true,--Iffy. Technically yes, but this can't be used to determine eligable target for dps only debuffs
7856 ["RaidCooldown"] = true,--Power Word: Barrier(Discipline) / Divine Hymn (Holy)
7857 ["RemoveDisease"] = true,
7858 ["MagicDispeller"] = true,
7859 },
7860 [258] = { --Shadow Priest
7861 ["Dps"] = true,
7862 ["Ranged"] = true,
7863 ["RangedDps"] = true,
7864 ["ManaUser"] = true,
7865 ["SpellCaster"] = true,
7866 ["CasterDps"] = true,
7867 ["MagicDispeller"] = true,
7868 },
7869 [259] = { --Assassination Rogue
7870 ["Dps"] = true,
7871 ["Melee"] = true,
7872 ["MeleeDps"] = true,
7873 ["Physical"] = true,
7874 ["HasInterrupt"] = true,
7875 ["HasImmunity"] = true,
7876 },
7877 [262] = { --Elemental Shaman
7878 ["Dps"] = true,
7879 ["Ranged"] = true,
7880 ["RangedDps"] = true,
7881 ["ManaUser"] = true,
7882 ["SpellCaster"] = true,
7883 ["CasterDps"] = true,
7884 ["RemoveCurse"] = true,
7885 ["MagicDispeller"] = true,
7886 ["HasInterrupt"] = true,
7887 },
7888 [263] = { --Enhancement Shaman
7889 ["Dps"] = true,
7890 ["Melee"] = true,
7891 ["MeleeDps"] = true,
7892 ["ManaUser"] = true,
7893 ["SpellCaster"] = true,
7894 ["Physical"] = true,
7895 ["RemoveCurse"] = true,
7896 ["MagicDispeller"] = true,
7897 ["HasInterrupt"] = true,
7898 },
7899 [264] = { --Restoration Shaman
7900 ["Healer"] = true,
7901 ["Ranged"] = true,
7902 ["ManaUser"] = true,
7903 ["SpellCaster"] = true,
7904 ["RaidCooldown"] = true,--Spirit Link Totem
7905 ["RemoveCurse"] = true,
7906 ["MagicDispeller"] = true,
7907 ["HasInterrupt"] = true,
7908 },
7909 [265] = { --Affliction Warlock
7910 ["Dps"] = true,
7911 ["Ranged"] = true,
7912 ["RangedDps"] = true,
7913 ["ManaUser"] = true,
7914 ["SpellCaster"] = true,
7915 ["CasterDps"] = true,
7916 },
7917 [268] = { --Brewmaster Monk
7918 ["Tank"] = true,
7919 ["Melee"] = true,
7920 ["Physical"] = true,
7921 ["RemovePoison"] = true,
7922 ["RemoveDisease"] = true,
7923 ["HasInterrupt"] = true,
7924 },
7925 [269] = { --Windwalker Monk
7926 ["Dps"] = true,
7927 ["Melee"] = true,
7928 ["MeleeDps"] = true,
7929 ["Physical"] = true,
7930 ["RemovePoison"] = true,
7931 ["RemoveDisease"] = true,
7932 ["HasInterrupt"] = true,
7933 },
7934 [270] = { --Mistweaver Monk
7935 ["Healer"] = true,
7936 ["Melee"] = true,
7937 ["Ranged"] = true,
7938 ["ManaUser"] = true,
7939 ["SpellCaster"] = true,
7940 ["RaidCooldown"] = true,--Revival
7941 ["RemovePoison"] = true,
7942 ["RemoveDisease"] = true,
7943 },
7944 [577] = { --Havok Demon Hunter
7945 ["Dps"] = true,
7946 ["Melee"] = true,
7947 ["MeleeDps"] = true,
7948 ["Physical"] = true,
7949 ["HasInterrupt"] = true,
7950 },
7951 [581] = { --Vengeance Demon Hunter
7952 ["Tank"] = true,
7953 ["Melee"] = true,
7954 ["Physical"] = true,
7955 ["HasInterrupt"] = true,
7956 },
7957 }
7958 specRoleTable[63] = specRoleTable[62]--Frost Mage same as arcane
7959 specRoleTable[64] = specRoleTable[62]--Fire Mage same as arcane
7960 specRoleTable[72] = specRoleTable[71]--Fury Warrior same as Arms
7961 specRoleTable[252] = specRoleTable[251]--Unholy DK same as frost
7962 specRoleTable[254] = specRoleTable[253]--Markmanship Hunter same as beast
7963 specRoleTable[257] = specRoleTable[256]--Holy Priest same as disc
7964 specRoleTable[260] = specRoleTable[259]--Combat Rogue same as Assassination
7965 specRoleTable[261] = specRoleTable[259]--Subtlety Rogue same as Assassination
7966 specRoleTable[266] = specRoleTable[265]--Demonology Warlock same as Affliction
7967 specRoleTable[267] = specRoleTable[265]--Destruction Warlock same as Affliction
7968
7969 --[[function bossModPrototype:GetRoleFlagValue(flag)
7970 if not flag then return false end
7971 local flags = {strsplit("|", flag)}
7972 for i = 1, #flags do
7973 local flagText = flags[i]
7974 flagText = flagText:gsub("-", "")
7975 if not specFlags[flagText] then
7976 print("bad flag found : "..flagText)
7977 end
7978 end
7979 self:GetRoleFlagValue2(flag)
7980 end]]
7981
7982 --to check flag is correct, remove comment block specFlags table and GetRoleFlagValue function, change this to GetRoleFlagValue2
7983 --disable flag check normally because double flag check comsumes more cpu on mod load.
7984 function bossModPrototype:GetRoleFlagValue(flag)
7985 if not flag then return false end
7986 if not currentSpecID then
7987 DBM:SetCurrentSpecInfo()
7988 end
7989 local flags = {strsplit("|", flag)}
7990 for i = 1, #flags do
7991 local flagText = flags[i]
7992 if flagText:match("^-") then
7993 flagText = flagText:gsub("-", "")
7994 if not specRoleTable[currentSpecID][flagText] then
7995 return true
7996 end
7997 else
7998 if specRoleTable[currentSpecID][flagText] then
7999 return true
8000 end
8001 end
8002 end
8003 return false
8004 end
8005
8006 function bossModPrototype:IsMeleeDps(uId)
8007 if uId then--This version includes ONLY melee dps
8008 local role = UnitGroupRolesAssigned(uId)
8009 if role == "HEALER" or role == "TANK" then--Auto filter healer from dps check
8010 return false
8011 end
8012 local _, class = UnitClass(uId)
8013 if class == "WARRIOR" or class == "ROGUE" or class == "DEATHKNIGHT" then
8014 return true
8015 end
8016 --Inspect throttle exists, so have to do it this way
8017 if class == "DRUID" or class == "SHAMAN" or class == "PALADIN" or class == "MONK" or class == "HUNTER" then
8018 local unitMaxPower = UnitPowerMax(uId)
8019 --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
8020 --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
8021 if (unitMaxPower < 101 and class == "HUNTER") or (unitMaxPower >= 150 and class == "SHAMAN" and unitMaxPower < 35000) or unitMaxPower < 35000 then
8022 return true
8023 end
8024 end
8025 return false
8026 end
8027 if not currentSpecID then
8028 DBM:SetCurrentSpecInfo()
8029 end
8030 if specRoleTable[currentSpecID]["MeleeDps"] then
8031 return true
8032 else
8033 return false
8034 end
8035 end
8036
8037 function bossModPrototype:IsMelee(uId)
8038 if uId then--This version includes monk healers as melee and tanks as melee
8039 local _, class = UnitClass(uId)
8040 if class == "WARRIOR" or class == "ROGUE" or class == "DEATHKNIGHT" or class == "MONK" then
8041 return true
8042 end
8043 --Inspect throttle exists, so have to do it this way
8044 if class == "DRUID" or class == "SHAMAN" or class == "PALADIN" then
8045 if UnitPowerMax(uId) < 35000 then
8046 return true
8047 end
8048 end
8049 return false
8050 end
8051 if not currentSpecID then
8052 DBM:SetCurrentSpecInfo()
8053 end
8054 if specRoleTable[currentSpecID]["Melee"] then
8055 return true
8056 else
8057 return false
8058 end
8059 end
8060
8061 function bossModPrototype:IsRanged()
8062 if not currentSpecID then
8063 DBM:SetCurrentSpecInfo()
8064 end
8065 if specRoleTable[currentSpecID]["Ranged"] then
8066 return true
8067 else
8068 return false
8069 end
8070 end
8071
8072 function bossModPrototype:IsSpellCaster()
8073 if not currentSpecID then
8074 DBM:SetCurrentSpecInfo()
8075 end
8076 if specRoleTable[currentSpecID]["SpellCaster"] then
8077 return true
8078 else
8079 return false
8080 end
8081 end
8082
8083 function bossModPrototype:IsMagicDispeller()
8084 if not currentSpecID then
8085 DBM:SetCurrentSpecInfo()
8086 end
8087 if specRoleTable[currentSpecID]["MagicDispeller"] then
8088 return true
8089 else
8090 return false
8091 end
8092 end
8093end
8094
8095function bossModPrototype:UnitClass(uId)
8096 if uId then--Return unit requested
8097 local _, class = UnitClass(uId)
8098 return class
8099 end
8100 return playerClass--else return "player"
8101end
8102
8103function bossModPrototype:IsTank()
8104 --IsTanking already handles external calls, no need here.
8105 if not currentSpecID then
8106 DBM:SetCurrentSpecInfo()
8107 end
8108 local _, _, _, _, role = GetSpecializationInfoByID(currentSpecID)
8109 if role == "TANK" then
8110 return true
8111 else
8112 return false
8113 end
8114end
8115
8116function bossModPrototype:IsDps(uId)
8117 if uId then--External unit call.
8118 if UnitGroupRolesAssigned(uId) == "DAMAGER" then
8119 return true
8120 end
8121 return false
8122 end
8123 if not currentSpecID then
8124 DBM:SetCurrentSpecInfo()
8125 end
8126 local _, _, _, _, role = GetSpecializationInfoByID(currentSpecID)
8127 if role == "DAMAGER" then
8128 return true
8129 else
8130 return false
8131 end
8132end
8133
8134function bossModPrototype:IsHealer(uId)
8135 if uId then--External unit call.
8136 if UnitGroupRolesAssigned(uId) == "HEALER" then
8137 return true
8138 end
8139 return false
8140 end
8141 if not currentSpecID then
8142 DBM:SetCurrentSpecInfo()
8143 end
8144 local _, _, _, _, role = GetSpecializationInfoByID(currentSpecID)
8145 if role == "HEALER" then
8146 return true
8147 else
8148 return false
8149 end
8150end
8151
8152function bossModPrototype:IsTanking(unit, boss, isName, onlyRequested)
8153 if isName then--Passed combat log name, so pull unit ID
8154 unit = DBM:GetRaidUnitId(unit)
8155 end
8156 if not unit then
8157 DBM:Debug("IsTanking passed with invalid unit", 2)
8158 return false
8159 end
8160 --Prefer threat target first
8161 if boss then--Only checking one bossID as requested
8162 local tanking, status = UnitDetailedThreatSituation(unit, boss)
8163 if tanking or (status == 3) then
8164 return true
8165 end
8166 else--Check all of them if one isn't defined
8167 for i = 1, 5 do
8168 --if UnitExists("boss"..i) then
8169 local tanking, status = UnitDetailedThreatSituation(unit, "boss"..i)
8170 if tanking or (status == 3) then
8171 return true
8172 end
8173 --end
8174 end
8175 end
8176 if not onlyRequested then
8177 --Use these as fallback if threat target not found
8178 if GetPartyAssignment("MAINTANK", unit, 1) then
8179 return true
8180 end
8181 if UnitGroupRolesAssigned(unit) == "TANK" then
8182 return true
8183 end
8184 end
8185 return false
8186end
8187
8188function bossModPrototype:GetNumAliveTanks()
8189 if not IsInGroup() then return 1 end--Solo raid, you're the "tank"
8190 local count = 0
8191 local uId = (IsInRaid() and "raid") or "party"
8192 for i = 1, DBM:GetNumRealGroupMembers() do
8193 if UnitGroupRolesAssigned(uId..i) == "TANK" and not UnitIsDeadOrGhost(uId..i) then
8194 count = count + 1
8195 end
8196 end
8197 return count
8198end
8199
8200----------------------------
8201-- Boss Health Function --
8202----------------------------
8203function DBM:GetBossHP(cId)
8204 local uId = bossHealthuIdCache[cId] or "target"
8205 if self:GetCIDFromGUID(UnitGUID(uId)) == cId and UnitHealthMax(uId) ~= 0 then
8206 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.
8207 local hp = UnitHealth(uId) / UnitHealthMax(uId) * 100
8208 bossHealth[cId] = hp
8209 return hp, uId, UnitName(uId)
8210 elseif self:GetCIDFromGUID(UnitGUID("focus")) == cId and UnitHealthMax("focus") ~= 0 then
8211 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.
8212 local hp = UnitHealth("focus") / UnitHealthMax("focus") * 100
8213 bossHealth[cId] = hp
8214 return hp, "focus", UnitName("focus")
8215 else
8216 for i = 1, 5 do
8217 local unitID = "boss"..i
8218 local guid = UnitGUID(unitID)
8219 if self:GetCIDFromGUID(guid) == cId and UnitHealthMax(unitID) ~= 0 then
8220 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.
8221 local hp = UnitHealth(unitID) / UnitHealthMax(unitID) * 100
8222 bossHealth[cId] = hp
8223 bossHealthuIdCache[cId] = unitID
8224 return hp, unitID, UnitName(unitID)
8225 end
8226 end
8227 local idType = (IsInRaid() and "raid") or "party"
8228 for i = 0, GetNumGroupMembers() do
8229 local unitId = ((i == 0) and "target") or idType..i.."target"
8230 local guid = UnitGUID(unitId)
8231 if self:GetCIDFromGUID(guid) == cId and UnitHealthMax(unitId) ~= 0 then
8232 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.
8233 local hp = UnitHealth(unitId) / UnitHealthMax(unitId) * 100
8234 bossHealth[cId] = hp
8235 bossHealthuIdCache[cId] = unitId
8236 return hp, unitId, UnitName(unitId)
8237 end
8238 end
8239 end
8240 return nil
8241end
8242
8243function DBM:GetBossHPByGUID(guid)
8244 local uId = bossHealthuIdCache[guid] or "target"
8245 if UnitGUID(uId) == guid and UnitHealthMax(uId) ~= 0 then
8246 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.
8247 local hp = UnitHealth(uId) / UnitHealthMax(uId) * 100
8248 bossHealth[guid] = hp
8249 return hp, uId, UnitName(uId)
8250 elseif UnitGUID("focus") == guid and UnitHealthMax("focus") ~= 0 then
8251 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.
8252 local hp = UnitHealth("focus") / UnitHealthMax("focus") * 100
8253 bossHealth[guid] = hp
8254 return hp, "focus", UnitName("focus")
8255 else
8256 for i = 1, 5 do
8257 local unitID = "boss"..i
8258 local guid2 = UnitGUID(unitID)
8259 if guid == guid2 and UnitHealthMax(unitID) ~= 0 then
8260 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.
8261 local hp = UnitHealth(unitID) / UnitHealthMax(unitID) * 100
8262 bossHealth[guid] = hp
8263 bossHealthuIdCache[guid] = unitID
8264 return hp, unitID, UnitName(unitID)
8265 end
8266 end
8267 local idType = (IsInRaid() and "raid") or "party"
8268 for i = 0, GetNumGroupMembers() do
8269 local unitId = ((i == 0) and "target") or idType..i.."target"
8270 local guid2 = UnitGUID(unitId)
8271 if guid == guid2 and UnitHealthMax(unitId) ~= 0 then
8272 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.
8273 local hp = UnitHealth(unitId) / UnitHealthMax(unitId) * 100
8274 bossHealth[guid] = hp
8275 bossHealthuIdCache[guid] = unitId
8276 return hp, unitId, UnitName(unitId)
8277 end
8278 end
8279 end
8280 return nil
8281end
8282
8283function DBM:GetBossHPByUnitID(uId)
8284 if UnitHealthMax(uId) ~= 0 then
8285 local hp = UnitHealth(uId) / UnitHealthMax(uId) * 100
8286 bossHealth[uId] = hp
8287 return hp, uId, UnitName(uId)
8288 end
8289 return nil
8290end
8291
8292function bossModPrototype:SetMainBossID(cid)
8293 self.mainBoss = cid
8294end
8295
8296function bossModPrototype:SetBossHPInfoToHighest(numBoss)
8297 if numBoss ~= false then
8298 self.numBoss = numBoss or (self.multiMobPullDetection and #self.multiMobPullDetection)
8299 end
8300 self.highesthealth = true
8301end
8302
8303function bossModPrototype:GetHighestBossHealth()
8304 local hp
8305 if not self.multiMobPullDetection or self.mainBoss then
8306 hp = bossHealth[self.mainBoss or self.combatInfo.mob or -1]
8307 if hp and (hp > 100 or hp <= 0) then
8308 hp = nil
8309 end
8310 else
8311 for _, mob in ipairs(self.multiMobPullDetection) do
8312 if (bossHealth[mob] or 0) > (hp or 0) and (bossHealth[mob] or 0) < 100 then-- ignore full health.
8313 hp = bossHealth[mob]
8314 end
8315 end
8316 end
8317 return hp
8318end
8319
8320function bossModPrototype:GetLowestBossHealth()
8321 local hp
8322 if not self.multiMobPullDetection or self.mainBoss then
8323 hp = bossHealth[self.mainBoss or self.combatInfo.mob or -1]
8324 if hp and (hp > 100 or hp <= 0) then
8325 hp = nil
8326 end
8327 else
8328 for _, mob in ipairs(self.multiMobPullDetection) do
8329 if (bossHealth[mob] or 100) < (hp or 100) and (bossHealth[mob] or 100) > 0 then-- ignore zero health.
8330 hp = bossHealth[mob]
8331 end
8332 end
8333 end
8334 return hp
8335end
8336
8337bossModPrototype.GetBossHP = DBM.GetBossHP
8338bossModPrototype.GetBossHPByGUID = DBM.GetBossHPByGUID
8339
8340-----------------------
8341-- Announce Object --
8342-----------------------
8343do
8344 local frame = CreateFrame("Frame", "DBMWarning", UIParent)
8345 local font1u = CreateFrame("Frame", "DBMWarning1Updater", UIParent)
8346 local font2u = CreateFrame("Frame", "DBMWarning2Updater", UIParent)
8347 local font3u = CreateFrame("Frame", "DBMWarning3Updater", UIParent)
8348 local font1 = frame:CreateFontString("DBMWarning1", "OVERLAY", "GameFontNormal")
8349 font1:SetWidth(1024)
8350 font1:SetHeight(0)
8351 font1:SetPoint("TOP", 0, 0)
8352 local font2 = frame:CreateFontString("DBMWarning2", "OVERLAY", "GameFontNormal")
8353 font2:SetWidth(1024)
8354 font2:SetHeight(0)
8355 font2:SetPoint("TOP", font1, "BOTTOM", 0, 0)
8356 local font3 = frame:CreateFontString("DBMWarning3", "OVERLAY", "GameFontNormal")
8357 font3:SetWidth(1024)
8358 font3:SetHeight(0)
8359 font3:SetPoint("TOP", font2, "BOTTOM", 0, 0)
8360 frame:SetMovable(1)
8361 frame:SetWidth(1)
8362 frame:SetHeight(1)
8363 frame:SetFrameStrata("HIGH")
8364 frame:SetClampedToScreen()
8365 frame:SetPoint("CENTER", UIParent, "CENTER", 0, 300)
8366 font1u:Hide()
8367 font2u:Hide()
8368 font3u:Hide()
8369
8370 local font1elapsed, font2elapsed, font3elapsed, moving
8371
8372 local function fontHide1()
8373 local duration = DBM.Options.WarningDuration2
8374 if font1elapsed > duration * 1.3 then
8375 font1u:Hide()
8376 font1:Hide()
8377 if frame.font1ticker then
8378 frame.font1ticker:Cancel()
8379 frame.font1ticker = nil
8380 end
8381 elseif font1elapsed > duration then
8382 font1elapsed = font1elapsed + 0.05
8383 local alpha = 1 - (font1elapsed - duration) / (duration * 0.3)
8384 font1:SetAlpha(alpha > 0 and alpha or 0)
8385 else
8386 font1elapsed = font1elapsed + 0.05
8387 font1:SetAlpha(1)
8388 end
8389 end
8390
8391 local function fontHide2()
8392 local duration = DBM.Options.WarningDuration2
8393 if font2elapsed > duration * 1.3 then
8394 font2u:Hide()
8395 font2:Hide()
8396 if frame.font2ticker then
8397 frame.font2ticker:Cancel()
8398 frame.font2ticker = nil
8399 end
8400 elseif font2elapsed > duration then
8401 font2elapsed = font2elapsed + 0.05
8402 local alpha = 1 - (font2elapsed - duration) / (duration * 0.3)
8403 font2:SetAlpha(alpha > 0 and alpha or 0)
8404 else
8405 font2elapsed = font2elapsed + 0.05
8406 font2:SetAlpha(1)
8407 end
8408 end
8409
8410 local function fontHide3()
8411 local duration = DBM.Options.WarningDuration2
8412 if font3elapsed > duration * 1.3 then
8413 font3u:Hide()
8414 font3:Hide()
8415 if frame.font3ticker then
8416 frame.font3ticker:Cancel()
8417 frame.font3ticker = nil
8418 end
8419 elseif font3elapsed > duration then
8420 font3elapsed = font3elapsed + 0.05
8421 local alpha = 1 - (font3elapsed - duration) / (duration * 0.3)
8422 font3:SetAlpha(alpha > 0 and alpha or 0)
8423 else
8424 font3elapsed = font3elapsed + 0.05
8425 font3:SetAlpha(1)
8426 end
8427 end
8428
8429 font1u:SetScript("OnUpdate", function(self)
8430 local diff = GetTime() - font1.lastUpdate
8431 local origSize = DBM.Options.WarningFontSize
8432 if diff > 0.4 then
8433 font1:SetTextHeight(origSize)
8434 self:Hide()
8435 elseif diff > 0.2 then
8436 font1:SetTextHeight(origSize * (1.5 - (diff-0.2) * 2.5))
8437 else
8438 font1:SetTextHeight(origSize * (1 + diff * 2.5))
8439 end
8440 end)
8441
8442 font2u:SetScript("OnUpdate", function(self)
8443 local diff = GetTime() - font2.lastUpdate
8444 local origSize = DBM.Options.WarningFontSize
8445 if diff > 0.4 then
8446 font2:SetTextHeight(origSize)
8447 self:Hide()
8448 elseif diff > 0.2 then
8449 font2:SetTextHeight(origSize * (1.5 - (diff-0.2) * 2.5))
8450 else
8451 font2:SetTextHeight(origSize * (1 + diff * 2.5))
8452 end
8453 end)
8454
8455 font3u:SetScript("OnUpdate", function(self)
8456 local diff = GetTime() - font3.lastUpdate
8457 local origSize = DBM.Options.WarningFontSize
8458 if diff > 0.4 then
8459 font3:SetTextHeight(origSize)
8460 self:Hide()
8461 elseif diff > 0.2 then
8462 font3:SetTextHeight(origSize * (1.5 - (diff-0.2) * 2.5))
8463 else
8464 font3:SetTextHeight(origSize * (1 + diff * 2.5))
8465 end
8466 end)
8467
8468 function DBM:UpdateWarningOptions()
8469 frame:ClearAllPoints()
8470 frame:SetPoint(self.Options.WarningPoint, UIParent, self.Options.WarningPoint, self.Options.WarningX, self.Options.WarningY)
8471 font1:SetFont(self.Options.WarningFont, self.Options.WarningFontSize, self.Options.WarningFontStyle == "None" and nil or self.Options.WarningFontStyle)
8472 font2:SetFont(self.Options.WarningFont, self.Options.WarningFontSize, self.Options.WarningFontStyle == "None" and nil or self.Options.WarningFontStyle)
8473 font3:SetFont(self.Options.WarningFont, self.Options.WarningFontSize, self.Options.WarningFontStyle == "None" and nil or self.Options.WarningFontStyle)
8474 if self.Options.WarningFontShadow then
8475 font1:SetShadowOffset(1, -1)
8476 font2:SetShadowOffset(1, -1)
8477 font3:SetShadowOffset(1, -1)
8478 else
8479 font1:SetShadowOffset(0, 0)
8480 font2:SetShadowOffset(0, 0)
8481 font3:SetShadowOffset(0, 0)
8482 end
8483 end
8484
8485 function DBM:AddWarning(text, force)
8486 local added = false
8487 if not frame.font1ticker then
8488 font1elapsed = 0
8489 font1.lastUpdate = GetTime()
8490 font1:SetText(text)
8491 font1:Show()
8492 font1u:Show()
8493 added = true
8494 frame.font1ticker = frame.font1ticker or C_TimerNewTicker(0.05, fontHide1)
8495 elseif not frame.font2ticker then
8496 font2elapsed = 0
8497 font2.lastUpdate = GetTime()
8498 font2:SetText(text)
8499 font2:Show()
8500 font2u:Show()
8501 added = true
8502 frame.font2ticker = frame.font2ticker or C_TimerNewTicker(0.05, fontHide2)
8503 elseif not frame.font3ticker or force then
8504 font3elapsed = 0
8505 font3.lastUpdate = GetTime()
8506 font3:SetText(text)
8507 font3:Show()
8508 font3u:Show()
8509 fontHide3()
8510 added = true
8511 frame.font3ticker = frame.font3ticker or C_TimerNewTicker(0.05, fontHide3)
8512 end
8513 if not added then
8514 local prevText1 = font2:GetText()
8515 local prevText2 = font3:GetText()
8516 font1:SetText(prevText1)
8517 font1elapsed = font2elapsed
8518 font2:SetText(prevText2)
8519 font2elapsed = font3elapsed
8520 self:AddWarning(text, true)
8521 end
8522 end
8523
8524 do
8525 local anchorFrame
8526 local function moveEnd(self)
8527 moving = false
8528 anchorFrame:Hide()
8529 if anchorFrame.ticker then
8530 anchorFrame.ticker:Cancel()
8531 anchorFrame.ticker = nil
8532 end
8533 font1elapsed = self.Options.WarningDuration2
8534 font2elapsed = self.Options.WarningDuration2
8535 font3elapsed = self.Options.WarningDuration2
8536 frame:SetFrameStrata("HIGH")
8537 self:Unschedule(moveEnd)
8538 self.Bars:CancelBar(DBM_CORE_MOVE_WARNING_BAR)
8539 end
8540
8541 function DBM:MoveWarning()
8542 if not anchorFrame then
8543 anchorFrame = CreateFrame("Frame", nil, frame)
8544 anchorFrame:SetWidth(32)
8545 anchorFrame:SetHeight(32)
8546 anchorFrame:EnableMouse(true)
8547 anchorFrame:SetPoint("TOP", frame, "TOP", 0, 32)
8548 anchorFrame:RegisterForDrag("LeftButton")
8549 anchorFrame:SetClampedToScreen()
8550 anchorFrame:Hide()
8551 local texture = anchorFrame:CreateTexture()
8552 texture:SetTexture("Interface\\Addons\\DBM-GUI\\textures\\dot.blp")
8553 texture:SetPoint("CENTER", anchorFrame, "CENTER", 0, 0)
8554 texture:SetWidth(32)
8555 texture:SetHeight(32)
8556 anchorFrame:SetScript("OnDragStart", function()
8557 frame:StartMoving()
8558 self:Unschedule(moveEnd)
8559 self.Bars:CancelBar(DBM_CORE_MOVE_WARNING_BAR)
8560 end)
8561 anchorFrame:SetScript("OnDragStop", function()
8562 frame:StopMovingOrSizing()
8563 local point, _, _, xOfs, yOfs = frame:GetPoint(1)
8564 self.Options.WarningPoint = point
8565 self.Options.WarningX = xOfs
8566 self.Options.WarningY = yOfs
8567 self:Schedule(15, moveEnd, self)
8568 self.Bars:CreateBar(15, DBM_CORE_MOVE_WARNING_BAR)
8569 end)
8570 end
8571 if anchorFrame:IsShown() then
8572 moveEnd(self)
8573 else
8574 moving = true
8575 anchorFrame:Show()
8576 anchorFrame.ticker = anchorFrame.ticker or C_TimerNewTicker(5, function() self:AddWarning(DBM_CORE_MOVE_WARNING_MESSAGE) end)
8577 self:AddWarning(DBM_CORE_MOVE_WARNING_MESSAGE)
8578 self:Schedule(15, moveEnd, self)
8579 self.Bars:CreateBar(15, DBM_CORE_MOVE_WARNING_BAR)
8580 frame:Show()
8581 frame:SetFrameStrata("TOOLTIP")
8582 frame:SetAlpha(1)
8583 end
8584 end
8585 end
8586
8587 local textureCode = " |T%s:12:12|t "
8588 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?
8589 local announcePrototype = {}
8590 local mt = {__index = announcePrototype}
8591
8592 -- TODO: is there a good reason that this is a weak table?
8593 local cachedColorFunctions = setmetatable({}, {__mode = "kv"})
8594
8595 local function setText(announceType, spellId, castTime, preWarnTime)
8596 local spellName
8597 if type(spellId) == "string" and spellId:match("ej%d+") then
8598 spellId = string.sub(spellId, 3)
8599 spellName = DBM:EJ_GetSectionInfo(spellId) or DBM_CORE_UNKNOWN
8600 else
8601 spellName = DBM:GetSpellInfo(spellId) or DBM_CORE_UNKNOWN
8602 end
8603 local text
8604 if announceType == "cast" then
8605 local spellHaste = select(4, DBM:GetSpellInfo(53142)) / 10000 -- 53142 = Dalaran Portal, should have 10000 ms cast time
8606 local timer = (select(4, DBM:GetSpellInfo(spellId)) or 1000) / spellHaste
8607 text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName, castTime or (timer / 1000))
8608 elseif announceType == "prewarn" then
8609 if type(preWarnTime) == "string" then
8610 text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName, preWarnTime)
8611 else
8612 text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName, DBM_CORE_SEC_FMT:format(tostring(preWarnTime or 5)))
8613 end
8614 elseif announceType == "stage" or announceType == "prestage" then
8615 text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(tostring(spellId))
8616 elseif announceType == "stagechange" then
8617 text = DBM_CORE_AUTO_ANNOUNCE_TEXTS.spell
8618 else
8619 text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName)
8620 end
8621 return text, spellName
8622 end
8623
8624 -- TODO: this function is an abomination, it needs to be rewritten. Also: check if these work-arounds are still necessary
8625 function announcePrototype:Show(...) -- todo: reduce amount of unneeded strings
8626 if not self.option or self.mod.Options[self.option] then
8627 if DBM.Options.DontShowBossAnnounces then return end -- don't show the announces if the spam filter option is set
8628 if DBM.Options.DontShowTargetAnnouncements and (self.announceType == "target" or self.announceType == "targetcount") and not self.noFilter then return end--don't show announces that are generic target announces
8629 local argTable = {...}
8630 local colorCode = ("|cff%.2x%.2x%.2x"):format(self.color.r * 255, self.color.g * 255, self.color.b * 255)
8631 if #self.combinedtext > 0 then
8632 --Throttle spam.
8633 if DBM.Options.WarningAlphabetical then
8634 tsort(self.combinedtext)
8635 end
8636 local combinedText = tconcat(self.combinedtext, "<, >")
8637 if self.combinedcount == 1 then
8638 combinedText = combinedText.." "..DBM_CORE_GENERIC_WARNING_OTHERS
8639 elseif self.combinedcount > 1 then
8640 combinedText = combinedText.." "..DBM_CORE_GENERIC_WARNING_OTHERS2:format(self.combinedcount)
8641 end
8642 --Process
8643 for i = 1, #argTable do
8644 if type(argTable[i]) == "string" then
8645 argTable[i] = combinedText
8646 end
8647 end
8648 end
8649 local message = pformat(self.text, unpack(argTable))
8650 local text = ("%s%s%s|r%s"):format(
8651 (DBM.Options.WarningIconLeft and self.icon and textureCode:format(self.icon)) or "",
8652 colorCode,
8653 message,
8654 (DBM.Options.WarningIconRight and self.icon and textureCode:format(self.icon)) or ""
8655 )
8656 self.combinedcount = 0
8657 self.combinedtext = {}
8658 if not cachedColorFunctions[self.color] then
8659 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)
8660 cachedColorFunctions[color] = function(cap)
8661 cap = cap:sub(2, -2)
8662 local noStrip = cap:match("noStrip ")
8663 if not noStrip then
8664 local name = cap
8665 if DBM.Options.StripServerName then
8666 cap = Ambiguate(cap, "short")
8667 end
8668 local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(name)] or color
8669 if playerColor then
8670 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)
8671 end
8672 else
8673 cap = cap:sub(9)
8674 end
8675 return cap
8676 end
8677 end
8678 text = text:gsub(">.-<", cachedColorFunctions[self.color])
8679 DBM:AddWarning(text)
8680 if DBM.Options.ShowWarningsInChat then
8681 if not DBM.Options.WarningIconChat then
8682 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?
8683 end
8684 self.mod:AddMsg(text, nil)
8685 end
8686 if self.sound > 0 then
8687 if self.sound > 1 and DBM.Options.ChosenVoicePack ~= "None" and self.sound <= SWFilterDisabed then return end
8688 if not self.option or self.mod.Options[self.option.."SWSound"] ~= "None" then
8689 DBM:PlaySoundFile(DBM.Options.RaidWarningSound)
8690 end
8691 end
8692 --Message: Full message text
8693 --Icon: Texture path for icon
8694 --Type: Announce type
8695 --SpellId: Raw spell or encounter journal Id if available.
8696 --Mod ID: Encounter ID as string, or a generic string for mods that don't have encounter ID (such as trash, dummy/test mods)
8697 fireEvent("DBM_Announce", message, self.icon, self.type, self.spellId, self.mod.id)
8698 else
8699 self.combinedcount = 0
8700 self.combinedtext = {}
8701 end
8702 end
8703
8704 function announcePrototype:CombinedShow(delay, ...)
8705 if self.option and not self.mod.Options[self.option] then return end
8706 if DBM.Options.DontShowBossAnnounces then return end -- don't show the announces if the spam filter option is set
8707 if DBM.Options.DontShowTargetAnnouncements and (self.announceType == "target" or self.announceType == "targetcount") and not self.noFilter then return end--don't show announces that are generic target announces
8708 local argTable = {...}
8709 for i = 1, #argTable do
8710 if type(argTable[i]) == "string" then
8711 if #self.combinedtext < 8 then--Throttle spam. We may not need more than 9 targets..
8712 if not checkEntry(self.combinedtext, argTable[i]) then
8713 self.combinedtext[#self.combinedtext + 1] = argTable[i]
8714 end
8715 else
8716 self.combinedcount = self.combinedcount + 1
8717 end
8718 end
8719 end
8720 unschedule(self.Show, self.mod, self)
8721 schedule(delay or 0.5, self.Show, self.mod, self, ...)
8722 end
8723
8724 function announcePrototype:Schedule(t, ...)
8725 return schedule(t, self.Show, self.mod, self, ...)
8726 end
8727
8728 function announcePrototype:Countdown(time, numAnnounces, ...)
8729 scheduleCountdown(time, numAnnounces, self.Show, self.mod, self, ...)
8730 end
8731
8732 function announcePrototype:Cancel(...)
8733 return unschedule(self.Show, self.mod, self, ...)
8734 end
8735
8736 function announcePrototype:Play(name, customPath)
8737 local voice = DBM.Options.ChosenVoicePack
8738 if voice == "None" then return end
8739 local always = DBM.Options.AlwaysPlayVoice
8740 if DBM.Options.DontShowTargetAnnouncements and (self.announceType == "target" or self.announceType == "targetcount") and not self.noFilter and not always then return end--don't show announces that are generic target announces
8741 if not DBM.Options.DontShowBossAnnounces and (not self.option or self.mod.Options[self.option]) or always then
8742 --Filter tank specific voice alerts for non tanks if tank filter enabled
8743 --But still allow AlwaysPlayVoice to play as well.
8744 if (name == "changemt" or name == "tauntboss") and DBM.Options.FilterTankSpec and not self.mod:IsTank() and not always then return end
8745 local path = customPath or "Interface\\AddOns\\DBM-VP"..voice.."\\"..name..".ogg"
8746 DBM:PlaySoundFile(path)
8747 end
8748 end
8749
8750 function announcePrototype:ScheduleVoice(t, ...)
8751 if DBM.Options.ChosenVoicePack == "None" then return end
8752 return schedule(t, self.Play, self.mod, self, ...)
8753 end
8754
8755 function announcePrototype:CancelVoice(...)
8756 if DBM.Options.ChosenVoicePack == "None" then return end
8757 return unschedule(self.Play, self.mod, self, ...)
8758 end
8759
8760 -- old constructor (no auto-localize)
8761 function bossModPrototype:NewAnnounce(text, color, icon, optionDefault, optionName, soundOption)
8762 if not text then
8763 error("NewAnnounce: you must provide announce text", 2)
8764 return
8765 end
8766 if type(optionName) == "number" then
8767 DBM:Debug("Non auto localized optionNames cannot be numbers, fix this for "..text)
8768 optionName = nil
8769 end
8770 if soundOption and type(soundOption) == "boolean" then
8771 soundOption = 0--No Sound
8772 end
8773 local obj = setmetatable(
8774 {
8775 text = self.localization.warnings[text],
8776 combinedtext = {},
8777 combinedcount = 0,
8778 color = DBM.Options.WarningColors[color or 1] or DBM.Options.WarningColors[1],
8779 sound = soundOption or 1,
8780 mod = self,
8781 icon = (type(icon) == "string" and icon:match("ej%d+") and select(4, DBM:EJ_GetSectionInfo(string.sub(icon, 3))) ~= "" and select(4, DBM:EJ_GetSectionInfo(string.sub(icon, 3)))) or (type(icon) == "number" and GetSpellTexture(icon)) or icon or "Interface\\Icons\\Spell_Nature_WispSplode",
8782 },
8783 mt
8784 )
8785 if optionName then
8786 obj.option = optionName
8787 self:AddBoolOption(obj.option, optionDefault, "announce")
8788 elseif not (optionName == false) then
8789 obj.option = text
8790 self:AddBoolOption(obj.option, optionDefault, "announce")
8791 end
8792 tinsert(self.announces, obj)
8793 return obj
8794 end
8795
8796 -- new constructor (partially auto-localized warnings and options, yay!)
8797 local function newAnnounce(self, announceType, spellId, color, icon, optionDefault, optionName, castTime, preWarnTime, soundOption, noFilter)
8798 if not spellId then
8799 error("newAnnounce: you must provide spellId", 2)
8800 return
8801 end
8802 local optionVersion
8803 if type(optionName) == "number" then
8804 optionVersion = optionName
8805 optionName = nil
8806 end
8807 if soundOption and type(soundOption) == "boolean" then
8808 soundOption = 0--No Sound
8809 end
8810 if type(spellId) == "string" and spellId:match("OptionVersion") then
8811 print("newAnnounce for "..color.." is using OptionVersion hack. this is depricated")
8812 return
8813 end
8814 local text, spellName = setText(announceType, spellId, icon, castTime, preWarnTime)
8815 icon = icon or spellId
8816 local obj = setmetatable( -- todo: fix duplicate code
8817 {
8818 text = text,
8819 combinedtext = {},
8820 combinedcount = 0,
8821 announceType = announceType,
8822 color = DBM.Options.WarningColors[color or 1] or DBM.Options.WarningColors[1],
8823 mod = self,
8824 icon = (type(icon) == "string" and icon:match("ej%d+") and select(4, DBM:EJ_GetSectionInfo(string.sub(icon, 3))) ~= "" and select(4, DBM:EJ_GetSectionInfo(string.sub(icon, 3)))) or (type(icon) == "number" and GetSpellTexture(icon)) or icon or "Interface\\Icons\\Spell_Nature_WispSplode",
8825 sound = soundOption or 1,
8826 type = announceType,
8827 spellId = spellId,
8828 spellName = spellName,
8829 noFilter = noFilter,
8830 castTime = castTime,
8831 preWarnTime = preWarnTime,
8832 },
8833 mt
8834 )
8835 local catType = "announce"--Default to General announce
8836 --Change if Personal or Other
8837 if announceType == "target" or announceType == "targetcount" or announceType == "stack" then
8838 catType = "announceother"
8839 end
8840 if optionName then
8841 obj.option = optionName
8842 self:AddBoolOption(obj.option, optionDefault, catType)
8843 elseif not (optionName == false) then
8844 obj.option = catType..spellId..announceType..(optionVersion or "")
8845 self:AddBoolOption(obj.option, optionDefault, catType)
8846 self.localization.options[obj.option] = DBM_CORE_AUTO_ANNOUNCE_OPTIONS[announceType]:format(spellId)
8847 end
8848 tinsert(self.announces, obj)
8849 return obj
8850 end
8851
8852 function bossModPrototype:NewYouAnnounce(spellId, color, ...)
8853 return newAnnounce(self, "you", spellId, color or 1, ...)
8854 end
8855
8856 function bossModPrototype:NewTargetNoFilterAnnounce(spellId, color, icon, optionDefault, optionName, castTime, preWarnTime, noSound, noFilter)
8857 return newAnnounce(self, "target", spellId, color or 3, icon, optionDefault, optionName, castTime, preWarnTime, noSound, true)
8858 end
8859
8860 function bossModPrototype:NewTargetAnnounce(spellId, color, ...)
8861 return newAnnounce(self, "target", spellId, color or 3, ...)
8862 end
8863
8864 function bossModPrototype:NewTargetCountAnnounce(spellId, color, ...)
8865 return newAnnounce(self, "targetcount", spellId, color or 3, ...)
8866 end
8867
8868 function bossModPrototype:NewSpellAnnounce(spellId, color, ...)
8869 return newAnnounce(self, "spell", spellId, color or 2, ...)
8870 end
8871
8872 function bossModPrototype:NewEndAnnounce(spellId, color, ...)
8873 return newAnnounce(self, "ends", spellId, color or 2, ...)
8874 end
8875
8876 function bossModPrototype:NewEndTargetAnnounce(spellId, color, ...)
8877 return newAnnounce(self, "endtarget", spellId, color or 2, ...)
8878 end
8879
8880 function bossModPrototype:NewFadesAnnounce(spellId, color, ...)
8881 return newAnnounce(self, "fades", spellId, color or 2, ...)
8882 end
8883
8884 function bossModPrototype:NewAddsLeftAnnounce(spellId, color, ...)
8885 return newAnnounce(self, "adds", spellId, color or 3, ...)
8886 end
8887
8888 function bossModPrototype:NewCountAnnounce(spellId, color, ...)
8889 return newAnnounce(self, "count", spellId, color or 2, ...)
8890 end
8891
8892 function bossModPrototype:NewStackAnnounce(spellId, color, ...)
8893 return newAnnounce(self, "stack", spellId, color or 2, ...)
8894 end
8895
8896 function bossModPrototype:NewCastAnnounce(spellId, color, castTime, icon, optionDefault, optionName, noArg, noSound)
8897 local optionVersion
8898 if type(optionName) == "number" then
8899 optionVersion = optionName
8900 optionName = nil
8901 end
8902 if type(spellId) == "string" and spellId:match("OptionVersion") then--LEGACY hack, remove when new DBM core and other mods released
8903 print("NewCastAnnounce is using OptionVersion and this is depricated for "..color)
8904 return
8905 end
8906 return newAnnounce(self, "cast", spellId, color or 3, icon, optionDefault, optionName, castTime, nil, noSound)
8907 end
8908
8909 function bossModPrototype:NewSoonAnnounce(spellId, color, ...)
8910 return newAnnounce(self, "soon", spellId, color or 1, ...)
8911 end
8912
8913 function bossModPrototype:NewPreWarnAnnounce(spellId, time, color, icon, optionDefault, optionName, noArg, noSound)
8914 local optionVersion
8915 if type(optionName) == "number" then
8916 optionVersion = optionName
8917 optionName = nil
8918 end
8919 if type(spellId) == "string" and spellId:match("OptionVersion") then--LEGACY hack, remove when new DBM core and other mods released
8920 print("NewCastAnnounce is using OptionVersion and this is depricated for "..color)
8921 return
8922 end
8923 return newAnnounce(self, "prewarn", spellId, color or 1, icon, optionDefault, optionName, nil, time, noSound)
8924 end
8925
8926 function bossModPrototype:NewPhaseAnnounce(stage, color, icon, ...)
8927 return newAnnounce(self, "stage", stage, color or 1, icon or "Interface\\Icons\\Spell_Nature_WispSplode", ...)
8928 end
8929
8930 function bossModPrototype:NewPhaseChangeAnnounce(color, icon, ...)
8931 return newAnnounce(self, "stagechange", 0, color or 1, icon or "Interface\\Icons\\Spell_Nature_WispSplode", ...)
8932 end
8933
8934 function bossModPrototype:NewPrePhaseAnnounce(stage, color, icon, ...)
8935 return newAnnounce(self, "prestage", stage, color or 1, icon or "Interface\\Icons\\Spell_Nature_WispSplode", ...)
8936 end
8937end
8938
8939--------------------
8940-- Sound Object --
8941--------------------
8942do
8943 --Sound Object (basically only used by countdown now)
8944 local soundPrototype = {}
8945 local mt = { __index = soundPrototype }
8946 function bossModPrototype:NewSound(spellId, optionDefault, optionName)
8947 if not (optionName == false) then--Basically, all mods still using NewSound.
8948 -- Because there are going to be users who update core and not old mods, we need to check and alert
8949 --I'll try to avoid this as much as possible by removing NewSound from all old mods in advance of dbm core update
8950 print("Error, NewSound depricated. Update your old DBM mods to remove this error")
8951 return
8952 end
8953 self.numSounds = self.numSounds and self.numSounds + 1 or 1
8954 local obj = setmetatable(
8955 {
8956 mod = self,
8957 },
8958 mt
8959 )
8960 return obj
8961 end
8962
8963 function soundPrototype:Play(file)
8964 DBM:PlaySoundFile(file)
8965 end
8966
8967 function soundPrototype:Schedule(t, ...)
8968 return schedule(t, self.Play, self.mod, self, ...)
8969 end
8970
8971 function soundPrototype:Cancel(...)
8972 return unschedule(self.Play, self.mod, self, ...)
8973 end
8974
8975 --Voice Object
8976 --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"
8977 local soundPrototype2 = {}
8978 local mt = { __index = soundPrototype2 }
8979 function bossModPrototype:NewVoice(spellId, optionDefault, optionName, optionVersion)
8980 if not spellId and not optionName then
8981 print("NewVoice: you must provide either spellId or optionName", 2)
8982 return
8983 end
8984 if type(spellId) == "string" and spellId:match("OptionVersion") then
8985 print("NewVoice for "..optionDefault.." is using OptionVersion hack. this is not needed, this only has 4 args, do this properly")
8986 return
8987 end
8988 self.numSounds = self.numSounds and self.numSounds + 1 or 1
8989 local obj = setmetatable(
8990 {
8991 mod = self,
8992 },
8993 mt
8994 )
8995 if #DBM.Voices < 2 then optionName = false end--Hide options if no voice packs are installed
8996 if optionName then
8997 if spellId then--Still need to use spell ID for voice pack filter if one is provided
8998 obj.option = "Voice"..spellId..(optionVersion or "")
8999 self:AddBoolOption(obj.option, optionDefault, "sound")
9000 self.localization.options[obj.option] = optionName
9001 else
9002 obj.option = optionName
9003 self:AddBoolOption(obj.option, optionDefault, "sound")
9004 end
9005 elseif not (optionName == false) then
9006 obj.option = "Voice"..spellId..(optionVersion or "")
9007 self:AddBoolOption(obj.option, optionDefault, "sound")
9008 self.localization.options[obj.option] = DBM_CORE_AUTO_VOICE_OPTION_TEXT:format(spellId)
9009 end
9010 return obj
9011 end
9012
9013 --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.
9014 function soundPrototype2:Play(name, customPath)
9015 local voice = DBM.Options.ChosenVoicePack
9016 if voice == "None" then return end
9017 local always = DBM.Options.AlwaysPlayVoice
9018 if not self.option or self.mod.Options[self.option] or always then
9019 --Filter tank specific voice alerts for non tanks if tank filter enabled
9020 --But still allow AlwaysPlayVoice to play as well.
9021 if (name == "changemt" or name == "tauntboss") and DBM.Options.FilterTankSpec and not self.mod:IsTank() and not always then return end
9022 local path = customPath or "Interface\\AddOns\\DBM-VP"..voice.."\\"..name..".ogg"
9023 DBM:PlaySoundFile(path)
9024 end
9025 end
9026
9027 function soundPrototype2:Schedule(t, ...)
9028 if DBM.Options.ChosenVoicePack == "None" then return end
9029 return schedule(t, self.Play, self.mod, self, ...)
9030 end
9031
9032 function soundPrototype2:Cancel(...)
9033 if DBM.Options.ChosenVoicePack == "None" then return end
9034 return unschedule(self.Play, self.mod, self, ...)
9035 end
9036end
9037
9038----------------------------
9039-- Countdown/out object --
9040----------------------------
9041do
9042 local countdownProtoType = {}
9043 local voice1, voice2, voice3 = nil, nil, nil
9044 local voice1max, voice2max, voice3max = 5, 5, 5
9045 local path1, path2, path3 = nil, nil, nil
9046 local mt = {__index = countdownProtoType}
9047
9048 function DBM:BuildVoiceCountdownCache()
9049 voice1 = self.Options.CountdownVoice
9050 voice2 = self.Options.CountdownVoice2
9051 voice3 = self.Options.CountdownVoice3v2
9052 local voicesFound = 0
9053 for i = 1, #self.Counts do
9054 local curVoice = self.Counts[i]
9055 if curVoice.value == voice1 then
9056 path1 = curVoice.path
9057 voice1max = curVoice.max
9058 end
9059 if curVoice.value == voice2 then
9060 path2 = curVoice.path
9061 voice2max = curVoice.max
9062 end
9063 if curVoice.value == voice3 then
9064 path3 = curVoice.path
9065 voice3max = curVoice.max
9066 end
9067 end
9068 end
9069
9070 function countdownProtoType:Start(timer, count)
9071 if not self.option or self.mod.Options[self.option] then
9072 timer = timer or self.timer or 10
9073 timer = timer < 2 and self.timer or timer
9074 count = count or self.count or 5
9075 if timer <= count then count = floor(timer) end
9076 if DBM.Options.DontPlayCountdowns then return end
9077 if not path1 or not path2 or not path3 then
9078 DBM:Debug("Voice cache not built at time of countdownProtoType:Start. On fly caching.", 3)
9079 DBM:BuildVoiceCountdownCache()
9080 end
9081 local voice, maxCount, path
9082 if self.alternateVoice == 2 then
9083 voice = voice2 or DBM.DefaultOptions.CountdownVoice2
9084 maxCount = voice2max or 10
9085 path = path2 or "Interface\\AddOns\\DBM-Core\\Sounds\\Kolt\\"
9086 elseif self.alternateVoice == 3 then
9087 voice = voice3 or DBM.DefaultOptions.CountdownVoice3v2
9088 maxCount = voice3max or 5
9089 path = path3 or "Interface\\AddOns\\DBM-Core\\Sounds\\Heroes\\Necromancer\\"
9090 else
9091 voice = voice1 or DBM.DefaultOptions.CountdownVoice
9092 maxCount = voice1max or 10
9093 path = path1 or "Interface\\AddOns\\DBM-Core\\Sounds\\Corsica\\"
9094 end
9095 if not path then--Should not happen but apparently it does somehow
9096 DBM:Debug("Voice path failed in countdownProtoType:Start.")
9097 return
9098 end
9099 if self.type == "Countout" then
9100 for i = 1, timer do
9101 if i < maxCount then
9102 self.sound5:Schedule(i, path..i..".ogg")
9103 end
9104 end
9105 else
9106 for i = count, 1, -1 do
9107 if i <= maxCount then
9108 self.sound5:Schedule(timer-i, path..i..".ogg")
9109 end
9110 end
9111 end
9112 end
9113 end
9114 countdownProtoType.Show = countdownProtoType.Start
9115
9116 function countdownProtoType:Schedule(t)
9117 return schedule(t, self.Start, self.mod, self)
9118 end
9119
9120 function countdownProtoType:Cancel()
9121 self.mod:Unschedule(self.Start, self)
9122 self.sound5:Cancel()
9123 end
9124 countdownProtoType.Stop = countdownProtoType.Cancel
9125
9126 local function newCountdown(self, countdownType, timer, spellId, optionDefault, optionName, count, textDisabled, altVoice)
9127 if not spellId and not optionName then
9128 print("NewCountdown: you must provide either spellId or optionName", 2)
9129 return
9130 end
9131 if type(timer) == "string" and timer:match("OptionVersion") then
9132 print("OptionVersion depricated for newCountdown :"..optionDefault)
9133 return
9134 end
9135 local optionVersion
9136 if type(optionName) == "number" then
9137 optionVersion = optionName
9138 optionName = nil
9139 end
9140 if altVoice == true then altVoice = 2 end--Compat
9141 if type(timer) == "string" then
9142 if timer:match("AltTwo") then
9143 altVoice = 3
9144 timer = tonumber(string.sub(timer, 7))
9145 elseif timer:match("Alt") then
9146 altVoice = 2
9147 timer = tonumber(string.sub(timer, 4))
9148 end
9149 end
9150 --TODO, maybe make this not use an entire sound object?
9151 local sound5 = self:NewSound(5, true, false)
9152 timer = timer or 10
9153 count = count or 4
9154 spellId = spellId or 39505
9155 local obj = setmetatable(
9156 {
9157 id = optionName or countdownType..spellId..(optionVersion or ""),
9158 type = countdownType,
9159 sound5 = sound5,
9160 timer = timer,
9161 count = count,
9162 textDisabled = textDisabled,
9163 alternateVoice = altVoice,
9164 mod = self
9165 },
9166 mt
9167 )
9168 if optionName then
9169 obj.option = obj.id
9170 self:AddBoolOption(obj.option, optionDefault, "sound")
9171 elseif not (optionName == false) then
9172 obj.option = obj.id
9173 self:AddBoolOption(obj.option, optionDefault, "sound")
9174 if countdownType == "Countdown" then
9175 self.localization.options[obj.option] = DBM_CORE_AUTO_COUNTDOWN_OPTION_TEXT:format(spellId)
9176 elseif countdownType == "CountdownFades" then
9177 self.localization.options[obj.option] = DBM_CORE_AUTO_COUNTDOWN_OPTION_TEXT2:format(spellId)
9178 elseif countdownType == "Countout" then
9179 self.localization.options[obj.option] = DBM_CORE_AUTO_COUNTOUT_OPTION_TEXT:format(spellId)
9180 end
9181 end
9182 tinsert(self.countdowns, obj)
9183 return obj
9184 end
9185
9186 function bossModPrototype:NewCountdown(...)
9187 return newCountdown(self, "Countdown", ...)
9188 end
9189
9190 function bossModPrototype:NewCountdownFades(...)
9191 return newCountdown(self, "CountdownFades", ...)
9192 end
9193
9194 function bossModPrototype:NewCountout(...)
9195 return newCountdown(self, "Countout", ...)
9196 end
9197end
9198
9199--------------------
9200-- Yell Object --
9201--------------------
9202do
9203 local voidForm = GetSpellInfo(194249)
9204 local yellPrototype = {}
9205 local mt = { __index = yellPrototype }
9206 local function newYell(self, yellType, spellId, yellText, optionDefault, optionName, chatType)
9207 if not spellId and not yellText then
9208 error("NewYell: you must provide either spellId or yellText", 2)
9209 return
9210 end
9211 if type(spellId) == "string" and spellId:match("OptionVersion") then
9212 print("newYell for: "..yellText.." is using OptionVersion hack. This is depricated")
9213 return
9214 end
9215 local optionVersion
9216 if type(optionName) == "number" then
9217 optionVersion = optionName
9218 optionName = nil
9219 end
9220 local displayText
9221 if not yellText then
9222 if type(spellId) == "string" and spellId:match("ej%d+") then
9223 displayText = DBM_CORE_AUTO_YELL_ANNOUNCE_TEXT[yellType]:format(DBM:EJ_GetSectionInfo(string.sub(spellId, 3)) or DBM_CORE_UNKNOWN)
9224 else
9225 displayText = DBM_CORE_AUTO_YELL_ANNOUNCE_TEXT[yellType]:format(DBM:GetSpellInfo(spellId) or DBM_CORE_UNKNOWN)
9226 end
9227 end
9228 --Passed spellid as yellText.
9229 --Auto localize spelltext using yellText instead
9230 if yellText and type(yellText) == "number" then
9231 displayText = DBM_CORE_AUTO_YELL_ANNOUNCE_TEXT[yellType]:format(DBM:GetSpellInfo(yellText) or DBM_CORE_UNKNOWN)
9232 end
9233 local obj = setmetatable(
9234 {
9235 text = displayText or yellText,
9236 mod = self,
9237 chatType = chatType,
9238 yellType = yellType
9239 },
9240 mt
9241 )
9242 if optionName then
9243 obj.option = optionName
9244 self:AddBoolOption(obj.option, optionDefault, "misc")
9245 elseif not (optionName == false) then
9246 obj.option = "Yell"..(spellId or yellText)..(yellType ~= "yell" and yellType or "")..(optionVersion or "")
9247 self:AddBoolOption(obj.option, optionDefault, "misc")
9248 self.localization.options[obj.option] = DBM_CORE_AUTO_YELL_OPTION_TEXT[yellType]:format(spellId)
9249 end
9250 return obj
9251 end
9252
9253 function yellPrototype:Yell(...)
9254 if DBM.Options.DontSendYells or self.yellType and self.yellType == "position" and DBM:UnitBuff("player", voidForm) then return end
9255 if not self.option or self.mod.Options[self.option] then
9256 if self.yellType == "combo" then
9257 SendChatMessage(pformat(self.text, ...), self.chatType or "YELL")
9258 else
9259 SendChatMessage(pformat(self.text, ...), self.chatType or "SAY")
9260 end
9261 end
9262 end
9263 yellPrototype.Show = yellPrototype.Yell
9264
9265 function yellPrototype:Schedule(t, ...)
9266 return schedule(t, self.Yell, self.mod, self, ...)
9267 end
9268
9269 function yellPrototype:Countdown(time, numAnnounces, ...)
9270 scheduleCountdown(time, numAnnounces, self.Yell, self.mod, self, ...)
9271 end
9272
9273 function yellPrototype:Cancel(...)
9274 return unschedule(self.Yell, self.mod, self, ...)
9275 end
9276
9277 function bossModPrototype:NewYell(...)
9278 return newYell(self, "yell", ...)
9279 end
9280
9281 function bossModPrototype:NewShortYell(...)
9282 return newYell(self, "shortyell", ...)
9283 end
9284
9285 function bossModPrototype:NewCountYell(...)
9286 return newYell(self, "count", ...)
9287 end
9288
9289 function bossModPrototype:NewFadesYell(...)
9290 return newYell(self, "fade", ...)
9291 end
9292
9293 function bossModPrototype:NewShortFadesYell(...)
9294 return newYell(self, "shortfade", ...)
9295 end
9296
9297 function bossModPrototype:NewIconFadesYell(...)
9298 return newYell(self, "iconfade", ...)
9299 end
9300
9301 function bossModPrototype:NewPosYell(...)
9302 return newYell(self, "position", ...)
9303 end
9304
9305 function bossModPrototype:NewComboYell(...)
9306 return newYell(self, "combo", ...)
9307 end
9308end
9309
9310------------------------------
9311-- Special Warning Object --
9312------------------------------
9313do
9314 local frame = CreateFrame("Frame", "DBMSpecialWarning", UIParent)
9315 local font1 = frame:CreateFontString("DBMSpecialWarning1", "OVERLAY", "ZoneTextFont")
9316 font1:SetWidth(1024)
9317 font1:SetHeight(0)
9318 font1:SetPoint("TOP", 0, 0)
9319 local font2 = frame:CreateFontString("DBMSpecialWarning2", "OVERLAY", "ZoneTextFont")
9320 font2:SetWidth(1024)
9321 font2:SetHeight(0)
9322 font2:SetPoint("TOP", font1, "BOTTOM", 0, 0)
9323 frame:SetMovable(1)
9324 frame:SetWidth(1)
9325 frame:SetHeight(1)
9326 frame:SetFrameStrata("HIGH")
9327 frame:SetClampedToScreen()
9328 frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
9329
9330 local font1elapsed, font2elapsed, moving
9331
9332 local function fontHide1()
9333 local duration = DBM.Options.SpecialWarningDuration2
9334 if font1elapsed > duration * 1.3 then
9335 font1:Hide()
9336 if frame.font1ticker then
9337 frame.font1ticker:Cancel()
9338 frame.font1ticker = nil
9339 end
9340 elseif font1elapsed > duration then
9341 font1elapsed = font1elapsed + 0.05
9342 local alpha = 1 - (font1elapsed - duration) / (duration * 0.3)
9343 font1:SetAlpha(alpha > 0 and alpha or 0)
9344 else
9345 font1elapsed = font1elapsed + 0.05
9346 font1:SetAlpha(1)
9347 end
9348 end
9349
9350 local function fontHide2()
9351 local duration = DBM.Options.SpecialWarningDuration2
9352 if font2elapsed > duration * 1.3 then
9353 font2:Hide()
9354 if frame.font2ticker then
9355 frame.font2ticker:Cancel()
9356 frame.font2ticker = nil
9357 end
9358 elseif font2elapsed > duration then
9359 font2elapsed = font2elapsed + 0.05
9360 local alpha = 1 - (font2elapsed - duration) / (duration * 0.3)
9361 font2:SetAlpha(alpha > 0 and alpha or 0)
9362 else
9363 font2elapsed = font2elapsed + 0.05
9364 font2:SetAlpha(1)
9365 end
9366 end
9367
9368 function DBM:UpdateSpecialWarningOptions()
9369 frame:ClearAllPoints()
9370 frame:SetPoint(self.Options.SpecialWarningPoint, UIParent, self.Options.SpecialWarningPoint, self.Options.SpecialWarningX, self.Options.SpecialWarningY)
9371 font1:SetFont(self.Options.SpecialWarningFont, self.Options.SpecialWarningFontSize2, self.Options.SpecialWarningFontStyle == "None" and nil or self.Options.SpecialWarningFontStyle)
9372 font2:SetFont(self.Options.SpecialWarningFont, self.Options.SpecialWarningFontSize2, self.Options.SpecialWarningFontStyle == "None" and nil or self.Options.SpecialWarningFontStyle)
9373 font1:SetTextColor(unpack(self.Options.SpecialWarningFontCol))
9374 font2:SetTextColor(unpack(self.Options.SpecialWarningFontCol))
9375 if self.Options.SpecialWarningFontShadow then
9376 font1:SetShadowOffset(1, -1)
9377 font2:SetShadowOffset(1, -1)
9378 else
9379 font1:SetShadowOffset(0, 0)
9380 font2:SetShadowOffset(0, 0)
9381 end
9382 end
9383
9384 function DBM:AddSpecialWarning(text, force)
9385 local added = false
9386 if not frame.font1ticker then
9387 font1elapsed = 0
9388 font1.lastUpdate = GetTime()
9389 font1:SetText(text)
9390 font1:Show()
9391 added = true
9392 frame.font1ticker = frame.font1ticker or C_TimerNewTicker(0.05, fontHide1)
9393 elseif not frame.font2ticker or force then
9394 font2elapsed = 0
9395 font2.lastUpdate = GetTime()
9396 font2:SetText(text)
9397 font2:Show()
9398 added = true
9399 frame.font2ticker = frame.font2ticker or C_TimerNewTicker(0.05, fontHide2)
9400 end
9401 if not added then
9402 local prevText1 = font2:GetText()
9403 font1:SetText(prevText1)
9404 font1elapsed = font2elapsed
9405 self:AddSpecialWarning(text, true)
9406 end
9407 end
9408
9409 do
9410 local anchorFrame
9411 local function moveEnd(self)
9412 moving = false
9413 anchorFrame:Hide()
9414 font1elapsed = self.Options.SpecialWarningDuration2
9415 font2elapsed = self.Options.SpecialWarningDuration2
9416 frame:SetFrameStrata("HIGH")
9417 self:Unschedule(moveEnd)
9418 self.Bars:CancelBar(DBM_CORE_MOVE_SPECIAL_WARNING_BAR)
9419 end
9420
9421 function DBM:MoveSpecialWarning()
9422 if not anchorFrame then
9423 anchorFrame = CreateFrame("Frame", nil, frame)
9424 anchorFrame:SetWidth(32)
9425 anchorFrame:SetHeight(32)
9426 anchorFrame:EnableMouse(true)
9427 anchorFrame:SetPoint("TOP", frame, "TOP", 0, 32)
9428 anchorFrame:RegisterForDrag("LeftButton")
9429 anchorFrame:SetClampedToScreen()
9430 anchorFrame:Hide()
9431 local texture = anchorFrame:CreateTexture()
9432 texture:SetTexture("Interface\\Addons\\DBM-GUI\\textures\\dot.blp")
9433 texture:SetPoint("CENTER", anchorFrame, "CENTER", 0, 0)
9434 texture:SetWidth(32)
9435 texture:SetHeight(32)
9436 anchorFrame:SetScript("OnDragStart", function()
9437 frame:StartMoving()
9438 self:Unschedule(moveEnd)
9439 self.Bars:CancelBar(DBM_CORE_MOVE_SPECIAL_WARNING_BAR)
9440 end)
9441 anchorFrame:SetScript("OnDragStop", function()
9442 frame:StopMovingOrSizing()
9443 local point, _, _, xOfs, yOfs = frame:GetPoint(1)
9444 self.Options.SpecialWarningPoint = point
9445 self.Options.SpecialWarningX = xOfs
9446 self.Options.SpecialWarningY = yOfs
9447 self:Schedule(15, moveEnd, self)
9448 self.Bars:CreateBar(15, DBM_CORE_MOVE_SPECIAL_WARNING_BAR)
9449 end)
9450 end
9451 if anchorFrame:IsShown() then
9452 moveEnd(self)
9453 else
9454 moving = true
9455 anchorFrame:Show()
9456 DBM:AddSpecialWarning(DBM_CORE_MOVE_SPECIAL_WARNING_TEXT)
9457 DBM:AddSpecialWarning(DBM_CORE_MOVE_SPECIAL_WARNING_TEXT)
9458 self:Schedule(15, moveEnd, self)
9459 self.Bars:CreateBar(15, DBM_CORE_MOVE_SPECIAL_WARNING_BAR)
9460 frame:Show()
9461 frame:SetFrameStrata("TOOLTIP")
9462 frame:SetAlpha(1)
9463 end
9464 end
9465 end
9466
9467 local specialWarningPrototype = {}
9468 local mt = {__index = specialWarningPrototype}
9469
9470 local function classColoringFunction(cap)
9471 cap = cap:sub(2, -2)
9472 local noStrip = cap:match("noStrip ")
9473 if not noStrip then
9474 local name = cap
9475 if DBM.Options.StripServerName then
9476 cap = Ambiguate(cap, "short")
9477 end
9478 if DBM.Options.SWarnClassColor then
9479 local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(name)]
9480 if playerColor then
9481 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)
9482 end
9483 end
9484 else
9485 cap = cap:sub(9)
9486 end
9487 return cap
9488 end
9489
9490 local textureCode = " |T%s:12:12|t "
9491
9492 local function setText(announceType, spellId, stacks)
9493 local text, spellName
9494 if type(spellId) == "string" and spellId:match("ej%d+") then
9495 spellName = DBM:EJ_GetSectionInfo(string.sub(spellId, 3)) or DBM_CORE_UNKNOWN
9496 else
9497 spellName = DBM:GetSpellInfo(spellId) or DBM_CORE_UNKNOWN
9498 end
9499 if announceType == "prewarn" then
9500 if type(stacks) == "string" then
9501 text = DBM_CORE_AUTO_SPEC_WARN_TEXTS[announceType]:format(spellName, stacks)
9502 else
9503 text = DBM_CORE_AUTO_SPEC_WARN_TEXTS[announceType]:format(spellName, DBM_CORE_SEC_FMT:format(tostring(stacks or 5)))
9504 end
9505 else
9506 text = DBM_CORE_AUTO_SPEC_WARN_TEXTS[announceType]:format(spellName)
9507 end
9508 return text, spellName
9509 end
9510
9511 function specialWarningPrototype:Show(...)
9512 if not DBM.Options.DontShowSpecialWarnings and not DBM.Options.DontShowSpecialWarningText and (not self.option or self.mod.Options[self.option]) and not moving and frame then
9513 if self.announceType == "taunt" and DBM.Options.FilterTankSpec and not self.mod:IsTank() then return end--Don't tell non tanks to taunt, ever.
9514 local argTable = {...}
9515 -- add a default parameter for move away warnings
9516 if self.announceType == "gtfo" then
9517 if #argTable == 0 then
9518 argTable[1] = DBM_CORE_BAD
9519 end
9520 end
9521 if #self.combinedtext > 0 then
9522 --Throttle spam.
9523 if DBM.Options.SWarningAlphabetical then
9524 tsort(self.combinedtext)
9525 end
9526 local combinedText = tconcat(self.combinedtext, "<, >")
9527 if self.combinedcount == 1 then
9528 combinedText = combinedText.." "..DBM_CORE_GENERIC_WARNING_OTHERS
9529 elseif self.combinedcount > 1 then
9530 combinedText = combinedText.." "..DBM_CORE_GENERIC_WARNING_OTHERS2:format(self.combinedcount)
9531 end
9532 --Process
9533 for i = 1, #argTable do
9534 if type(argTable[i]) == "string" then
9535 argTable[i] = combinedText
9536 end
9537 end
9538 end
9539 local message = pformat(self.text, unpack(argTable))
9540 local text = ("%s%s%s"):format(
9541 (DBM.Options.SpecialWarningIcon and self.icon and textureCode:format(self.icon)) or "",
9542 message,
9543 (DBM.Options.SpecialWarningIcon and self.icon and textureCode:format(self.icon)) or ""
9544 )
9545 local noteHasName = false
9546 if self.option then
9547 local noteText = self.mod.Options[self.option .. "SWNote"]
9548 if noteText and type(noteText) == "string" and noteText ~= "" then--Filter false bool and empty strings
9549 local count1 = self.announceType == "count" or self.announceType == "switchcount" or self.announceType == "targetcount"
9550 local count2 = self.announceType == "interruptcount"
9551 if count1 or count2 then--Counts support different note for EACH count
9552 local noteCount
9553 local notesTable = {string.split("/", noteText)}
9554 if count1 then
9555 noteCount = argTable[1]--Count should be first arg in table
9556 elseif count2 then
9557 noteCount = argTable[2]--Count should be second arg in table
9558 end
9559 if type(noteCount) == "string" then
9560 --Probably a hypehnated double count like inferno slice or marked for death
9561 local mainCount = string.split("-", noteCount)
9562 noteCount = tonumber(mainCount)
9563 end
9564 noteText = notesTable[noteCount]
9565 if noteText and type(noteText) == "string" and noteText ~= "" then--Refilter after string split to make sure a note for this count exists
9566 local hasPlayerName = noteText:find(playerName)
9567 if DBM.Options.SWarnNameInNote and hasPlayerName then
9568 noteHasName = 5
9569 end
9570 --Terminate special warning, it's an interrupt count warning without player name and filter enabled
9571 if count2 and DBM.Options.FilterInterruptNoteName and not hasPlayerName then return end
9572 noteText = " ("..noteText..")"
9573 text = text..noteText
9574 end
9575 else--Non count warnings will have one note, period
9576 if DBM.Options.SWarnNameInNote and noteText:find(playerName) then
9577 noteHasName = 5
9578 end
9579 if self.announceType and self.announceType:find("switch") then
9580 noteText = noteText:gsub(">.-<", classColoringFunction)--Class color note text before combining with warning text.
9581 end
9582 noteText = " ("..noteText..")"
9583 text = text..noteText
9584 end
9585 end
9586 end
9587 --No stripping on switch warnings, ever. They will NEVER have player name, but often have adds with "-" in name
9588 if self.announceType and not self.announceType:find("switch") then
9589 text = text:gsub(">.-<", classColoringFunction)
9590 end
9591 DBM:AddSpecialWarning(text)
9592 self.combinedcount = 0
9593 self.combinedtext = {}
9594 if DBM.Options.ShowSWarningsInChat then
9595 local colorCode = ("|cff%.2x%.2x%.2x"):format(DBM.Options.SpecialWarningFontCol[1] * 255, DBM.Options.SpecialWarningFontCol[2] * 255, DBM.Options.SpecialWarningFontCol[3] * 255)
9596 self.mod:AddMsg(colorCode.."["..DBM_CORE_MOVE_SPECIAL_WARNING_TEXT.."] "..text.."|r", nil)
9597 end
9598 if not UnitIsDeadOrGhost("player") and DBM.Options.ShowFlashFrame then
9599 if noteHasName then
9600 local repeatCount = DBM.Options.SpecialWarningFlashRepeat5 and DBM.Options.SpecialWarningFlashRepeatAmount or 0
9601 DBM.Flash:Show(DBM.Options.SpecialWarningFlashCol5[1],DBM.Options.SpecialWarningFlashCol5[2], DBM.Options.SpecialWarningFlashCol5[3], DBM.Options.SpecialWarningFlashDura5, DBM.Options.SpecialWarningFlashAlph5, repeatCount)
9602 else
9603 local number = self.flash
9604 local repeatCount = DBM.Options["SpecialWarningFlashRepeat"..number] and DBM.Options.SpecialWarningFlashRepeatAmount or 0
9605 local flashcolor = DBM.Options["SpecialWarningFlashCol"..number]
9606 DBM.Flash:Show(flashcolor[1], flashcolor[2], flashcolor[3], DBM.Options["SpecialWarningFlashDura"..number], DBM.Options["SpecialWarningFlashAlph"..number], repeatCount)
9607 end
9608 end
9609 --Text: Full message text
9610 --Type: Announce type
9611 --SpellId: Raw spell or encounter journal Id if available.
9612 --Mod ID: Encounter ID as string, or a generic string for mods that don't have encounter ID (such as trash, dummy/test mods)
9613 fireEvent("DBM_Announce", text, self.type, self.spellId, self.mod.id)
9614 if self.sound then
9615 local soundId = self.option and self.mod.Options[self.option .. "SWSound"] or self.flash
9616 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
9617 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") then return end
9618 if not self.option or self.mod.Options[self.option.."SWSound"] ~= "None" then
9619 DBM:PlaySpecialWarningSound(soundId or 1)
9620 end
9621 end
9622 else
9623 self.combinedcount = 0
9624 self.combinedtext = {}
9625 end
9626 end
9627
9628 function specialWarningPrototype:CombinedShow(delay, ...)
9629 if DBM.Options.DontShowSpecialWarnings or DBM.Options.DontShowSpecialWarningText then return end
9630 if self.option and not self.mod.Options[self.option] then return end
9631 local argTable = {...}
9632 for i = 1, #argTable do
9633 if type(argTable[i]) == "string" then
9634 if #self.combinedtext < 8 then--Throttle spam. We may not need more than 9 targets..
9635 if not checkEntry(self.combinedtext, argTable[i]) then
9636 self.combinedtext[#self.combinedtext + 1] = argTable[i]
9637 end
9638 else
9639 self.combinedcount = self.combinedcount + 1
9640 end
9641 end
9642 end
9643 unschedule(self.Show, self.mod, self)
9644 schedule(delay or 0.5, self.Show, self.mod, self, ...)
9645 end
9646
9647 function specialWarningPrototype:DelayedShow(delay, ...)
9648 unschedule(self.Show, self.mod, self, ...)
9649 schedule(delay or 0.5, self.Show, self.mod, self, ...)
9650 end
9651
9652 function specialWarningPrototype:Schedule(t, ...)
9653 return schedule(t, self.Show, self.mod, self, ...)
9654 end
9655
9656 function specialWarningPrototype:Countdown(time, numAnnounces, ...)
9657 scheduleCountdown(time, numAnnounces, self.Show, self.mod, self, ...)
9658 end
9659
9660 function specialWarningPrototype:Cancel(t, ...)
9661 return unschedule(self.Show, self.mod, self, ...)
9662 end
9663
9664 function specialWarningPrototype:Play(name, customPath)
9665 local always = DBM.Options.AlwaysPlayVoice
9666 local voice = DBM.Options.ChosenVoicePack
9667 if voice == "None" then return end
9668 if not DBM.Options.DontShowSpecialWarnings and (not self.option or self.mod.Options[self.option]) or always then
9669 --Filter tank specific voice alerts for non tanks if tank filter enabled
9670 --But still allow AlwaysPlayVoice to play as well.
9671 if (name == "changemt" or name == "tauntboss") and DBM.Options.FilterTankSpec and not self.mod:IsTank() and not always then return end
9672 local path = customPath or "Interface\\AddOns\\DBM-VP"..voice.."\\"..name..".ogg"
9673 DBM:PlaySoundFile(path)
9674 end
9675 end
9676
9677 function specialWarningPrototype:ScheduleVoice(t, ...)
9678 if DBM.Options.ChosenVoicePack == "None" then return end
9679 return schedule(t, self.Play, self.mod, self, ...)
9680 end
9681
9682 function specialWarningPrototype:CancelVoice(...)
9683 if DBM.Options.ChosenVoicePack == "None" then return end
9684 return unschedule(self.Play, self.mod, self, ...)
9685 end
9686
9687 function bossModPrototype:NewSpecialWarning(text, optionDefault, optionName, optionVersion, runSound, hasVoice)
9688 if not text then
9689 error("NewSpecialWarning: you must provide special warning text", 2)
9690 return
9691 end
9692 if type(text) == "string" and text:match("OptionVersion") then
9693 print("NewSpecialWarning: you must provide remove optionversion hack for "..optionDefault)
9694 return
9695 end
9696 if runSound == true then
9697 runSound = 2
9698 elseif not runSound then
9699 runSound = 1
9700 end
9701 if hasVoice == true then--if not a number, set it to 2, old mods that don't use new numbered system
9702 hasVoice = 2
9703 end
9704 local obj = setmetatable(
9705 {
9706 text = self.localization.warnings[text],
9707 combinedtext = {},
9708 combinedcount = 0,
9709 mod = self,
9710 sound = runSound>0,
9711 flash = runSound,--Set flash color to hard coded runsound (even if user sets custom sounds)
9712 hasVoice = hasVoice,
9713 },
9714 mt
9715 )
9716 local optionId = optionName or optionName ~= false and text
9717 if optionId then
9718 obj.voiceOptionId = hasVoice and "Voice"..optionId or nil
9719 obj.option = optionId..(optionVersion or "")
9720 self:AddSpecialWarningOption(optionId, optionDefault, runSound, "announce")
9721 end
9722 tinsert(self.specwarns, obj)
9723 return obj
9724 end
9725
9726 local function newSpecialWarning(self, announceType, spellId, stacks, optionDefault, optionName, optionVersion, runSound, hasVoice)
9727 if not spellId then
9728 error("newSpecialWarning: you must provide spellId", 2)
9729 return
9730 end
9731 if runSound == true then
9732 runSound = 2
9733 elseif not runSound then
9734 runSound = 1
9735 end
9736 if hasVoice == true then--if not a number, set it to 2, old mods that don't use new numbered system
9737 hasVoice = 2
9738 end
9739 local text, spellName = setText(announceType, spellId, stacks)
9740 local obj = setmetatable( -- todo: fix duplicate code
9741 {
9742 text = text,
9743 combinedtext = {},
9744 combinedcount = 0,
9745 announceType = announceType,
9746 mod = self,
9747 sound = runSound>0,
9748 flash = runSound,--Set flash color to hard coded runsound (even if user sets custom sounds)
9749 hasVoice = hasVoice,
9750 type = announceType,
9751 spellId = spellId,
9752 spellName = spellName,
9753 stacks = stacks,
9754 icon = (type(spellId) == "string" and spellId:match("ej%d+") and select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3))) ~= "" and select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3)))) or (type(spellId) == "number" and GetSpellTexture(spellId)) or nil
9755 },
9756 mt
9757 )
9758 if optionName then
9759 obj.option = optionName
9760 elseif not (optionName == false) then
9761 obj.option = "SpecWarn"..spellId..announceType..(optionVersion or "")
9762 if announceType == "stack" then
9763 self.localization.options[obj.option] = DBM_CORE_AUTO_SPEC_WARN_OPTIONS[announceType]:format(stacks or 3, spellId)
9764 elseif announceType == "prewarn" then
9765 self.localization.options[obj.option] = DBM_CORE_AUTO_SPEC_WARN_OPTIONS[announceType]:format(tostring(stacks or 5), spellId)
9766 else
9767 self.localization.options[obj.option] = DBM_CORE_AUTO_SPEC_WARN_OPTIONS[announceType]:format(spellId)
9768 end
9769 end
9770 if obj.option then
9771 local catType = "announce"--Default to General announce
9772 --Directly affects another target (boss or player) that you need to know about
9773 if announceType == "target" or announceType == "targetcount" or announceType == "close" or announceType == "reflect" then
9774 catType = "announceother"
9775 --Directly affects you
9776 elseif announceType == "you" or announceType == "youcount" or announceType == "youpos" or announceType == "move" or announceType == "dodge" or announceType == "moveaway" or announceType == "run" or announceType == "stack" or announceType == "moveto" or announceType == "soakpos" then
9777 catType = "announcepersonal"
9778 --Things you have to do to fulfil your role
9779 elseif announceType == "taunt" or announceType == "dispel" or announceType == "interrupt" or announceType == "interruptcount" or announceType == "switch" or announceType == "switchcount" then
9780 catType = "announcerole"
9781 end
9782 self:AddSpecialWarningOption(obj.option, optionDefault, runSound, catType)
9783 end
9784 obj.voiceOptionId = hasVoice and "Voice"..spellId or nil
9785 tinsert(self.specwarns, obj)
9786 return obj
9787 end
9788
9789 function bossModPrototype:NewSpecialWarningSpell(text, optionDefault, ...)
9790 return newSpecialWarning(self, "spell", text, nil, optionDefault, ...)
9791 end
9792
9793 function bossModPrototype:NewSpecialWarningEnd(text, optionDefault, ...)
9794 return newSpecialWarning(self, "ends", text, nil, optionDefault, ...)
9795 end
9796
9797 function bossModPrototype:NewSpecialWarningFades(text, optionDefault, ...)
9798 return newSpecialWarning(self, "fades", text, nil, optionDefault, ...)
9799 end
9800
9801 function bossModPrototype:NewSpecialWarningSoon(text, optionDefault, ...)
9802 return newSpecialWarning(self, "soon", text, nil, optionDefault, ...)
9803 end
9804
9805 function bossModPrototype:NewSpecialWarningDispel(text, optionDefault, ...)
9806 return newSpecialWarning(self, "dispel", text, nil, optionDefault, ...)
9807 end
9808
9809 function bossModPrototype:NewSpecialWarningInterrupt(text, optionDefault, ...)
9810 return newSpecialWarning(self, "interrupt", text, nil, optionDefault, ...)
9811 end
9812
9813 function bossModPrototype:NewSpecialWarningInterruptCount(text, optionDefault, ...)
9814 return newSpecialWarning(self, "interruptcount", text, nil, optionDefault, ...)
9815 end
9816
9817 function bossModPrototype:NewSpecialWarningYou(text, optionDefault, ...)
9818 return newSpecialWarning(self, "you", text, nil, optionDefault, ...)
9819 end
9820
9821 function bossModPrototype:NewSpecialWarningYouCount(text, optionDefault, ...)
9822 return newSpecialWarning(self, "youcount", text, nil, optionDefault, ...)
9823 end
9824
9825 function bossModPrototype:NewSpecialWarningYouPos(text, optionDefault, ...)
9826 return newSpecialWarning(self, "youpos", text, nil, optionDefault, ...)
9827 end
9828
9829 function bossModPrototype:NewSpecialWarningSoakPos(text, optionDefault, ...)
9830 return newSpecialWarning(self, "soakpos", text, nil, optionDefault, ...)
9831 end
9832
9833 function bossModPrototype:NewSpecialWarningTarget(text, optionDefault, ...)
9834 return newSpecialWarning(self, "target", text, nil, optionDefault, ...)
9835 end
9836
9837 function bossModPrototype:NewSpecialWarningTargetCount(text, optionDefault, ...)
9838 return newSpecialWarning(self, "targetcount", text, nil, optionDefault, ...)
9839 end
9840
9841 function bossModPrototype:NewSpecialWarningDefensive(text, optionDefault, ...)
9842 return newSpecialWarning(self, "defensive", text, nil, optionDefault, ...)
9843 end
9844
9845 function bossModPrototype:NewSpecialWarningTaunt(text, optionDefault, ...)
9846 return newSpecialWarning(self, "taunt", text, nil, optionDefault, ...)
9847 end
9848
9849 function bossModPrototype:NewSpecialWarningClose(text, optionDefault, ...)
9850 return newSpecialWarning(self, "close", text, nil, optionDefault, ...)
9851 end
9852
9853 function bossModPrototype:NewSpecialWarningMove(text, optionDefault, ...)
9854 return newSpecialWarning(self, "move", text, nil, optionDefault, ...)
9855 end
9856
9857 function bossModPrototype:NewSpecialWarningGTFO(text, optionDefault, ...)
9858 return newSpecialWarning(self, "gtfo", text, nil, optionDefault, ...)
9859 end
9860
9861 function bossModPrototype:NewSpecialWarningDodge(text, optionDefault, ...)
9862 return newSpecialWarning(self, "dodge", text, nil, optionDefault, ...)
9863 end
9864
9865 function bossModPrototype:NewSpecialWarningDodgeLoc(text, optionDefault, ...)
9866 return newSpecialWarning(self, "dodgeloc", text, nil, optionDefault, ...)
9867 end
9868
9869 function bossModPrototype:NewSpecialWarningMoveAway(text, optionDefault, ...)
9870 return newSpecialWarning(self, "moveaway", text, nil, optionDefault, ...)
9871 end
9872
9873 function bossModPrototype:NewSpecialWarningMoveTo(text, optionDefault, ...)
9874 return newSpecialWarning(self, "moveto", text, nil, optionDefault, ...)
9875 end
9876
9877 function bossModPrototype:NewSpecialWarningJump(text, optionDefault, ...)
9878 return newSpecialWarning(self, "jump", text, nil, optionDefault, ...)
9879 end
9880
9881 function bossModPrototype:NewSpecialWarningRun(text, optionDefault, ...)
9882 return newSpecialWarning(self, "run", text, nil, optionDefault, ...)
9883 end
9884
9885 function bossModPrototype:NewSpecialWarningCast(text, optionDefault, ...)
9886 return newSpecialWarning(self, "cast", text, nil, optionDefault, ...)
9887 end
9888
9889 function bossModPrototype:NewSpecialWarningLookAway(text, optionDefault, ...)
9890 return newSpecialWarning(self, "lookaway", text, nil, optionDefault, ...)
9891 end
9892
9893 function bossModPrototype:NewSpecialWarningReflect(text, optionDefault, ...)
9894 return newSpecialWarning(self, "reflect", text, nil, optionDefault, ...)
9895 end
9896
9897 function bossModPrototype:NewSpecialWarningCount(text, optionDefault, ...)
9898 return newSpecialWarning(self, "count", text, nil, optionDefault, ...)
9899 end
9900
9901 function bossModPrototype:NewSpecialWarningStack(text, optionDefault, stacks, ...)
9902 if type(text) == "string" and text:match("OptionVersion") then
9903 print("NewSpecialWarning: you must provide remove optionversion hack for "..optionDefault)
9904 end
9905 return newSpecialWarning(self, "stack", text, stacks, optionDefault, ...)
9906 end
9907
9908 function bossModPrototype:NewSpecialWarningSwitch(text, optionDefault, ...)
9909 return newSpecialWarning(self, "switch", text, nil, optionDefault, ...)
9910 end
9911
9912 function bossModPrototype:NewSpecialWarningSwitchCount(text, optionDefault, ...)
9913 return newSpecialWarning(self, "switchcount", text, nil, optionDefault, ...)
9914 end
9915
9916 function bossModPrototype:NewSpecialWarningAdds(text, optionDefault, ...)
9917 return newSpecialWarning(self, "Adds", text, nil, optionDefault, ...)
9918 end
9919
9920 function bossModPrototype:NewSpecialWarningAddsCustom(text, optionDefault, ...)
9921 return newSpecialWarning(self, "Addscustom", text, nil, optionDefault, ...)
9922 end
9923
9924 function bossModPrototype:NewSpecialWarningPreWarn(text, optionDefault, time, ...)
9925 if type(text) == "string" and text:match("OptionVersion") then
9926 print("NewSpecialWarning: you must provide remove optionversion hack for "..optionDefault)
9927 end
9928 return newSpecialWarning(self, "prewarn", text, time, optionDefault, ...)
9929 end
9930
9931 function DBM:PlayCountSound(number, forceVoice)
9932 if number > 10 then return end
9933 local voice
9934 if forceVoice then--For options example
9935 voice = forceVoice
9936 else
9937 voice = self.Options.CountdownVoice
9938 end
9939 local path
9940 local maxCount = 5
9941 for i = 1, #self.Counts do
9942 if self.Counts[i].value == voice then
9943 path = self.Counts[i].path
9944 maxCount = self.Counts[i].max
9945 break
9946 end
9947 end
9948 if not path or (number > maxCount) then return end
9949 self:PlaySoundFile(path..number..".ogg")
9950 end
9951
9952 function DBM:RegisterCountSound(t, v, p, m)
9953 --Prevent duplicate insert
9954 for i = 1, #self.Counts do
9955 if self.Counts[i].value == v then return end
9956 end
9957 --Insert into counts table.
9958 if t and v and p and m then
9959 tinsert(self.Counts, { text = t, value = v, path = p, max = m })
9960 end
9961 end
9962
9963 function DBM:CheckVoicePackVersion(value)
9964 local activeVP = self.Options.ChosenVoicePack
9965 --Check if voice pack out of date
9966 if activeVP ~= "None" and activeVP == value then
9967 if self.VoiceVersions[value] < 7 then--Version will be bumped when new voice packs released that contain new voices.
9968 if not self.Options.DontShowReminders then
9969 self:AddMsg(DBM_CORE_VOICE_PACK_OUTDATED)
9970 end
9971 SWFilterDisabed = self.VoiceVersions[value]--Set disable to version on current voice pack
9972 else
9973 SWFilterDisabed = 7
9974 end
9975 end
9976 end
9977
9978 function DBM:PlaySpecialWarningSound(soundId)
9979 local sound = type(soundId) == "number" and self.Options["SpecialWarningSound" .. (soundId == 1 and "" or soundId)] or soundId or self.Options.SpecialWarningSound
9980 self:PlaySoundFile(sound)
9981 end
9982
9983 local function testWarningEnd()
9984 frame:SetFrameStrata("HIGH")
9985 end
9986
9987 function DBM:ShowTestSpecialWarning(text, number, noSound)
9988 if moving then
9989 return
9990 end
9991 self:AddSpecialWarning(DBM_CORE_MOVE_SPECIAL_WARNING_TEXT)
9992 frame:SetFrameStrata("TOOLTIP")
9993 self:Unschedule(testWarningEnd)
9994 self:Schedule(self.Options.SpecialWarningDuration2 * 1.3, testWarningEnd)
9995 if number and not noSound then
9996 self:PlaySpecialWarningSound(number)
9997 end
9998 if self.Options.ShowFlashFrame and number then
9999 local flashColor = self.Options["SpecialWarningFlashCol"..number]
10000 local repeatCount = self.Options["SpecialWarningFlashRepeat"..number] and self.Options.SpecialWarningFlashRepeatAmount or 0
10001 self.Flash:Show(flashColor[1], flashColor[2], flashColor[3], self.Options["SpecialWarningFlashDura"..number], self.Options["SpecialWarningFlashAlph"..number], repeatCount)
10002 end
10003 end
10004end
10005
10006--------------------
10007-- Timer Object --
10008--------------------
10009do
10010 local timerPrototype = {}
10011 local mt = {__index = timerPrototype}
10012
10013 function timerPrototype:Start(timer, ...)
10014 if DBM.Options.DontShowBossTimers then return end
10015 if timer and type(timer) ~= "number" then
10016 return self:Start(nil, timer, ...) -- first argument is optional!
10017 end
10018 if not self.option or self.mod.Options[self.option] then
10019 if self.type and self.type:find("count") and not self.allowdouble then--cdcount, nextcount. remove previous timer.
10020 for i = #self.startedTimers, 1, -1 do
10021 if DBM.Options.AutoCorrectTimer or (DBM.Options.DebugMode and DBM.Options.DebugLevel > 1) then
10022 local bar = DBM.Bars:GetBar(self.startedTimers[i])
10023 if bar then
10024 local remaining = ("%.1f"):format(bar.timer)
10025 local ttext = _G[bar.frame:GetName().."BarName"]:GetText() or ""
10026 ttext = ttext.."("..self.id..")"
10027 if bar.timer > 0.2 then
10028 if timer then
10029 self.correctedCast = timer - bar.timer--Store what lowest timer is in timer object
10030 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)
10031 end
10032 DBM:Debug("Timer "..ttext.. " refreshed before expired. Remaining time is : "..remaining, 2)
10033 end
10034 end
10035 end
10036 DBM.Bars:CancelBar(self.startedTimers[i])
10037 fireEvent("DBM_TimerStop", self.startedTimers[i])
10038 self.startedTimers[i] = nil
10039 end
10040 end
10041 local timer = timer and ((timer > 0 and timer) or self.timer + timer) or self.timer
10042 --AI timer api:
10043 --Starting ai timer with (1) indicates it's a first timer after pull
10044 --Starting timer with (2) or (3) indicates it's a stage 2 or stage 3 first timer
10045 --Starting AI timer with anything above 3 indicarets it's a regular timer and to use shortest time in between two regular casts
10046 if self.type == "ai" then--A learning timer
10047 if not DBM.Options.AITimer then return end
10048 if timer > 4 then--Normal behavior.
10049 local newPhase = false
10050 for i = 1, 4 do
10051 --Check for any phase timers that are strings, if a string it means last cast of this ability was first case of a given stage
10052 if self["phase"..i.."CastTimer"] and type(self["phase"..i.."CastTimer"]) == "string" then--This is first cast of spell, we need to generate self.firstPullTimer
10053 self["phase"..i.."CastTimer"] = tonumber(self["phase"..i.."CastTimer"])
10054 self["phase"..i.."CastTimer"] = GetTime() - self["phase"..i.."CastTimer"]--We have generated a self.phase1CastTimer! Next pull, DBM should know timer for first cast next pull. FANCY!
10055 DBM:Debug("AI timer learned a first timer for current phase of "..self["phase"..i.."CastTimer"], 2)
10056 newPhase = true
10057 end
10058 end
10059 if self.lastCast and not newPhase then--We have a GetTime() on last cast and it's not affected by a phase change
10060 local timeLastCast = GetTime() - self.lastCast--Get time between current cast and last cast
10061 if timeLastCast > 4 then--Prevent infinite loop cpu hang. Plus anything shorter than 5 seconds doesn't need a timer
10062 if not self.lowestSeenCast or (self.lowestSeenCast and self.lowestSeenCast > timeLastCast) then--Always use lowest seen cast for a timer
10063 self.lowestSeenCast = timeLastCast
10064 DBM:Debug("AI timer learned a new lowest timer of "..self.lowestSeenCast, 2)
10065 end
10066 end
10067 end
10068 self.lastCast = GetTime()
10069 if self.lowestSeenCast then--Always use lowest seen cast for timer
10070 timer = self.lowestSeenCast
10071 else
10072 return--Don't start the bogus timer shoved into timer field in the mod
10073 end
10074 else--AI timer passed with 4 or less is indicating phase change, with timer as phase number
10075 if self["phase"..timer.."CastTimer"] and type(self["phase"..timer.."CastTimer"]) == "number" then
10076 timer = self["phase"..timer.."CastTimer"]
10077 else--No first pull timer generated yet, set it to GetTime, as a string
10078 self["phase"..timer.."CastTimer"] = tostring(GetTime())
10079 return--Don't start the x second timer
10080 end
10081 end
10082 end
10083 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
10084 if DBM.Options.AutoCorrectTimer or (DBM.Options.DebugMode and DBM.Options.DebugLevel > 1) then
10085 if not self.type or (self.type ~= "target" and self.type ~= "active" and self.type ~= "fades" and self.type ~= "ai") then
10086 local bar = DBM.Bars:GetBar(id)
10087 if bar then
10088 local remaining = ("%.1f"):format(bar.timer)
10089 local ttext = _G[bar.frame:GetName().."BarName"]:GetText() or ""
10090 ttext = ttext.."("..self.id..")"
10091 if bar.timer > 0.2 then
10092 self.correctedCast = timer - bar.timer--Store what lowest timer is for advanced user feature
10093 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
10094 DBM:Debug("Timer "..ttext.. " refreshed before expired. Remaining time is : "..remaining, 2)
10095 end
10096 end
10097 end
10098 end
10099 if DBM.Options.AutoCorrectTimer and self.correctedCast and self.correctedDiff and self.correctedDiff == difficultyIndex and self.correctedCast < timer then
10100 local debugtemp = timer - self.correctedCast
10101 DBM:Debug("Timer autocorrected by "..debugtemp, 2)
10102 timer = self.correctedCast
10103 end
10104 local colorId = 0
10105 if self.option then
10106 colorId = self.mod.Options[self.option .. "TColor"]
10107 elseif self.colorType and type(self.colorType) == "string" then--No option for specific timer, but another bool option given that tells us where to look for TColor
10108 colorId = self.mod.Options[self.colorType .. "TColor"] or 0
10109 end
10110 local bar = DBM.Bars:CreateBar(timer, id, self.icon, nil, nil, nil, nil, colorId)
10111 if not bar then
10112 return false, "error" -- creating the timer failed somehow, maybe hit the hard-coded timer limit of 15
10113 end
10114 local msg = ""
10115 if self.type and not self.text then
10116 msg = pformat(self.mod:GetLocalizedTimerText(self.type, self.spellId, self.name), ...)
10117 else
10118 if type(self.text) == "number" then
10119 msg = pformat(self.mod:GetLocalizedTimerText(self.type, self.text, self.name), ...)
10120 else
10121 msg = pformat(self.text, ...)
10122 end
10123 end
10124 msg = msg:gsub(">.-<", stripServerName)
10125 bar:SetText(msg, self.inlineIcon)
10126 --ID: Internal DBM timer ID
10127 --msg: Timer Text
10128 --timer: Raw timer value (number).
10129 --Icon: Texture Path for Icon
10130 --type: Timer type (Cooldowns: cd, cdcount, nextcount, nextsource, cdspecial, nextspecial, stage, ai. Durations: target, active, fades, roleplay. Casting: cast)
10131 --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
10132 --colorID: Type classification (1-Add, 2-Aoe, 3-targeted ability, 4-Interrupt, 5-Role, 6-Stage, 7-User(custom))
10133 --Mod ID: Encounter ID as string, or a generic string for mods that don't have encounter ID (such as trash, dummy/test mods)
10134 fireEvent("DBM_TimerStart", id, msg, timer, self.icon, self.type, self.spellId, colorId, self.mod.id)
10135 tinsert(self.startedTimers, id)
10136 self.mod:Unschedule(removeEntry, self.startedTimers, id)
10137 self.mod:Schedule(timer, removeEntry, self.startedTimers, id)
10138 return bar
10139 else
10140 return false, "disabled"
10141 end
10142 end
10143 timerPrototype.Show = timerPrototype.Start
10144
10145 function timerPrototype:DelayedStart(delay, ...)
10146 unschedule(self.Start, self.mod, self, ...)
10147 schedule(delay or 0.5, self.Start, self.mod, self, ...)
10148 end
10149 timerPrototype.DelayedShow = timerPrototype.DelayedStart
10150
10151 function timerPrototype:Schedule(t, ...)
10152 return schedule(t, self.Start, self.mod, self, ...)
10153 end
10154
10155 function timerPrototype:Unschedule(...)
10156 return unschedule(self.Start, self.mod, self, ...)
10157 end
10158
10159 function timerPrototype:Stop(...)
10160 if select("#", ...) == 0 then
10161 for i = #self.startedTimers, 1, -1 do
10162 fireEvent("DBM_TimerStop", self.startedTimers[i])
10163 DBM.Bars:CancelBar(self.startedTimers[i])
10164 self.startedTimers[i] = nil
10165 end
10166 else
10167 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
10168 for i = #self.startedTimers, 1, -1 do
10169 if self.startedTimers[i] == id then
10170 fireEvent("DBM_TimerStop", id)
10171 DBM.Bars:CancelBar(id)
10172 tremove(self.startedTimers, i)
10173 end
10174 end
10175 end
10176 if self.type == "ai" then--A learning timer
10177 if not DBM.Options.AITimer then return end
10178 for i = 1, 4 do
10179 --Check for any phase timers that are strings and never got a chance to become AI timers, then wipe them
10180 if self["phase"..i.."CastTimer"] and type(self["phase"..i.."CastTimer"]) == "string" then
10181 self["phase"..i.."CastTimer"] = nil
10182 DBM:Debug("Wiping incomplete new timer of stage "..i, 2)
10183 end
10184 end
10185 end
10186 end
10187
10188 function timerPrototype:Cancel(...)
10189 self:Stop(...)
10190 self:Unschedule(...)
10191 end
10192
10193 function timerPrototype:GetTime(...)
10194 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
10195 local bar = DBM.Bars:GetBar(id)
10196 return bar and (bar.totalTime - bar.timer) or 0, (bar and bar.totalTime) or 0
10197 end
10198
10199 function timerPrototype:GetRemaining(...)
10200 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
10201 local bar = DBM.Bars:GetBar(id)
10202 return bar and bar.timer or 0
10203 end
10204
10205 function timerPrototype:IsStarted(...)
10206 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
10207 local bar = DBM.Bars:GetBar(id)
10208 return bar and true
10209 end
10210
10211 function timerPrototype:SetTimer(timer)
10212 self.timer = timer
10213 end
10214
10215 function timerPrototype:Update(elapsed, totalTime, ...)
10216 if DBM.Options.DontShowBossTimers then return end
10217 if self:GetTime(...) == 0 then
10218 self:Start(totalTime, ...)
10219 end
10220 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
10221 fireEvent("DBM_TimerUpdate", id, elapsed, totalTime)
10222 return DBM.Bars:UpdateBar(id, elapsed, totalTime)
10223 end
10224
10225 function timerPrototype:AddTime(extendAmount, ...)
10226 if DBM.Options.DontShowBossTimers then return end
10227 if self:GetTime(...) == 0 then
10228 return self:Start(extendAmount, ...)
10229 else
10230 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
10231 local bar = DBM.Bars:GetBar(id)
10232 if bar then
10233 local elapsed, total = (bar.totalTime - bar.timer), bar.totalTime
10234 if elapsed and total then
10235 fireEvent("DBM_TimerUpdate", id, elapsed, total+extendAmount)
10236 return DBM.Bars:UpdateBar(id, elapsed, total+extendAmount)
10237 end
10238 end
10239 end
10240 end
10241
10242 function timerPrototype:UpdateIcon(icon, ...)
10243 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
10244 local bar = DBM.Bars:GetBar(id)
10245 if bar then
10246 return bar:SetIcon((type(icon) == "string" and icon:match("ej%d+") and select(4, DBM:EJ_GetSectionInfo(string.sub(icon, 3))) ~= "" and select(4, DBM:EJ_GetSectionInfo(string.sub(icon, 3)))) or (type(icon) == "number" and GetSpellTexture(icon)) or icon or "Interface\\Icons\\Spell_Nature_WispSplode")
10247 end
10248 end
10249
10250 function timerPrototype:UpdateName(name, ...)
10251 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
10252 local bar = DBM.Bars:GetBar(id)
10253 if bar then
10254 return bar:SetText(name, self.inlineIcon)
10255 end
10256 end
10257
10258 function timerPrototype:SetColor(c, ...)
10259 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
10260 local bar = DBM.Bars:GetBar(id)
10261 if bar then
10262 return bar:SetColor(c)
10263 end
10264 end
10265
10266 function timerPrototype:DisableEnlarge(...)
10267 local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
10268 local bar = DBM.Bars:GetBar(id)
10269 if bar then
10270 bar.small = true
10271 end
10272 end
10273
10274 function timerPrototype:AddOption(optionDefault, optionName, colorType)
10275 if optionName ~= false then
10276 self.option = optionName or self.id
10277 self.mod:AddBoolOption(self.option, optionDefault, "timer", nil, colorType)
10278 end
10279 end
10280
10281 function bossModPrototype:NewTimer(timer, name, icon, optionDefault, optionName, colorType, inlineIcon, r, g, b)
10282 if r and type(r) == "string" then
10283 DBM:Debug("|cffff0000r probably has inline icon in it and needs to be fixed for |r"..name..r)
10284 r = nil--Fix it for users
10285 end
10286 if inlineIcon and type(inlineIcon) == "number" then
10287 DBM:Debug("|cffff0000spellID texture path or colorType is in inlineIcon field and needs to be fixed for |r"..name..inlineIcon)
10288 inlineIcon = nil--Fix it for users
10289 end
10290 local icon = (type(icon) == "string" and icon:match("ej%d+") and select(4, DBM:EJ_GetSectionInfo(string.sub(icon, 3))) ~= "" and select(4, DBM:EJ_GetSectionInfo(string.sub(icon, 3)))) or (type(icon) == "number" and GetSpellTexture(icon)) or icon or "Interface\\Icons\\Spell_Nature_WispSplode"
10291 local obj = setmetatable(
10292 {
10293 text = self.localization.timers[name],
10294 timer = timer,
10295 id = name,
10296 icon = icon,
10297 colorType = colorType,
10298 inlineIcon = inlineIcon,
10299 r = r,
10300 g = g,
10301 b = b,
10302 startedTimers = {},
10303 mod = self,
10304 },
10305 mt
10306 )
10307 obj:AddOption(optionDefault, optionName, colorType)
10308 tinsert(self.timers, obj)
10309 return obj
10310 end
10311
10312 -- new constructor for the new auto-localized timer types
10313 -- note that the function might look unclear because it needs to handle different timer types, especially achievement timers need special treatment
10314 local function newTimer(self, timerType, timer, spellId, timerText, optionDefault, optionName, colorType, texture, inlineIcon, r, g, b)
10315 if type(timer) == "string" and timer:match("OptionVersion") then
10316 DBM:Debug("|cffff0000OptionVersion hack depricated, remove it from: |r"..spellId)
10317 return
10318 end
10319 if type(colorType) == "number" and colorType > 6 then
10320 DBM:Debug("|cffff0000texture is in the colorType arg for: |r"..spellId)
10321 end
10322 --Use option optionName for optionVersion as well, no reason to split.
10323 --This ensures that remaining arg positions match for auto generated and regular NewTimer
10324 local optionVersion
10325 if type(optionName) == "number" then
10326 optionVersion = optionName
10327 optionName = nil
10328 end
10329 local allowdouble
10330 if type(timer) == "string" and timer:match("d%d+") then
10331 allowdouble = true
10332 timer = tonumber(string.sub(timer, 2))
10333 end
10334 local spellName, icon
10335 local unparsedId = spellId
10336 if timerType == "achievement" then
10337 spellName = select(2, GetAchievementInfo(spellId))
10338 icon = type(texture) == "number" and select(10, GetAchievementInfo(texture)) or texture or spellId and select(10, GetAchievementInfo(spellId))
10339 elseif timerType == "cdspecial" or timerType == "nextspecial" or timerType == "stage" then
10340 icon = type(texture) == "number" and GetSpellTexture(texture) or texture or type(spellId) == "string" and select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3))) ~= "" and select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3))) or (type(spellId) == "number" and GetSpellTexture(spellId)) or "Interface\\Icons\\Spell_Nature_WispSplode"
10341 if timerType == "stage" then
10342 colorType = 6
10343 end
10344 elseif timerType == "roleplay" then
10345 icon = type(texture) == "number" and GetSpellTexture(texture) or texture or type(spellId) == "string" and select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3))) ~= "" and select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3))) or (type(spellId) == "number" and GetSpellTexture(spellId)) or "Interface\\Icons\\Spell_Holy_BorrowedTime"
10346 colorType = 6
10347 elseif timerType == "adds" or timerType == "addscustom" then
10348 icon = type(texture) == "number" and GetSpellTexture(texture) or texture or type(spellId) == "string" and select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3))) ~= "" and select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3))) or (type(spellId) == "number" and GetSpellTexture(spellId)) or "Interface\\Icons\\Spell_Nature_WispSplode"
10349 colorType = 1
10350 else
10351 if type(spellId) == "string" and spellId:match("ej%d+") then
10352 spellName = DBM:EJ_GetSectionInfo(string.sub(spellId, 3)) or ""
10353 else
10354 spellName = DBM:GetSpellInfo(spellId or 0)
10355 end
10356 if spellName then
10357 icon = type(texture) == "number" and GetSpellTexture(texture) or texture or type(spellId) == "string" and select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3))) ~= "" and select(4, DBM:EJ_GetSectionInfo(string.sub(spellId, 3))) or (type(spellId) == "number" and GetSpellTexture(spellId))
10358 else
10359 icon = nil
10360 end
10361 end
10362 spellName = spellName or tostring(spellId)
10363 local timerTextValue
10364 --If timertext is a number, accept it as a secondary auto translate spellid
10365 if timerText and type(timerText) == "number" and DBM.Options.ShortTimerText then
10366 timerTextValue = timerText
10367 else
10368 timerTextValue = self.localization.timers[timerText]
10369 end
10370 local id = "Timer"..(spellId or 0)..timerType..(optionVersion or "")
10371 local obj = setmetatable(
10372 {
10373 text = timerTextValue,
10374 type = timerType,
10375 spellId = spellId,
10376 name = spellName,
10377 timer = timer,
10378 id = id,
10379 icon = icon,
10380 colorType = colorType,
10381 inlineIcon = inlineIcon,
10382 r = r,
10383 g = g,
10384 b = b,
10385 allowdouble = allowdouble,
10386 startedTimers = {},
10387 mod = self,
10388 },
10389 mt
10390 )
10391 obj:AddOption(optionDefault, optionName, colorType)
10392 tinsert(self.timers, obj)
10393 -- todo: move the string creation to the GUI with SetFormattedString...
10394 if timerType == "achievement" then
10395 self.localization.options[id] = DBM_CORE_AUTO_TIMER_OPTIONS[timerType]:format(GetAchievementLink(spellId):gsub("%[(.+)%]", "%1"))
10396 elseif timerType == "cdspecial" or timerType == "nextspecial" or timerType == "stage" or timerType == "roleplay" then--Timers without spellid, generic
10397 self.localization.options[id] = DBM_CORE_AUTO_TIMER_OPTIONS[timerType]--Using more than 1 stage timer or more than 1 special timer will break this, fortunately you should NEVER use more than 1 of either in a mod
10398 else
10399 self.localization.options[id] = DBM_CORE_AUTO_TIMER_OPTIONS[timerType]:format(unparsedId)
10400 end
10401 return obj
10402 end
10403
10404 function bossModPrototype:NewTargetTimer(...)
10405 return newTimer(self, "target", ...)
10406 end
10407
10408 function bossModPrototype:NewBuffActiveTimer(...)
10409 return newTimer(self, "active", ...)
10410 end
10411
10412 function bossModPrototype:NewBuffFadesTimer(...)
10413 return newTimer(self, "fades", ...)
10414 end
10415
10416 function bossModPrototype:NewCastTimer(timer, ...)
10417 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 ;)
10418 local spellId = timer
10419 timer = select(4, DBM:GetSpellInfo(spellId)) or 1000 -- GetSpellInfo takes YOUR spell haste into account...WTF?
10420 local spellHaste = select(4, DBM:GetSpellInfo(53142)) / 10000 -- 53142 = Dalaran Portal, should have 10000 ms cast time
10421 timer = timer / spellHaste -- calculate the real cast time of the spell...
10422 return self:NewCastTimer(timer / 1000, spellId, ...)
10423 end
10424 return newTimer(self, "cast", timer, ...)
10425 end
10426
10427 function bossModPrototype:NewCastSourceTimer(timer, ...)
10428 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 ;)
10429 local spellId = timer
10430 timer = select(4, DBM:GetSpellInfo(spellId)) or 1000 -- GetSpellInfo takes YOUR spell haste into account...WTF?
10431 local spellHaste = select(4, DBM:GetSpellInfo(53142)) / 10000 -- 53142 = Dalaran Portal, should have 10000 ms cast time
10432 timer = timer / spellHaste -- calculate the real cast time of the spell...
10433 return self:NewCastSourceTimer(timer / 1000, spellId, ...)
10434 end
10435 return newTimer(self, "castsource", timer, ...)
10436 end
10437
10438 function bossModPrototype:NewCDTimer(...)
10439 return newTimer(self, "cd", ...)
10440 end
10441
10442 function bossModPrototype:NewCDCountTimer(...)
10443 return newTimer(self, "cdcount", ...)
10444 end
10445
10446 function bossModPrototype:NewCDSourceTimer(...)
10447 return newTimer(self, "cdsource", ...)
10448 end
10449
10450 function bossModPrototype:NewNextTimer(...)
10451 return newTimer(self, "next", ...)
10452 end
10453
10454 function bossModPrototype:NewNextCountTimer(...)
10455 return newTimer(self, "nextcount", ...)
10456 end
10457
10458 function bossModPrototype:NewNextSourceTimer(...)
10459 return newTimer(self, "nextsource", ...)
10460 end
10461
10462 function bossModPrototype:NewAchievementTimer(...)
10463 return newTimer(self, "achievement", ...)
10464 end
10465
10466 function bossModPrototype:NewCDSpecialTimer(...)
10467 return newTimer(self, "cdspecial", ...)
10468 end
10469
10470 function bossModPrototype:NewNextSpecialTimer(...)
10471 return newTimer(self, "nextspecial", ...)
10472 end
10473
10474 function bossModPrototype:NewPhaseTimer(...)
10475 return newTimer(self, "stage", ...)
10476 end
10477
10478 function bossModPrototype:NewRPTimer(...)
10479 return newTimer(self, "roleplay", ...)
10480 end
10481
10482 function bossModPrototype:NewAddsTimer(...)
10483 return newTimer(self, "adds", ...)
10484 end
10485
10486 function bossModPrototype:NewAddsCustomTimer(...)
10487 return newTimer(self, "addscustom", ...)
10488 end
10489
10490 function bossModPrototype:NewAITimer(...)
10491 return newTimer(self, "ai", ...)
10492 end
10493
10494 function bossModPrototype:GetLocalizedTimerText(timerType, spellId, Name)
10495 local spellName
10496 if Name then
10497 spellName = Name--Pull from name stored in object
10498 elseif spellId then
10499 DBM:Debug("|cffff0000GetLocalizedTimerText fallback, this should not happen and is a bug. this fallback should be deleted if this message is never seen after async code is live|r")
10500 if timerType == "achievement" then
10501 spellName = select(2, GetAchievementInfo(spellId))
10502 elseif type(spellId) == "string" and spellId:match("ej%d+") then
10503 spellName = DBM:EJ_GetSectionInfo(string.sub(spellId, 3))
10504 else
10505 spellName = DBM:GetSpellInfo(spellId)
10506 end
10507 end
10508 return pformat(DBM_CORE_AUTO_TIMER_TEXTS[timerType], spellName)
10509 end
10510end
10511
10512------------------------------
10513-- Berserk/Combat Objects --
10514------------------------------
10515do
10516 local enragePrototype = {}
10517 local mt = {__index = enragePrototype}
10518
10519 function enragePrototype:Start(timer)
10520 timer = timer or self.timer or 600
10521 timer = timer <= 0 and self.timer - timer or timer
10522 self.bar:SetTimer(timer)
10523 self.bar:Start()
10524 if self.warning1 then
10525 if timer > 660 then self.warning1:Schedule(timer - 600, 10, DBM_CORE_MIN) end
10526 if timer > 300 then self.warning1:Schedule(timer - 300, 5, DBM_CORE_MIN) end
10527 if timer > 180 then self.warning2:Schedule(timer - 180, 3, DBM_CORE_MIN) end
10528 end
10529 if self.warning2 then
10530 if timer > 60 then self.warning2:Schedule(timer - 60, 1, DBM_CORE_MIN) end
10531 if timer > 30 then self.warning2:Schedule(timer - 30, 30, DBM_CORE_SEC) end
10532 if timer > 10 then self.warning2:Schedule(timer - 10, 10, DBM_CORE_SEC) end
10533 end
10534 if self.countdown then
10535 if not DBM.Options.DontPlayPTCountdown then
10536 self.countdown:Start(timer)
10537 end
10538 end
10539 end
10540
10541 function enragePrototype:Schedule(t)
10542 return self.owner:Schedule(t, self.Start, self)
10543 end
10544
10545 function enragePrototype:Cancel()
10546 self.owner:Unschedule(self.Start, self)
10547 if self.warning1 then
10548 self.warning1:Cancel()
10549 end
10550 if self.warning2 then
10551 self.warning2:Cancel()
10552 end
10553 if self.countdown then
10554 self.countdown:Cancel()
10555 end
10556 self.bar:Stop()
10557 end
10558 enragePrototype.Stop = enragePrototype.Cancel
10559
10560 function bossModPrototype:NewBerserkTimer(timer, text, barText, barIcon)
10561 timer = timer or 600
10562 local warning1 = self:NewAnnounce(text or DBM_CORE_GENERIC_WARNING_BERSERK, 1, nil, "warning_berserk", false)
10563 local warning2 = self:NewAnnounce(text or DBM_CORE_GENERIC_WARNING_BERSERK, 4, nil, "warning_berserk", false)
10564 local bar = self:NewTimer(timer, barText or DBM_CORE_GENERIC_TIMER_BERSERK, barIcon or 28131, nil, "timer_berserk")
10565 local obj = setmetatable(
10566 {
10567 warning1 = warning1,
10568 warning2 = warning2,
10569 bar = bar,
10570 timer = timer,
10571 owner = self
10572 },
10573 mt
10574 )
10575 return obj
10576 end
10577
10578 function bossModPrototype:NewCombatTimer(timer, text, barText, barIcon)
10579 timer = timer or 10
10580 local bar = self:NewTimer(timer, barText or DBM_CORE_GENERIC_TIMER_COMBAT, barIcon or "Interface\\Icons\\ability_warrior_offensivestance", nil, "timer_combat")
10581 local countdown = self:NewCountdown(0, 0, nil, false, nil, true)
10582 local obj = setmetatable(
10583 {
10584 bar = bar,
10585 timer = timer,
10586 countdown = countdown,
10587 owner = self
10588 },
10589 mt
10590 )
10591 return obj
10592 end
10593end
10594
10595---------------
10596-- Options --
10597---------------
10598function bossModPrototype:AddBoolOption(name, default, cat, func, extraOption)
10599 cat = cat or "misc"
10600 self.DefaultOptions[name] = (default == nil) or default
10601 if cat == "timer" then
10602 self.DefaultOptions[name.."TColor"] = extraOption or 0
10603 end
10604 if default and type(default) == "string" then
10605 default = self:GetRoleFlagValue(default)
10606 end
10607 self.Options[name] = (default == nil) or default
10608 if cat == "timer" then
10609 self.Options[name.."TColor"] = extraOption or 0
10610 end
10611 self:SetOptionCategory(name, cat)
10612 if func then
10613 self.optionFuncs = self.optionFuncs or {}
10614 self.optionFuncs[name] = func
10615 end
10616end
10617
10618function bossModPrototype:AddSpecialWarningOption(name, default, defaultSound, cat)
10619 cat = cat or "misc"
10620 self.DefaultOptions[name] = (default == nil) or default
10621 self.DefaultOptions[name.."SWSound"] = defaultSound or 1
10622 self.DefaultOptions[name.."SWNote"] = true
10623 if default and type(default) == "string" then
10624 default = self:GetRoleFlagValue(default)
10625 end
10626 self.Options[name] = (default == nil) or default
10627 self.Options[name.."SWSound"] = defaultSound or 1
10628 self.Options[name.."SWNote"] = true
10629 self:SetOptionCategory(name, cat)
10630end
10631
10632function bossModPrototype:AddSetIconOption(name, spellId, default, isHostile)
10633 self.DefaultOptions[name] = (default == nil) or default
10634 if default and type(default) == "string" then
10635 default = self:GetRoleFlagValue(default)
10636 end
10637 self.Options[name] = (default == nil) or default
10638 self:SetOptionCategory(name, "misc")
10639 if isHostile then
10640 if not self.findFastestComputer then
10641 self.findFastestComputer = {}
10642 end
10643 self.findFastestComputer[#self.findFastestComputer + 1] = name
10644 self.localization.options[name] = DBM_CORE_AUTO_ICONS_OPTION_TEXT2:format(spellId)
10645 else
10646 self.localization.options[name] = DBM_CORE_AUTO_ICONS_OPTION_TEXT:format(spellId)
10647 end
10648end
10649
10650function bossModPrototype:AddArrowOption(name, spellId, default, isRunTo)
10651 if isRunTo == true then isRunTo = 2 end--Support legacy
10652 self.DefaultOptions[name] = (default == nil) or default
10653 if default and type(default) == "string" then
10654 default = self:GetRoleFlagValue(default)
10655 end
10656 self.Options[name] = (default == nil) or default
10657 self:SetOptionCategory(name, "misc")
10658 if isRunTo == 2 then
10659 self.localization.options[name] = DBM_CORE_AUTO_ARROW_OPTION_TEXT:format(spellId)
10660 elseif isRunTo == 3 then
10661 self.localization.options[name] = DBM_CORE_AUTO_ARROW_OPTION_TEXT3:format(spellId)
10662 else
10663 self.localization.options[name] = DBM_CORE_AUTO_ARROW_OPTION_TEXT2:format(spellId)
10664 end
10665end
10666
10667function bossModPrototype:AddRangeFrameOption(range, spellId, default)
10668 self.DefaultOptions["RangeFrame"] = (default == nil) or default
10669 if default and type(default) == "string" then
10670 default = self:GetRoleFlagValue(default)
10671 end
10672 self.Options["RangeFrame"] = (default == nil) or default
10673 self:SetOptionCategory("RangeFrame", "misc")
10674 if spellId then
10675 self.localization.options["RangeFrame"] = DBM_CORE_AUTO_RANGE_OPTION_TEXT:format(range, spellId)
10676 else
10677 self.localization.options["RangeFrame"] = DBM_CORE_AUTO_RANGE_OPTION_TEXT_SHORT:format(range)
10678 end
10679end
10680
10681function bossModPrototype:AddHudMapOption(name, spellId, default)
10682 self.DefaultOptions[name] = (default == nil) or default
10683 if default and type(default) == "string" then
10684 default = self:GetRoleFlagValue(default)
10685 end
10686 self.Options[name] = (default == nil) or default
10687 self:SetOptionCategory(name, "misc")
10688 if spellId then
10689 self.localization.options[name] = DBM_CORE_AUTO_HUD_OPTION_TEXT:format(spellId)
10690 else
10691 self.localization.options[name] = DBM_CORE_AUTO_HUD_OPTION_TEXT_MULTI
10692 end
10693end
10694
10695function bossModPrototype:AddNamePlateOption(name, spellId, default)
10696 if not spellId then
10697 error("AddNamePlateOption must provide valid spellId", 2)
10698 end
10699 self.DefaultOptions[name] = (default == nil) or default
10700 if default and type(default) == "string" then
10701 default = self:GetRoleFlagValue(default)
10702 end
10703 self.Options[name] = (default == nil) or default
10704 self:SetOptionCategory(name, "misc")
10705 self.localization.options[name] = DBM_CORE_AUTO_NAMEPLATE_OPTION_TEXT:format(spellId)
10706end
10707
10708function bossModPrototype:AddInfoFrameOption(spellId, default)
10709 self.DefaultOptions["InfoFrame"] = (default == nil) or default
10710 if default and type(default) == "string" then
10711 default = self:GetRoleFlagValue(default)
10712 end
10713 self.Options["InfoFrame"] = (default == nil) or default
10714 self:SetOptionCategory("InfoFrame", "misc")
10715 if spellId then
10716 self.localization.options["InfoFrame"] = DBM_CORE_AUTO_INFO_FRAME_OPTION_TEXT:format(spellId)
10717 else
10718 self.localization.options["InfoFrame"] = DBM_CORE_AUTO_INFO_FRAME_OPTION_TEXT2
10719 end
10720end
10721
10722function bossModPrototype:AddReadyCheckOption(questId, default)
10723 self.readyCheckQuestId = questId
10724 self.DefaultOptions["ReadyCheck"] = (default == nil) or default
10725 if default and type(default) == "string" then
10726 default = self:GetRoleFlagValue(default)
10727 end
10728 self.Options["ReadyCheck"] = (default == nil) or default
10729 self.localization.options["ReadyCheck"] = DBM_CORE_AUTO_READY_CHECK_OPTION_TEXT
10730 self:SetOptionCategory("ReadyCheck", "misc")
10731end
10732
10733function bossModPrototype:AddSliderOption(name, minValue, maxValue, valueStep, default, cat, func)
10734 cat = cat or "misc"
10735 self.DefaultOptions[name] = {type = "slider", value = default or 0}
10736 self.Options[name] = default or 0
10737 self:SetOptionCategory(name, cat)
10738 self.sliders = self.sliders or {}
10739 self.sliders[name] = {
10740 minValue = minValue,
10741 maxValue = maxValue,
10742 valueStep = valueStep,
10743 }
10744 if func then
10745 self.optionFuncs = self.optionFuncs or {}
10746 self.optionFuncs[name] = func
10747 end
10748end
10749
10750function bossModPrototype:AddButton(name, onClick, cat, func)
10751 cat = cat or "misc"
10752 self:SetOptionCategory(name, cat)
10753 self.buttons = self.buttons or {}
10754 self.buttons[name] = onClick
10755 if func then
10756 self.optionFuncs = self.optionFuncs or {}
10757 self.optionFuncs[name] = func
10758 end
10759end
10760
10761-- 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
10762-- this will be fixed as soon as it is necessary due to removed options ;-)
10763function bossModPrototype:AddDropdownOption(name, options, default, cat, func)
10764 cat = cat or "misc"
10765 self.DefaultOptions[name] = {type = "dropdown", value = default}
10766 self.Options[name] = default
10767 self:SetOptionCategory(name, cat)
10768 self.dropdowns = self.dropdowns or {}
10769 self.dropdowns[name] = options
10770 if func then
10771 self.optionFuncs = self.optionFuncs or {}
10772 self.optionFuncs[name] = func
10773 end
10774end
10775
10776function bossModPrototype:AddOptionSpacer(cat)
10777 cat = cat or "misc"
10778 if self.optionCategories[cat] then
10779 tinsert(self.optionCategories[cat], DBM_OPTION_SPACER)
10780 end
10781end
10782
10783function bossModPrototype:AddOptionLine(text, cat)
10784 cat = cat or "misc"
10785 if not self.optionCategories[cat] then
10786 self.optionCategories[cat] = {}
10787 end
10788 if self.optionCategories[cat] then
10789 tinsert(self.optionCategories[cat], {line = true, text = text})
10790 end
10791end
10792
10793function bossModPrototype:AddAnnounceSpacer()
10794 return self:AddOptionSpacer("announce")
10795end
10796
10797function bossModPrototype:AddTimerSpacer()
10798 return self:AddOptionSpacer("timer")
10799end
10800
10801function bossModPrototype:AddAnnounceLine(text)
10802 return self:AddOptionLine(text, "announce")
10803end
10804
10805function bossModPrototype:AddTimerLine(text)
10806 return self:AddOptionLine(text, "timer")
10807end
10808
10809function bossModPrototype:AddMiscLine(text)
10810 return self:AddOptionLine(text, "misc")
10811end
10812
10813function bossModPrototype:RemoveOption(name)
10814 self.Options[name] = nil
10815 for i, options in pairs(self.optionCategories) do
10816 removeEntry(options, name)
10817 if #options == 0 then
10818 self.optionCategories[i] = nil
10819 end
10820 end
10821 if self.optionFuncs then
10822 self.optionFuncs[name] = nil
10823 end
10824end
10825
10826function bossModPrototype:SetOptionCategory(name, cat)
10827 for _, options in pairs(self.optionCategories) do
10828 removeEntry(options, name)
10829 end
10830 if not self.optionCategories[cat] then
10831 self.optionCategories[cat] = {}
10832 end
10833 tinsert(self.optionCategories[cat], name)
10834end
10835
10836--------------
10837-- Combat --
10838--------------
10839function bossModPrototype:RegisterCombat(cType, ...)
10840 if cType then
10841 cType = cType:lower()
10842 end
10843 local info = {
10844 type = cType,
10845 mob = self.creatureId,
10846 eId = self.encounterId,
10847 name = self.localization.general.name or self.id,
10848 msgs = (cType ~= "combat") and {...},
10849 mod = self
10850 }
10851 if self.multiMobPullDetection then
10852 info.multiMobPullDetection = self.multiMobPullDetection
10853 end
10854 if self.multiEncounterPullDetection then
10855 info.multiEncounterPullDetection = self.multiEncounterPullDetection
10856 end
10857 if self.noESDetection then
10858 info.noESDetection = self.noESDetection
10859 end
10860 if self.noEEDetection then
10861 info.noEEDetection = self.noEEDetection
10862 end
10863 if self.noRegenDetection then
10864 info.noRegenDetection = self.noRegenDetection
10865 end
10866 if self.noWBEsync then
10867 info.noWBEsync = self.noWBEsync
10868 end
10869 -- use pull-mobs as kill mobs by default, can be overriden by RegisterKill
10870 if self.multiMobPullDetection then
10871 for i, v in ipairs(self.multiMobPullDetection) do
10872 info.killMobs = info.killMobs or {}
10873 info.killMobs[v] = true
10874 end
10875 end
10876 self.combatInfo = info
10877 if not self.zones then return end
10878 for v in pairs(self.zones) do
10879 combatInfo[v] = combatInfo[v] or {}
10880 tinsert(combatInfo[v], info)
10881 end
10882end
10883
10884-- needs to be called _AFTER_ RegisterCombat
10885function bossModPrototype:RegisterKill(msgType, ...)
10886 if not self.combatInfo then
10887 error("mod.combatInfo not yet initialized, use mod:RegisterCombat before using this method", 2)
10888 end
10889 if msgType == "kill" then
10890 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
10891 self.combatInfo.killMobs = {}
10892 for i = 1, select("#", ...) do
10893 local v = select(i, ...)
10894 if type(v) == "number" then
10895 self.combatInfo.killMobs[v] = true
10896 end
10897 end
10898 end
10899 else
10900 self.combatInfo.killType = msgType
10901 self.combatInfo.killMsgs = {}
10902 for i = 1, select("#", ...) do
10903 local v = select(i, ...)
10904 self.combatInfo.killMsgs[v] = true
10905 end
10906 end
10907end
10908
10909function bossModPrototype:SetDetectCombatInVehicle(flag)
10910 if not self.combatInfo then
10911 error("mod.combatInfo not yet initialized, use mod:RegisterCombat before using this method", 2)
10912 end
10913 self.combatInfo.noCombatInVehicle = not flag
10914end
10915
10916function bossModPrototype:SetCreatureID(...)
10917 self.creatureId = ...
10918 if select("#", ...) > 1 then
10919 self.multiMobPullDetection = {...}
10920 if self.combatInfo then
10921 self.combatInfo.multiMobPullDetection = self.multiMobPullDetection
10922 if self.inCombat then
10923 --Called mid combat, fix some variables
10924 self.numBoss = #self.multiMobPullDetection
10925 self.vb.bossLeft = self.numBoss
10926 end
10927 end
10928 for i = 1, select("#", ...) do
10929 local cId = select(i, ...)
10930 bossIds[cId] = true
10931 end
10932 else
10933 local cId = ...
10934 bossIds[cId] = true
10935 end
10936end
10937
10938function bossModPrototype:SetEncounterID(...)
10939 self.encounterId = ...
10940 if select("#", ...) > 1 then
10941 self.multiEncounterPullDetection = {...}
10942 if self.combatInfo then
10943 self.combatInfo.multiEncounterPullDetection = self.multiEncounterPullDetection
10944 end
10945 end
10946end
10947
10948function bossModPrototype:DisableESCombatDetection()
10949 self.noESDetection = true
10950 if self.combatInfo then
10951 self.combatInfo.noESDetection = true
10952 end
10953end
10954
10955function bossModPrototype:DisableEEKillDetection()
10956 self.noEEDetection = true
10957 if self.combatInfo then
10958 self.combatInfo.noEEDetection = true
10959 end
10960end
10961
10962function bossModPrototype:DisableRegenDetection()
10963 self.noRegenDetection = true
10964 if self.combatInfo then
10965 self.combatInfo.noRegenDetection = true
10966 end
10967end
10968
10969function bossModPrototype:DisableWBEngageSync()
10970 self.noWBEsync = true
10971 if self.combatInfo then
10972 self.combatInfo.noWBEsync = true
10973 end
10974end
10975
10976function bossModPrototype:IsInCombat()
10977 return self.inCombat
10978end
10979
10980function bossModPrototype:IsAlive()
10981 return not UnitIsDeadOrGhost("player")
10982end
10983
10984function bossModPrototype:SetMinCombatTime(t)
10985 self.minCombatTime = t
10986end
10987
10988-- needs to be called after RegisterCombat
10989function bossModPrototype:SetWipeTime(t)
10990 if not self.combatInfo then
10991 error("mod.combatInfo not yet initialized, use mod:RegisterCombat before using this method", 2)
10992 end
10993 self.combatInfo.wipeTimer = t
10994end
10995
10996-- fix for LFR ToES Tsulong combat detection bug after killed.
10997function bossModPrototype:SetReCombatTime(t, t2)--T1, after kill. T2 after wipe
10998 self.reCombatTime = t
10999 self.reCombatTime2 = t2
11000end
11001
11002function bossModPrototype:SetOOCBWComms()
11003 tinsert(oocBWComms, self)
11004end
11005
11006-----------------------
11007-- Synchronization --
11008-----------------------
11009function bossModPrototype:SendSync(event, ...)
11010 event = event or ""
11011 local arg = select("#", ...) > 0 and strjoin("\t", tostringall(...)) or ""
11012 local str = ("%s\t%s\t%s\t%s"):format(self.id, self.revision or 0, event, arg)
11013 local spamId = self.id .. event .. arg -- *not* the same as the sync string, as it doesn't use the revision information
11014 local time = GetTime()
11015 --Mod syncs are more strict and enforce latency threshold always.
11016 --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.
11017 if not modSyncSpam[spamId] or (time - modSyncSpam[spamId]) > 8 then
11018 self:ReceiveSync(event, nil, self.revision or 0, tostringall(...))
11019 sendSync("M", str)
11020 end
11021end
11022
11023function bossModPrototype:SendBigWigsSync(msg, extra)
11024 msg = "B^".. msg
11025 if extra then
11026 msg = msg .."^".. extra
11027 end
11028 if IsInGroup() then
11029 SendAddonMessage("BigWigs", msg, IsInGroup(2) and "INSTANCE_CHAT" or "RAID")
11030 end
11031end
11032
11033function bossModPrototype:ReceiveSync(event, sender, revision, ...)
11034 local spamId = self.id .. event .. strjoin("\t", ...)
11035 local time = GetTime()
11036 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
11037 modSyncSpam[spamId] = time
11038 -- we have to use the sender as last argument for compatibility reasons (stupid old API...)
11039 -- avoid table allocations for frequently used number of arguments
11040 if select("#", ...) <= 1 then
11041 -- syncs with no arguments have an empty argument (also for compatibility reasons)
11042 self:OnSync(event, ... or "", sender)
11043 elseif select("#", ...) == 2 then
11044 self:OnSync(event, ..., select(2, ...), sender)
11045 else
11046 local tmp = { ... }
11047 tmp[#tmp + 1] = sender
11048 self:OnSync(event, unpack(tmp))
11049 end
11050 end
11051end
11052
11053function bossModPrototype:SetRevision(revision)
11054 revision = tonumber(revision or "")
11055 if not revision then
11056 -- bad revision: either forgot the svn keyword or using git svn
11057 revision = DBM.Revision
11058 end
11059 self.revision = revision
11060end
11061
11062function bossModPrototype:SetMinSyncRevision(revision)
11063 self.minSyncRevision = revision
11064end
11065
11066function bossModPrototype:SetHotfixNoticeRev(revision)
11067 self.hotfixNoticeRev = revision
11068end
11069
11070-----------------
11071-- Scheduler --
11072-----------------
11073function bossModPrototype:Schedule(t, f, ...)
11074 return schedule(t, f, self, ...)
11075end
11076
11077function bossModPrototype:Unschedule(f, ...)
11078 return unschedule(f, self, ...)
11079end
11080
11081function bossModPrototype:ScheduleMethod(t, method, ...)
11082 if not self[method] then
11083 error(("Method %s does not exist"):format(tostring(method)), 2)
11084 end
11085 return self:Schedule(t, self[method], self, ...)
11086end
11087bossModPrototype.ScheduleEvent = bossModPrototype.ScheduleMethod
11088
11089function bossModPrototype:UnscheduleMethod(method, ...)
11090 if not self[method] then
11091 error(("Method %s does not exist"):format(tostring(method)), 2)
11092 end
11093 return self:Unschedule(self[method], self, ...)
11094end
11095bossModPrototype.UnscheduleEvent = bossModPrototype.UnscheduleMethod
11096
11097-------------
11098-- Icons --
11099-------------
11100
11101do
11102 local scanExpires = {}
11103 local addsIcon = {}
11104 local addsIconSet = {}
11105
11106 function bossModPrototype:SetIcon(target, icon, timer)
11107 if not target then return end--Fix a rare bug where target becomes nil at last second (end combat fires and clears targets)
11108 if DBM.Options.DontSetIcons or not enableIcons or DBM:GetRaidRank(playerName) == 0 then
11109 return
11110 end
11111 self:UnscheduleMethod("SetIcon", target)
11112 if type(icon) ~= "number" or type(target) ~= "string" then--icon/target probably backwards.
11113 DBM:Debug("|cffff0000SetIcon is being used impropperly. Check icon/target order|r")
11114 return--Fail silently instead of spamming icon lua errors if we screw up
11115 end
11116 icon = icon and icon >= 0 and icon <= 8 and icon or 8
11117 local uId = DBM:GetRaidUnitId(target)
11118 if uId and UnitIsUnit(uId, "player") and DBM:GetNumRealGroupMembers() < 2 then return end--Solo raid, no reason to put icon on yourself.
11119 if uId or UnitExists(target) then--target accepts uid, unitname both.
11120 uId = uId or target
11121 --save previous icon into a table.
11122 local oldIcon = self:GetIcon(uId) or 0
11123 if not self.iconRestore[uId] then
11124 self.iconRestore[uId] = oldIcon
11125 end
11126 --set icon
11127 if oldIcon ~= icon then--Don't set icon if it's already set to what we're setting it to
11128 SetRaidTarget(uId, self.iconRestore[uId] and icon == 0 and self.iconRestore[uId] or icon)
11129 end
11130 --schedule restoring old icon if timer enabled.
11131 if timer then
11132 self:ScheduleMethod(timer, "SetIcon", target, 0)
11133 end
11134 end
11135 end
11136
11137 do
11138 local iconSortTable = {}
11139 local iconSet = 0
11140
11141 local function sort_by_group(v1, v2)
11142 return DBM:GetRaidSubgroup(DBM:GetUnitFullName(v1)) < DBM:GetRaidSubgroup(DBM:GetUnitFullName(v2))
11143 end
11144
11145 local function clearSortTable()
11146 twipe(iconSortTable)
11147 iconSet = 0
11148 end
11149
11150 function bossModPrototype:SetIconByAlphaTable(returnFunc)
11151 tsort(iconSortTable)--Sorted alphabetically
11152 for i = 1, #iconSortTable do
11153 local target = iconSortTable[i]
11154 if i > 8 then
11155 DBM:Debug("|cffff0000Too many players to set icons, reconsider where using icons|r", 2)
11156 return
11157 end
11158 if not self.iconRestore[target] then
11159 local oldIcon = self:GetIcon(target) or 0
11160 self.iconRestore[target] = oldIcon
11161 end
11162 SetRaidTarget(target, i)--Icons match number in table in alpha sort
11163 if returnFunc then
11164 self[returnFunc](self, target, i)--Send icon and target to returnFunc. (Generally used by announce icon targets to raid chat feature)
11165 end
11166 end
11167 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.
11168 end
11169
11170 function bossModPrototype:SetAlphaIcon(delay, target, maxIcon, returnFunc)
11171 if not target then return end
11172 if DBM.Options.DontSetIcons or not enableIcons or DBM:GetRaidRank(playerName) == 0 then
11173 return
11174 end
11175 local uId = DBM:GetRaidUnitId(target)
11176 if uId or UnitExists(target) then--target accepts uid, unitname both.
11177 uId = uId or target
11178 local foundDuplicate = false
11179 for i = #iconSortTable, 1, -1 do
11180 if iconSortTable[i] == uId then
11181 foundDuplicate = true
11182 break
11183 end
11184 end
11185 if not foundDuplicate then
11186 iconSet = iconSet + 1
11187 tinsert(iconSortTable, uId)
11188 end
11189 self:UnscheduleMethod("SetIconByAlphaTable")
11190 if maxIcon and iconSet == maxIcon then
11191 self:SetIconByAlphaTable(returnFunc)
11192 elseif self:LatencyCheck() then--lag can fail the icons so we check it before allowing.
11193 self:ScheduleMethod(delay or 0.5, "SetIconByAlphaTable", returnFunc)
11194 end
11195 end
11196 end
11197
11198 function bossModPrototype:SetIconBySortedTable(startIcon, reverseIcon, returnFunc)
11199 tsort(iconSortTable, sort_by_group)
11200 local icon = startIcon or 1
11201 for i, v in ipairs(iconSortTable) do
11202 if not self.iconRestore[v] then
11203 local oldIcon = self:GetIcon(v) or 0
11204 self.iconRestore[v] = oldIcon
11205 end
11206 SetRaidTarget(v, icon)--do not use SetIcon function again. It already checked in SetSortedIcon function.
11207 if reverseIcon then
11208 icon = icon - 1
11209 else
11210 icon = icon + 1
11211 end
11212 if returnFunc then
11213 self[returnFunc](self, v, icon)--Send icon and target to returnFunc. (Generally used by announce icon targets to raid chat feature)
11214 end
11215 end
11216 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.
11217 end
11218
11219 function bossModPrototype:SetSortedIcon(delay, target, startIcon, maxIcon, reverseIcon, returnFunc)
11220 if not target then return end
11221 if DBM.Options.DontSetIcons or not enableIcons or DBM:GetRaidRank(playerName) == 0 then
11222 return
11223 end
11224 if not startIcon then startIcon = 1 end
11225 startIcon = startIcon and startIcon >= 0 and startIcon <= 8 and startIcon or 8
11226 local uId = DBM:GetRaidUnitId(target)
11227 if uId or UnitExists(target) then--target accepts uid, unitname both.
11228 uId = uId or target
11229 local foundDuplicate = false
11230 for i = #iconSortTable, 1, -1 do
11231 if iconSortTable[i] == uId then
11232 foundDuplicate = true
11233 break
11234 end
11235 end
11236 if not foundDuplicate then
11237 iconSet = iconSet + 1
11238 tinsert(iconSortTable, uId)
11239 end
11240 self:UnscheduleMethod("SetIconBySortedTable")
11241 if maxIcon and iconSet == maxIcon then
11242 self:SetIconBySortedTable(startIcon, reverseIcon, returnFunc)
11243 elseif self:LatencyCheck() then--lag can fail the icons so we check it before allowing.
11244 self:ScheduleMethod(delay or 0.5, "SetIconBySortedTable", startIcon, reverseIcon, returnFunc)
11245 end
11246 end
11247 end
11248 end
11249
11250 function bossModPrototype:GetIcon(uId)
11251 return UnitExists(uId) and GetRaidTargetIndex(uId)
11252 end
11253
11254 function bossModPrototype:RemoveIcon(target)
11255 return self:SetIcon(target, 0)
11256 end
11257
11258 function bossModPrototype:ClearIcons()
11259 if IsInRaid() then
11260 for i = 1, GetNumGroupMembers() do
11261 if UnitExists("raid"..i) and GetRaidTargetIndex("raid"..i) then
11262 SetRaidTarget("raid"..i, 0)
11263 end
11264 end
11265 else
11266 for i = 1, GetNumSubgroupMembers() do
11267 if UnitExists("party"..i) and GetRaidTargetIndex("party"..i) then
11268 SetRaidTarget("party"..i, 0)
11269 end
11270 end
11271 end
11272 end
11273
11274 function bossModPrototype:CanSetIcon(optionName)
11275 if canSetIcons[optionName] then
11276 return true
11277 end
11278 return false
11279 end
11280
11281 local mobUids = {"mouseover", "target", "boss1", "boss2", "boss3", "boss4", "boss5"}
11282 function bossModPrototype:ScanForMobs(creatureID, iconSetMethod, mobIcon, maxIcon, scanInterval, scanningTime, optionName, isFriendly, secondCreatureID, skipMarked)
11283 if not optionName then optionName = self.findFastestComputer[1] end
11284 if canSetIcons[optionName] then
11285 --Declare variables.
11286 local timeNow = GetTime()
11287 local creatureID = creatureID--This function must not be used to boss, so remove self.creatureId. Accepts cid, guid and cid table
11288 local iconSetMethod = iconSetMethod or 0--Set IconSetMethod -- 0: Descending / 1:Ascending / 2: Force Set / 9:Force Stop
11289 --With different scanID, this function can support multi scanning same time. Required for Nazgrim.
11290 local scanID = 0
11291 if type(creatureID) == "number" then
11292 scanID = creatureID --guid and table no not supports multi scanning. only cid supports multi scanning
11293 end
11294 if iconSetMethod == 9 then--Force stop scanning
11295 --clear variables
11296 scanExpires[scanID] = nil
11297 addsIcon[scanID] = nil
11298 addsIconSet[scanID] = nil
11299 return
11300 end
11301 if not addsIcon[scanID] then addsIcon[scanID] = mobIcon or 8 end
11302 if not addsIconSet[scanID] then addsIconSet[scanID] = 0 end
11303 if not scanExpires[scanID] then scanExpires[scanID] = timeNow + scanningTime end
11304 local maxIcon = maxIcon or 8 --We only have 8 icons.
11305 local isFriendly = isFriendly or false
11306 local secondCreatureID = secondCreatureID or 0
11307 local scanInterval = scanInterval or 0.2
11308 local scanningTime = scanningTime or 8
11309 --DO SCAN NOW
11310 for _, unitid2 in ipairs(mobUids) do
11311 local guid2 = UnitGUID(unitid2)
11312 local cid2 = self:GetCIDFromGUID(guid2)
11313 local isEnemy = UnitIsEnemy("player", unitid2) or true--If api returns nil, assume it's an enemy
11314 local isFiltered = false
11315 if (not isFriendly and not isEnemy) or (skipMarked and not GetRaidTargetIndex(unitid2)) then
11316 isFiltered = true
11317 DBM:Debug("ScanForMobs aborting because filtered mob", 2)
11318 end
11319 if not isFiltered then
11320 if guid2 and type(creatureID) == "table" and creatureID[cid2] and not addsGUIDs[guid2] then
11321 DBM:Debug("Match found, SHOULD be setting icon", 2)
11322 if type(creatureID[cid2]) == "number" then
11323 SetRaidTarget(unitid2, creatureID[cid2])
11324 else
11325 SetRaidTarget(unitid2, addsIcon[scanID])
11326 if iconSetMethod == 1 then
11327 addsIcon[scanID] = addsIcon[scanID] + 1
11328 else
11329 addsIcon[scanID] = addsIcon[scanID] - 1
11330 end
11331 end
11332 addsGUIDs[guid2] = true
11333 addsIconSet[scanID] = addsIconSet[scanID] + 1
11334 if addsIconSet[scanID] >= maxIcon then--stop scan immediately to save cpu
11335 --clear variables
11336 scanExpires[scanID] = nil
11337 addsIcon[scanID] = nil
11338 addsIconSet[scanID] = nil
11339 return
11340 end
11341 elseif guid2 and (guid2 == creatureID or cid2 == creatureID or cid2 == secondCreatureID) and not addsGUIDs[guid2] then
11342 DBM:Debug("Match found, SHOULD be setting icon", 2)
11343 if iconSetMethod == 2 then
11344 SetRaidTarget(unitid2, mobIcon)
11345 else
11346 SetRaidTarget(unitid2, addsIcon[scanID])
11347 if iconSetMethod == 1 then
11348 addsIcon[scanID] = addsIcon[scanID] + 1
11349 else
11350 addsIcon[scanID] = addsIcon[scanID] - 1
11351 end
11352 end
11353 addsGUIDs[guid2] = true
11354 addsIconSet[scanID] = addsIconSet[scanID] + 1
11355 if addsIconSet[scanID] >= maxIcon then--stop scan immediately to save cpu
11356 --clear variables
11357 scanExpires[scanID] = nil
11358 addsIcon[scanID] = nil
11359 addsIconSet[scanID] = nil
11360 return
11361 end
11362 end
11363 end
11364 end
11365 for uId in DBM:GetGroupMembers() do
11366 local unitid = uId.."target"
11367 local guid = UnitGUID(unitid)
11368 local cid = self:GetCIDFromGUID(guid)
11369 local isEnemy = UnitIsEnemy("player", unitid) or true--If api returns nil, assume it's an enemy
11370 local isFiltered = false
11371 if (not isFriendly and not isEnemy) or (skipMarked and not GetRaidTargetIndex(unitid)) then
11372 isFiltered = true
11373 DBM:Debug("ScanForMobs aborting because filtered mob", 2)
11374 end
11375 if not isFiltered then
11376 if guid and type(creatureID) == "table" and creatureID[cid] and not addsGUIDs[guid] then
11377 DBM:Debug("Match found, SHOULD be setting icon", 2)
11378 if type(creatureID[cid]) == "number" then
11379 SetRaidTarget(unitid, creatureID[cid])
11380 else
11381 SetRaidTarget(unitid, addsIcon[scanID])
11382 if iconSetMethod == 1 then
11383 addsIcon[scanID] = addsIcon[scanID] + 1
11384 else
11385 addsIcon[scanID] = addsIcon[scanID] - 1
11386 end
11387 end
11388 addsGUIDs[guid] = true
11389 addsIconSet[scanID] = addsIconSet[scanID] + 1
11390 if addsIconSet[scanID] >= maxIcon then--stop scan immediately to save cpu
11391 --clear variables
11392 scanExpires[scanID] = nil
11393 addsIcon[scanID] = nil
11394 addsIconSet[scanID] = nil
11395 return
11396 end
11397 elseif guid and (guid == creatureID or cid == creatureID or cid == secondCreatureID) and not addsGUIDs[guid] then
11398 DBM:Debug("Match found, SHOULD be setting icon", 2)
11399 if iconSetMethod == 2 then
11400 SetRaidTarget(unitid, mobIcon)
11401 else
11402 SetRaidTarget(unitid, addsIcon[scanID])
11403 if iconSetMethod == 1 then
11404 addsIcon[scanID] = addsIcon[scanID] + 1
11405 else
11406 addsIcon[scanID] = addsIcon[scanID] - 1
11407 end
11408 end
11409 addsGUIDs[guid] = true
11410 addsIconSet[scanID] = addsIconSet[scanID] + 1
11411 if addsIconSet[scanID] >= maxIcon then--stop scan immediately to save cpu
11412 --clear variables
11413 scanExpires[scanID] = nil
11414 addsIcon[scanID] = nil
11415 addsIconSet[scanID] = nil
11416 return
11417 end
11418 end
11419 end
11420 end
11421 if timeNow < scanExpires[scanID] then--scan for limited times.
11422 self:ScheduleMethod(scanInterval, "ScanForMobs", creatureID, iconSetMethod, mobIcon, maxIcon, scanInterval, scanningTime, optionName, isFriendly, secondCreatureID)
11423 else
11424 DBM:Debug("Stopping ScanForMobs for: "..(optionName or "nil"), 2)
11425 --clear variables
11426 scanExpires[scanID] = nil
11427 addsIcon[scanID] = nil
11428 addsIconSet[scanID] = nil
11429 --Do not wipe adds GUID table here, it's wiped by :Stop() which is called by EndCombat
11430 end
11431 else
11432 DBM:Debug("Not elected to set icons for "..(optionName or "nil"), 2)
11433 end
11434 end
11435end
11436
11437-----------------------
11438-- Model Functions --
11439-----------------------
11440function bossModPrototype:SetModelScale(scale)
11441 self.modelScale = scale
11442end
11443
11444function bossModPrototype:SetModelOffset(x, y, z)
11445 self.modelOffsetX = x
11446 self.modelOffsetY = y
11447 self.modelOffsetZ = z
11448end
11449
11450function bossModPrototype:SetModelRotation(r)
11451 self.modelRotation = r
11452end
11453
11454function bossModPrototype:SetModelMoveSpeed(v)
11455 self.modelMoveSpeed = v
11456end
11457
11458function bossModPrototype:SetModelID(id)
11459 self.modelId = id
11460end
11461
11462function 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.
11463 self.modelSoundLong = long
11464 self.modelSoundShort = short
11465end
11466
11467function bossModPrototype:EnableModel()
11468 self.modelEnabled = true
11469end
11470
11471function bossModPrototype:DisableModel()
11472 self.modelEnabled = nil
11473end
11474
11475--------------------
11476-- Localization --
11477--------------------
11478function bossModPrototype:GetLocalizedStrings()
11479 self.localization.miscStrings.name = self.localization.general.name
11480 return self.localization.miscStrings
11481end
11482
11483-- Not really good, needs a few updates
11484do
11485 local modLocalizations = {}
11486 local modLocalizationPrototype = {}
11487 local mt = {__index = modLocalizationPrototype}
11488 local returnKey = {__index = function(t, k) return k end}
11489 local defaultCatLocalization = {
11490 __index = setmetatable({
11491 timer = DBM_CORE_OPTION_CATEGORY_TIMERS,
11492 announce = DBM_CORE_OPTION_CATEGORY_WARNINGS,
11493 announceother = DBM_CORE_OPTION_CATEGORY_WARNINGS_OTHER,
11494 announcepersonal = DBM_CORE_OPTION_CATEGORY_WARNINGS_YOU,
11495 announcerole = DBM_CORE_OPTION_CATEGORY_WARNINGS_ROLE,
11496 sound = DBM_CORE_OPTION_CATEGORY_SOUNDS,
11497 misc = MISCELLANEOUS
11498 }, returnKey)
11499 }
11500 local defaultTimerLocalization = {
11501 __index = setmetatable({
11502 timer_berserk = DBM_CORE_GENERIC_TIMER_BERSERK,
11503 timer_combat = DBM_CORE_GENERIC_TIMER_COMBAT
11504 }, returnKey)
11505 }
11506 local defaultAnnounceLocalization = {
11507 __index = setmetatable({
11508 warning_berserk = DBM_CORE_GENERIC_WARNING_BERSERK
11509 }, returnKey)
11510 }
11511 local defaultOptionLocalization = {
11512 __index = setmetatable({
11513 timer_berserk = DBM_CORE_OPTION_TIMER_BERSERK,
11514 timer_combat = DBM_CORE_OPTION_TIMER_COMBAT,
11515 }, returnKey)
11516 }
11517 local defaultMiscLocalization = {
11518 __index = {}
11519 }
11520
11521 function modLocalizationPrototype:SetGeneralLocalization(t)
11522 for i, v in pairs(t) do
11523 self.general[i] = v
11524 end
11525 end
11526
11527 function modLocalizationPrototype:SetWarningLocalization(t)
11528 for i, v in pairs(t) do
11529 self.warnings[i] = v
11530 end
11531 end
11532
11533 function modLocalizationPrototype:SetTimerLocalization(t)
11534 for i, v in pairs(t) do
11535 self.timers[i] = v
11536 end
11537 end
11538
11539 function modLocalizationPrototype:SetOptionLocalization(t)
11540 for i, v in pairs(t) do
11541 self.options[i] = v
11542 end
11543 end
11544
11545 function modLocalizationPrototype:SetOptionCatLocalization(t)
11546 for i, v in pairs(t) do
11547 self.cats[i] = v
11548 end
11549 end
11550
11551 function modLocalizationPrototype:SetMiscLocalization(t)
11552 for i, v in pairs(t) do
11553 self.miscStrings[i] = v
11554 end
11555 end
11556
11557 function DBM:CreateModLocalization(name)
11558 name = tostring(name)
11559 local obj = {
11560 general = setmetatable({}, returnKey),
11561 warnings = setmetatable({}, defaultAnnounceLocalization),
11562 options = setmetatable({}, defaultOptionLocalization),
11563 timers = setmetatable({}, defaultTimerLocalization),
11564 miscStrings = setmetatable({}, defaultMiscLocalization),
11565 cats = setmetatable({}, defaultCatLocalization),
11566 }
11567 setmetatable(obj, mt)
11568 modLocalizations[name] = obj
11569 return obj
11570 end
11571
11572 function DBM:GetModLocalization(name)
11573 name = tostring(name)
11574 return modLocalizations[name] or self:CreateModLocalization(name)
11575 end
11576end