Guest User

Untitled

a guest
Jan 26th, 2017
446
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 428.19 KB | None | 0 0
  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. -------------------------------
  43. DBM = {
  44. Revision = tonumber(("$Revision: 15744 $"):sub(12, -3)),
  45. DisplayVersion = "7.1.12 alpha", -- the string that is shown as version
  46. ReleaseRevision = 15725 -- the revision of the latest stable version that is available
  47. }
  48. DBM.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
  52. if not DBM.Revision then
  53. DBM.Revision = DBM.ReleaseRevision
  54. end
  55.  
  56. local testBuild = false
  57. if IsTestBuild() then
  58. testBuild = true
  59. end
  60.  
  61. -- dual profile setup
  62. local _, class = UnitClass("player")
  63. DBM_UseDualProfile = true
  64. if class == "MAGE" or class == "WARLOCK" and class == "ROGUE" then
  65. DBM_UseDualProfile = false
  66. end
  67. DBM_CharSavedRevision = 2
  68.  
  69. --Hard code STANDARD_TEXT_FONT since skinning mods like to taint it (or worse, set it to nil, wtf?)
  70. local standardFont = STANDARD_TEXT_FONT
  71. if (LOCALE_koKR) then
  72. standardFont = "Fonts\\2002.TTF"
  73. elseif (LOCALE_zhCN) then
  74. standardFont = "Fonts\\ARKai_T.ttf"
  75. elseif (LOCALE_zhTW) then
  76. standardFont = "Fonts\\blei00d.TTF"
  77. elseif (LOCALE_ruRU) then
  78. standardFont = "Fonts\\FRIZQT___CYR.TTF"
  79. else
  80. standardFont = "Fonts\\FRIZQT__.TTF"
  81. end
  82.  
  83. DBM.DefaultOptions = {
  84. WarningColors = {
  85. {r = 0.41, g = 0.80, b = 0.94}, -- Color 1 - #69CCF0 - Turqoise
  86. {r = 0.95, g = 0.95, b = 0.00}, -- Color 2 - #F2F200 - Yellow
  87. {r = 1.00, g = 0.50, b = 0.00}, -- Color 3 - #FF8000 - Orange
  88. {r = 1.00, g = 0.10, b = 0.10}, -- Color 4 - #FF1A1A - Red
  89. },
  90. RaidWarningSound = "Sound\\Doodad\\BellTollNightElf.ogg",
  91. SpecialWarningSound = "Sound\\Spells\\PVPFlagTaken.ogg",
  92. SpecialWarningSound2 = "Sound\\Creature\\AlgalonTheObserver\\UR_Algalon_BHole01.ogg",
  93. SpecialWarningSound3 = "Sound\\Creature\\KilJaeden\\KILJAEDEN02.ogg",
  94. SpecialWarningSound4 = "Sound\\Creature\\HoodWolf\\HoodWolfTransformPlayer01.ogg",
  95. SpecialWarningSound5 = "Sound\\Creature\\Loathstare\\Loa_Naxx_Aggro02.ogg",
  96. ModelSoundValue = "Short",
  97. CountdownVoice = "Corsica",
  98. CountdownVoice2 = "Kolt",
  99. CountdownVoice3v2 = "HoTS_R",
  100. ChosenVoicePack = "None",
  101. VoiceOverSpecW2 = "DefaultOnly",
  102. AlwaysPlayVoice = false,
  103. ShowCountdownText = false,
  104. Enabled = true,
  105. ShowWarningsInChat = true,
  106. ShowSWarningsInChat = true,
  107. WarningIconLeft = true,
  108. WarningIconRight = true,
  109. WarningIconChat = true,
  110. WarningAlphabetical = true,
  111. StripServerName = true,
  112. ShowAllVersions = true,
  113. ShowPizzaMessage = true,
  114. ShowEngageMessage = true,
  115. ShowDefeatMessage = true,
  116. ShowGuildMessages = true,
  117. AutoRespond = true,
  118. StatusEnabled = true,
  119. WhisperStats = false,
  120. DisableStatusWhisper = false,
  121. HideBossEmoteFrame = true,
  122. SpamBlockBossWhispers = true,
  123. ShowMinimapButton = false,
  124. ShowFlashFrame = true,
  125. SWarningAlphabetical = true,
  126. SWarnNameInNote = true,
  127. CustomSounds = 0,
  128. AlwaysShowHealthFrame = false,
  129. ShowBigBrotherOnCombatStart = false,
  130. FilterTankSpec = true,
  131. FilterInterrupt = true,
  132. FilterInterruptNoteName = false,
  133. FilterDispel = true,
  134. FilterSelfHud = true,
  135. AutologBosses = false,
  136. AdvancedAutologBosses = false,
  137. LogOnlyRaidBosses = false,
  138. UseSoundChannel = "Master",
  139. LFDEnhance = true,
  140. WorldBossNearAlert = false,
  141. RLReadyCheckSound = true,
  142. AFKHealthWarning = false,
  143. AutoReplySound = true,
  144. HideObjectivesFrame = true,
  145. HideGarrisonToasts = true,
  146. HideGuildChallengeUpdates = true,
  147. HideQuestTooltips = true,
  148. HideTooltips = false,
  149. DisableSFX = false,
  150. EnableModels = true,
  151. RangeFrameFrames = "radar",
  152. RangeFrameUpdates = "Average",
  153. RangeFramePoint = "CENTER",
  154. RangeFrameX = 50,
  155. RangeFrameY = -50,
  156. RangeFrameSound1 = "none",
  157. RangeFrameSound2 = "none",
  158. RangeFrameLocked = false,
  159. RangeFrameRadarPoint = "CENTER",
  160. RangeFrameRadarX = 100,
  161. RangeFrameRadarY = -100,
  162. InfoFramePoint = "CENTER",
  163. InfoFrameX = 75,
  164. InfoFrameY = -75,
  165. InfoFrameShowSelf = false,
  166. InfoFrameLines = 0,
  167. HPFramePoint = "CENTER",
  168. HPFrameX = -50,
  169. HPFrameY = 50,
  170. HPFrameMaxEntries = 5,
  171. WarningDuration = 4,
  172. WarningPoint = "CENTER",
  173. WarningX = 0,
  174. WarningY = 260,
  175. WarningFont = standardFont,
  176. WarningFontSize = 20,
  177. WarningFontStyle = "None",
  178. WarningFontShadow = true,
  179. SpecialWarningDuration = 4,
  180. SpecialWarningPoint = "CENTER",
  181. SpecialWarningX = 0,
  182. SpecialWarningY = 75,
  183. SpecialWarningFont = standardFont,
  184. SpecialWarningFontSize2 = 40,
  185. SpecialWarningFontStyle = "THICKOUTLINE",
  186. SpecialWarningFontShadow = false,
  187. SpecialWarningIcon = true,
  188. SpecialWarningFontCol = {1.0, 0.7, 0.0},--Yellow, with a tint of orange
  189. SpecialWarningFlashCol1 = {1.0, 1.0, 0.0},--Yellow
  190. SpecialWarningFlashCol2 = {1.0, 0.5, 0.0},--Orange
  191. SpecialWarningFlashCol3 = {1.0, 0.0, 0.0},--Red
  192. SpecialWarningFlashCol4 = {1.0, 0.0, 1.0},--Purple
  193. SpecialWarningFlashCol5 = {0.2, 1.0, 1.0},--Tealish
  194. SpecialWarningFlashDura1 = 0.4,
  195. SpecialWarningFlashDura2 = 0.4,
  196. SpecialWarningFlashDura3 = 1,
  197. SpecialWarningFlashDura4 = 0.7,
  198. SpecialWarningFlashDura5 = 1,
  199. SpecialWarningFlashAlph1 = 0.3,
  200. SpecialWarningFlashAlph2 = 0.3,
  201. SpecialWarningFlashAlph3 = 0.4,
  202. SpecialWarningFlashAlph4 = 0.4,
  203. SpecialWarningFlashAlph5 = 0.5,
  204. SpecialWarningFlashRepeat1 = false,
  205. SpecialWarningFlashRepeat2 = false,
  206. SpecialWarningFlashRepeat3 = true,
  207. SpecialWarningFlashRepeat4 = false,
  208. SpecialWarningFlashRepeat5 = true,
  209. SpecialWarningFlashRepeatAmount = 2,--Repeat 2 times, mean 3 flashes (first plus 2 repeat)
  210. SWarnClassColor = true,
  211. HUDColorOverride = false,
  212. HUDSizeOverride = false,
  213. HUDAlphaOverride = false,
  214. HUDTextureOverride = false,
  215. HUDSize1 = 5,
  216. HUDSize2 = 5,
  217. HUDSize3 = 5,
  218. HUDSize4 = 5,
  219. HUDAlpha1 = 0.5,
  220. HUDAlpha2 = 0.5,
  221. HUDAlpha3 = 0.5,
  222. HUDAlpha4 = 0.5,
  223. HUDColor1 = {1.0, 1.0, 0.0},--Yellow
  224. HUDColor2 = {1.0, 0.0, 0.0},--Red
  225. HUDColor3 = {1.0, 0.5, 0.0},--Orange
  226. HUDColor4 = {0.0, 1.0, 0.0},--Green
  227. HUDTexture1 = "highlight",
  228. HUDTexture2 = "highlight",
  229. HUDTexture3 = "highlight",
  230. HUDTexture4 = "highlight",
  231. HealthFrameGrowUp = false,
  232. HealthFrameLocked = false,
  233. HealthFrameWidth = 200,
  234. ArrowPosX = 0,
  235. ArrowPosY = -150,
  236. ArrowPoint = "TOP",
  237. -- global boss mod settings (overrides mod-specific settings for some options)
  238. DontShowBossAnnounces = false,
  239. DontShowSpecialWarnings = false,
  240. DontShowBossTimers = false,
  241. DontShowUserTimers = false,
  242. DontShowFarWarnings = true,
  243. DontSetIcons = false,
  244. DontRestoreIcons = false,
  245. DontShowRangeFrame = false,
  246. DontRestoreRange = false,
  247. DontShowInfoFrame = false,
  248. DontShowHudMap2 = false,
  249. DontShowHealthFrame = false,
  250. DontPlayCountdowns = false,
  251. DontSendYells = false,
  252. BlockNoteShare = false,
  253. DontShowPT2 = false,
  254. DontShowPTCountdownText = false,
  255. DontPlayPTCountdown = false,
  256. DontShowPTText = false,
  257. DontShowPTNoID = false,
  258. DontShowCTCount = false,
  259. DontShowFlexMessage = false,
  260. PTCountThreshold = 5,
  261. LatencyThreshold = 250,
  262. BigBrotherAnnounceToRaid = false,
  263. SettingsMessageShown = false,
  264. ForumsMessageShown = false,
  265. PGMessageShown = false,
  266. MCMessageShown = false,
  267. BCTWMessageShown = false,
  268. WOTLKTWMessageShown = false,
  269. CATATWMessageShown = false,
  270. AlwaysShowSpeedKillTimer = true,
  271. CRT_Enabled = false,
  272. ShowRespawn = true,
  273. ShowQueuePop = true,
  274. HelpMessageVersion = 3,
  275. NewsMessageShown = 4,
  276. MoviesSeen = {},
  277. MovieFilter = "AfterFirst",
  278. LastRevision = 0,
  279. FilterSayAndYell = false,
  280. DebugMode = false,
  281. DebugLevel = 1,
  282. RoleSpecAlert = true,
  283. CheckGear = true,
  284. WorldBossAlert = false,
  285. AutoAcceptFriendInvite = false,
  286. AutoAcceptGuildInvite = false,
  287. FakeBWVersion = false,
  288. AITimer = true,
  289. AutoCorrectTimer = false,
  290. ShortTimerText = true,
  291. ChatFrame = "DEFAULT_CHAT_FRAME",
  292. }
  293.  
  294. DBM.Bars = DBT:New()
  295. DBM.Mods = {}
  296. DBM.ModLists = {}
  297. DBM.Counts = {
  298. { text = "Moshne (Male)", value = "Mosh", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Mosh\\", max = 5},
  299. { text = "Corsica (Female)",value = "Corsica", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Corsica\\", max = 10},
  300. { text = "Koltrane (Male)",value = "Kolt", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Kolt\\", max = 10},
  301. { text = "Pewsey (Male)",value = "Pewsey", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Pewsey\\", max = 10},
  302. { text = "Bear (Male Child)",value = "Bear", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Bear\\", max = 10},
  303. { text = "Overwatch: Announcer", value = "Overwatch", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Announcer\\", max = 10},
  304. { text = "Overwatch: Bastion", value = "Bastion", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Bastion\\", max = 5},
  305. { text = "Overwatch: D.Va", value = "DVa", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\DVa\\", max = 5},
  306. { text = "Overwatch: D.Va (Korean)", value = "DVakr", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\DVa\\kr\\", max = 5},
  307. { text = "Overwatch: Genji", value = "Genji", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Genji\\", max = 5},
  308. { text = "Overwatch: Genji (Japanese)", value = "Genjijp", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Genji\\jp\\", max = 5},
  309. { text = "Overwatch: Hanzo", value = "Hanzo", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Hanzo\\", max = 5},
  310. { text = "Overwatch: Junkrat", value = "Junkrat", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Junkrat\\", max = 5},
  311. { text = "Overwatch: Lucio", value = "Lucio", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Lucio\\", max = 5},
  312. { text = "Overwatch: Mccree", value = "Mccree", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Mccree\\", max = 5},
  313. { text = "Overwatch: Mei", value = "Mei", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Mei\\", max = 5},
  314. { text = "Overwatch: Mei (Chinese)", value = "Meicn", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Mei\\cn\\", max = 5},
  315. { text = "Overwatch: Mercy", value = "Mercy", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Mercy\\", max = 5},
  316. { text = "Overwatch: Pharah", value = "Pharah", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Pharah\\", max = 5},
  317. { text = "Overwatch: Reaper", value = "Reaper", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Reaper\\", max = 5},
  318. { text = "Overwatch: Reinhardt", value = "Reinhardt", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Reinhardt\\", max = 5},
  319. { text = "Overwatch: Roadhog", value = "Roadhog", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Roadhog\\", max = 5},
  320. { text = "Overwatch: Soldier", value = "Soldier", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Soldier\\", max = 5},
  321. { text = "Overwatch: Symmetra", value = "Symmetra", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Symmetra\\", max = 5},
  322. { text = "Overwatch: Torbjorn", value = "Torbjorn", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Torbjorn\\", max = 5},
  323. { text = "Overwatch: Tracer", value = "Tracer", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Tracer\\", max = 5},
  324. { text = "Overwatch: Widowmaker", value = "Widowmaker", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Widowmaker\\", max = 5},
  325. { text = "Overwatch: Winston", value = "Winston", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Winston\\", max = 5},
  326. { text = "Overwatch: Zarya", value = "Zarya", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Zarya\\", max = 5},
  327. { text = "Overwatch: Zenyatta", value = "Zenyatta", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Overwatch\\Zenyatta\\", max = 5},
  328. { text = "HoTS: Default", value = "HoTS_D", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Heroes\\Default\\", max = 5},
  329. { text = "HoTS: Blackheart", value = "HoTS_B", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Heroes\\Blackheart\\", max = 5},
  330. { text = "HoTS: Gardens", value = "HoTS_G", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Heroes\\Gardens\\", max = 5},
  331. { text = "HoTS: Lady of Thorns", value = "HoTS_T", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Heroes\\LadyOfThorns\\", max = 5},
  332. { text = "HoTS: Necromancer", value = "HoTS_N", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Heroes\\Necromancer\\", max = 5},
  333. { text = "HoTS: Ravenlord", value = "HoTS_R", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Heroes\\RavenLord\\", max = 5},
  334. { text = "HoTS: Snake", value = "HoTS_S", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Heroes\\Snake\\", max = 5},
  335. { text = "Anshlun (ptBR Male)",value = "Anshlun", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Anshlun\\", max = 10},
  336. { text = "Neryssa (ptBR Female)",value = "Neryssa", path = "Interface\\AddOns\\DBM-Core\\Sounds\\Neryssa\\", max = 10},
  337. }
  338.  
  339. ------------------------
  340. -- Global Identifiers --
  341. ------------------------
  342. DBM_DISABLE_ZONE_DETECTION = newproxy(false)
  343. DBM_OPTION_SPACER = newproxy(false)
  344.  
  345. --------------
  346. -- Locals --
  347. --------------
  348. local bossModPrototype = {}
  349. local usedProfile = "Default"
  350. local dbmIsEnabled = true
  351. local lastCombatStarted = GetTime()
  352. local loadcIds = {}
  353. local inCombat = {}
  354. local combatInfo = {}
  355. local bossIds = {}
  356. local updateFunctions = {}
  357. local raid = {}
  358. local modSyncSpam = {}
  359. local autoRespondSpam = {}
  360. local chatPrefix = "<Deadly Boss Mods> "
  361. local chatPrefixShort = "<DBM> "
  362. local ver = ("%s (r%d)"):format(DBM.DisplayVersion, DBM.Revision)
  363. local mainFrame = CreateFrame("Frame", "DBMMainFrame")
  364. local newerVersionPerson = {}
  365. local newerRevisionPerson = {}
  366. local combatInitialized = false
  367. local healthCombatInitialized = false
  368. local pformat
  369. local schedulerFrame = CreateFrame("Frame", "DBMScheduler")
  370. schedulerFrame:Hide()
  371. local startScheduler
  372. local schedule
  373. local unschedule
  374. local unscheduleAll
  375. local loadOptions
  376. local checkWipe
  377. local checkBossHealth
  378. local checkCustomBossHealth
  379. local loopCRTimer
  380. local fireEvent
  381. local playerName = UnitName("player")
  382. local playerLevel = UnitLevel("player")
  383. local playerRealm = GetRealmName()
  384. local connectedServers = GetAutoCompleteRealms()
  385. local LastInstanceMapID = -1
  386. local LastGroupSize = 0
  387. local LastInstanceType = nil
  388. local queuedBattlefield = {}
  389. local loadDelay = nil
  390. local loadDelay2 = nil
  391. local noDelay = true
  392. local watchFrameRestore = false
  393. local bossHealth = {}
  394. local bossHealthuIdCache = {}
  395. local bossuIdCache = {}
  396. local savedDifficulty, difficultyText, difficultyIndex
  397. local lastBossEngage = {}
  398. local lastBossDefeat = {}
  399. local bossuIdFound = false
  400. local timerRequestInProgress = false
  401. local updateNotificationDisplayed = 0
  402. local showConstantReminder = 0
  403. local tooltipsHidden = false
  404. local SWFilterDisabed = 3
  405. local currentSpecGroup = GetSpecialization() or 1
  406. local currentSpecID, currentSpecName
  407. local cSyncSender = {}
  408. local cSyncReceived = 0
  409. local eeSyncSender = {}
  410. local eeSyncReceived = 0
  411. local canSetIcons = {}
  412. local iconSetRevision = {}
  413. local iconSetPerson = {}
  414. local addsGUIDs = {}
  415. local targetEventsRegistered = false
  416. local targetMonitor = nil
  417. local statusWhisperDisabled = false
  418. local wowVersionString, _, _, wowTOC = GetBuildInfo()
  419. local dbmToc = 0
  420. local UpdateChestTimer
  421. local breakTimerStart
  422.  
  423. local fakeBWVersion, fakeBWHash = 39, "7aa089f"
  424. local versionQueryString, versionResponseString = "Q^%d^%s", "V^%d^%s"
  425.  
  426. local enableIcons = true -- set to false when a raid leader or a promoted player has a newer version of DBM
  427. local guiRequested = false
  428.  
  429. local bannedMods = { -- a list of "banned" (meaning they are replaced by another mod like DBM-Battlegrounds (replaced by DBM-PvP)) boss mods, these mods will not be loaded by DBM (and they wont show up in the GUI)
  430. "DBM-Battlegrounds", --replaced by DBM-PvP
  431. -- ZG and ZA are now part of the party mods for Cataclysm
  432. "DBM-ZulAman",
  433. "DBM-ZG",
  434. "DBM-SiegeOfOrgrimmar",--Block legacy version. New version is "DBM-SiegeOfOrgrimmarV2"
  435. "DBM-HighMail",
  436. "DBM-ProvingGrounds-MoP",--Renamed to DBM-ProvingGrounds in 6.0 version since blizzard updated content for WoD
  437. "DBM-VPKiwiBeta",--Renamed to DBM-VPKiwi in final version.
  438. "DBM-Suramar",--Renamed to DBM-Nighthold
  439. }
  440.  
  441.  
  442. -----------------
  443. -- Libraries --
  444. -----------------
  445. local LL
  446. if LibStub("LibLatency", true) then
  447. LL = LibStub("LibLatency")
  448. end
  449.  
  450.  
  451. --------------------------------------------------------
  452. -- Cache frequently used global variables in locals --
  453. --------------------------------------------------------
  454. local DBM = DBM
  455. -- these global functions are accessed all the time by the event handler
  456. -- so caching them is worth the effort
  457. local ipairs, pairs, next = ipairs, pairs, next
  458. local tinsert, tremove, twipe, tsort, tconcat = table.insert, table.remove, table.wipe, table.sort, table.concat
  459. local type, select = type, select
  460. local GetTime = GetTime
  461. local bband = bit.band
  462. local floor, mhuge, mmin, mmax = math.floor, math.huge, math.min, math.max
  463. local GetNumGroupMembers, GetRaidRosterInfo = GetNumGroupMembers, GetRaidRosterInfo
  464. local UnitName, GetUnitName = UnitName, GetUnitName
  465. local IsInRaid, IsInGroup, IsInInstance = IsInRaid, IsInGroup, IsInInstance
  466. local UnitAffectingCombat, InCombatLockdown, IsEncounterInProgress, UnitPlayerOrPetInRaid, UnitPlayerOrPetInParty = UnitAffectingCombat, InCombatLockdown, IsEncounterInProgress, UnitPlayerOrPetInRaid, UnitPlayerOrPetInParty
  467. local UnitGUID, UnitHealth, UnitHealthMax, UnitBuff = UnitGUID, UnitHealth, UnitHealthMax, UnitBuff
  468. local UnitExists, UnitIsDead, UnitIsFriend, UnitIsUnit, UnitIsAFK = UnitExists, UnitIsDead, UnitIsFriend, UnitIsUnit, UnitIsAFK
  469. local GetSpellInfo, EJ_GetSectionInfo, GetSpellTexture, GetSpellCooldown = GetSpellInfo, EJ_GetSectionInfo, GetSpellTexture, GetSpellCooldown
  470. local EJ_GetEncounterInfo, EJ_GetCreatureInfo, GetDungeonInfo = EJ_GetEncounterInfo, EJ_GetCreatureInfo, GetDungeonInfo
  471. local GetInstanceInfo = GetInstanceInfo
  472. local UnitPosition, GetCurrentMapDungeonLevel, GetMapInfo, GetCurrentMapZone, SetMapToCurrentZone, GetPlayerMapAreaID = UnitPosition, GetCurrentMapDungeonLevel, GetMapInfo, GetCurrentMapZone, SetMapToCurrentZone, GetPlayerMapAreaID
  473. local GetSpecialization, GetSpecializationInfo, GetSpecializationInfoByID = GetSpecialization, GetSpecializationInfo, GetSpecializationInfoByID
  474. local UnitDetailedThreatSituation = UnitDetailedThreatSituation
  475. local GetPartyAssignment, UnitGroupRolesAssigned, UnitIsGroupLeader, UnitIsGroupAssistant, UnitRealmRelationship = GetPartyAssignment, UnitGroupRolesAssigned, UnitIsGroupLeader, UnitIsGroupAssistant, UnitRealmRelationship
  476. local LoadAddOn, GetAddOnInfo, GetAddOnEnableState, GetAddOnMetadata, GetNumAddOns = LoadAddOn, GetAddOnInfo, GetAddOnEnableState, GetAddOnMetadata, GetNumAddOns
  477. local PlaySoundFile, PlaySound = PlaySoundFile, PlaySound
  478. local Ambiguate = Ambiguate
  479. local C_TimerNewTicker, C_TimerAfter = C_Timer.NewTicker, C_Timer.After
  480. local BNGetGameAccountInfo = BNGetGameAccountInfo
  481.  
  482. -- for Phanx' Class Colors
  483. local RAID_CLASS_COLORS = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
  484.  
  485. ---------------------------------
  486. -- General (local) functions --
  487. ---------------------------------
  488. -- checks if a given value is in an array
  489. -- returns true if it finds the value, false otherwise
  490. local function checkEntry(t, val)
  491. for i, v in ipairs(t) do
  492. if v == val then
  493. return true
  494. end
  495. end
  496. return false
  497. end
  498.  
  499. local function findEntry(t, val)
  500. for i, v in ipairs(t) do
  501. if v and val and val:find(v) then
  502. return true
  503. end
  504. end
  505. return false
  506. end
  507.  
  508. -- removes all occurrences of a value in an array
  509. -- returns true if at least one occurrence was remove, false otherwise
  510. local function removeEntry(t, val)
  511. local existed = false
  512. for i = #t, 1, -1 do
  513. if t[i] == val then
  514. tremove(t, i)
  515. existed = true
  516. end
  517. end
  518. return existed
  519. end
  520.  
  521. -- automatically sends an addon message to the appropriate channel (INSTANCE_CHAT, RAID or PARTY)
  522. local function sendSync(prefix, msg)
  523. msg = msg or ""
  524. if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and IsInInstance() and not C_Garrison:IsOnGarrisonMap() then--For BGs, LFR and LFG (we also check IsInInstance() so if you're in queue but fighting something outside like a world boss, it'll sync in "RAID" instead)
  525. SendAddonMessage("D4", prefix .. "\t" .. msg, "INSTANCE_CHAT")
  526. else
  527. if IsInRaid() then
  528. SendAddonMessage("D4", prefix .. "\t" .. msg, "RAID")
  529. elseif IsInGroup(LE_PARTY_CATEGORY_HOME) then
  530. SendAddonMessage("D4", prefix .. "\t" .. msg, "PARTY")
  531. else--for solo raid
  532. SendAddonMessage("D4", prefix .. "\t" .. msg, "WHISPER", playerName)
  533. end
  534. end
  535. end
  536.  
  537. local function strFromTime(time)
  538. if type(time) ~= "number" then time = 0 end
  539. time = floor(time*100)/100
  540. if time < 60 then
  541. return DBM_CORE_TIMER_FORMAT_SECS:format(time)
  542. elseif time % 60 == 0 then
  543. return DBM_CORE_TIMER_FORMAT_MINS:format(time/60)
  544. else
  545. return DBM_CORE_TIMER_FORMAT:format(time/60, time % 60)
  546. end
  547. end
  548.  
  549. do
  550. -- fail-safe format, replaces missing arguments with unknown
  551. -- note: doesn't handle cases like %%%s correctly at the moment (should become %unknown, but becomes %%s)
  552. -- also, the end of the format directive is not detected in all cases, but handles everything that occurs in our boss mods ;)
  553. --> not suitable for general-purpose use, just for our warnings and timers (where an argument like a spell-target might be nil due to missing target information from unreliable detection methods)
  554. local function replace(cap1, cap2)
  555. return cap1 == "%" and DBM_CORE_UNKNOWN
  556. end
  557.  
  558. function pformat(fstr, ...)
  559. local ok, str = pcall(format, fstr, ...)
  560. return ok and str or fstr:gsub("(%%+)([^%%%s<]+)", replace):gsub("%%%%", "%%")
  561. end
  562. end
  563.  
  564. -- sends a whisper to a player by his or her character name or BNet presence id
  565. -- returns true if the message was sent, nil otherwise
  566. local function sendWhisper(target, msg)
  567. if type(target) == "number" then
  568. if not BNIsSelf(target) then -- never send BNet whispers to ourselves
  569. BNSendWhisper(target, msg)
  570. return true
  571. end
  572. elseif type(target) == "string" then
  573. -- whispering to ourselves here is okay and somewhat useful for whisper-warnings
  574. SendChatMessage(msg, "WHISPER", nil, target)
  575. return true
  576. end
  577. end
  578. local BNSendWhisper = sendWhisper
  579.  
  580. local function stripServerName(cap)
  581. cap = cap:sub(2, -2)
  582. if DBM.Options.StripServerName then
  583. cap = Ambiguate(cap, "short")
  584. end
  585. return cap
  586. end
  587.  
  588. local function countDownTextDelay(timer)
  589. TimerTracker_OnEvent(TimerTracker, "START_TIMER", 2, timer+1, timer+1)
  590. end
  591.  
  592. --------------
  593. -- Events --
  594. --------------
  595. do
  596. local registeredEvents = {}
  597. local registeredSpellIds = {}
  598. local unfilteredCLEUEvents = {}
  599. local registeredUnitEventIds = {}
  600. local argsMT = {__index = {}}
  601. local args = setmetatable({}, argsMT)
  602.  
  603. function argsMT.__index:IsSpellID(a1, a2, a3, a4, a5)
  604. local v = self.spellId
  605. return v == a1 or v == a2 or v == a3 or v == a4 or v == a5
  606. end
  607.  
  608. function argsMT.__index:IsPlayer()
  609. return bband(args.destFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) ~= 0 and bband(args.destFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
  610. end
  611.  
  612. function argsMT.__index:IsPlayerSource()
  613. return bband(args.sourceFlags, COMBATLOG_OBJECT_AFFILIATION_MINE) ~= 0 and bband(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
  614. end
  615.  
  616. function argsMT.__index:IsNPC()
  617. return bband(args.destFlags, COMBATLOG_OBJECT_TYPE_NPC) ~= 0
  618. end
  619.  
  620. function argsMT.__index:IsPet()
  621. return bband(args.destFlags, COMBATLOG_OBJECT_TYPE_PET) ~= 0
  622. end
  623.  
  624. function argsMT.__index:IsPetSource()
  625. return bband(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PET) ~= 0
  626. end
  627.  
  628. function argsMT.__index:IsSrcTypePlayer()
  629. return bband(args.sourceFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
  630. end
  631.  
  632. function argsMT.__index:IsDestTypePlayer()
  633. return bband(args.destFlags, COMBATLOG_OBJECT_TYPE_PLAYER) ~= 0
  634. end
  635.  
  636. function argsMT.__index:IsSrcTypeHostile()
  637. return bband(args.sourceFlags, COMBATLOG_OBJECT_REACTION_HOSTILE) ~= 0
  638. end
  639.  
  640. function argsMT.__index:IsDestTypeHostile()
  641. return bband(args.destFlags, COMBATLOG_OBJECT_REACTION_HOSTILE) ~= 0
  642. end
  643.  
  644. function argsMT.__index:GetSrcCreatureID()
  645. return DBM:GetCIDFromGUID(self.sourceGUID)
  646. end
  647.  
  648. function argsMT.__index:GetDestCreatureID()
  649. return DBM:GetCIDFromGUID(self.destGUID)
  650. end
  651.  
  652. local function handleEvent(self, event, ...)
  653. local isUnitEvent = event:sub(0, 5) == "UNIT_" and event ~= "UNIT_DIED" and event ~= "UNIT_DESTROYED"
  654. if self == mainFrame and isUnitEvent then
  655. -- UNIT_* events that come from mainFrame are _UNFILTERED variants and need their suffix
  656. event = event .. "_UNFILTERED"
  657. isUnitEvent = false -- not actually a real unit id for this function...
  658. end
  659. if not registeredEvents[event] or not dbmIsEnabled then return end
  660. for i, v in ipairs(registeredEvents[event]) do
  661. local zones = v.zones
  662. local handler = v[event]
  663. local modEvents = v.registeredUnitEvents
  664. if handler and (not isUnitEvent or not modEvents or modEvents[event .. ...]) and (not zones or zones[LastInstanceMapID]) and not (v.isTrashMod and #inCombat > 0) then
  665. handler(v, ...)
  666. end
  667. end
  668. end
  669.  
  670. local registerUnitEvent, unregisterUnitEvent, registerSpellId, unregisterSpellId, registerCLEUEvent, unregisterCLEUEvent
  671. do
  672. local frames = {} -- frames that are being used for unit events, one frame per unit id (this could be optimized, as it currently creates a new frame even for a different event, but that's not worth the effort as 90% of all calls are just boss1 anyways)
  673.  
  674. function registerUnitEvent(mod, event, ...)
  675. mod.registeredUnitEvents = mod.registeredUnitEvents or {}
  676. for i = 1, select("#", ...) do
  677. local uId = select(i, ...)
  678. if not uId then break end
  679. local frame = frames[uId]
  680. if not frame then
  681. frame = CreateFrame("Frame")
  682. if uId == "mouseover" then
  683. -- work-around for mouse-over events (broken!)
  684. frame:SetScript("OnEvent", function(self, event, uId, ...)
  685. -- we registered mouseover events, so we only want mouseover events, thanks.
  686. handleEvent(self, event, "mouseover", ...)
  687. end)
  688. else
  689. frame:SetScript("OnEvent", handleEvent)
  690. end
  691. frames[uId] = frame
  692. end
  693. registeredUnitEventIds[event .. uId] = (registeredUnitEventIds[event .. uId] or 0) + 1
  694. mod.registeredUnitEvents[event .. uId] = true
  695. frame:RegisterUnitEvent(event, uId)
  696. end
  697. end
  698.  
  699. function unregisterUnitEvent(mod, event, ...)
  700. for i = 1, select("#", ...) do
  701. local uId = select(i, ...)
  702. if not uId then break end
  703. local frame = frames[uId]
  704. local refs = (registeredUnitEventIds[event .. uId] or 1) - 1
  705. registeredUnitEventIds[event .. uId] = refs
  706. if refs <= 0 then
  707. registeredUnitEventIds[event .. uId] = nil
  708. if frame then
  709. frame:UnregisterEvent(event)
  710. end
  711. end
  712. if mod.registeredUnitEvents and mod.registeredUnitEvents[event .. uId] then
  713. mod.registeredUnitEvents[event .. uId] = nil
  714. end
  715. end
  716. for i = #registeredEvents[event], 1, -1 do
  717. if registeredEvents[event][i] == mod then
  718. tremove(registeredEvents[event], i)
  719. end
  720. end
  721. if #registeredEvents[event] == 0 then
  722. registeredEvents[event] = nil
  723. end
  724. end
  725.  
  726. function registerSpellId(event, spellId)
  727. if type(spellId) == "string" then--Something is screwed up, like SPELL_AURA_APPLIED DOSE
  728. DBM:AddMsg("DBM RegisterEvents Error: "..spellId.." is not a number!")
  729. end
  730. if spellId and not GetSpellInfo(spellId) then
  731. DBM:AddMsg("DBM RegisterEvents Error: "..spellId.." spell id does not exist!")
  732. return
  733. end
  734. if not registeredSpellIds[event] then
  735. registeredSpellIds[event] = {}
  736. end
  737. registeredSpellIds[event][spellId] = (registeredSpellIds[event][spellId] or 0) + 1
  738. end
  739.  
  740. function unregisterSpellId(event, spellId)
  741. if not registeredSpellIds[event] then return end
  742. local refs = (registeredSpellIds[event][spellId] or 1) - 1
  743. registeredSpellIds[event][spellId] = refs
  744. if refs <= 0 then
  745. registeredSpellIds[event][spellId] = nil
  746. end
  747. end
  748.  
  749. --There are 2 tables. unfilteredCLEUEvents and registeredSpellIds table.
  750. --unfilteredCLEUEvents saves UNFILTERED cleu event count. this is count table to prevent bad unregister.
  751. --registeredSpellIds tables filtered table. this saves event and spell ids. works smiliar with unfilteredCLEUEvents table.
  752. function registerCLEUEvent(mod, event)
  753. local argTable = {strsplit(" ", event)}
  754. -- filtered cleu event. save information in registeredSpellIds table.
  755. if #argTable > 1 then
  756. event = argTable[1]
  757. for i = 2, #argTable do
  758. registerSpellId(event, tonumber(argTable[i]))
  759. end
  760. -- no args. works as unfiltered. save information in unfilteredCLEUEvents table.
  761. else
  762. unfilteredCLEUEvents[event] = (unfilteredCLEUEvents[event] or 0) + 1
  763. end
  764. registeredEvents[event] = registeredEvents[event] or {}
  765. tinsert(registeredEvents[event], mod)
  766. end
  767.  
  768. function unregisterCLEUEvent(mod, event)
  769. local argTable = {strsplit(" ", event)}
  770. local eventCleared = false
  771. -- filtered cleu event. save information in registeredSpellIds table.
  772. if #argTable > 1 then
  773. event = argTable[1]
  774. for i = 2, #argTable do
  775. unregisterSpellId(event, tonumber(argTable[i]))
  776. end
  777. local remainingSpellIdCount = 0
  778. if registeredSpellIds[event] then
  779. for i, v in pairs(registeredSpellIds[event]) do
  780. remainingSpellIdCount = remainingSpellIdCount + 1
  781. end
  782. end
  783. if remainingSpellIdCount == 0 then
  784. registeredSpellIds[event] = nil
  785. -- if unfilteredCLEUEvents and registeredSpellIds do not exists, clear registeredEvents.
  786. if not unfilteredCLEUEvents[event] then
  787. eventCleared = true
  788. end
  789. end
  790. -- no args. works as unfiltered. save information in unfilteredCLEUEvents table.
  791. else
  792. local refs = (unfilteredCLEUEvents[event] or 1) - 1
  793. unfilteredCLEUEvents[event] = refs
  794. if refs <= 0 then
  795. unfilteredCLEUEvents[event] = nil
  796. -- if unfilteredCLEUEvents and registeredSpellIds do not exists, clear registeredEvents.
  797. if not registeredSpellIds[event] then
  798. eventCleared = true
  799. end
  800. end
  801. end
  802. for i = #registeredEvents[event], 1, -1 do
  803. if registeredEvents[event][i] == mod then
  804. registeredEvents[event][i] = {}
  805. break
  806. end
  807. end
  808. if eventCleared then
  809. registeredEvents[event] = nil
  810. end
  811. end
  812. end
  813.  
  814. -- UNIT_* events are special: they can take 'parameters' like this: "UNIT_HEALTH boss1 boss2" which only trigger the event for the given unit ids
  815. function DBM:RegisterEvents(...)
  816. for i = 1, select("#", ...) do
  817. local event = select(i, ...)
  818. -- spell events with special care.
  819. if event:sub(0, 6) == "SPELL_" or event:sub(0, 6) == "RANGE_" then
  820. registerCLEUEvent(self, event)
  821. else
  822. local eventWithArgs = event
  823. -- unit events need special care
  824. if event:sub(0, 5) == "UNIT_" and event ~= "UNIT_DIED" and event ~= "UNIT_DESTROYED" then
  825. -- unit events are limited to 8 "parameters", as there is no good reason to ever use more than 5 (it's just that the code old code supported 8 (boss1-5, target, focus))
  826. local arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8
  827. event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 = strsplit(" ", event)
  828. if not arg1 and event:sub(event:len() - 10) ~= "_UNFILTERED" then -- no arguments given, support for legacy mods
  829. eventWithArgs = event .. " boss1 boss2 boss3 boss4 boss5 target focus"
  830. event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 = strsplit(" ", eventWithArgs)
  831. end
  832. if event:sub(event:len() - 10) == "_UNFILTERED" then
  833. -- we really want *all* unit ids
  834. mainFrame:RegisterEvent(event:sub(0, -12))
  835. else
  836. registerUnitEvent(self, event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)
  837. end
  838. -- spell events with filter
  839. else
  840. -- normal events
  841. mainFrame:RegisterEvent(event)
  842. end
  843. registeredEvents[eventWithArgs] = registeredEvents[eventWithArgs] or {}
  844. tinsert(registeredEvents[eventWithArgs], self)
  845. if event ~= eventWithArgs then
  846. registeredEvents[event] = registeredEvents[event] or {}
  847. tinsert(registeredEvents[event], self)
  848. end
  849. end
  850. end
  851. end
  852.  
  853. local function unregisterUEvent(mod, event)
  854. if event:sub(0, 5) == "UNIT_" and event ~= "UNIT_DIED" and event ~= "UNIT_DESTROYED" then
  855. local event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 = strsplit(" ", event)
  856. if event:sub(event:len() - 10) == "_UNFILTERED" then
  857. mainFrame:UnregisterEvent(event:sub(0, -12))
  858. else
  859. unregisterUnitEvent(mod, event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8)
  860. end
  861. end
  862. end
  863.  
  864. local function findRealEvent(t, val)
  865. for i, v in ipairs(t) do
  866. local event = strsplit(" ", v)
  867. if event == val then
  868. return v
  869. end
  870. end
  871. end
  872.  
  873. function DBM:UnregisterInCombatEvents(srmOnly)
  874. for event, mods in pairs(registeredEvents) do
  875. if srmOnly then
  876. local i = 1
  877. while mods[i] do
  878. if mods[i] == self and event == "SPELL_AURA_REMOVED" then
  879. local findEvent = findRealEvent(self.inCombatOnlyEvents, "SPELL_AURA_REMOVED")
  880. if findEvent then
  881. unregisterCLEUEvent(self, findEvent)
  882. break
  883. end
  884. end
  885. i = i +1
  886. end
  887. elseif (event:sub(0, 6) == "SPELL_" or event:sub(0, 6) == "RANGE_") then
  888. local i = 1
  889. while mods[i] do
  890. if mods[i] == self and event ~= "SPELL_AURA_REMOVED" then
  891. local findEvent = findRealEvent(self.inCombatOnlyEvents, event)
  892. if findEvent then
  893. unregisterCLEUEvent(self, findEvent)
  894. break
  895. end
  896. end
  897. i = i +1
  898. end
  899. else
  900. local match = false
  901. for i = #mods, 1, -1 do
  902. if mods[i] == self and checkEntry(self.inCombatOnlyEvents, event) then
  903. tremove(mods, i)
  904. match = true
  905. end
  906. end
  907. if #mods == 0 or (match and event:sub(0, 5) == "UNIT_" and event:sub(0, -10) ~= "_UNFILTERED" and event ~= "UNIT_DIED" and event ~= "UNIT_DESTROYED") then -- unit events have their own reference count
  908. unregisterUEvent(self, event)
  909. end
  910. if #mods == 0 then
  911. registeredEvents[event] = nil
  912. end
  913. end
  914. end
  915. end
  916.  
  917. function DBM:RegisterShortTermEvents(...)
  918. if self.shortTermEventsRegistered then
  919. return
  920. end
  921. self.shortTermRegisterEvents = {...}
  922. for k, v in pairs(self.shortTermRegisterEvents) do
  923. if v:sub(0, 5) == "UNIT_" and v:sub(v:len() - 10) ~= "_UNFILTERED" and not v:find(" ") and v ~= "UNIT_DIED" and v ~= "UNIT_DESTROYED" then
  924. -- legacy event, oh noes
  925. self.shortTermRegisterEvents[k] = v .. " boss1 boss2 boss3 boss4 boss5 target focus"
  926. end
  927. end
  928. self.shortTermEventsRegistered = 1
  929. self:RegisterEvents(unpack(self.shortTermRegisterEvents))
  930. end
  931.  
  932. function DBM:UnregisterShortTermEvents()
  933. if self.shortTermRegisterEvents then
  934. for event, mods in pairs(registeredEvents) do
  935. if event:sub(0, 6) == "SPELL_" or event:sub(0, 6) == "RANGE_" then
  936. local i = 1
  937. while mods[i] do
  938. if mods[i] == self then
  939. local findEvent = findRealEvent(self.shortTermRegisterEvents, event)
  940. if findEvent then
  941. unregisterCLEUEvent(self, findEvent)
  942. break
  943. end
  944. end
  945. i = i +1
  946. end
  947. else
  948. local match = false
  949. for i = #mods, 1, -1 do
  950. if mods[i] == self and checkEntry(self.shortTermRegisterEvents, event) then
  951. tremove(mods, i)
  952. match = true
  953. end
  954. end
  955. if #mods == 0 or (match and event:sub(0, 5) == "UNIT_" and event:sub(0, -10) ~= "_UNFILTERED" and event ~= "UNIT_DIED" and event ~= "UNIT_DESTROYED") then
  956. unregisterUEvent(self, event)
  957. end
  958. if #mods == 0 then
  959. registeredEvents[event] = nil
  960. end
  961. end
  962. end
  963. self.shortTermEventsRegistered = nil
  964. self.shortTermRegisterEvents = nil
  965. end
  966. end
  967.  
  968. DBM:RegisterEvents("ADDON_LOADED")
  969.  
  970. function DBM:FilterRaidBossEmote(msg, ...)
  971. return handleEvent(nil, "CHAT_MSG_RAID_BOSS_EMOTE_FILTERED", msg:gsub("\124c%x+(.-)\124r", "%1"), ...)
  972. end
  973.  
  974. local noArgTableEvents = {
  975. SWING_DAMAGE = true,
  976. SWING_MISSED = true,
  977. RANGE_DAMAGE = true,
  978. RANGE_MISSED = true,
  979. SPELL_DAMAGE = true,
  980. SPELL_BUILDING_DAMAGE = true,
  981. SPELL_MISSED = true,
  982. SPELL_ABSORBED = true,
  983. SPELL_HEAL = true,
  984. SPELL_ENERGIZE = true,
  985. SPELL_PERIODIC_ENERGIZE = true,
  986. SPELL_PERIODIC_MISSED = true,
  987. SPELL_PERIODIC_DAMAGE = true,
  988. SPELL_PERIODIC_DRAIN = true,
  989. SPELL_PERIODIC_LEECH = true,
  990. SPELL_PERIODIC_ENERGIZE = true,
  991. SPELL_DRAIN = true,
  992. SPELL_LEECH = true,
  993. SPELL_CAST_FAILED = true
  994. }
  995. function DBM:COMBAT_LOG_EVENT_UNFILTERED(timestamp, event, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, ...)
  996. if not registeredEvents[event] then return end
  997. local eventSub6 = event:sub(0, 6)
  998. if (eventSub6 == "SPELL_" or eventSub6 == "RANGE_") and not unfilteredCLEUEvents[event] then
  999. local spellId = ...
  1000. if not registeredSpellIds[event][spellId] then return end
  1001. end
  1002. -- process some high volume events without building the whole table which is somewhat faster
  1003. -- this prevents work-around with mods that used to have their own event handler to prevent this overhead
  1004. if noArgTableEvents[event] then
  1005. return handleEvent(nil, event, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, ...)
  1006. else
  1007. twipe(args)
  1008. args.timestamp = timestamp
  1009. args.event = event
  1010. args.sourceGUID = sourceGUID
  1011. args.sourceName = sourceName
  1012. args.sourceFlags = sourceFlags
  1013. args.sourceRaidFlags = sourceRaidFlags
  1014. args.destGUID = destGUID
  1015. args.destName = destName
  1016. args.destFlags = destFlags
  1017. args.destRaidFlags = destRaidFlags
  1018. if eventSub6 == "SPELL_" then
  1019. args.spellId, args.spellName = ...
  1020. if event == "SPELL_AURA_APPLIED" or event == "SPELL_AURA_REFRESH" or event == "SPELL_AURA_REMOVED" then
  1021. if not args.sourceName then
  1022. args.sourceName = args.destName
  1023. args.sourceGUID = args.destGUID
  1024. args.sourceFlags = args.destFlags
  1025. end
  1026. elseif event == "SPELL_AURA_APPLIED_DOSE" or event == "SPELL_AURA_REMOVED_DOSE" then
  1027. local _
  1028. _, _, _, _, args.amount = ...
  1029. if not args.sourceName then
  1030. args.sourceName = args.destName
  1031. args.sourceGUID = args.destGUID
  1032. args.sourceFlags = args.destFlags
  1033. end
  1034. elseif event == "SPELL_INTERRUPT" or event == "SPELL_DISPEL" or event == "SPELL_DISPEL_FAILED" or event == "SPELL_AURA_STOLEN" then
  1035. local _
  1036. _, _, _, args.extraSpellId, args.extraSpellName = ...
  1037. end
  1038. elseif event == "UNIT_DIED" or event == "UNIT_DESTROYED" then
  1039. args.sourceName = args.destName
  1040. args.sourceGUID = args.destGUID
  1041. args.sourceFlags = args.destFlags
  1042. elseif event == "ENVIRONMENTAL_DAMAGE" then
  1043. args.environmentalType, args.amount, args.overkill, args.school, args.resisted, args.blocked, args.absorbed, args.critical, args.glancing, args.crushing = ...
  1044. end
  1045. return handleEvent(nil, event, args)
  1046. end
  1047. end
  1048. mainFrame:SetScript("OnEvent", handleEvent)
  1049. end
  1050.  
  1051. --------------
  1052. -- OnLoad --
  1053. --------------
  1054. do
  1055. local isLoaded = false
  1056. local onLoadCallbacks = {}
  1057.  
  1058. local function runDelayedFunctions(self)
  1059. noDelay = false
  1060. --Check if voice pack missing
  1061. local activeVP = self.Options.ChosenVoicePack
  1062. if activeVP ~= "None" then
  1063. if not self.VoiceVersions[activeVP] or (self.VoiceVersions[activeVP] and self.VoiceVersions[activeVP] == 0) then--A voice pack is selected that does not belong
  1064. self.Options.ChosenVoicePack = "None"--Set ChosenVoicePack back to None
  1065. self:AddMsg(DBM_CORE_VOICE_MISSING)
  1066. end
  1067. else
  1068. if #self.Voices > 1 then
  1069. --At least one voice pack installed but activeVP set to "None"
  1070. self:AddMsg(DBM_CORE_VOICE_DISABLED)
  1071. end
  1072. end
  1073. --Check if any of countdown sounds are using missing voice pack
  1074. local voice1 = self.Options.CountdownVoice
  1075. local voice2 = self.Options.CountdownVoice2
  1076. local voice3 = self.Options.CountdownVoice3v2
  1077. if voice1 == "None" then--Migrate to new setting
  1078. self.Options.CountdownVoice = self.DefaultOptions.CountdownVoice
  1079. self.Options.DontPlayCountdowns = true
  1080. end
  1081. local found1, found2, found3 = false, false, false
  1082. for i = 1, #self.Counts do
  1083. local voice = self.Counts[i].value
  1084. if voice == self.Options.CountdownVoice then
  1085. found1 = true
  1086. end
  1087. if voice == self.Options.CountdownVoice2 then
  1088. found2 = true
  1089. end
  1090. if voice == self.Options.CountdownVoice3v2 then
  1091. found3 = true
  1092. end
  1093. end
  1094. if not found1 then
  1095. self:AddMsg(DBM_CORE_VOICE_COUNT_MISSING:format(1))
  1096. self.Options.CountdownVoice = self.DefaultOptions.CountdownVoice
  1097. end
  1098. if not found2 then
  1099. self:AddMsg(DBM_CORE_VOICE_COUNT_MISSING:format(2))
  1100. self.Options.CountdownVoice2 = self.DefaultOptions.CountdownVoice2
  1101. end
  1102. if not found3 then
  1103. self:AddMsg(DBM_CORE_VOICE_COUNT_MISSING:format(3))
  1104. self.Options.CountdownVoice3v2 = self.DefaultOptions.CountdownVoice3v2
  1105. end
  1106. self:BuildVoiceCountdownCache()
  1107. --Break timer recovery
  1108. --Try local settings
  1109. if self.Options.tempBreak2 then
  1110. local timer, startTime = string.split("/", self.Options.tempBreak2)
  1111. local elapsed = time() - tonumber(startTime)
  1112. local remaining = timer - elapsed
  1113. if remaining > 0 then
  1114. breakTimerStart(DBM, remaining, playerName)
  1115. else--It must have ended while we were offline, kill variable.
  1116. self.Options.tempBreak2 = nil
  1117. end
  1118. end
  1119. end
  1120.  
  1121. -- register a callback that will be executed once the addon is fully loaded (ADDON_LOADED fired, saved vars are available)
  1122. function DBM:RegisterOnLoadCallback(cb)
  1123. if isLoaded then
  1124. cb()
  1125. else
  1126. onLoadCallbacks[#onLoadCallbacks + 1] = cb
  1127. end
  1128. end
  1129.  
  1130. function DBM:ADDON_LOADED(modname)
  1131. if modname == "DBM-Core" and not isLoaded then
  1132. dbmToc = tonumber(GetAddOnMetadata("DBM-Core", "X-Min-Interface"))
  1133. isLoaded = true
  1134. for i, v in ipairs(onLoadCallbacks) do
  1135. xpcall(v, geterrorhandler())
  1136. end
  1137. onLoadCallbacks = nil
  1138. loadOptions(self)
  1139. if GetAddOnEnableState(playerName, "VEM-Core") >= 1 then
  1140. self:Disable(true)
  1141. C_TimerAfter(10, function() self:AddMsg(DBM_CORE_VEM) end)
  1142. C_TimerAfter(20, function() self:AddMsg(DBM_CORE_VEM) end)
  1143. return
  1144. end
  1145. if GetAddOnEnableState(playerName, "DBM-Profiles") >= 1 then
  1146. self:Disable(true)
  1147. C_TimerAfter(10, function() self:AddMsg(DBM_CORE_3RDPROFILES) end)
  1148. C_TimerAfter(20, function() self:AddMsg(DBM_CORE_3RDPROFILES) end)
  1149. return
  1150. end
  1151. if GetAddOnEnableState(playerName, "DPMCore") >= 1 then
  1152. self:Disable(true)
  1153. C_TimerAfter(10, function() self:AddMsg(DBM_CORE_DPMCORE) end)
  1154. C_TimerAfter(20, function() self:AddMsg(DBM_CORE_DPMCORE) end)
  1155. return
  1156. end
  1157. self.Bars:LoadOptions("DBM")
  1158. self.Arrow:LoadPosition()
  1159. if not self.Options.ShowMinimapButton then self:HideMinimapButton() end
  1160. local soundChannels = tonumber(GetCVar("Sound_NumChannels")) or 24--if set to 24, may return nil, Defaults usually do
  1161. if soundChannels < 64 then
  1162. SetCVar("Sound_NumChannels", 64)
  1163. end
  1164. self.AddOns = {}
  1165. self.Voices = { {text = "None",value = "None"}, }--Create voice table, with default "None" value
  1166. self.VoiceVersions = {}
  1167. for i = 1, GetNumAddOns() do
  1168. local addonName = GetAddOnInfo(i)
  1169. local enabled = GetAddOnEnableState(playerName, i)
  1170. local minToc = tonumber(GetAddOnMetadata(i, "X-Min-Interface"))
  1171. if GetAddOnMetadata(i, "X-DBM-Mod") and enabled ~= 0 then
  1172. if checkEntry(bannedMods, addonName) then
  1173. self:AddMsg("The mod " .. addonName .. " is deprecated and will not be available. Please remove the folder " .. addonName .. " from your Interface" .. (IsWindowsClient() and "\\" or "/") .. "AddOns folder to get rid of this message. Check for an updated version of " .. addonName .. " that is compatible with your game version.")
  1174. elseif not testBuild and minToc and minToc > wowTOC then
  1175. self:Debug(i.." not loaded because mod requires minimum toc of "..minToc)
  1176. else
  1177. local mapIdTable = {strsplit(",", GetAddOnMetadata(i, "X-DBM-Mod-MapID") or "")}
  1178. tinsert(self.AddOns, {
  1179. sort = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Sort") or mhuge) or mhuge,
  1180. type = GetAddOnMetadata(i, "X-DBM-Mod-Type") or "OTHER",
  1181. category = GetAddOnMetadata(i, "X-DBM-Mod-Category") or "Other",
  1182. name = GetAddOnMetadata(i, "X-DBM-Mod-Name") or GetRealZoneText(tonumber(mapIdTable[1])) or DBM_CORE_UNKNOWN,
  1183. mapId = mapIdTable,
  1184. subTabs = GetAddOnMetadata(i, "X-DBM-Mod-SubCategoriesID") and {strsplit(",", GetAddOnMetadata(i, "X-DBM-Mod-SubCategoriesID"))} or GetAddOnMetadata(i, "X-DBM-Mod-SubCategories") and {strsplit(",", GetAddOnMetadata(i, "X-DBM-Mod-SubCategories"))},
  1185. oneFormat = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-Single-Format") or 0) == 1,
  1186. hasLFR = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-LFR") or 0) == 1,
  1187. hasChallenge = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-Challenge") or 0) == 1,
  1188. noHeroic = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-No-Heroic") or 0) == 1,
  1189. noStatistics = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-No-Statistics") or 0) == 1,
  1190. hasMythic = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-Mythic") or 0) == 1,
  1191. hasTimeWalker = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-Has-TimeWalker") or 0) == 1,
  1192. isWorldBoss = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-World-Boss") or 0) == 1,
  1193. minRevision = tonumber(GetAddOnMetadata(i, "X-DBM-Mod-MinCoreRevision") or 0),
  1194. modId = addonName,
  1195. })
  1196. for i = #self.AddOns[#self.AddOns].mapId, 1, -1 do
  1197. local id = tonumber(self.AddOns[#self.AddOns].mapId[i])
  1198. if id then
  1199. self.AddOns[#self.AddOns].mapId[i] = id
  1200. else
  1201. tremove(self.AddOns[#self.AddOns].mapId, i)
  1202. end
  1203. end
  1204. if self.AddOns[#self.AddOns].subTabs then
  1205. for k, v in ipairs(self.AddOns[#self.AddOns].subTabs) do
  1206. local id = tonumber(self.AddOns[#self.AddOns].subTabs[k])
  1207. if id then
  1208. self.AddOns[#self.AddOns].subTabs[k] = GetRealZoneText(id):trim() or id
  1209. else
  1210. self.AddOns[#self.AddOns].subTabs[k] = (self.AddOns[#self.AddOns].subTabs[k]):trim()
  1211. end
  1212. end
  1213. end
  1214. if GetAddOnMetadata(i, "X-DBM-Mod-LoadCID") then
  1215. local idTable = {strsplit(",", GetAddOnMetadata(i, "X-DBM-Mod-LoadCID"))}
  1216. for i = 1, #idTable do
  1217. loadcIds[tonumber(idTable[i]) or ""] = addonName
  1218. end
  1219. end
  1220. end
  1221. end
  1222. if GetAddOnMetadata(i, "X-DBM-Voice") and enabled ~= 0 then
  1223. if checkEntry(bannedMods, addonName) then
  1224. self:AddMsg("The mod " .. addonName .. " is deprecated and will not be available. Please remove the folder " .. addonName .. " from your Interface" .. (IsWindowsClient() and "\\" or "/") .. "AddOns folder to get rid of this message. Check for an updated version of " .. addonName .. " that is compatible with your game version.")
  1225. else
  1226. local voiceValue = GetAddOnMetadata(i, "X-DBM-Voice-ShortName")
  1227. local voiceVersion = tonumber(GetAddOnMetadata(i, "X-DBM-Voice-Version") or 0)
  1228. if voiceVersion > 0 then--Do not insert voice version 0 into THIS table. 0 should be used by voice packs that insert only countdown
  1229. tinsert(self.Voices, { text = GetAddOnMetadata(i, "X-DBM-Voice-Name"), value = voiceValue })
  1230. end
  1231. self.VoiceVersions[voiceValue] = voiceVersion
  1232. self:Schedule(10, self.CheckVoicePackVersion, self, voiceValue)--Still at 1 since the count sounds won't break any mods or affect filter. V2 if support countsound path
  1233. if GetAddOnMetadata(i, "X-DBM-Voice-HasCount") then--Supports adding countdown options, insert new countdown into table
  1234. tinsert(self.Counts, { text = GetAddOnMetadata(i, "X-DBM-Voice-Name"), value = "VP:"..voiceValue, path = "Interface\\AddOns\\DBM-VP"..voiceValue.."\\count\\", max = 10})
  1235. end
  1236. end
  1237. end
  1238. end
  1239. tsort(self.AddOns, function(v1, v2) return v1.sort < v2.sort end)
  1240. self:RegisterEvents(
  1241. "COMBAT_LOG_EVENT_UNFILTERED",
  1242. "GROUP_ROSTER_UPDATE",
  1243. "INSTANCE_GROUP_SIZE_CHANGED",
  1244. "CHAT_MSG_ADDON",
  1245. "BN_CHAT_MSG_ADDON",
  1246. "PLAYER_REGEN_DISABLED",
  1247. "PLAYER_REGEN_ENABLED",
  1248. "INSTANCE_ENCOUNTER_ENGAGE_UNIT",
  1249. "UNIT_TARGETABLE_CHANGED",
  1250. "UNIT_SPELLCAST_SUCCEEDED boss1 boss2 boss3 boss4 boss5",
  1251. "UNIT_TARGET_UNFILTERED",
  1252. "ENCOUNTER_START",
  1253. "ENCOUNTER_END",
  1254. "BOSS_KILL",
  1255. "UNIT_DIED",
  1256. "UNIT_DESTROYED",
  1257. "UNIT_HEALTH mouseover target focus player",
  1258. "CHAT_MSG_WHISPER",
  1259. "CHAT_MSG_BN_WHISPER",
  1260. "CHAT_MSG_MONSTER_YELL",
  1261. "CHAT_MSG_MONSTER_EMOTE",
  1262. "CHAT_MSG_MONSTER_SAY",
  1263. "CHAT_MSG_RAID_BOSS_EMOTE",
  1264. "RAID_BOSS_EMOTE",
  1265. "RAID_BOSS_WHISPER",
  1266. "PLAYER_ENTERING_WORLD",
  1267. "LFG_ROLE_CHECK_SHOW",
  1268. "LFG_PROPOSAL_SHOW",
  1269. "LFG_PROPOSAL_FAILED",
  1270. "LFG_PROPOSAL_SUCCEEDED",
  1271. "READY_CHECK",
  1272. "UPDATE_BATTLEFIELD_STATUS",
  1273. "CINEMATIC_START",
  1274. "PLAYER_LEVEL_UP",
  1275. "PLAYER_SPECIALIZATION_CHANGED",
  1276. "PARTY_INVITE_REQUEST",
  1277. "LOADING_SCREEN_DISABLED",
  1278. "SCENARIO_CRITERIA_UPDATE"
  1279. )
  1280. RolePollPopup:UnregisterEvent("ROLE_POLL_BEGIN")
  1281. self:GROUP_ROSTER_UPDATE()
  1282. C_TimerAfter(1.5, function()
  1283. combatInitialized = true
  1284. end)
  1285. C_TimerAfter(20, function()--Delay UNIT_HEALTH combat start for 20 sec. (not to break Timer Recovery stuff)
  1286. healthCombatInitialized = true
  1287. end)
  1288. self:Schedule(10, runDelayedFunctions, self)
  1289. end
  1290. end
  1291. end
  1292.  
  1293.  
  1294. -----------------
  1295. -- Callbacks --
  1296. -----------------
  1297. do
  1298. local callbacks = {}
  1299.  
  1300. function fireEvent(event, ...)
  1301. if not callbacks[event] then return end
  1302. for i, v in ipairs(callbacks[event]) do
  1303. local ok, err = pcall(v, event, ...)
  1304. if not ok then DBM:AddMsg(("Error while executing callback %s for event %s: %s"):format(tostring(v), tostring(event), err)) end
  1305. end
  1306. end
  1307.  
  1308. function DBM:IsCallbackRegistered(event, f)
  1309. if not event or type(f) ~= "function" then
  1310. error("Usage: IsCallbackRegistered(event, callbackFunc)", 2)
  1311. end
  1312. if not callbacks[event] then return end
  1313. for i = 1, #callbacks[event] do
  1314. if callbacks[event][i] == f then return true end
  1315. end
  1316. return false
  1317. end
  1318.  
  1319. function DBM:RegisterCallback(event, f)
  1320. if not event or type(f) ~= "function" then
  1321. error("Usage: DBM:RegisterCallback(event, callbackFunc)", 2)
  1322. end
  1323. callbacks[event] = callbacks[event] or {}
  1324. tinsert(callbacks[event], f)
  1325. return #callbacks[event]
  1326. end
  1327.  
  1328. function DBM:UnregisterCallback(event, f)
  1329. if not event or not callbacks[event] then return end
  1330. if f then
  1331. if type(f) ~= "function" then
  1332. error("Usage: UnregisterCallback(event, callbackFunc)", 2)
  1333. end
  1334. --> checking from the end to start and not stoping after found one result in case of a func being twice registered.
  1335. for i = #callbacks[event], 1, -1 do
  1336. if callbacks[event][i] == f then tremove (callbacks[event], i) end
  1337. end
  1338. else
  1339. callbacks[event] = nil
  1340. end
  1341. end
  1342. end
  1343.  
  1344.  
  1345. --------------------------
  1346. -- OnUpdate/Scheduler --
  1347. --------------------------
  1348. do
  1349. -- stack that stores a few tables (up to 8) which will be recycled
  1350. local popCachedTable, pushCachedTable
  1351. local numChachedTables = 0
  1352. do
  1353. local tableCache = nil
  1354.  
  1355. -- gets a table from the stack, it will then be recycled.
  1356. function popCachedTable()
  1357. local t = tableCache
  1358. if t then
  1359. tableCache = t.next
  1360. numChachedTables = numChachedTables - 1
  1361. end
  1362. return t
  1363. end
  1364.  
  1365. -- tries to push a table on the stack
  1366. -- only tables with <= 4 array entries are accepted as cached tables are only used for tasks with few arguments as we don't want to have big arrays wasting our precious memory space doing nothing...
  1367. -- also, the maximum number of cached tables is limited to 8 as DBM rarely has more than eight scheduled tasks with less than 4 arguments at the same time
  1368. -- this is just to re-use all the tables of the small tasks that are scheduled all the time (like the wipe detection)
  1369. -- note that the cache does not use weak references anywhere for performance reasons, so a cached table will never be deleted by the garbage collector
  1370. function pushCachedTable(t)
  1371. if numChachedTables < 8 and #t <= 4 then
  1372. twipe(t)
  1373. t.next = tableCache
  1374. tableCache = t
  1375. numChachedTables = numChachedTables + 1
  1376. end
  1377. end
  1378. end
  1379.  
  1380. -- priority queue (min-heap) that stores all scheduled tasks.
  1381. -- insert: O(log n)
  1382. -- deleteMin: O(log n)
  1383. -- getMin: O(1)
  1384. -- removeAllMatching: O(n)
  1385. local insert, removeAllMatching, getMin, deleteMin
  1386. do
  1387. local heap = {}
  1388. local firstFree = 1
  1389.  
  1390. -- gets the next task
  1391. function getMin()
  1392. return heap[1]
  1393. end
  1394.  
  1395. -- restores the heap invariant by moving an item up
  1396. local function siftUp(n)
  1397. local parent = floor(n / 2)
  1398. while n > 1 and heap[parent].time > heap[n].time do -- move the element up until the heap invariant is restored, meaning the element is at the top or the element's parent is <= the element
  1399. heap[n], heap[parent] = heap[parent], heap[n] -- swap the element with its parent
  1400. n = parent
  1401. parent = floor(n / 2)
  1402. end
  1403. end
  1404.  
  1405. -- restores the heap invariant by moving an item down
  1406. local function siftDown(n)
  1407. local m -- position of the smaller child
  1408. while 2 * n < firstFree do -- #children >= 1
  1409. -- swap the element with its smaller child
  1410. if 2 * n + 1 == firstFree then -- n does not have a right child --> it only has a left child as #children >= 1
  1411. m = 2 * n -- left child
  1412. elseif heap[2 * n].time < heap[2 * n + 1].time then -- #children = 2 and left child < right child
  1413. m = 2 * n -- left child
  1414. else -- #children = 2 and right child is smaller than the left one
  1415. m = 2 * n + 1 -- right
  1416. end
  1417. if heap[n].time <= heap[m].time then -- n is <= its smallest child --> heap invariant restored
  1418. return
  1419. end
  1420. heap[n], heap[m] = heap[m], heap[n]
  1421. n = m
  1422. end
  1423. end
  1424.  
  1425. -- inserts a new element into the heap
  1426. function insert(ele)
  1427. heap[firstFree] = ele
  1428. siftUp(firstFree)
  1429. firstFree = firstFree + 1
  1430. end
  1431.  
  1432. -- deletes the min element
  1433. function deleteMin()
  1434. local min = heap[1]
  1435. firstFree = firstFree - 1
  1436. heap[1] = heap[firstFree]
  1437. heap[firstFree] = nil
  1438. siftDown(1)
  1439. return min
  1440. end
  1441.  
  1442. -- removes multiple scheduled tasks from the heap
  1443. -- note that this function is comparatively slow by design as it has to check all tasks and allows partial matches
  1444. function removeAllMatching(f, mod, ...)
  1445. -- remove all elements that match the signature, this destroyes the heap and leaves a normal array
  1446. local v, match
  1447. local foundMatch = false
  1448. for i = #heap, 1, -1 do -- iterate backwards over the array to allow usage of table.remove
  1449. v = heap[i]
  1450. if (not f or v.func == f) and (not mod or v.mod == mod) then
  1451. match = true
  1452. for i = 1, select("#", ...) do
  1453. if select(i, ...) ~= v[i] then
  1454. match = false
  1455. break
  1456. end
  1457. end
  1458. if match then
  1459. tremove(heap, i)
  1460. firstFree = firstFree - 1
  1461. foundMatch = true
  1462. end
  1463. end
  1464. end
  1465. -- rebuild the heap from the array in O(n)
  1466. if foundMatch then
  1467. for i = floor((firstFree - 1) / 2), 1, -1 do
  1468. siftDown(i)
  1469. end
  1470. end
  1471. end
  1472. end
  1473.  
  1474.  
  1475. local wrappers = {}
  1476. local function range(max, cur, ...)
  1477. cur = cur or 1
  1478. if cur > max then
  1479. return ...
  1480. end
  1481. return cur, range(max, cur + 1, select(2, ...))
  1482. end
  1483. local function getWrapper(n)
  1484. wrappers[n] = wrappers[n] or loadstring(([[
  1485. return function(func, tbl)
  1486. return func(]] .. ("tbl[%s], "):rep(n):sub(0, -3) .. [[)
  1487. end
  1488. ]]):format(range(n)))()
  1489. return wrappers[n]
  1490. end
  1491.  
  1492. local nextModSyncSpamUpdate = 0
  1493. --mainFrame:SetScript("OnUpdate", function(self, elapsed)
  1494. local function onUpdate(self, elapsed)
  1495. local time = GetTime()
  1496.  
  1497. -- execute scheduled tasks
  1498. local nextTask = getMin()
  1499. while nextTask and nextTask.func and nextTask.time <= time do
  1500. deleteMin()
  1501. local n = nextTask.n
  1502. if n == #nextTask then
  1503. nextTask.func(unpack(nextTask))
  1504. else
  1505. -- too many nil values (or a trailing nil)
  1506. -- this is bad because unpack will not work properly
  1507. -- TODO: is there a better solution?
  1508. getWrapper(n)(nextTask.func, nextTask)
  1509. end
  1510. pushCachedTable(nextTask)
  1511. nextTask = getMin()
  1512. end
  1513.  
  1514. -- execute OnUpdate handlers of all modules
  1515. local foundModFunctions = 0
  1516. for i, v in pairs(updateFunctions) do
  1517. foundModFunctions = foundModFunctions + 1
  1518. if i.Options.Enabled and (not i.zones or i.zones[LastInstanceMapID]) then
  1519. i.elapsed = (i.elapsed or 0) + elapsed
  1520. if i.elapsed >= (i.updateInterval or 0) then
  1521. v(i, i.elapsed)
  1522. i.elapsed = 0
  1523. end
  1524. end
  1525. end
  1526.  
  1527. -- clean up sync spam timers and auto respond spam blockers
  1528. if time > nextModSyncSpamUpdate then
  1529. nextModSyncSpamUpdate = time + 20
  1530. -- TODO: optimize this; using next(t, k) all the time on nearly empty hash tables is not a good idea...doesn't really matter here as modSyncSpam only very rarely contains more than 4 entries...
  1531. -- we now do this just every 20 seconds since the earlier assumption about modSyncSpam isn't true any longer
  1532. -- note that not removing entries at all would be just a small memory leak and not a problem (the sync functions themselves check the timestamp)
  1533. local k, v = next(modSyncSpam, nil)
  1534. if v and (time - v > 8) then
  1535. modSyncSpam[k] = nil
  1536. end
  1537. end
  1538. if not nextTask and foundModFunctions == 0 then--Nothing left, stop scheduler
  1539. schedulerFrame:SetScript("OnUpdate", nil)
  1540. schedulerFrame:Hide()
  1541. end
  1542. end
  1543.  
  1544. function startScheduler()
  1545. if not schedulerFrame:IsShown() then
  1546. schedulerFrame:Show()
  1547. schedulerFrame:SetScript("OnUpdate", onUpdate)
  1548. end
  1549. end
  1550.  
  1551. function schedule(t, f, mod, ...)
  1552. if type(f) ~= "function" then
  1553. error("usage: schedule(time, func, [mod, args...])", 2)
  1554. end
  1555. startScheduler()
  1556. local v
  1557. if numChachedTables > 0 and select("#", ...) <= 4 then -- a cached table is available and all arguments fit into an array with four slots
  1558. v = popCachedTable()
  1559. v.time = GetTime() + t
  1560. v.func = f
  1561. v.mod = mod
  1562. v.n = select("#", ...)
  1563. for i = 1, v.n do
  1564. v[i] = select(i, ...)
  1565. end
  1566. -- clear slots if necessary
  1567. for i = v.n + 1, 4 do
  1568. v[i] = nil
  1569. end
  1570. else -- create a new table
  1571. v = {time = GetTime() + t, func = f, mod = mod, n = select("#", ...), ...}
  1572. end
  1573. insert(v)
  1574. end
  1575.  
  1576. function unschedule(f, mod, ...)
  1577. if not f and not mod then
  1578. -- you really want to kill the complete scheduler? call unscheduleAll
  1579. error("cannot unschedule everything, pass a function and/or a mod")
  1580. end
  1581. return removeAllMatching(f, mod, ...)
  1582. end
  1583.  
  1584. function unscheduleAll()
  1585. return removeAllMatching()
  1586. end
  1587. end
  1588.  
  1589. function DBM:Schedule(t, f, ...)
  1590. if type(f) ~= "function" then
  1591. error("usage: DBM:Schedule(time, func, [args...])", 2)
  1592. end
  1593. return schedule(t, f, nil, ...)
  1594. end
  1595.  
  1596. function DBM:Unschedule(f, ...)
  1597. return unschedule(f, nil, ...)
  1598. end
  1599.  
  1600. ---------------
  1601. -- Profile --
  1602. ---------------
  1603. function DBM:CreateProfile(name)
  1604. if not name or name == "" or name:find(" ") then
  1605. self:AddMsg(DBM_CORE_PROFILE_CREATE_ERROR)
  1606. return
  1607. end
  1608. if DBM_AllSavedOptions[name] then
  1609. self:AddMsg(DBM_CORE_PROFILE_CREATE_ERROR_D:format(name))
  1610. return
  1611. end
  1612. -- create profile
  1613. usedProfile = name
  1614. DBM_UsedProfile = usedProfile
  1615. DBM_AllSavedOptions[usedProfile] = DBM_AllSavedOptions[usedProfile] or {}
  1616. self:AddDefaultOptions(DBM_AllSavedOptions[usedProfile], self.DefaultOptions)
  1617. self.Options = DBM_AllSavedOptions[usedProfile]
  1618. -- rearrange position
  1619. self.Bars:CreateProfile("DBM")
  1620. self:RepositionFrames()
  1621. self:AddMsg(DBM_CORE_PROFILE_CREATED:format(name))
  1622. end
  1623.  
  1624. function DBM:ApplyProfile(name)
  1625. if not name or not DBM_AllSavedOptions[name] then
  1626. self:AddMsg(DBM_CORE_PROFILE_APPLY_ERROR:format(name or DBM_CORE_UNKNOWN))
  1627. return
  1628. end
  1629. usedProfile = name
  1630. DBM_UsedProfile = usedProfile
  1631. self:AddDefaultOptions(DBM_AllSavedOptions[usedProfile], self.DefaultOptions)
  1632. self.Options = DBM_AllSavedOptions[usedProfile]
  1633. -- rearrange position
  1634. self.Bars:ApplyProfile("DBM")
  1635. self:RepositionFrames()
  1636. self:AddMsg(DBM_CORE_PROFILE_APPLIED:format(name))
  1637. end
  1638.  
  1639. function DBM:CopyProfile(name)
  1640. if not name or not DBM_AllSavedOptions[name] then
  1641. self:AddMsg(DBM_CORE_PROFILE_COPY_ERROR:format(name or DBM_CORE_UNKNOWN))
  1642. return
  1643. elseif name == usedProfile then
  1644. self:AddMsg(DBM_CORE_PROFILE_COPY_ERROR_SELF)
  1645. return
  1646. end
  1647. DBM_AllSavedOptions[usedProfile] = DBM_AllSavedOptions[name]
  1648. self:AddDefaultOptions(DBM_AllSavedOptions[usedProfile], self.DefaultOptions)
  1649. self.Options = DBM_AllSavedOptions[usedProfile]
  1650. -- rearrange position
  1651. self.Bars:CopyProfile(name, "DBM")
  1652. self:RepositionFrames()
  1653. self:AddMsg(DBM_CORE_PROFILE_COPIED:format(name))
  1654. end
  1655.  
  1656. function DBM:DeleteProfile(name)
  1657. if not name or not DBM_AllSavedOptions[name] then
  1658. self:AddMsg(DBM_CORE_PROFILE_DELETE_ERROR:format(name or DBM_CORE_UNKNOWN))
  1659. return
  1660. elseif name == "Default" then-- Default profile cannot be deleted.
  1661. self:AddMsg(DBM_CORE_PROFILE_CANNOT_DELETE)
  1662. return
  1663. end
  1664. --Delete
  1665. DBM_AllSavedOptions[name] = nil
  1666. usedProfile = "Default"--Restore to default
  1667. DBM_UsedProfile = usedProfile
  1668. self.Options = DBM_AllSavedOptions[usedProfile]
  1669. if not self.Options then
  1670. -- the default profile got lost somehow (maybe WoW crashed and the saved variables file got corrupted)
  1671. self:CreateProfile("Default")
  1672. end
  1673. -- rearrange position
  1674. self.Bars:DeleteProfile(name, "DBM")
  1675. self:RepositionFrames()
  1676. self:AddMsg(DBM_CORE_PROFILE_DELETED:format(name))
  1677. end
  1678.  
  1679. function DBM:RepositionFrames()
  1680. -- rearrange position
  1681. self:UpdateWarningOptions()
  1682. self:UpdateSpecialWarningOptions()
  1683. self.Arrow:LoadPosition()
  1684. if DBMBossHealth then
  1685. DBMBossHealth:ClearAllPoints()
  1686. DBMBossHealth:SetPoint(self.Options.HPFramePoint, UIParent, self.Options.HPFramePoint, self.Options.HPFrameX, self.Options.HPFrameY)
  1687. end
  1688. if DBMRangeCheck then
  1689. DBMRangeCheck:ClearAllPoints()
  1690. DBMRangeCheck:SetPoint(self.Options.RangeFramePoint, UIParent, self.Options.RangeFramePoint, self.Options.RangeFrameX, self.Options.RangeFrameY)
  1691. end
  1692. if DBMRangeCheckRadar then
  1693. DBMRangeCheckRadar:ClearAllPoints()
  1694. DBMRangeCheckRadar:SetPoint(self.Options.RangeFrameRadarPoint, UIParent, self.Options.RangeFrameRadarPoint, self.Options.RangeFrameRadarX, self.Options.RangeFrameRadarY)
  1695. end
  1696. if DBMInfoFrame then
  1697. DBMInfoFrame:ClearAllPoints()
  1698. DBMInfoFrame:SetPoint(self.Options.InfoFramePoint, UIParent, self.Options.InfoFramePoint, self.Options.InfoFrameX, self.Options.InfoFrameY)
  1699. end
  1700. end
  1701.  
  1702. ----------------------
  1703. -- Slash Commands --
  1704. ----------------------
  1705. do
  1706. local function Pull(timer)
  1707. local LFGTankException = IsPartyLFG() and UnitGroupRolesAssigned("player") == "TANK"--Tanks in LFG need to be able to send pull timer even if someone refuses to pass lead. LFG locks roles so no one can abuse this.
  1708. if (DBM:GetRaidRank(playerName) == 0 and IsInGroup() and not LFGTankException) or select(2, IsInInstance()) == "pvp" or IsEncounterInProgress() then
  1709. return DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
  1710. end
  1711. local targetName = (UnitExists("target") and UnitIsEnemy("player", "target")) and UnitName("target") or nil--Filter non enemies in case player isn't targetting bos but another player/pet
  1712. if targetName then
  1713. sendSync("PT", timer.."\t"..LastInstanceMapID.."\t"..targetName)
  1714. else
  1715. sendSync("PT", timer.."\t"..LastInstanceMapID)
  1716. end
  1717. end
  1718. local function Break(timer)
  1719. if IsInGroup() and (DBM:GetRaidRank(playerName) == 0 or IsPartyLFG()) or IsEncounterInProgress() or select(2, IsInInstance()) == "pvp" then--No break timers if not assistant or if it's dungeon/raid finder/BG
  1720. DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
  1721. return
  1722. end
  1723. if timer > 60 then
  1724. DBM:AddMsg(DBM_CORE_BREAK_USAGE)
  1725. return
  1726. end
  1727. timer = timer * 60
  1728. sendSync("BT", timer)
  1729. end
  1730.  
  1731. SLASH_DEADLYBOSSMODS1 = "/dbm"
  1732. SLASH_DEADLYBOSSMODSRPULL1 = "/rpull"
  1733. SLASH_DEADLYBOSSMODSDWAY1 = "/dway"--/way not used because DBM would load before TomTom and can't check
  1734. SlashCmdList["DEADLYBOSSMODSDWAY"] = function(msg)
  1735. if DBM:HasMapRestrictions() then
  1736. DBM:AddMsg(DBM_CORE_NO_ARROW)
  1737. return
  1738. end
  1739. local x, y = string.split(" ", msg:sub(1):trim())
  1740. local xNum, yNum = tonumber(x or ""), tonumber(y or "")
  1741. local success
  1742. if xNum and yNum then
  1743. DBM.Arrow:ShowRunTo(xNum, yNum, 0.5, nil, true)
  1744. success = true
  1745. else--Check if they used , instead of space.
  1746. x, y = string.split(",", msg:sub(1):trim())
  1747. xNum, yNum = tonumber(x or ""), tonumber(y or "")
  1748. if xNum and yNum then
  1749. DBM.Arrow:ShowRunTo(xNum, yNum, 0.5, nil, true)
  1750. success = true
  1751. end
  1752. end
  1753. if not success then
  1754. DBM:AddMsg(DBM_ARROW_WAY_USAGE)
  1755. else
  1756. DBM:AddMsg(DBM_ARROW_WAY_SUCCESS)
  1757. end
  1758. end
  1759. if not BigWigs then
  1760. --Register pull and break slash commands for BW converts, if BW isn't loaded
  1761. --This shouldn't raise an issue since BW SHOULD load before DBM in any case they are both present.
  1762. SLASH_DEADLYBOSSMODSPULL1 = "/pull"
  1763. SLASH_DEADLYBOSSMODSBREAK1 = "/break"
  1764. SlashCmdList["DEADLYBOSSMODSPULL"] = function(msg)
  1765. Pull(tonumber(msg) or 10)
  1766. end
  1767. SlashCmdList["DEADLYBOSSMODSBREAK"] = function(msg)
  1768. Break(tonumber(msg) or 10)
  1769. end
  1770. end
  1771. SlashCmdList["DEADLYBOSSMODSRPULL"] = function(msg)
  1772. Pull(30)
  1773. end
  1774. SlashCmdList["DEADLYBOSSMODS"] = function(msg)
  1775. local cmd = msg:lower()
  1776. if cmd == "ver" or cmd == "version" then
  1777. DBM:ShowVersions(false)
  1778. elseif cmd == "ver2" or cmd == "version2" then
  1779. DBM:ShowVersions(true)
  1780. elseif cmd == "unlock" or cmd == "move" then
  1781. DBM.Bars:ShowMovableBar()
  1782. elseif cmd == "help2" then
  1783. for i, v in ipairs(DBM_CORE_SLASHCMD_HELP2) do DBM:AddMsg(v) end
  1784. elseif cmd == "help" then
  1785. for i, v in ipairs(DBM_CORE_SLASHCMD_HELP) do DBM:AddMsg(v) end
  1786. elseif cmd:sub(1, 13) == "timer endloop" then
  1787. DBM:CreatePizzaTimer(time, "", nil, nil, nil, nil, true)
  1788. elseif cmd:sub(1, 5) == "timer" then
  1789. local time, text = msg:match("^%w+ ([%d:]+) (.+)$")
  1790. if not (time and text) then
  1791. for i, v in ipairs(DBM_CORE_TIMER_USAGE) do DBM:AddMsg(v) end
  1792. --DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
  1793. return
  1794. end
  1795. local min, sec = string.split(":", time)
  1796. min = tonumber(min or "") or 0
  1797. sec = tonumber(sec or "")
  1798. if min and not sec then
  1799. sec = min
  1800. min = 0
  1801. end
  1802. time = min * 60 + sec
  1803. DBM:CreatePizzaTimer(time, text)
  1804. elseif cmd:sub(1, 6) == "ctimer" then
  1805. local time, text = msg:match("^%w+ ([%d:]+) (.+)$")
  1806. if not (time and text) then
  1807. DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
  1808. return
  1809. end
  1810. local min, sec = string.split(":", time)
  1811. min = tonumber(min or "") or 0
  1812. sec = tonumber(sec or "")
  1813. if min and not sec then
  1814. sec = min
  1815. min = 0
  1816. end
  1817. time = min * 60 + sec
  1818. DBM:CreatePizzaTimer(time, text, nil, nil, true)
  1819. elseif cmd:sub(1, 6) == "ltimer" then
  1820. local time, text = msg:match("^%w+ ([%d:]+) (.+)$")
  1821. if not (time and text) then
  1822. DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
  1823. return
  1824. end
  1825. local min, sec = string.split(":", time)
  1826. min = tonumber(min or "") or 0
  1827. sec = tonumber(sec or "")
  1828. if min and not sec then
  1829. sec = min
  1830. min = 0
  1831. end
  1832. time = min * 60 + sec
  1833. DBM:CreatePizzaTimer(time, text, nil, nil, nil, true)
  1834. elseif cmd:sub(1, 7) == "cltimer" then
  1835. local time, text = msg:match("^%w+ ([%d:]+) (.+)$")
  1836. if not (time and text) then
  1837. DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
  1838. return
  1839. end
  1840. local min, sec = string.split(":", time)
  1841. min = tonumber(min or "") or 0
  1842. sec = tonumber(sec or "")
  1843. if min and not sec then
  1844. sec = min
  1845. min = 0
  1846. end
  1847. time = min * 60 + sec
  1848. DBM:CreatePizzaTimer(time, text, nil, nil, true, true)
  1849. elseif cmd:sub(1, 15) == "broadcast timer" then--Standard Timer
  1850. local permission = true
  1851. if DBM:GetRaidRank(playerName) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then
  1852. DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
  1853. permission = false
  1854. end
  1855. local time, text = msg:match("^%w+ %w+ ([%d:]+) (.+)$")
  1856. if not (time and text) then
  1857. DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
  1858. return
  1859. end
  1860. local min, sec = string.split(":", time)
  1861. min = tonumber(min or "") or 0
  1862. sec = tonumber(sec or "")
  1863. if min and not sec then
  1864. sec = min
  1865. min = 0
  1866. end
  1867. time = min * 60 + sec
  1868. DBM:CreatePizzaTimer(time, text, permission)
  1869. elseif cmd:sub(1, 16) == "broadcast ctimer" then
  1870. local permission = true
  1871. if DBM:GetRaidRank(playerName) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then
  1872. DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
  1873. permission = false
  1874. end
  1875. local time, text = msg:match("^%w+ %w+ ([%d:]+) (.+)$")
  1876. if not (time and text) then
  1877. DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
  1878. return
  1879. end
  1880. local min, sec = string.split(":", time)
  1881. min = tonumber(min or "") or 0
  1882. sec = tonumber(sec or "")
  1883. if min and not sec then
  1884. sec = min
  1885. min = 0
  1886. end
  1887. time = min * 60 + sec
  1888. DBM:CreatePizzaTimer(time, text, permission, nil, true)
  1889. elseif cmd:sub(1, 16) == "broadcast ltimer" then
  1890. local permission = true
  1891. if DBM:GetRaidRank(playerName) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then
  1892. DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
  1893. permission = false
  1894. end
  1895. local time, text = msg:match("^%w+ %w+ ([%d:]+) (.+)$")
  1896. if not (time and text) then
  1897. DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
  1898. return
  1899. end
  1900. local min, sec = string.split(":", time)
  1901. min = tonumber(min or "") or 0
  1902. sec = tonumber(sec or "")
  1903. if min and not sec then
  1904. sec = min
  1905. min = 0
  1906. end
  1907. time = min * 60 + sec
  1908. DBM:CreatePizzaTimer(time, text, permission, nil, nil, true)
  1909. elseif cmd:sub(1, 17) == "broadcast cltimer" then
  1910. local permission = true
  1911. if DBM:GetRaidRank(playerName) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then
  1912. DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
  1913. permission = false
  1914. end
  1915. local time, text = msg:match("^%w+ %w+ ([%d:]+) (.+)$")
  1916. if not (time and text) then
  1917. DBM:AddMsg(DBM_PIZZA_ERROR_USAGE)
  1918. return
  1919. end
  1920. local min, sec = string.split(":", time)
  1921. min = tonumber(min or "") or 0
  1922. sec = tonumber(sec or "")
  1923. if min and not sec then
  1924. sec = min
  1925. min = 0
  1926. end
  1927. time = min * 60 + sec
  1928. DBM:CreatePizzaTimer(time, text, permission, nil, true, true)
  1929. elseif cmd:sub(0,5) == "break" then
  1930. local timer = tonumber(cmd:sub(6)) or 5
  1931. Break(timer)
  1932. elseif cmd:sub(1, 4) == "pull" then
  1933. local timer = tonumber(cmd:sub(5)) or 10
  1934. Pull(timer)
  1935. elseif cmd:sub(1, 5) == "rpull" then
  1936. Pull(30)
  1937. elseif cmd:sub(1, 3) == "lag" then
  1938. if not LL then
  1939. DBM:AddMsg(DBM_CORE_UPDATE_REQUIRES_RELAUNCH)
  1940. return
  1941. end
  1942. LL:RequestLatency()
  1943. DBM:AddMsg(DBM_CORE_LAG_CHECKING)
  1944. C_TimerAfter(5, function() DBM:ShowLag() end)
  1945. elseif cmd:sub(1, 3) == "hud" then
  1946. if DBM:HasMapRestrictions() then
  1947. DBM:AddMsg(DBM_CORE_NO_HUD)
  1948. return
  1949. end
  1950. local hudType, target, duration = string.split(" ", msg:sub(4):trim())
  1951. if hudType == "" then
  1952. for i, v in ipairs(DBM_CORE_HUD_USAGE) do
  1953. DBM:AddMsg(v)
  1954. end
  1955. return
  1956. end
  1957. local hudDuration = tonumber(duration) or 1200--if no duration defined. 20 minutes to cover even longest of fights
  1958. local success = false
  1959. if type(hudType) == "string" and hudType:trim() ~= "" then
  1960. if hudType:upper() == "HIDE" then
  1961. DBMHudMap:Disable()
  1962. return
  1963. end
  1964. if not target then
  1965. DBM:AddMsg(DBM_CORE_HUD_INVALID_TARGET)
  1966. return
  1967. end
  1968. local uId
  1969. if target:upper() == "TARGET" and UnitExists("target") then
  1970. uId = "target"
  1971. elseif target:upper() == "FOCUS" and UnitExists("focus") then
  1972. uId = "focus"
  1973. else--Try to use it as player name
  1974. uId = DBM:GetRaidUnitId(target)
  1975. end
  1976. if not uId then
  1977. DBM:AddMsg(DBM_CORE_HUD_INVALID_TARGET)
  1978. return
  1979. end
  1980. if UnitIsUnit("player", uId) and not DBM.Options.DebugMode then--Don't allow hud to self, except if debug mode is enabled, then hud to self useful for testing
  1981. DBM:AddMsg(DBM_CORE_HUD_INVALID_SELF)
  1982. return
  1983. end
  1984. if hudType:upper() == "ARROW" then
  1985. local _, targetClass = UnitClass(uId)
  1986. local color2 = RAID_CLASS_COLORS[targetClass]
  1987. local m1 = DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "party", playerName, 0.1, hudDuration, 0, 1, 0, 1, nil, false):Appear()
  1988. local m2 = DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "party", UnitName(uId), 0.75, hudDuration, color2.r, color2.g, color2.b, 1, nil, false):Appear()
  1989. m2:EdgeTo(m1, nil, hudDuration, 0, 1, 0, 1)
  1990. success = true
  1991. elseif hudType:upper() == "DOT" then
  1992. local _, targetClass = UnitClass(uId)
  1993. local color2 = RAID_CLASS_COLORS[targetClass]
  1994. DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "party", UnitName(uId), 0.75, hudDuration, color2.r, color2.g, color2.b, 1, nil, false):Appear()
  1995. success = true
  1996. elseif hudType:upper() == "GREEN" then
  1997. DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "highlight", UnitName(uId), 3.5, hudDuration, 0, 1, 0, 0.5, nil, false):Pulse(0.5, 0.5)
  1998. success = true
  1999. elseif hudType:upper() == "RED" then
  2000. DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "highlight", UnitName(uId), 3.5, hudDuration, 1, 0, 0, 0.5, nil, false):Pulse(0.5, 0.5)
  2001. success = true
  2002. elseif hudType:upper() == "YELLOW" then
  2003. DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "highlight", UnitName(uId), 3.5, hudDuration, 1, 1, 0, 0.5, nil, false):Pulse(0.5, 0.5)
  2004. success = true
  2005. elseif hudType:upper() == "BLUE" then
  2006. DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "highlight", UnitName(uId), 3.5, hudDuration, 0, 0, 1, 0.5, nil, false):Pulse(0.5, 0.5)
  2007. success = true
  2008. elseif hudType:upper() == "ICON" then
  2009. local icon = GetRaidTargetIndex(uId)
  2010. if not icon then
  2011. DBM:AddMsg(DBM_CORE_HUD_INVALID_ICON)
  2012. return
  2013. end
  2014. if icon == 8 then
  2015. DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "skull", UnitName(uId), 3.5, hudDuration, 1, 1, 1, 0.5, nil, false):Pulse(0.5, 0.5)
  2016. elseif icon == 7 then
  2017. DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "cross", UnitName(uId), 3.5, hudDuration, 1, 1, 1, 0.5, nil, false):Pulse(0.5, 0.5)
  2018. elseif icon == 6 then
  2019. DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "square", UnitName(uId), 3.5, hudDuration, 1, 1, 1, 0.5, nil, false):Pulse(0.5, 0.5)
  2020. elseif icon == 5 then
  2021. DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "moon", UnitName(uId), 3.5, hudDuration, 1, 1, 1, 0.5, nil, false):Pulse(0.5, 0.5)
  2022. elseif icon == 4 then
  2023. DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "triangle", UnitName(uId), 3.5, hudDuration, 1, 1, 1, 0.5, nil, false):Pulse(0.5, 0.5)
  2024. elseif icon == 3 then
  2025. DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "diamond", UnitName(uId), 3.5, hudDuration, 1, 1, 1, 0.5, nil, false):Pulse(0.5, 0.5)
  2026. elseif icon == 2 then
  2027. DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "circle", UnitName(uId), 3.5, hudDuration, 1, 1, 1, 0.5, nil, false):Pulse(0.5, 0.5)
  2028. elseif icon == 1 then
  2029. DBMHudMap:RegisterRangeMarkerOnPartyMember(12345, "star", UnitName(uId), 3.5, hudDuration, 1, 1, 1, 0.5, nil, false):Pulse(0.5, 0.5)
  2030. end
  2031. success = true
  2032. else
  2033. DBM:AddMsg(DBM_CORE_HUD_INVALID_TYPE)
  2034. end
  2035. end
  2036. if success then
  2037. DBM:AddMsg(DBM_CORE_HUD_SUCCESS:format(strFromTime(hudDuration)))
  2038. end
  2039. elseif cmd:sub(1, 5) == "arrow" then
  2040. if DBM:HasMapRestrictions() then
  2041. DBM:AddMsg(DBM_CORE_NO_ARROW)
  2042. return
  2043. end
  2044. local x, y, z = string.split(" ", msg:sub(6):trim())
  2045. local xNum, yNum, zNum = tonumber(x or ""), tonumber(y or ""), tonumber(z or "")
  2046. local success
  2047. if xNum and yNum then
  2048. DBM.Arrow:ShowRunTo(xNum, yNum, 0)
  2049. success = true
  2050. elseif type(x) == "string" and x:trim() ~= "" then
  2051. local subCmd = x:trim()
  2052. if subCmd:upper() == "HIDE" then
  2053. DBM.Arrow:Hide()
  2054. success = true
  2055. elseif subCmd:upper() == "MOVE" then
  2056. DBM.Arrow:Move()
  2057. success = true
  2058. elseif subCmd:upper() == "TARGET" then
  2059. DBM.Arrow:ShowRunTo("target")
  2060. success = true
  2061. elseif subCmd:upper() == "FOCUS" then
  2062. DBM.Arrow:ShowRunTo("focus")
  2063. success = true
  2064. elseif subCmd:upper() == "MAP" then
  2065. DBM.Arrow:ShowRunTo(yNum, zNum, 0, nil, true)
  2066. success = true
  2067. elseif DBM:GetRaidUnitId(subCmd) then
  2068. DBM.Arrow:ShowRunTo(subCmd)
  2069. success = true
  2070. end
  2071. end
  2072. if not success then
  2073. for i, v in ipairs(DBM_ARROW_ERROR_USAGE) do
  2074. DBM:AddMsg(v)
  2075. end
  2076. end
  2077. elseif cmd:sub(1, 7) == "lockout" or cmd:sub(1, 3) == "ids" then
  2078. if DBM:GetRaidRank(playerName) == 0 then
  2079. return DBM:AddMsg(DBM_ERROR_NO_PERMISSION)
  2080. end
  2081. if not IsInRaid() then
  2082. return DBM:AddMsg(DBM_ERROR_NO_RAID)
  2083. end
  2084. DBM:RequestInstanceInfo()
  2085. elseif cmd:sub(1, 10) == "debuglevel" then
  2086. local level = tonumber(cmd:sub(11)) or 1
  2087. if level < 1 or level > 3 then
  2088. DBM:AddMsg("Invalid Value. Debug Level must be between 1 and 3.")
  2089. return
  2090. end
  2091. DBM.Options.DebugLevel = level
  2092. DBM:AddMsg("Debug Level is " .. level)
  2093. elseif cmd:sub(1, 5) == "debug" then
  2094. DBM.Options.DebugMode = DBM.Options.DebugMode == false and true or false
  2095. DBM:AddMsg("Debug Message is " .. (DBM.Options.DebugMode and "ON" or "OFF"))
  2096. elseif cmd:sub(1, 8) == "whereiam" or cmd:sub(1, 8) == "whereami" then
  2097. if DBM:HasMapRestrictions() then
  2098. DBM:AddMsg("Location debug not available do to instance restrictions")
  2099. return
  2100. end
  2101. local x, y, _, map = UnitPosition("player")
  2102. SetMapToCurrentZone()
  2103. local mapID = GetCurrentMapAreaID()
  2104. local mapx, mapy = GetPlayerMapPosition("player")
  2105. DBM:AddMsg(("Location Information\nYou are at zone %u (%s): x=%f, y=%f.\nLocal Map ID %u (%s): x=%f, y=%f"):format(map, GetRealZoneText(map), x, y, mapID, GetZoneText(), mapx, mapy))
  2106. elseif cmd:sub(1, 7) == "request" then
  2107. DBM:Unschedule(DBM.RequestTimers)
  2108. DBM:RequestTimers(1)
  2109. DBM:RequestTimers(2)
  2110. DBM:RequestTimers(3)
  2111. else
  2112. DBM:LoadGUI()
  2113. end
  2114. end
  2115. end
  2116.  
  2117. do
  2118. local function updateRangeFrame(r, reverse)
  2119. if DBM.RangeCheck:IsShown() then
  2120. DBM.RangeCheck:Hide(true)
  2121. else
  2122. if DBM:HasMapRestrictions() then
  2123. DBM:AddMsg(DBM_CORE_NO_RANGE)
  2124. elseif IsInInstance() then
  2125. DBM:AddMsg(DBM_CORE_NO_RANGE_SOON)
  2126. end
  2127. if r and (r < 201) then
  2128. DBM.RangeCheck:Show(r, nil, true, nil, reverse)
  2129. else
  2130. DBM.RangeCheck:Show(10, nil, true, nil, reverse)
  2131. end
  2132. end
  2133. end
  2134. SLASH_DBMRANGE1 = "/range"
  2135. SLASH_DBMRANGE2 = "/distance"
  2136. SLASH_DBMHUDAR1 = "/hudar"
  2137. SLASH_DBMRRANGE1 = "/rrange"
  2138. SLASH_DBMRRANGE2 = "/rdistance"
  2139. SlashCmdList["DBMRANGE"] = function(msg)
  2140. local r = tonumber(msg) or 10
  2141. updateRangeFrame(r, false)
  2142. end
  2143. SlashCmdList["DBMHUDAR"] = function(msg)
  2144. local r = tonumber(msg) or 10
  2145. DBMHudMap:ToggleHudar(r)
  2146. end
  2147. SlashCmdList["DBMRRANGE"] = function(msg)
  2148. local r = tonumber(msg) or 10
  2149. updateRangeFrame(r, true)
  2150. end
  2151. end
  2152.  
  2153. do
  2154. local sortMe = {}
  2155. local OutdatedUsers = {}
  2156.  
  2157. local function sort(v1, v2)
  2158. if v1.revision and not v2.revision then
  2159. return true
  2160. elseif v2.revision and not v1.revision then
  2161. return false
  2162. elseif v1.revision and v2.revision then
  2163. return v1.revision > v2.revision
  2164. else
  2165. return (v1.bwversion or 0) > (v2.bwversion or 0)
  2166. end
  2167. end
  2168.  
  2169. function DBM:ShowVersions(notify)
  2170. for i, v in pairs(raid) do
  2171. tinsert(sortMe, v)
  2172. end
  2173. tsort(sortMe, sort)
  2174. twipe(OutdatedUsers)
  2175. self:AddMsg(DBM_CORE_VERSIONCHECK_HEADER)
  2176. for i, v in ipairs(sortMe) do
  2177. local name = v.name
  2178. local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(name)]
  2179. if playerColor then
  2180. name = ("|r|cff%.2x%.2x%.2x%s|r|cff%.2x%.2x%.2x"):format(playerColor.r * 255, playerColor.g * 255, playerColor.b * 255, name, 0.41 * 255, 0.8 * 255, 0.94 * 255)
  2181. end
  2182. if v.displayVersion and not v.bwversion then--DBM, no BigWigs
  2183. if self.Options.ShowAllVersions then
  2184. self:AddMsg(DBM_CORE_VERSIONCHECK_ENTRY:format(name, "DBM "..v.displayVersion, "r"..v.revision, v.VPVersion or ""), false)--Only display VP version if not running two mods
  2185. end
  2186. if notify and v.revision < self.ReleaseRevision then
  2187. SendChatMessage(chatPrefixShort..DBM_CORE_YOUR_VERSION_OUTDATED, "WHISPER", nil, v.name)
  2188. end
  2189. elseif self.Options.ShowAllVersions and v.displayVersion and v.bwversion then--DBM & BigWigs
  2190. self:AddMsg(DBM_CORE_VERSIONCHECK_ENTRY_TWO:format(name, "DBM "..v.displayVersion, "r"..v.revision, DBM_BIG_WIGS, versionResponseString:format(v.bwversion, v.bwhash)), false)
  2191. elseif self.Options.ShowAllVersions and not v.displayVersion and v.bwversion then--BigWigs, No DBM
  2192. self:AddMsg(DBM_CORE_VERSIONCHECK_ENTRY:format(name, DBM_BIG_WIGS, versionResponseString:format(v.bwversion, v.bwhash), ""), false)
  2193. else
  2194. if self.Options.ShowAllVersions then
  2195. self:AddMsg(DBM_CORE_VERSIONCHECK_ENTRY_NO_DBM:format(name), false)
  2196. end
  2197. end
  2198. end
  2199. local TotalUsers = #sortMe
  2200. local NoDBM = 0
  2201. local NoBigwigs = 0
  2202. local OldMod = 0
  2203. for i = #sortMe, 1, -1 do
  2204. if not sortMe[i].revision then
  2205. NoDBM = NoDBM + 1
  2206. end
  2207. if not (sortMe[i].bwversion) then
  2208. NoBigwigs = NoBigwigs + 1
  2209. end
  2210. --Table sorting sorts dbm to top, bigwigs underneath. Highest version dbm always at top. so sortMe[1]
  2211. --This check compares all dbm version to highest RELEASE version in raid.
  2212. if sortMe[i].revision and (sortMe[i].revision < sortMe[1].version) or sortMe[i].bwversion and (sortMe[i].bwversion < fakeBWVersion) then
  2213. OldMod = OldMod + 1
  2214. local name = sortMe[i].name
  2215. local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(name)]
  2216. if playerColor then
  2217. name = ("|r|cff%.2x%.2x%.2x%s|r|cff%.2x%.2x%.2x"):format(playerColor.r * 255, playerColor.g * 255, playerColor.b * 255, name, 0.41 * 255, 0.8 * 255, 0.94 * 255)
  2218. end
  2219. tinsert(OutdatedUsers, name)
  2220. end
  2221. end
  2222. local TotalDBM = TotalUsers - NoDBM
  2223. local TotalBW = TotalUsers - NoBigwigs
  2224. self:AddMsg("---", false)
  2225. self:AddMsg(DBM_CORE_VERSIONCHECK_FOOTER:format(TotalDBM, TotalBW), false)
  2226. self:AddMsg(DBM_CORE_VERSIONCHECK_OUTDATED:format(OldMod, #OutdatedUsers > 0 and tconcat(OutdatedUsers, ", ") or NONE), false)
  2227. twipe(OutdatedUsers)
  2228. twipe(sortMe)
  2229. for i = #sortMe, 1, -1 do
  2230. sortMe[i] = nil
  2231. end
  2232. end
  2233. end
  2234.  
  2235.  
  2236. -- Lag checking
  2237. do
  2238. local sortLag = {}
  2239. local nolagResponse = {}
  2240. local function sortit(v1, v2)
  2241. return (v1.worldlag or 0) < (v2.worldlag or 0)
  2242. end
  2243. function DBM:ShowLag()
  2244. for i, v in pairs(raid) do
  2245. tinsert(sortLag, v)
  2246. end
  2247. tsort(sortLag, sortit)
  2248. self:AddMsg(DBM_CORE_LAG_HEADER)
  2249. for i, v in ipairs(sortLag) do
  2250. local name = v.name
  2251. local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(name)]
  2252. if playerColor then
  2253. name = ("|r|cff%.2x%.2x%.2x%s|r|cff%.2x%.2x%.2x"):format(playerColor.r * 255, playerColor.g * 255, playerColor.b * 255, name, 0.41 * 255, 0.8 * 255, 0.94 * 255)
  2254. end
  2255. if v.worldlag then
  2256. self:AddMsg(DBM_CORE_LAG_ENTRY:format(name, v.worldlag, v.homelag), false)
  2257. else
  2258. tinsert(nolagResponse, v.name)
  2259. end
  2260. end
  2261. if #nolagResponse > 0 then
  2262. self:AddMsg(DBM_CORE_LAG_FOOTER:format(tconcat(nolagResponse, ", ")), false)
  2263. for i = #nolagResponse, 1, -1 do
  2264. nolagResponse[i] = nil
  2265. end
  2266. end
  2267. for i = #sortLag, 1, -1 do
  2268. sortLag[i] = nil
  2269. end
  2270. end
  2271. if LL then
  2272. LL:Register("DBM", function(homelag, worldlag, sender, channel)
  2273. if sender and raid[sender] then
  2274. raid[sender].homelag = homelag
  2275. raid[sender].worldlag = worldlag
  2276. end
  2277. end)
  2278. else
  2279. DBM:AddMsg(DBM_CORE_UPDATE_REQUIRES_RELAUNCH)
  2280. end
  2281.  
  2282. end
  2283.  
  2284. -------------------
  2285. -- Pizza Timer --
  2286. -------------------
  2287. do
  2288.  
  2289. local function loopTimer(time, text, broadcast, sender, count)
  2290. DBM:CreatePizzaTimer(time, text, broadcast, sender, count, true)
  2291. end
  2292.  
  2293. local ignore = {}
  2294. local fakeMod -- dummy mod for the count sound effects
  2295. --Standard Pizza Timer
  2296. function DBM:CreatePizzaTimer(time, text, broadcast, sender, count, loop, terminate)
  2297. if not fakeMod then
  2298. fakeMod = self:NewMod("CreateCountTimerDummy")
  2299. self:GetModLocalization("CreateCountTimerDummy"):SetGeneralLocalization{ name = DBM_CORE_MINIMAP_TOOLTIP_HEADER }
  2300. fakeMod.countdown = fakeMod:NewCountdown(0, 0, nil, nil, nil, true)
  2301. end
  2302. if terminate or time == 0 then
  2303. self:Unschedule(loopTimer)
  2304. fakeMod.countdown:Cancel()
  2305. self.Bars:CancelBar(text)
  2306. self:Unschedule(countDownTextDelay)
  2307. TimerTracker_OnEvent(TimerTracker, "PLAYER_ENTERING_WORLD")
  2308. return
  2309. end
  2310. if sender and ignore[sender] then return end
  2311. text = text:sub(1, 16)
  2312. text = text:gsub("%%t", UnitName("target") or "<no target>")
  2313. if time < 3 then
  2314. self:AddMsg(DBM_PIZZA_ERROR_USAGE)
  2315. return
  2316. end
  2317. self.Bars:CreateBar(time, text, "Interface\\Icons\\Spell_Holy_BorrowedTime")
  2318. if broadcast then
  2319. if count then
  2320. sendSync("CU", ("%s\t%s"):format(time, text))
  2321. else
  2322. sendSync("U", ("%s\t%s"):format(time, text))
  2323. end
  2324. end
  2325. if sender then self:ShowPizzaInfo(text, sender) end
  2326. if count then
  2327. if not fakeMod then
  2328. local threshold = self.Options.PTCountThreshold
  2329. fakeMod = self:NewMod("CreateCountTimerDummy")
  2330. self:GetModLocalization("CreateCountTimerDummy"):SetGeneralLocalization{ name = DBM_CORE_MINIMAP_TOOLTIP_HEADER }
  2331. local adjustedThreshold = 5
  2332. if threshold > 10 then
  2333. adjustedThreshold = 10
  2334. else
  2335. adjustedThreshold = floor(threshold)
  2336. end
  2337. fakeMod.countdown = fakeMod:NewCountdown(0, 0, nil, nil, adjustedThreshold, true)
  2338. end
  2339. if not self.Options.DontPlayPTCountdown then
  2340. fakeMod.countdown:Cancel()
  2341. fakeMod.countdown:Start(time)
  2342. end
  2343. if not self.Options.DontShowPTCountdownText then
  2344. self:Unschedule(countDownTextDelay)
  2345. TimerTracker_OnEvent(TimerTracker, "PLAYER_ENTERING_WORLD")
  2346. local threshold = self.Options.PTCountThreshold
  2347. if time > threshold then
  2348. self:Schedule(time-threshold, countDownTextDelay, threshold)
  2349. else
  2350. TimerTracker_OnEvent(TimerTracker, "START_TIMER", 2, time, time)
  2351. end
  2352. end
  2353. end
  2354. if loop then
  2355. self:Unschedule(loopTimer)--Only one loop timer supported at once doing this, but much cleaner this way
  2356. self:Schedule(time, loopTimer, time, text, broadcast, sender, count)
  2357. end
  2358. end
  2359.  
  2360. function DBM:AddToPizzaIgnore(name)
  2361. ignore[name] = true
  2362. end
  2363. end
  2364.  
  2365. function DBM:ShowPizzaInfo(id, sender)
  2366. if self.Options.ShowPizzaMessage then
  2367. self:AddMsg(DBM_PIZZA_SYNC_INFO:format(sender, id))
  2368. end
  2369. end
  2370.  
  2371. ------------------
  2372. -- Hyperlinks --
  2373. ------------------
  2374. do
  2375. local ignore, cancel
  2376. local popuplevel = 0
  2377. local function showPopupConfirmIgnore(ignore, cancel)
  2378. local popup = CreateFrame("Frame", "DBMHyperLinks", UIParent)
  2379. popup:SetBackdrop({bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
  2380. edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
  2381. tile = true, tileSize = 16, edgeSize = 16,
  2382. insets = {left = 1, right = 1, top = 1, bottom = 1}}
  2383. )
  2384. popup:SetSize(500, 80)
  2385. popup:SetPoint("TOP", UIParent, "TOP", 0, -200)
  2386. popup:SetFrameStrata("DIALOG")
  2387. popup:SetFrameLevel(popuplevel)
  2388. popuplevel = popuplevel + 1
  2389.  
  2390. local text = popup:CreateFontString()
  2391. text:SetFontObject(ChatFontNormal)
  2392. text:SetWidth(470)
  2393. text:SetWordWrap(true)
  2394. text:SetPoint("TOP", popup, "TOP", 0, -15)
  2395. text:SetText(DBM_PIZZA_CONFIRM_IGNORE:format(ignore))
  2396.  
  2397. local accept = CreateFrame("Button", nil, popup)
  2398. accept:SetNormalTexture("Interface\\Buttons\\UI-DialogBox-Button-Up")
  2399. accept:SetPushedTexture("Interface\\Buttons\\UI-DialogBox-Button-Down")
  2400. accept:SetHighlightTexture("Interface\\Buttons\\UI-DialogBox-Button-Highlight", "ADD")
  2401. accept:SetSize(128, 35)
  2402. accept:SetPoint("BOTTOM", popup, "BOTTOM", -75, 0)
  2403. accept:SetScript("OnClick", function(f) DBM:AddToPizzaIgnore(ignore) DBM.Bars:CancelBar(cancel) f:GetParent():Hide() end)
  2404.  
  2405. local atext = accept:CreateFontString()
  2406. atext:SetFontObject(ChatFontNormal)
  2407. atext:SetPoint("CENTER", accept, "CENTER", 0, 5)
  2408. atext:SetText(YES)
  2409.  
  2410. local decline = CreateFrame("Button", nil, popup)
  2411. decline:SetNormalTexture("Interface\\Buttons\\UI-DialogBox-Button-Up")
  2412. decline:SetPushedTexture("Interface\\Buttons\\UI-DialogBox-Button-Down")
  2413. decline:SetHighlightTexture("Interface\\Buttons\\UI-DialogBox-Button-Highlight", "ADD")
  2414. decline:SetSize(128, 35)
  2415. decline:SetPoint("BOTTOM", popup, "BOTTOM", 75, 0)
  2416. decline:SetScript("OnClick", function(f) f:GetParent():Hide() end)
  2417.  
  2418. local dtext = decline:CreateFontString()
  2419. dtext:SetFontObject(ChatFontNormal)
  2420. dtext:SetPoint("CENTER", decline, "CENTER", 0, 5)
  2421. dtext:SetText(NO)
  2422. PlaySound("igMainMenuOpen")
  2423. end
  2424.  
  2425. local function linkHook(self, link, string, button, ...)
  2426. local linkType, arg1, arg2, arg3, arg4, arg5, arg6 = strsplit(":", link)
  2427. if linkType ~= "DBM" then
  2428. return
  2429. end
  2430. if arg1 == "cancel" then
  2431. DBM.Bars:CancelBar(link:match("DBM:cancel:(.+):nil$"))
  2432. elseif arg1 == "ignore" then
  2433. cancel = link:match("DBM:ignore:(.+):[^%s:]+$")
  2434. ignore = link:match(":([^:]+)$")
  2435. showPopupConfirmIgnore(ignore, cancel)
  2436. elseif arg1 == "update" then
  2437. DBM:ShowUpdateReminder(arg2, arg3) -- displayVersion, revision
  2438. elseif arg == "localizersneeded" then
  2439. DBM:ShowUpdateReminder(nil, nil, DBM_FORUMS_COPY_URL_DIALOG, "http://www.deadlybossmods.com/forum/viewtopic.php?f=3&t=5")
  2440. elseif arg1 == "forumsnews" then
  2441. DBM:ShowUpdateReminder(nil, nil, DBM_FORUMS_COPY_URL_DIALOG_NEWS, "http://www.deadlybossmods.com/forum/viewtopic.php?f=3&p=171#p171")
  2442. elseif arg1 == "forums" then
  2443. DBM:ShowUpdateReminder(nil, nil, DBM_FORUMS_COPY_URL_DIALOG)
  2444. elseif arg1 == "showRaidIdResults" then
  2445. DBM:ShowRaidIDRequestResults()
  2446. elseif arg1 == "noteshare" then
  2447. local mod = DBM:GetModByName(arg2 or "")
  2448. if mod then
  2449. DBM:ShowNoteEditor(mod, arg3, arg4, arg5, arg6)
  2450. else--Should not happen, since mod was verified before getting this far, but just in case
  2451. DBM:Debug("Bad note share, mod not valid")
  2452. end
  2453. end
  2454. end
  2455.  
  2456. DEFAULT_CHAT_FRAME:HookScript("OnHyperlinkClick", linkHook) -- handles the weird case that the default chat frame is not one of the normal chat frames (3rd party chat frames or whatever causes this)
  2457. local i = 1
  2458. while _G["ChatFrame" .. i] do
  2459. if _G["ChatFrame" .. i] ~= DEFAULT_CHAT_FRAME then
  2460. _G["ChatFrame" .. i]:HookScript("OnHyperlinkClick", linkHook)
  2461. end
  2462. i = i + 1
  2463. end
  2464. end
  2465.  
  2466. do
  2467. local old = ItemRefTooltip.SetHyperlink -- we have to hook this function since the default ChatFrame code assumes that all links except for player and channel links are valid arguments for this function
  2468. function ItemRefTooltip:SetHyperlink(link, ...)
  2469. if link and link:sub(0, 4) == "DBM:" then
  2470. return
  2471. end
  2472. return old(self, link, ...)
  2473. end
  2474. end
  2475.  
  2476.  
  2477. -----------------
  2478. -- GUI Stuff --
  2479. -----------------
  2480. do
  2481. local callOnLoad = {}
  2482. function DBM:LoadGUI()
  2483. if GetAddOnEnableState(playerName, "VEM-Core") >= 1 then
  2484. self:AddMsg(DBM_CORE_VEM)
  2485. return
  2486. end
  2487. if GetAddOnEnableState(playerName, "DBM-Profiles") >= 1 then
  2488. self:AddMsg(DBM_CORE_3RDPROFILES)
  2489. return
  2490. end
  2491. if GetAddOnEnableState(playerName, "DPMCore") >= 1 then
  2492. self:AddMsg(DBM_CORE_DPMCORE)
  2493. return
  2494. end
  2495. if not dbmIsEnabled then
  2496. DBM:AddMsg(DBM_CORE_UPDATEREMINDER_DISABLE)
  2497. return
  2498. end
  2499. if not IsAddOnLoaded("DBM-GUI") then
  2500. if InCombatLockdown() then
  2501. guiRequested = true
  2502. self:AddMsg(DBM_CORE_LOAD_GUI_COMBAT)
  2503. return
  2504. end
  2505. local enabled = GetAddOnEnableState(playerName, "DBM-GUI")
  2506. if enabled == 0 then
  2507. EnableAddOn("DBM-GUI")
  2508. end
  2509. local loaded, reason = LoadAddOn("DBM-GUI")
  2510. if not loaded then
  2511. if reason then
  2512. self:AddMsg(DBM_CORE_LOAD_GUI_ERROR:format(tostring(_G["ADDON_"..reason or ""])))
  2513. else
  2514. self:AddMsg(DBM_CORE_LOAD_GUI_ERROR:format(DBM_CORE_UNKNOWN))
  2515. end
  2516. return false
  2517. end
  2518. tsort(callOnLoad, function(v1, v2) return v1[2] < v2[2] end)
  2519. for i, v in ipairs(callOnLoad) do v[1]() end
  2520. collectgarbage("collect")
  2521. end
  2522. return DBM_GUI:ShowHide()
  2523. end
  2524.  
  2525. function DBM:RegisterOnGuiLoadCallback(f, sort)
  2526. tinsert(callOnLoad, {f, sort or mhuge})
  2527. end
  2528. end
  2529.  
  2530.  
  2531. ----------------------
  2532. -- Minimap Button --
  2533. ----------------------
  2534. do
  2535. local dragMode = nil
  2536.  
  2537. local function moveButton(self)
  2538. if dragMode == "free" then
  2539. local centerX, centerY = Minimap:GetCenter()
  2540. local x, y = GetCursorPosition()
  2541. x, y = x / self:GetEffectiveScale() - centerX, y / self:GetEffectiveScale() - centerY
  2542. self:ClearAllPoints()
  2543. self:SetPoint("CENTER", x, y)
  2544. else
  2545. local centerX, centerY = Minimap:GetCenter()
  2546. local x, y = GetCursorPosition()
  2547. x, y = x / self:GetEffectiveScale() - centerX, y / self:GetEffectiveScale() - centerY
  2548. centerX, centerY = math.abs(x), math.abs(y)
  2549. centerX, centerY = (centerX / math.sqrt(centerX^2 + centerY^2)) * 80, (centerY / sqrt(centerX^2 + centerY^2)) * 80
  2550. centerX = x < 0 and -centerX or centerX
  2551. centerY = y < 0 and -centerY or centerY
  2552. self:ClearAllPoints()
  2553. self:SetPoint("CENTER", centerX, centerY)
  2554. end
  2555. end
  2556.  
  2557. local button = CreateFrame("Button", "DBMMinimapButton", Minimap)
  2558. button:SetHeight(32)
  2559. button:SetWidth(32)
  2560. button:SetFrameStrata("MEDIUM")
  2561. button:SetPoint("CENTER", -65.35, -38.8)
  2562. button:SetMovable(true)
  2563. button:SetUserPlaced(true)
  2564. button:SetNormalTexture("Interface\\AddOns\\DBM-Core\\textures\\Minimap-Button-Up")
  2565. button:SetPushedTexture("Interface\\AddOns\\DBM-Core\\textures\\Minimap-Button-Down")
  2566. button:SetHighlightTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")
  2567.  
  2568. button:SetScript("OnMouseDown", function(self, button)
  2569. if IsShiftKeyDown() and IsAltKeyDown() then
  2570. dragMode = "free"
  2571. self:SetScript("OnUpdate", moveButton)
  2572. elseif IsShiftKeyDown() or button == "RightButton" then
  2573. dragMode = nil
  2574. self:SetScript("OnUpdate", moveButton)
  2575. end
  2576. end)
  2577. button:SetScript("OnMouseUp", function(self)
  2578. self:SetScript("OnUpdate", nil)
  2579. end)
  2580. button:SetScript("OnClick", function(self, button)
  2581. if IsShiftKeyDown() or button == "RightButton" then return end
  2582. DBM:LoadGUI()
  2583. end)
  2584. button:SetScript("OnEnter", function(self)
  2585. GameTooltip_SetDefaultAnchor(GameTooltip, self)
  2586. GameTooltip:SetText(DBM_CORE_MINIMAP_TOOLTIP_HEADER, 1, 1, 1)
  2587. GameTooltip:AddLine(ver, NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, 1)
  2588. GameTooltip:AddLine(" ")
  2589. GameTooltip:AddLine(DBM_CORE_MINIMAP_TOOLTIP_FOOTER, RAID_CLASS_COLORS.MAGE.r, RAID_CLASS_COLORS.MAGE.g, RAID_CLASS_COLORS.MAGE.b, 1)
  2590. GameTooltip:Show()
  2591. end)
  2592. button:SetScript("OnLeave", function(self)
  2593. GameTooltip:Hide()
  2594. end)
  2595.  
  2596. function DBM:ToggleMinimapButton()
  2597. self.Options.ShowMinimapButton = not self.Options.ShowMinimapButton
  2598. if self.Options.ShowMinimapButton then
  2599. button:Show()
  2600. else
  2601. button:Hide()
  2602. end
  2603. end
  2604.  
  2605. function DBM:HideMinimapButton()
  2606. return button:Hide()
  2607. end
  2608. end
  2609.  
  2610. -------------------------------------------------
  2611. -- Raid/Party Handling and Unit ID Utilities --
  2612. -------------------------------------------------
  2613. do
  2614. local inRaid = false
  2615.  
  2616. local raidGuids = {}
  2617. local iconSeter = {}
  2618.  
  2619. -- save playerinfo into raid table on load. (for solo raid)
  2620. DBM:RegisterOnLoadCallback(function()
  2621. C_TimerAfter(6, function()
  2622. if not raid[playerName] then
  2623. raid[playerName] = {}
  2624. raid[playerName].name = playerName
  2625. raid[playerName].shortname = playerName
  2626. raid[playerName].guid = UnitGUID("player")
  2627. raid[playerName].rank = 0
  2628. raid[playerName].class = class
  2629. raid[playerName].id = "player"
  2630. raid[playerName].groupId = 0
  2631. raid[playerName].revision = DBM.Revision
  2632. raid[playerName].version = DBM.ReleaseRevision
  2633. raid[playerName].displayVersion = DBM.DisplayVersion
  2634. raid[playerName].locale = GetLocale()
  2635. raid[playerName].enabledIcons = tostring(not DBM.Options.DontSetIcons)
  2636. raidGuids[UnitGUID("player") or ""] = playerName
  2637. end
  2638. end)
  2639. end)
  2640.  
  2641. local function updateAllRoster(self)
  2642. if IsInRaid() then
  2643. if not inRaid then
  2644. inRaid = true
  2645. sendSync("H")
  2646. SendAddonMessage("BigWigs", versionQueryString:format(0, fakeBWHash), IsPartyLFG() and "INSTANCE_CHAT" or "RAID")
  2647. self:Schedule(2, self.RoleCheck, false, self)
  2648. fireEvent("raidJoin", playerName)
  2649. if BigWigs and BigWigs.db.profile.raidicon and not self.Options.DontSetIcons then--Both DBM and bigwigs have raid icon marking turned on.
  2650. self:AddMsg(DBM_CORE_BIGWIGS_ICON_CONFLICT)--Warn that one of them should be turned off to prevent conflict (which they turn off is obviously up to raid leaders preference, dbm accepts either ore turned off to stop this alert)
  2651. end
  2652. end
  2653. for i = 1, GetNumGroupMembers() do
  2654. local name, rank, subgroup, _, _, className = GetRaidRosterInfo(i)
  2655. -- Maybe GetNumGroupMembers() bug? Seems that GetNumGroupMembers() rarely returns bad value, causing GetRaidRosterInfo() returns to nil.
  2656. -- Filter name = nil to prevent nil table error.
  2657. if name then
  2658. local id = "raid" .. i
  2659. local shortname = UnitName(id)
  2660. if (not raid[name]) and inRaid then
  2661. fireEvent("raidJoin", name)
  2662. end
  2663. raid[name] = raid[name] or {}
  2664. raid[name].name = name
  2665. raid[name].shortname = shortname
  2666. raid[name].rank = rank
  2667. raid[name].subgroup = subgroup
  2668. raid[name].class = className
  2669. raid[name].id = id
  2670. raid[name].groupId = i
  2671. raid[name].guid = UnitGUID(id) or ""
  2672. raid[name].updated = true
  2673. raidGuids[UnitGUID(id) or ""] = name
  2674. end
  2675. end
  2676. enableIcons = false
  2677. twipe(iconSeter)
  2678. for i, v in pairs(raid) do
  2679. if not v.updated then
  2680. raidGuids[v.guid] = nil
  2681. raid[i] = nil
  2682. removeEntry(newerVersionPerson, i)
  2683. fireEvent("raidLeave", i)
  2684. else
  2685. v.updated = nil
  2686. if v.revision and v.rank > 0 and (v.enabledIcons or "") == "true" then
  2687. iconSeter[#iconSeter + 1] = v.revision.." "..v.name
  2688. end
  2689. end
  2690. end
  2691. if #iconSeter > 0 then
  2692. tsort(iconSeter, function(a, b) return a > b end)
  2693. local elected = iconSeter[1]
  2694. if playerName == elected:sub(elected:find(" ") + 1) then
  2695. enableIcons = true
  2696. end
  2697. end
  2698. elseif IsInGroup() then
  2699. if not inRaid then
  2700. -- joined a new party
  2701. inRaid = true
  2702. sendSync("H")
  2703. SendAddonMessage("BigWigs", versionQueryString:format(0, fakeBWHash), IsPartyLFG() and "INSTANCE_CHAT" or "RAID")
  2704. self:Schedule(2, self.RoleCheck, false, self)
  2705. fireEvent("partyJoin", playerName)
  2706. end
  2707. for i = 0, GetNumSubgroupMembers() do
  2708. local id
  2709. if (i == 0) then
  2710. id = "player"
  2711. else
  2712. id = "party"..i
  2713. end
  2714. local name = GetUnitName(id, true)
  2715. local shortname = UnitName(id)
  2716. local rank = UnitIsGroupLeader(id) and 2 or 0
  2717. local _, className = UnitClass(id)
  2718. if (not raid[name]) and inRaid then
  2719. fireEvent("partyJoin", name)
  2720. end
  2721. raid[name] = raid[name] or {}
  2722. raid[name].name = name
  2723. raid[name].shortname = shortname
  2724. raid[name].guid = UnitGUID(id) or ""
  2725. raid[name].rank = rank
  2726. raid[name].class = className
  2727. raid[name].id = id
  2728. raid[name].groupId = i
  2729. raid[name].updated = true
  2730. raidGuids[UnitGUID(id) or ""] = name
  2731. end
  2732. enableIcons = false
  2733. twipe(iconSeter)
  2734. for i, v in pairs(raid) do
  2735. if not v.updated then
  2736. raidGuids[v.guid] = nil
  2737. raid[i] = nil
  2738. removeEntry(newerVersionPerson, i)
  2739. fireEvent("partyLeave", i)
  2740. else
  2741. v.updated = nil
  2742. if v.revision and v.rank > 0 and (v.enabledIcons or "") == "true" then
  2743. iconSeter[#iconSeter + 1] = v.revision.." "..v.name
  2744. end
  2745. end
  2746. end
  2747. if #iconSeter > 0 then
  2748. tsort(iconSeter, function(a, b) return a > b end)
  2749. local elected = iconSeter[1]
  2750. if playerName == elected:sub(elected:find(" ") + 1) then
  2751. enableIcons = true
  2752. end
  2753. end
  2754. else
  2755. -- left the current group/raid
  2756. inRaid = false
  2757. enableIcons = true
  2758. fireEvent("raidLeave", playerName)
  2759. twipe(raid)
  2760. twipe(newerVersionPerson)
  2761. -- restore playerinfo into raid table on raidleave. (for solo raid)
  2762. raid[playerName] = {}
  2763. raid[playerName].name = playerName
  2764. raid[playerName].shortname = playerName
  2765. raid[playerName].guid = UnitGUID("player")
  2766. raid[playerName].rank = 0
  2767. raid[playerName].class = class
  2768. raid[playerName].id = "player"
  2769. raid[playerName].groupId = 0
  2770. raid[playerName].revision = DBM.Revision
  2771. raid[playerName].version = DBM.ReleaseRevision
  2772. raid[playerName].displayVersion = DBM.DisplayVersion
  2773. raid[playerName].locale = GetLocale()
  2774. raidGuids[UnitGUID("player")] = playerName
  2775. end
  2776. end
  2777.  
  2778. function DBM:GROUP_ROSTER_UPDATE()
  2779. self:Unschedule(updateAllRoster)
  2780. self:Schedule(1.5, updateAllRoster, self)
  2781. end
  2782.  
  2783. function DBM:INSTANCE_GROUP_SIZE_CHANGED()
  2784. local _, _, _, _, _, _, _, _, instanceGroupSize = GetInstanceInfo()
  2785. LastGroupSize = instanceGroupSize
  2786. end
  2787.  
  2788. function DBM:GetNumRealPlayersInZone()
  2789. if not IsInGroup() then return 1 end
  2790. local total = 0
  2791. local _, _, _, currentMapId = UnitPosition("player")
  2792. if IsInRaid() then
  2793. for i = 1, GetNumGroupMembers() do
  2794. local _, _, _, targetMapId = UnitPosition("raid"..i)
  2795. if targetMapId == currentMapId then
  2796. total = total + 1
  2797. end
  2798. end
  2799. else
  2800. total = 1--add player/self for "party" count
  2801. for i = 1, GetNumSubgroupMembers() do
  2802. local _, _, _, targetMapId = UnitPosition("party"..i)
  2803. if targetMapId == currentMapId then
  2804. total = total + 1
  2805. end
  2806. end
  2807. end
  2808. return total
  2809. end
  2810.  
  2811. function DBM:GetRaidRank(name)
  2812. local name = name or playerName
  2813. if name == playerName then--If name is player, try to get actual rank. Because raid[name].rank sometimes seems returning 0 even player is promoted.
  2814. return UnitIsGroupLeader("player") and 2 or UnitIsGroupAssistant("player") and 1 or 0
  2815. else
  2816. return (raid[name] and raid[name].rank) or 0
  2817. end
  2818. end
  2819.  
  2820. function DBM:GetRaidSubgroup(name)
  2821. return (raid[name] and raid[name].subgroup) or 0
  2822. end
  2823.  
  2824. function DBM:GetRaidClass(name)
  2825. return (raid[name] and raid[name].class) or "UNKNOWN"
  2826. end
  2827.  
  2828. function DBM:GetRaidUnitId(name)
  2829. return raid[name] and raid[name].id
  2830. end
  2831.  
  2832. function DBM:GetUnitFullName(uId)
  2833. return GetUnitName(uId, true)
  2834. end
  2835.  
  2836. function DBM:GetFullPlayerNameByGUID(guid)
  2837. return raidGuids[guid]
  2838. end
  2839.  
  2840. function DBM:GetPlayerNameByGUID(guid)
  2841. return raidGuids[guid] and raidGuids[guid]:gsub("%-.*$", "")
  2842. end
  2843.  
  2844. function DBM:GetGroupId(name)
  2845. local raidMember = raid[name] or raid[GetUnitName(name, true) or ""]
  2846. return raidMember and raidMember.groupId or 0
  2847. end
  2848. end
  2849.  
  2850. do
  2851. -- yes, we still do avoid memory allocations during fights; so we don't use a closure around a counter here
  2852. -- this seems to be the easiest way to write an iterator that returns the unit id *string* as first argument without a memory allocation
  2853. local function raidIterator(groupMembers, uId)
  2854. local a, b = uId:byte(-2, -1)
  2855. local i = (a >= 0x30 and a <= 0x39 and (a - 0x30) * 10 or 0) + b - 0x30
  2856. if i < groupMembers then
  2857. return "raid" .. i + 1, i + 1
  2858. end
  2859. end
  2860.  
  2861. local function partyIterator(groupMembers, uId)
  2862. if not uId then
  2863. return "player", 0
  2864. elseif uId == "player" then
  2865. if groupMembers > 0 then
  2866. return "party1", 1
  2867. end
  2868. else
  2869. local i = uId:byte(-1) - 0x30
  2870. if i < groupMembers then
  2871. return "party" .. i + 1, i + 1
  2872. end
  2873. end
  2874. end
  2875.  
  2876. local function soloIterator(_, state)
  2877. if not state then -- no state == first call
  2878. return "player", 0
  2879. end
  2880. end
  2881.  
  2882. -- returns the unit ids of all raid or party members, including the player's own id
  2883. -- limitations: will break if there are ever raids with more than 99 players or partys with more than 10
  2884. function DBM:GetGroupMembers()
  2885. if IsInRaid() then
  2886. return raidIterator, GetNumGroupMembers(), "raid0"
  2887. elseif IsInGroup() then
  2888. return partyIterator, GetNumSubgroupMembers(), nil
  2889. else
  2890. -- solo!
  2891. return soloIterator, nil, nil
  2892. end
  2893. end
  2894. end
  2895.  
  2896. function DBM:GetNumGroupMembers()
  2897. return IsInGroup() and GetNumGroupMembers() or 1
  2898. end
  2899.  
  2900. --For returning the number of players actually in zone with us for status functions
  2901. --This is very touchy though and will fail if everyone isn't in same SUB zone (ie same room/area)
  2902. --It should work for pretty much any case but outdoor
  2903. function DBM:GetNumRealGroupMembers()
  2904. if not IsInInstance() then--Not accurate outside of instances (such as world bosses)
  2905. return IsInGroup() and GetNumGroupMembers() or 1--So just return regular group members.
  2906. end
  2907. local _, _, _, currentMapId = UnitPosition("player")
  2908. local realGroupMembers = 0
  2909. if IsInGroup() then
  2910. for uId in self:GetGroupMembers() do
  2911. local _, _, _, targetMapId = UnitPosition(uId)
  2912. if targetMapId == currentMapId then
  2913. realGroupMembers = realGroupMembers + 1
  2914. end
  2915. end
  2916. else
  2917. return 1
  2918. end
  2919. return realGroupMembers
  2920. end
  2921.  
  2922. function DBM:GetUnitCreatureId(uId)
  2923. local guid = UnitGUID(uId)
  2924. return self:GetCIDFromGUID(guid)
  2925. end
  2926.  
  2927. --Creature/Vehicle/Pet
  2928. ----<type>:<subtype>:<realmID>:<mapID>:<serverID>:<dbID>:<creationbits>
  2929. --Player/Item
  2930. ----<type>:<realmID>:<dbID>
  2931. function DBM:GetCIDFromGUID(guid)
  2932. local type, _, playerdbID, _, _, cid, creationbits = strsplit("-", guid or "")
  2933. if type and (type == "Creature" or type == "Vehicle" or type == "Pet") then
  2934. return tonumber(cid)
  2935. elseif type and (type == "Player" or type == "Item") then
  2936. return tonumber(playerdbID)
  2937. end
  2938. return 0
  2939. end
  2940.  
  2941. function DBM:IsCreatureGUID(guid)
  2942. local type = strsplit("-", guid or "")
  2943. if type and (type == "Creature" or type == "Vehicle") then--To determine, add pet or not?
  2944. return true
  2945. end
  2946. return false
  2947. end
  2948.  
  2949. function DBM:GetBossUnitId(name)
  2950. for i = 1, 5 do
  2951. if UnitName("boss" .. i) == name then
  2952. return "boss" .. i
  2953. end
  2954. end
  2955. for uId in self:GetGroupMembers() do
  2956. if UnitName(uId .. "target") == name and not UnitIsPlayer(uId .. "target") then
  2957. return uId .. "target"
  2958. end
  2959. end
  2960. end
  2961.  
  2962. ---------------
  2963. -- Options --
  2964. ---------------
  2965. function DBM:AddDefaultOptions(t1, t2)
  2966. for i, v in pairs(t2) do
  2967. if t1[i] == nil then
  2968. t1[i] = v
  2969. elseif type(v) == "table" and type(t1[i]) == "table" then
  2970. self:AddDefaultOptions(t1[i], v)
  2971. end
  2972. end
  2973. end
  2974.  
  2975. function DBM:LoadModOptions(modId, inCombat, first)
  2976. local oldSavedVarsName = modId:gsub("-", "").."_SavedVars"
  2977. local savedVarsName = modId:gsub("-", "").."_AllSavedVars"
  2978. local savedStatsName = modId:gsub("-", "").."_SavedStats"
  2979. local fullname = playerName.."-"..playerRealm
  2980. local profileNum = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
  2981. if not _G[savedVarsName] then _G[savedVarsName] = {} end
  2982. local savedOptions = _G[savedVarsName][fullname] or {}
  2983. local savedStats = _G[savedStatsName] or {}
  2984. local existId = {}
  2985. for i, id in ipairs(DBM.ModLists[modId]) do
  2986. existId[id] = true
  2987. -- init
  2988. if not savedOptions[id] then savedOptions[id] = {} end
  2989. local mod = DBM:GetModByName(id)
  2990. -- migrate old option
  2991. if _G[oldSavedVarsName] and _G[oldSavedVarsName][id] then
  2992. self:Debug("LoadModOptions: Found old options, importing", 2)
  2993. local oldTable = _G[oldSavedVarsName][id]
  2994. _G[oldSavedVarsName][id] = nil
  2995. savedOptions[id][profileNum] = oldTable
  2996. end
  2997. if not savedOptions[id][profileNum] and not first then--previous profile not found. load defaults
  2998. self:Debug("LoadModOptions: No saved options, creating defaults for profile "..profileNum, 2)
  2999. local defaultOptions = {}
  3000. for option, optionValue in pairs(mod.DefaultOptions) do
  3001. if type(optionValue) == "table" then
  3002. optionValue = optionValue.value
  3003. elseif type(optionValue) == "string" then
  3004. optionValue = mod:GetRoleFlagValue(optionValue)
  3005. end
  3006. defaultOptions[option] = optionValue
  3007. end
  3008. savedOptions[id][profileNum] = defaultOptions
  3009. else
  3010. savedOptions[id][profileNum] = savedOptions[id][profileNum] or mod.Options
  3011. --check new option
  3012. for option, optionValue in pairs(mod.DefaultOptions) do
  3013. if savedOptions[id][profileNum][option] == nil then
  3014. if type(optionValue) == "table" then
  3015. optionValue = optionValue.value
  3016. elseif type(optionValue) == "string" then
  3017. optionValue = mod:GetRoleFlagValue(optionValue)
  3018. end
  3019. savedOptions[id][profileNum][option] = optionValue
  3020. end
  3021. end
  3022. --clean unused saved variables (do not work on combat load)
  3023. if not inCombat then
  3024. for option, optionValue in pairs(savedOptions[id][profileNum]) do
  3025. if mod.DefaultOptions[option] == nil then
  3026. savedOptions[id][profileNum][option] = nil
  3027. elseif mod.DefaultOptions[option] and (type(mod.DefaultOptions[option]) == "table") then--recover broken dropdown option
  3028. if savedOptions[id][profileNum][option] and (type(savedOptions[id][profileNum][option]) == "boolean") then
  3029. savedOptions[id][profileNum][option] = mod.DefaultOptions[option].value
  3030. end
  3031. --Fix default options for colored bar by type that were set to 0 because no defaults existed at time they were created, but do now.
  3032. elseif option:find("TColor") then
  3033. if savedOptions[id][profileNum][option] and savedOptions[id][profileNum][option] == 0 and mod.DefaultOptions[option] and mod.DefaultOptions[option] ~= 0 then
  3034. savedOptions[id][profileNum][option] = mod.DefaultOptions[option]
  3035. end
  3036. end
  3037. end
  3038. end
  3039. end
  3040. --apply saved option to actual option table
  3041. mod.Options = savedOptions[id][profileNum]
  3042. --stats init (only first load)
  3043. if first then
  3044. savedStats[id] = savedStats[id] or {}
  3045. local stats = savedStats[id]
  3046. stats.normalKills = stats.normalKills or 0
  3047. stats.normalPulls = stats.normalPulls or 0
  3048. stats.heroicKills = stats.heroicKills or 0
  3049. stats.heroicPulls = stats.heroicPulls or 0
  3050. stats.challengeKills = stats.challengeKills or 0
  3051. stats.challengePulls = stats.challengePulls or 0
  3052. stats.challengeBestRank = stats.challengeBestRank or 0
  3053. stats.mythicKills = stats.mythicKills or 0
  3054. stats.mythicPulls = stats.mythicPulls or 0
  3055. stats.normal25Kills = stats.normal25Kills or 0
  3056. stats.normal25Kills = stats.normal25Kills or 0
  3057. stats.normal25Pulls = stats.normal25Pulls or 0
  3058. stats.heroic25Kills = stats.heroic25Kills or 0
  3059. stats.heroic25Pulls = stats.heroic25Pulls or 0
  3060. stats.lfr25Kills = stats.lfr25Kills or 0
  3061. stats.lfr25Pulls = stats.lfr25Pulls or 0
  3062. stats.timewalkerKills = stats.timewalkerKills or 0
  3063. stats.timewalkerPulls = stats.timewalkerPulls or 0
  3064. mod.stats = stats
  3065. --run OnInitialize function
  3066. if mod.OnInitialize then mod:OnInitialize() end
  3067. end
  3068. end
  3069. --clean unused saved variables (do not work on combat load)
  3070. if not inCombat then
  3071. for id, table in pairs(savedOptions) do
  3072. if not existId[id] and not id:find("talent") then
  3073. savedOptions[id] = nil
  3074. end
  3075. end
  3076. for id, table in pairs(savedStats) do
  3077. if not existId[id] then
  3078. savedStats[id] = nil
  3079. end
  3080. end
  3081. end
  3082. _G[savedVarsName][fullname] = savedOptions
  3083. if profileNum > 0 then
  3084. _G[savedVarsName][fullname]["talent"..profileNum] = currentSpecName
  3085. self:Debug("LoadModOptions: Finished loading "..(_G[savedVarsName][fullname]["talent"..profileNum] or DBM_CORE_UNKNOWN))
  3086. end
  3087. _G[savedStatsName] = savedStats
  3088. if not first and DBM_GUI and DBM_GUI.currentViewing and DBM_GUI_OptionsFrame:IsShown() then
  3089. DBM_GUI_OptionsFrame:DisplayFrame(DBM_GUI.currentViewing)
  3090. end
  3091. end
  3092.  
  3093. function DBM:SpecChanged(force)
  3094. if not force and not DBM_UseDualProfile then return end
  3095. --Load Options again.
  3096. self:Debug("SpecChanged fired", 2)
  3097. for modId, idTable in pairs(self.ModLists) do
  3098. self:LoadModOptions(modId)
  3099. end
  3100. end
  3101.  
  3102. function DBM:PLAYER_LEVEL_UP()
  3103. playerLevel = UnitLevel("player")
  3104. if playerLevel < 15 and playerLevel > 9 then
  3105. self:PLAYER_SPECIALIZATION_CHANGED()
  3106. end
  3107. end
  3108.  
  3109. function DBM:LoadAllModDefaultOption(modId)
  3110. -- modId is string like "DBM-Highmaul"
  3111. if not modId or not self.ModLists[modId] then return end
  3112. -- prevent error
  3113. if not currentSpecID then
  3114. self:SetCurrentSpecInfo()
  3115. end
  3116. -- variable init
  3117. local savedVarsName = modId:gsub("-", "").."_AllSavedVars"
  3118. local fullname = playerName.."-"..playerRealm
  3119. local profileNum = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
  3120. -- prevent nil table error
  3121. if not _G[savedVarsName] then _G[savedVarsName] = {} end
  3122. for i, id in ipairs(self.ModLists[modId]) do
  3123. -- prevent nil table error
  3124. if not _G[savedVarsName][fullname][id] then _G[savedVarsName][fullname][id] = {} end
  3125. -- actual do load default option
  3126. local mod = self:GetModByName(id)
  3127. local defaultOptions = {}
  3128. for option, optionValue in pairs(mod.DefaultOptions) do
  3129. if type(optionValue) == "table" then
  3130. optionValue = optionValue.value
  3131. elseif type(optionValue) == "string" then
  3132. optionValue = mod:GetRoleFlagValue(optionValue)
  3133. end
  3134. defaultOptions[option] = optionValue
  3135. end
  3136. mod.Options = {}
  3137. mod.Options = defaultOptions
  3138. _G[savedVarsName][fullname][id][profileNum] = {}
  3139. _G[savedVarsName][fullname][id][profileNum] = mod.Options
  3140. end
  3141. self:AddMsg(DBM_CORE_ALLMOD_DEFAULT_LOADED)
  3142. -- update gui if showing
  3143. if DBM_GUI and DBM_GUI.currentViewing and DBM_GUI_OptionsFrame:IsShown() then
  3144. DBM_GUI_OptionsFrame:DisplayFrame(DBM_GUI.currentViewing)
  3145. end
  3146. end
  3147.  
  3148. function DBM:LoadModDefaultOption(mod)
  3149. -- mod must be table
  3150. if not mod then return end
  3151. -- prevent error
  3152. if not currentSpecID then
  3153. self:SetCurrentSpecInfo()
  3154. end
  3155. -- variable init
  3156. local savedVarsName = (mod.modId):gsub("-", "").."_AllSavedVars"
  3157. local fullname = playerName.."-"..playerRealm
  3158. local profileNum = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
  3159. -- prevent nil table error
  3160. if not _G[savedVarsName] then _G[savedVarsName] = {} end
  3161. if not _G[savedVarsName][fullname] then _G[savedVarsName][fullname] = {} end
  3162. if not _G[savedVarsName][fullname][mod.id] then _G[savedVarsName][fullname][mod.id] = {} end
  3163. -- do load default
  3164. local defaultOptions = {}
  3165. for option, optionValue in pairs(mod.DefaultOptions) do
  3166. if type(optionValue) == "table" then
  3167. optionValue = optionValue.value
  3168. elseif type(optionValue) == "string" then
  3169. optionValue = mod:GetRoleFlagValue(optionValue)
  3170. end
  3171. defaultOptions[option] = optionValue
  3172. end
  3173. mod.Options = {}
  3174. mod.Options = defaultOptions
  3175. _G[savedVarsName][fullname][mod.id][profileNum] = {}
  3176. _G[savedVarsName][fullname][mod.id][profileNum] = mod.Options
  3177. self:AddMsg(DBM_CORE_MOD_DEFAULT_LOADED)
  3178. -- update gui if showing
  3179. if DBM_GUI and DBM_GUI.currentViewing and DBM_GUI_OptionsFrame:IsShown() then
  3180. DBM_GUI_OptionsFrame:DisplayFrame(DBM_GUI.currentViewing)
  3181. end
  3182. end
  3183.  
  3184. function DBM:CopyAllModOption(modId, sourceName, sourceProfile)
  3185. -- modId is string like "DBM-Highmaul"
  3186. if not modId or not sourceName or not sourceProfile or not DBM.ModLists[modId] then return end
  3187. -- prevent error
  3188. if not currentSpecID then
  3189. self:SetCurrentSpecInfo()
  3190. end
  3191. -- variable init
  3192. local savedVarsName = modId:gsub("-", "").."_AllSavedVars"
  3193. local targetName = playerName.."-"..playerRealm
  3194. local targetProfile = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
  3195. -- do not copy setting itself
  3196. if targetName == sourceName and targetProfile == sourceProfile then
  3197. self:AddMsg(DBM_CORE_MPROFILE_COPY_SELF_ERROR)
  3198. return
  3199. end
  3200. -- prevent nil table error
  3201. if not _G[savedVarsName] then _G[savedVarsName] = {} end
  3202. -- check source is exist
  3203. if not _G[savedVarsName][sourceName] then
  3204. self:AddMsg(DBM_CORE_MPROFILE_COPY_S_ERROR)
  3205. return
  3206. end
  3207. local targetOptions = _G[savedVarsName][targetName] or {}
  3208. for i, id in ipairs(self.ModLists[modId]) do
  3209. -- check source is exist
  3210. if not _G[savedVarsName][sourceName][id] then
  3211. self:AddMsg(DBM_CORE_MPROFILE_COPY_S_ERROR)
  3212. return
  3213. end
  3214. if not _G[savedVarsName][sourceName][id][sourceProfile] then
  3215. self:AddMsg(DBM_CORE_MPROFILE_COPY_S_ERROR)
  3216. return
  3217. end
  3218. -- prevent nil table error
  3219. if not _G[savedVarsName][targetName][id] then _G[savedVarsName][targetName][id] = {} end
  3220. -- copy table
  3221. _G[savedVarsName][targetName][id][targetProfile] = {}--clear before copy
  3222. _G[savedVarsName][targetName][id][targetProfile] = _G[savedVarsName][sourceName][id][sourceProfile]
  3223. --check new option
  3224. local mod = self:GetModByName(id)
  3225. for option, optionValue in pairs(mod.Options) do
  3226. if _G[savedVarsName][targetName][id][targetProfile][option] == nil then
  3227. _G[savedVarsName][targetName][id][targetProfile][option] = optionValue
  3228. end
  3229. end
  3230. -- apply to options table
  3231. mod.Options = {}
  3232. mod.Options = _G[savedVarsName][targetName][id][targetProfile]
  3233. end
  3234. if targetProfile > 0 then
  3235. _G[savedVarsName][targetName]["talent"..targetProfile] = currentSpecName
  3236. end
  3237. self:AddMsg(DBM_CORE_MPROFILE_COPY_SUCCESS:format(sourceName, sourceProfile))
  3238. -- update gui if showing
  3239. if DBM_GUI and DBM_GUI.currentViewing and DBM_GUI_OptionsFrame:IsShown() then
  3240. DBM_GUI_OptionsFrame:DisplayFrame(DBM_GUI.currentViewing)
  3241. end
  3242. end
  3243.  
  3244. function DBM:CopyAllModTypeOption(modId, sourceName, sourceProfile, Type)
  3245. -- modId is string like "DBM-Highmaul"
  3246. if not modId or not sourceName or not sourceProfile or not self.ModLists[modId] or not Type then return end
  3247. -- prevent error
  3248. if not currentSpecID then
  3249. self:SetCurrentSpecInfo()
  3250. end
  3251. -- variable init
  3252. local savedVarsName = modId:gsub("-", "").."_AllSavedVars"
  3253. local targetName = playerName.."-"..playerRealm
  3254. local targetProfile = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
  3255. -- do not copy setting itself
  3256. if targetName == sourceName and targetProfile == sourceProfile then
  3257. self:AddMsg(DBM_CORE_MPROFILE_COPYS_SELF_ERROR)
  3258. return
  3259. end
  3260. -- prevent nil table error
  3261. if not _G[savedVarsName] then _G[savedVarsName] = {} end
  3262. -- check source is exist
  3263. if not _G[savedVarsName][sourceName] then
  3264. self:AddMsg(DBM_CORE_MPROFILE_COPYS_S_ERROR)
  3265. return
  3266. end
  3267. local targetOptions = _G[savedVarsName][targetName] or {}
  3268. for i, id in ipairs(self.ModLists[modId]) do
  3269. -- check source is exist
  3270. if not _G[savedVarsName][sourceName][id] then
  3271. self:AddMsg(DBM_CORE_MPROFILE_COPYS_S_ERROR)
  3272. return
  3273. end
  3274. if not _G[savedVarsName][sourceName][id][sourceProfile] then
  3275. self:AddMsg(DBM_CORE_MPROFILE_COPYS_S_ERROR)
  3276. return
  3277. end
  3278. -- prevent nil table error
  3279. if not _G[savedVarsName][targetName][id] then _G[savedVarsName][targetName][id] = {} end
  3280. -- copy table
  3281. for option, optionValue in pairs(_G[savedVarsName][sourceName][id][sourceProfile]) do
  3282. if option:find(Type) then
  3283. _G[savedVarsName][targetName][id][targetProfile][option] = optionValue
  3284. end
  3285. end
  3286. -- apply to options table
  3287. local mod = self:GetModByName(id)
  3288. mod.Options = {}
  3289. mod.Options = _G[savedVarsName][targetName][id][targetProfile]
  3290. end
  3291. if targetProfile > 0 then
  3292. _G[savedVarsName][targetName]["talent"..targetProfile] = currentSpecName
  3293. end
  3294. self:AddMsg(DBM_CORE_MPROFILE_COPYS_SUCCESS:format(sourceName, sourceProfile))
  3295. -- update gui if showing
  3296. if DBM_GUI and DBM_GUI.currentViewing and DBM_GUI_OptionsFrame:IsShown() then
  3297. DBM_GUI_OptionsFrame:DisplayFrame(DBM_GUI.currentViewing)
  3298. end
  3299. end
  3300.  
  3301. function DBM:DeleteAllModOption(modId, name, profile)
  3302. -- modId is string like "DBM-Highmaul"
  3303. if not modId or not name or not profile or not self.ModLists[modId] then return end
  3304. -- prevent error
  3305. if not currentSpecID then
  3306. self:SetCurrentSpecInfo()
  3307. end
  3308. -- variable init
  3309. local savedVarsName = modId:gsub("-", "").."_AllSavedVars"
  3310. local fullname = playerName.."-"..playerRealm
  3311. local profileNum = playerLevel > 9 and DBM_UseDualProfile and currentSpecGroup or 0
  3312. -- cannot delete current profile.
  3313. if fullname == name and profileNum == profile then
  3314. self:AddMsg(DBM_CORE_MPROFILE_DELETE_SELF_ERROR)
  3315. return
  3316. end
  3317. -- prevent nil table error
  3318. if not _G[savedVarsName] then _G[savedVarsName] = {} end
  3319. if not _G[savedVarsName][name] then
  3320. self:AddMsg(DBM_CORE_MPROFILE_DELETE_S_ERROR)
  3321. return
  3322. end
  3323. for i, id in ipairs(self.ModLists[modId]) do
  3324. -- prevent nil table error
  3325. if not _G[savedVarsName][name][id] then
  3326. self:AddMsg(DBM_CORE_MPROFILE_DELETE_S_ERROR)
  3327. return
  3328. end
  3329. -- delete
  3330. _G[savedVarsName][name][id][profile] = nil
  3331. end
  3332. _G[savedVarsName][name]["talent"..profile] = nil
  3333. self:AddMsg(DBM_CORE_MPROFILE_DELETE_SUCCESS:format(name, profile))
  3334. end
  3335.  
  3336. function DBM:ClearAllStats(modId)
  3337. -- modId is string like "DBM-Highmaul"
  3338. if not modId or not self.ModLists[modId] then return end
  3339. -- variable init
  3340. local savedStatsName = modId:gsub("-", "").."_SavedStats"
  3341. -- prevent nil table error
  3342. if not _G[savedStatsName] then _G[savedStatsName] = {} end
  3343. for i, id in ipairs(self.ModLists[modId]) do
  3344. local mod = self:GetModByName(id)
  3345. -- prevent nil table error
  3346. local defaultStats = {}
  3347. defaultStats.normalKills = 0
  3348. defaultStats.normalPulls = 0
  3349. defaultStats.heroicKills = 0
  3350. defaultStats.heroicPulls = 0
  3351. defaultStats.challengeKills = 0
  3352. defaultStats.challengePulls = 0
  3353. defaultStats.challengeBestRank = 0
  3354. defaultStats.mythicKills = 0
  3355. defaultStats.mythicPulls = 0
  3356. defaultStats.normal25Kills = 0
  3357. defaultStats.normal25Kills = 0
  3358. defaultStats.normal25Pulls = 0
  3359. defaultStats.heroic25Kills = 0
  3360. defaultStats.heroic25Pulls = 0
  3361. defaultStats.lfr25Kills = 0
  3362. defaultStats.lfr25Pulls = 0
  3363. defaultStats.timewalkerKills = 0
  3364. defaultStats.timewalkerPulls = 0
  3365. mod.stats = {}
  3366. mod.stats = defaultStats
  3367. _G[savedStatsName][id] = {}
  3368. _G[savedStatsName][id] = defaultStats
  3369. end
  3370. self:AddMsg(DBM_CORE_ALLMOD_STATS_RESETED)
  3371. DBM_GUI:UpdateModList()
  3372. end
  3373.  
  3374. do
  3375. function loadOptions(self)
  3376. --init
  3377. if not DBM_AllSavedOptions then DBM_AllSavedOptions = {} end
  3378. usedProfile = DBM_UsedProfile or usedProfile
  3379. if not usedProfile or (usedProfile ~= "Default" and not DBM_AllSavedOptions[usedProfile]) then
  3380. -- DBM.Option is not loaded. so use print function
  3381. print(DBM_CORE_PROFILE_NOT_FOUND)
  3382. usedProfile = "Default"
  3383. end
  3384. DBM_UsedProfile = usedProfile
  3385. --migrate old options
  3386. if DBM_SavedOptions and not DBM_AllSavedOptions[usedProfile] then
  3387. DBM_AllSavedOptions[usedProfile] = DBM_SavedOptions
  3388. end
  3389. self.Options = DBM_AllSavedOptions[usedProfile] or {}
  3390. dbmIsEnabled = true
  3391. self:AddDefaultOptions(self.Options, self.DefaultOptions)
  3392. DBM_AllSavedOptions[usedProfile] = self.Options
  3393.  
  3394. -- force enable dual profile (change default)
  3395. if DBM_CharSavedRevision < 12976 then
  3396. if class ~= "MAGE" and class ~= "WARLOCK" and class ~= "ROGUE" then
  3397. DBM_UseDualProfile = true
  3398. end
  3399. end
  3400. DBM_CharSavedRevision = self.Revision
  3401.  
  3402. -- load special warning options
  3403. self:UpdateWarningOptions()
  3404. self:UpdateSpecialWarningOptions()
  3405. --Fix old options that use .wav instead of .ogg, to prevent no sounds bug as of 6.1+
  3406. if self.Options.RaidWarningSound:find(".wav") then self.Options.RaidWarningSound = self.DefaultOptions.RaidWarningSound end
  3407. if self.Options.SpecialWarningSound:find(".wav") then self.Options.SpecialWarningSound = self.DefaultOptions.SpecialWarningSound end
  3408. if self.Options.SpecialWarningSound2:find(".wav") then self.Options.SpecialWarningSound2 = self.DefaultOptions.SpecialWarningSound2 end
  3409. if self.Options.SpecialWarningSound3:find(".wav") then self.Options.SpecialWarningSound3 = self.DefaultOptions.SpecialWarningSound3 end
  3410. if self.Options.SpecialWarningSound4:find(".wav") then self.Options.SpecialWarningSound4 = self.DefaultOptions.SpecialWarningSound4 end
  3411. end
  3412. end
  3413.  
  3414. do
  3415. local lastLFGAlert = 0
  3416. function DBM:LFG_ROLE_CHECK_SHOW()
  3417. if not UnitIsGroupLeader("player") and self.Options.LFDEnhance and GetTime() - lastLFGAlert > 5 then
  3418. self:FlashClientIcon()
  3419. self:PlaySoundFile("Sound\\interface\\levelup2.ogg", true)--Because regular sound uses SFX channel which is too low of volume most of time
  3420. lastLFGAlert = GetTime()
  3421. end
  3422. end
  3423. end
  3424.  
  3425. function DBM:LFG_PROPOSAL_SHOW()
  3426. if self.Options.ShowQueuePop and not self.Options.DontShowBossTimers then
  3427. self.Bars:CreateBar(40, DBM_LFG_INVITE, "Interface\\Icons\\Spell_Holy_BorrowedTime")
  3428. end
  3429. if self.Options.LFDEnhance then
  3430. self:FlashClientIcon()
  3431. self:PlaySoundFile("Sound\\interface\\levelup2.ogg", true)--Because regular sound uses SFX channel which is too low of volume most of time
  3432. end
  3433. end
  3434.  
  3435. function DBM:LFG_PROPOSAL_FAILED()
  3436. self.Bars:CancelBar(DBM_LFG_INVITE)
  3437. end
  3438.  
  3439. function DBM:LFG_PROPOSAL_SUCCEEDED()
  3440. self.Bars:CancelBar(DBM_LFG_INVITE)
  3441. end
  3442.  
  3443. function DBM:READY_CHECK()
  3444. if self.Options.RLReadyCheckSound then--readycheck sound, if ora3 not installed (bad to have 2 mods do it)
  3445. self:FlashClientIcon()
  3446. if not BINDING_HEADER_oRA3 then
  3447. self:PlaySoundFile("Sound\\interface\\levelup2.ogg", true)--Because regular sound uses SFX channel which is too low of volume most of time
  3448. end
  3449. end
  3450. end
  3451.  
  3452. function DBM:PLAYER_SPECIALIZATION_CHANGED()
  3453. local lastSpecID = currentSpecID
  3454. self:SetCurrentSpecInfo()
  3455. if currentSpecID ~= lastSpecID then--Don't fire specchanged unless spec actually has changed.
  3456. self:SpecChanged()
  3457. if IsInGroup() then
  3458. self:RoleCheck(false)
  3459. end
  3460. end
  3461. end
  3462.  
  3463. do
  3464. local function AcceptPartyInvite()
  3465. AcceptGroup()
  3466. for i=1, STATICPOPUP_NUMDIALOGS do
  3467. local whichDialog = _G["StaticPopup"..i].which
  3468. if whichDialog == "PARTY_INVITE" or whichDialog == "PARTY_INVITE_XREALM" then
  3469. _G["StaticPopup"..i].inviteAccepted = 1
  3470. StaticPopup_Hide(whichDialog)
  3471. break
  3472. end
  3473. end
  3474. end
  3475.  
  3476. function DBM:PARTY_INVITE_REQUEST(sender)
  3477. --First off, if you are in queue for something, lets not allow guildies or friends boot you from it.
  3478. if (IsInInstance() and not C_Garrison:IsOnGarrisonMap()) or GetLFGMode(1) or GetLFGMode(2) or GetLFGMode(3) or GetLFGMode(4) or GetLFGMode(5) then return end
  3479. --First check realID
  3480. if self.Options.AutoAcceptFriendInvite then
  3481. local _, numBNetOnline = BNGetNumFriends()
  3482. for i = 1, numBNetOnline do
  3483. local presenceID, _, _, _, _, _, _, isOnline = BNGetFriendInfo(i)
  3484. local friendIndex = BNGetFriendIndex(presenceID)--Check if they are on more than one client at once (very likely with new launcher)
  3485. for i=1, BNGetNumFriendGameAccounts(friendIndex) do
  3486. local _, toonName, client = BNGetFriendGameAccountInfo(friendIndex, i)
  3487. if toonName and client == BNET_CLIENT_WOW then--Check if toon name exists and if client is wow. If yes to both, we found right client
  3488. if toonName == sender then--Now simply see if this is sender
  3489. AcceptPartyInvite()
  3490. return
  3491. end
  3492. end
  3493. end
  3494. end
  3495. -- Check regular non-BNet friends
  3496. local nf = GetNumFriends()
  3497. for i = 1, nf do
  3498. local toonName = GetFriendInfo(i)
  3499. if toonName == sender then
  3500. AcceptPartyInvite()
  3501. return
  3502. end
  3503. end
  3504. end
  3505. --Second check guildies
  3506. if self.Options.AutoAcceptGuildInvite then
  3507. local totalMembers, numOnlineGuildMembers, numOnlineAndMobileMembers = GetNumGuildMembers()
  3508. local scanTotal = GetGuildRosterShowOffline() and totalMembers or numOnlineAndMobileMembers
  3509. for i=1, scanTotal do
  3510. --At this time, it's not easy to tell an officer from a non officer
  3511. --since a guild might have ranks 1-3 or even 1-4 be officers/leader while another might only be 1-2
  3512. --therefor, this feature is just a "yes/no" for if sender is a guildy
  3513. local name, rank, rankIndex = GetGuildRosterInfo(i)
  3514. if not name then break end
  3515. name = Ambiguate(name, "none")
  3516. if sender == name then
  3517. AcceptPartyInvite()
  3518. return
  3519. end
  3520. end
  3521. end
  3522. end
  3523. end
  3524.  
  3525. function DBM:PLAYER_REGEN_ENABLED()
  3526. if loadDelay then
  3527. self:Debug("loadDelay is activating LoadMod again")
  3528. self:LoadMod(loadDelay)
  3529. end
  3530. if loadDelay2 then
  3531. self:Debug("loadDelay2 is activating LoadMod again")
  3532. self:LoadMod(loadDelay2)
  3533. end
  3534. if guiRequested and not IsAddOnLoaded("DBM-GUI") then
  3535. guiRequested = false
  3536. self:LoadGUI()
  3537. end
  3538. end
  3539.  
  3540. function DBM:UPDATE_BATTLEFIELD_STATUS()
  3541. for i = 1, 2 do
  3542. if GetBattlefieldStatus(i) == "confirm" then
  3543. if self.Options.ShowQueuePop and not self.Options.DontShowBossTimers then
  3544. queuedBattlefield[i] = select(2, GetBattlefieldStatus(i))
  3545. self.Bars:CreateBar(85, queuedBattlefield[i], "Interface\\Icons\\Spell_Holy_BorrowedTime") -- need to confirm the timer
  3546. end
  3547. if self.Options.LFDEnhance then
  3548. self:PlaySoundFile("Sound\\interface\\levelup2.ogg", true)--Because regular sound uses SFX channel which is too low of volume most of time
  3549. end
  3550. elseif queuedBattlefield[i] then
  3551. self.Bars:CancelBar(queuedBattlefield[i])
  3552. queuedBattlefield[i] = nil
  3553. end
  3554. end
  3555. end
  3556.  
  3557. function DBM:SCENARIO_CRITERIA_UPDATE()
  3558. local _, currentStage, numStages = C_Scenario.GetInfo()
  3559. if #inCombat > 0 and currentStage > numStages and C_Scenario.IsInScenario() then
  3560. for i = #inCombat, 1, -1 do
  3561. local v = inCombat[i]
  3562. if v.inScenario then
  3563. self:EndCombat(v)
  3564. end
  3565. end
  3566. end
  3567. end
  3568.  
  3569. --------------------------------
  3570. -- Load Boss Mods on Demand --
  3571. --------------------------------
  3572. do
  3573. local function checkMods(self)
  3574. if difficultyIndex == 24 then
  3575. --Surely a less shitty way of checking "this is a BC dungeon"?
  3576. if (LastInstanceMapID == 540 or LastInstanceMapID == 558 or LastInstanceMapID == 556 or LastInstanceMapID == 555 or LastInstanceMapID == 542 or LastInstanceMapID == 546 or LastInstanceMapID == 545 or LastInstanceMapID == 547 or LastInstanceMapID == 553 or LastInstanceMapID == 554 or LastInstanceMapID == 552 or LastInstanceMapID == 557 or LastInstanceMapID == 269 or LastInstanceMapID == 560 or LastInstanceMapID == 543 or LastInstanceMapID == 585) and not self.Options.BCTWMessageShown and not GetAddOnInfo("DBM-Party-BC") then
  3577. self.Options.BCTWMessageShown = true
  3578. self:AddMsg(DBM_CORE_MOD_AVAILABLE:format("DBM-Party-BC"))
  3579. --Surely a less shitty way of checking "this is a wrath dungeon"?
  3580. elseif (LastInstanceMapID == 619 or LastInstanceMapID == 601 or LastInstanceMapID == 595 or LastInstanceMapID == 600 or LastInstanceMapID == 604 or LastInstanceMapID == 602 or LastInstanceMapID == 599 or LastInstanceMapID == 576 or LastInstanceMapID == 578 or LastInstanceMapID == 574 or LastInstanceMapID == 575 or LastInstanceMapID == 608 or LastInstanceMapID == 658 or LastInstanceMapID == 632 or LastInstanceMapID == 668 or LastInstanceMapID == 650) and not self.Options.WOTLKTWMessageShown and not GetAddOnInfo("DBM-Party-WotLK") then
  3581. self.Options.WOTLKTWMessageShown = true
  3582. self:AddMsg(DBM_CORE_MOD_AVAILABLE:format("DBM-Party-WotLK"))
  3583. elseif (LastInstanceMapID == 755 or LastInstanceMapID == 645 or LastInstanceMapID == 36 or LastInstanceMapID == 670 or LastInstanceMapID == 644 or LastInstanceMapID == 33 or LastInstanceMapID == 643 or LastInstanceMapID == 725 or LastInstanceMapID == 657 or LastInstanceMapID == 309 or LastInstanceMapID == 859 or LastInstanceMapID == 568 or LastInstanceMapID == 938 or LastInstanceMapID == 940 or LastInstanceMapID == 939) and not self.Options.CATATWMessageShown and not GetAddOnInfo("DBM-Party-Cataclysm") then
  3584. self.Options.CATATWMessageShown = true--
  3585. self:AddMsg(DBM_CORE_MOD_AVAILABLE:format("DBM-Party-Cataclysm"))
  3586. end
  3587. else
  3588. if LastInstanceMapID == 1148 and not self.Options.PGMessageShown and not GetAddOnInfo("DBM-ProvingGrounds") then
  3589. self.Options.PGMessageShown = true
  3590. self:AddMsg(DBM_CORE_MOD_AVAILABLE:format("DBM-ProvingGrounds"))
  3591. elseif LastInstanceMapID == 409 and not self.Options.MCMessageShown and not GetAddOnInfo("DBM-MC") then
  3592. self.Options.MCMessageShown = true
  3593. self:AddMsg(DBM_CORE_MOD_AVAILABLE:format("DBM-MC"))
  3594. end
  3595. end
  3596. end
  3597. local function SecondaryLoadCheck(self)
  3598. local _, instanceType, difficulty, _, _, _, _, mapID, instanceGroupSize = GetInstanceInfo()
  3599. self:Debug("Instance Check fired with mapID "..mapID.." and difficulty "..difficulty, 2)
  3600. if LastInstanceMapID == mapID then
  3601. self:Debug("No action taken because mapID hasn't changed since last check", 2)
  3602. return
  3603. end--ID hasn't changed, don't waste cpu doing anything else (example situation, porting into garrosh phase 4 is a loading screen)
  3604. LastInstanceMapID = mapID
  3605. LastGroupSize = instanceGroupSize
  3606. difficultyIndex = difficulty
  3607. if instanceType == "none" or C_Garrison:IsOnGarrisonMap() then
  3608. LastInstanceType = "none"
  3609. if not targetEventsRegistered then
  3610. self:RegisterShortTermEvents("UPDATE_MOUSEOVER_UNIT")
  3611. targetEventsRegistered = true
  3612. end
  3613. else
  3614. LastInstanceType = instanceType
  3615. if targetEventsRegistered then
  3616. self:UnregisterShortTermEvents()
  3617. targetEventsRegistered = false
  3618. end
  3619. if savedDifficulty == "worldboss" then
  3620. for i = #inCombat, 1, -1 do
  3621. self:EndCombat(inCombat[i], true)
  3622. end
  3623. end
  3624. end
  3625. -- LoadMod
  3626. self:LoadModsOnDemand("mapId", mapID)
  3627. checkMods(self)
  3628. end
  3629. --Faster and more accurate loading for instances, but useless outside of them
  3630. function DBM:LOADING_SCREEN_DISABLED()
  3631. self.Bars:CancelBar(DBM_LFG_INVITE)--Disable bar here since LFG_PROPOSAL_SUCCEEDED seems broken right now
  3632. timerRequestInProgress = false
  3633. self:Debug("LOADING_SCREEN_DISABLED fired")
  3634. self:Unschedule(SecondaryLoadCheck)
  3635. self:Schedule(1, SecondaryLoadCheck, self)
  3636. self:Schedule(5, SecondaryLoadCheck, self)
  3637. if DBM:HasMapRestrictions() then
  3638. DBM.Arrow:Hide()
  3639. DBMHudMap:Disable()
  3640. if DBM.RangeCheck:IsRadarShown() then
  3641. DBM.RangeCheck:Hide(true)
  3642. end
  3643. end
  3644. end
  3645.  
  3646. function DBM:LoadModsOnDemand(checkTable, checkValue)
  3647. self:Debug("LoadModsOnDemand fired")
  3648. for i, v in ipairs(self.AddOns) do
  3649. local modTable = v[checkTable]
  3650. local enabled = GetAddOnEnableState(playerName, v.modId)
  3651. --self:Debug(v.modId.." is "..enabled, 2)
  3652. if not IsAddOnLoaded(v.modId) and modTable and checkEntry(modTable, checkValue) then
  3653. if enabled ~= 0 then
  3654. self:LoadMod(v)
  3655. else
  3656. self:Debug("Not loading "..v.name.." because it is not enabled")
  3657. end
  3658. end
  3659. end
  3660. self:ScenarioCheck()--Do not filter. Because ScenarioCheck function includes filter.
  3661. end
  3662. end
  3663.  
  3664. --Scenario mods
  3665. function DBM:ScenarioCheck()
  3666. if dbmIsEnabled and combatInfo[LastInstanceMapID] then
  3667. for i, v in ipairs(combatInfo[LastInstanceMapID]) do
  3668. if (v.type == "scenario") and checkEntry(v.msgs, LastInstanceMapID) then
  3669. self:StartCombat(v.mod, 0, "LOADING_SCREEN_DISABLED")
  3670. end
  3671. end
  3672. end
  3673. end
  3674.  
  3675. --In combat and it's not a raid boss. We'll just delay mod load until we leave combat to avoid "script ran to long errors"
  3676. --This should avoid most load problems (especially in LFR) When zoning in while in combat which causes the mod to fail to load/work correctly
  3677. --IF we are fighting a boss, we don't have much of a choice but to try and load anyways since script ran too long isn't actually a guarentee.
  3678. --The main place we should force a mod load in combat is for IsEncounterInProgress because i'm pretty sure blizzard waves "script ran too long" function for a small amount of time after a DC
  3679. --Outdoor bosses will try to ignore check, if they fail, well, then we need to try and find ways to make the mods that can't load in combat smaller or load faster.
  3680. function DBM:LoadMod(mod, force)
  3681. if type(mod) ~= "table" then
  3682. self:Debug("LoadMod failed because mod table not valid")
  3683. return false
  3684. end
  3685. if mod.isWorldBoss and not IsInInstance() and not force then
  3686. return
  3687. end--Don't load world boss mod this way.
  3688. if mod.minRevision > self.Revision then
  3689. if self:AntiSpam(60, "VER_MISMATCH") then--Throttle message in case person keeps trying to load mod (or it's a world boss player keeps targeting
  3690. self:AddMsg(DBM_CORE_LOAD_MOD_VER_MISMATCH:format(mod.name))
  3691. end
  3692. return
  3693. end
  3694. if InCombatLockdown() and not IsEncounterInProgress() and IsInInstance() and not noDelay then
  3695. self:Debug("LoadMod delayed do to combat")
  3696. if not loadDelay then--Prevent duplicate DBM_CORE_LOAD_MOD_COMBAT message.
  3697. self:AddMsg(DBM_CORE_LOAD_MOD_COMBAT:format(tostring(mod.name)))
  3698. end
  3699. if loadDelay and loadDelay ~= mod then--Check if load delay exists, but make sure this isn't a loop of same mod before making a second load delay
  3700. loadDelay2 = mod
  3701. else
  3702. loadDelay = mod
  3703. end
  3704. return
  3705. end
  3706. if not currentSpecID then
  3707. self:SetCurrentSpecInfo()
  3708. end
  3709. EJ_SetDifficulty(difficultyIndex)--Work around blizzard crash bug where other mods (like Boss) screw with Ej difficulty value, which makes EJ_GetSectionInfo crash the game when called with invalid difficulty index set.
  3710. self:Debug("LoadAddOn should have fired for "..mod.name, 2)
  3711. local loaded, reason = LoadAddOn(mod.modId)
  3712. if not loaded then
  3713. if reason then
  3714. self:AddMsg(DBM_CORE_LOAD_MOD_ERROR:format(tostring(mod.name), tostring(_G["ADDON_"..reason or ""])))
  3715. else
  3716. self:Debug("LoadAddOn failed and did not give reason")
  3717. -- self:AddMsg(DBM_CORE_LOAD_MOD_ERROR:format(tostring(mod.name), DBM_CORE_UNKNOWN)) -- wtf, this should never happen....(but it does happen sometimes if you reload your UI in an instance...)
  3718. end
  3719. return false
  3720. else
  3721. self:Debug("LoadAddOn should have succeeded for "..mod.name, 2)
  3722. self:AddMsg(DBM_CORE_LOAD_MOD_SUCCESS:format(tostring(mod.name)))
  3723. self:LoadModOptions(mod.modId, InCombatLockdown(), true)
  3724. if DBM_GUI then
  3725. DBM_GUI:UpdateModList()
  3726. end
  3727. if LastInstanceType ~= "pvp" and #inCombat == 0 and IsInGroup() then--do timer recovery only mod load
  3728. if not timerRequestInProgress then
  3729. timerRequestInProgress = true
  3730. -- Request timer to 3 person to prevent failure.
  3731. self:Unschedule(self.RequestTimers)
  3732. self:Schedule(7, self.RequestTimers, self, 1)
  3733. self:Schedule(10, self.RequestTimers, self, 2)
  3734. self:Schedule(13, self.RequestTimers, self, 3)
  3735. C_TimerAfter(15, function() timerRequestInProgress = false end)
  3736. end
  3737. end
  3738. if not InCombatLockdown() then--We loaded in combat because a raid boss was in process, but lets at least delay the garbage collect so at least load mod is half as bad, to do our best to avoid "script ran too long"
  3739. collectgarbage("collect")
  3740. end
  3741. if loadDelay2 == mod then
  3742. loadDelay2 = nil
  3743. elseif loadDelay == mod then
  3744. loadDelay = nil
  3745. end
  3746. return true
  3747. end
  3748. end
  3749.  
  3750. do
  3751. local function loadModByUnit(uId)
  3752. if not uId then
  3753. uId = "mouseover"
  3754. else
  3755. uId = uId.."target"
  3756. end
  3757. if IsInInstance() or not UnitIsFriend("player", uId) and UnitIsDead("player") or UnitIsDead(uId) then return end--If you're in an instance no reason to waste cpu. If THE BOSS dead, no reason to load a mod for it. To prevent rare lua error, needed to filter on player dead.
  3758. local guid = UnitGUID(uId)
  3759. if guid and DBM:IsCreatureGUID(guid) then
  3760. local cId = DBM:GetCIDFromGUID(guid)
  3761. for bosscId, addon in pairs(loadcIds) do
  3762. local enabled = GetAddOnEnableState(playerName, addon)
  3763. if cId and bosscId and cId == bosscId and not IsAddOnLoaded(addon) and enabled ~= 0 then
  3764. for i, v in ipairs(DBM.AddOns) do
  3765. if v.modId == addon then
  3766. DBM:LoadMod(v, true)
  3767. break
  3768. end
  3769. end
  3770. end
  3771. end
  3772. end
  3773. end
  3774.  
  3775. --Loading routeens hacks for world bosses based on target or mouseover.
  3776. function DBM:UPDATE_MOUSEOVER_UNIT()
  3777. loadModByUnit()
  3778. end
  3779.  
  3780. function DBM:UNIT_TARGET_UNFILTERED(uId)
  3781. if targetEventsRegistered then--Allow outdoor mod loading
  3782. loadModByUnit(uId)
  3783. end
  3784. --Debug options for seeing where BossUnitTargetScanner can be used.
  3785. if (self.Options.DebugLevel > 2 or (Transcriptor and Transcriptor:IsLogging())) and (uId == "boss1" or uId == "boss2" or uId == "boss3" or uId == "boss4" or uId == "boss5") then
  3786. local targetName = uId == "boss1" and UnitName("boss1target") or uId == "boss2" and UnitName("boss2target") or uId == "boss3" and UnitName("boss3target") or uId == "boss4" and UnitName("boss4target") or uId == "boss5" and UnitName("boss5target") or "nil"
  3787. self:Debug(uId.." changed targets to "..targetName)
  3788. end
  3789. --Active BossUnitTargetScanner
  3790. if targetMonitor and UnitExists(uId.."target") and UnitPlayerOrPetInRaid(uId.."target") then
  3791. self:Debug("targetMonitor exists, target exists", 2)
  3792. local modId, unitId, returnFunc = string.split("\t", targetMonitor)
  3793. self:Debug("targetMonitor: "..modId..", "..unitId..", "..returnFunc, 2)
  3794. local tanking, status = UnitDetailedThreatSituation(unitId, unitId.."target")--Tanking may return 0 if npc is temporarily looking at an NPC (IE fracture) but status will still be 3 on true tank
  3795. if tanking or (status == 3) then
  3796. self:Debug("targetMonitor ending, it's a tank", 2)
  3797. return
  3798. end--It's a tank/highest threat, this method ignores tanks
  3799. local mod = self:GetModByName(modId)
  3800. self:Debug("targetMonitor success, a valid target that's not a tank", 2)
  3801. mod[returnFunc](mod, self:GetUnitFullName(unitId.."target"), unitId.."target", unitId)--Return results to warning function with all variables.
  3802. targetMonitor = nil
  3803. end
  3804. end
  3805. end
  3806.  
  3807. -----------------------------
  3808. -- Handle Incoming Syncs --
  3809. -----------------------------
  3810.  
  3811. do
  3812. local function checkForActualPull()
  3813. if #inCombat == 0 then
  3814. DBM:StopLogging()
  3815. end
  3816. end
  3817.  
  3818. local syncHandlers = {}
  3819. local whisperSyncHandlers = {}
  3820.  
  3821. -- DBM uses the following prefixes since 4.1 as pre-4.1 sync code is going to be incompatible anways, so this is the perfect opportunity to throw away the old and long names
  3822. -- M = Mod
  3823. -- C = Combat start
  3824. -- GC = Guild Combat Start
  3825. -- IS = Icon set info
  3826. -- K = Kill
  3827. -- H = Hi!
  3828. -- V = Incoming version information
  3829. -- U = User Timer
  3830. -- PT = Pull Timer (for sound effects, the timer itself is still sent as a normal timer)
  3831. -- RT = Request Timers
  3832. -- CI = Combat Info
  3833. -- TI = Timer Info
  3834. -- IR = Instance Info Request
  3835. -- IRE = Instance Info Requested Ended/Canceled
  3836. -- II = Instance Info
  3837. -- WBE = World Boss engage info
  3838. -- WBD = World Boss defeat info
  3839. -- DSW = Disable Send Whisper
  3840. -- NS = Note Share
  3841.  
  3842. syncHandlers["M"] = function(sender, mod, revision, event, ...)
  3843. mod = DBM:GetModByName(mod or "")
  3844. if mod and event and revision then
  3845. revision = tonumber(revision) or 0
  3846. mod:ReceiveSync(event, sender, revision, ...)
  3847. end
  3848. end
  3849.  
  3850. syncHandlers["NS"] = function(sender, modid, modvar, text, abilityName)
  3851. if sender == playerName then return end
  3852. if DBM.Options.BlockNoteShare or InCombatLockdown() then return end
  3853. if IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and IsInInstance() and not C_Garrison:IsOnGarrisonMap() then return end
  3854. --^^You are in LFR, BG, or LFG. Block note syncs. They shouldn't be sendable, but in case someone edits DBM^^
  3855. local mod = DBM:GetModByName(modid or "")
  3856. local ability = abilityName or DBM_CORE_UNKNOWN
  3857. if mod and modvar and text and text ~= "" then
  3858. if DBM:AntiSpam(5, modvar) then--Don't allow calling same note more than once per 5 seconds
  3859. DBM:AddMsg(DBM_CORE_NOTE_SHARE_SUCCESS:format(sender, abilityName))
  3860. --Need to use modid in URL because we cannot insert a mod table into one
  3861. DBM:AddMsg(("|HDBM:noteshare:%s:%s:%s:%s:%s|h|cff3588ff[%s]"):format(modid, modvar, ability, text, sender, DBM_CORE_NOTE_SHARE_LINK))
  3862. else
  3863. DBM:Debug(sender.." is attempting to send too many notes so notes are being throttled")
  3864. end
  3865. else
  3866. DBM:AddMsg(DBM_CORE_NOTE_SHARE_FAIL:format(sender, ability))
  3867. end
  3868. end
  3869.  
  3870. syncHandlers["C"] = function(sender, delay, mod, modRevision, startHp, dbmRevision, modHFRevision)
  3871. if not dbmIsEnabled or sender == playerName then return end
  3872. if LastInstanceType == "pvp" then return end
  3873. if LastInstanceType == "none" and (not UnitAffectingCombat("player") or #inCombat > 0) then--world boss
  3874. local senderuId = DBM:GetRaidUnitId(sender)
  3875. if not senderuId then return end--Should never happen, but just in case. If happens, MANY "C" syncs are sent. losing 1 no big deal.
  3876. local _, _, _, playerZone = UnitPosition("player")
  3877. local _, _, _, senderZone = UnitPosition(senderuId)
  3878. if playerZone ~= senderZone then return end--not same zone
  3879. local range = DBM.RangeCheck:GetDistance("player", senderuId)--Same zone, so check range
  3880. if not range or range > 120 then return end
  3881. end
  3882. if not cSyncSender[sender] then
  3883. cSyncSender[sender] = true
  3884. cSyncReceived = cSyncReceived + 1
  3885. if cSyncReceived > 2 then -- need at least 3 sync to combat start. (for security)
  3886. local lag = select(4, GetNetStats()) / 1000
  3887. delay = tonumber(delay or 0) or 0
  3888. mod = DBM:GetModByName(mod or "")
  3889. modRevision = tonumber(modRevision or 0) or 0
  3890. dbmRevision = tonumber(dbmRevision or 0) or 0
  3891. modHFRevision = tonumber(modHFRevision or 0) or 0
  3892. startHp = tonumber(startHp or -1) or -1
  3893. if dbmRevision < 10481 then return end
  3894. if mod and delay and (not mod.zones or mod.zones[LastInstanceMapID]) and (not mod.minSyncRevision or modRevision >= mod.minSyncRevision) then
  3895. DBM:StartCombat(mod, delay + lag, "SYNC from - "..sender, true, startHp)
  3896. if (mod.revision < modHFRevision) and (mod.revision > 1000) then--mod.revision because we want to compare to OUR revision not senders
  3897. if DBM:AntiSpam(3, "HOTFIX") then
  3898. if DBM.HighestRelease < modHFRevision then--There is a newer RELEASE version of DBM out that has this mods fixes
  3899. showConstantReminder = 2
  3900. DBM:AddMsg(DBM_CORE_UPDATEREMINDER_HOTFIX)
  3901. else--This mods fixes are in an alpha version
  3902. DBM:AddMsg(DBM_CORE_UPDATEREMINDER_HOTFIX_ALPHA)
  3903. end
  3904. end
  3905. end
  3906. end
  3907. end
  3908. end
  3909. end
  3910.  
  3911. syncHandlers["DSW"] = function(sender)
  3912. if (DBM:GetRaidRank(sender) ~= 2 or not IsInGroup()) then return end--If not on group, we're probably sender, don't disable status. IF not leader, someone is trying to spoof this, block that too
  3913. statusWhisperDisabled = true
  3914. DBM:Debug("Raid leader has disabled status whispers")
  3915. end
  3916.  
  3917. syncHandlers["IS"] = function(sender, guid, ver, optionName)
  3918. ver = tonumber(ver)
  3919. if ver > (iconSetRevision[optionName] or 0) then--Save first synced version and person, ignore same version. refresh occurs only above version (fastest person)
  3920. iconSetRevision[optionName] = ver
  3921. iconSetPerson[optionName] = guid
  3922. end
  3923. if iconSetPerson[optionName] == UnitGUID("player") then--Check if that highest version was from ourself
  3924. canSetIcons[optionName] = true
  3925. else--Not from self, it means someone with a higher version than us probably sent it
  3926. canSetIcons[optionName] = false
  3927. end
  3928. local name = DBM:GetFullPlayerNameByGUID(iconSetPerson[optionName]) or DBM_CORE_UNKNOWN
  3929. DBM:Debug(name.." was elected icon setter for "..optionName, 2)
  3930. end
  3931.  
  3932. syncHandlers["K"] = function(sender, cId)
  3933. if select(2, IsInInstance()) == "pvp" then return end
  3934. cId = tonumber(cId or "")
  3935. if cId then DBM:OnMobKill(cId, true) end
  3936. end
  3937.  
  3938. syncHandlers["EE"] = function(sender, eId, success, mod, modRevision)
  3939. if select(2, IsInInstance()) == "pvp" then return end
  3940. eId = tonumber(eId or "")
  3941. success = tonumber(success)
  3942. mod = DBM:GetModByName(mod or "")
  3943. modRevision = tonumber(modRevision or 0) or 0
  3944. if mod and eId and success and (not mod.minSyncRevision or modRevision >= mod.minSyncRevision) and not eeSyncSender[sender] then
  3945. eeSyncSender[sender] = true
  3946. eeSyncReceived = eeSyncReceived + 1
  3947. if eeSyncReceived > 2 then -- need at least 3 person to combat end. (for security)
  3948. DBM:EndCombat(mod, success == 0)
  3949. end
  3950. end
  3951. end
  3952.  
  3953. local dummyMod -- dummy mod for the pull timer
  3954. local dummyMod2 -- dummy mod for the break timer
  3955. syncHandlers["PT"] = function(sender, timer, lastMapID, target)
  3956. DBM:Schedule(timer - 15, SendAddonMessage, "MyWAPrefix","StartTimer","RAID")
  3957. if DBM.Options.DontShowUserTimers then return end
  3958. local LFGTankException = IsPartyLFG() and UnitGroupRolesAssigned(sender) == "TANK"
  3959. if (DBM:GetRaidRank(sender) == 0 and IsInGroup() and not LFGTankException) or select(2, IsInInstance()) == "pvp" or IsEncounterInProgress() then
  3960. return
  3961. end
  3962. if (lastMapID and tonumber(lastMapID) ~= LastInstanceMapID) or (not lastMapID and DBM.Options.DontShowPTNoID) then return end
  3963. timer = tonumber(timer or 0)
  3964. if timer > 60 then
  3965. return
  3966. end
  3967. if not dummyMod then
  3968. local threshold = DBM.Options.PTCountThreshold
  3969. local adjustedThreshold = 5
  3970. if threshold > 10 then
  3971. adjustedThreshold = 10
  3972. else
  3973. adjustedThreshold = floor(threshold)
  3974. end
  3975. dummyMod = DBM:NewMod("PullTimerCountdownDummy")
  3976. DBM:GetModLocalization("PullTimerCountdownDummy"):SetGeneralLocalization{ name = DBM_CORE_MINIMAP_TOOLTIP_HEADER }
  3977. dummyMod.countdown = dummyMod:NewCountdown(0, 0, nil, nil, adjustedThreshold, true)
  3978. dummyMod.text = dummyMod:NewAnnounce("%s", 1, "Interface\\Icons\\ability_warrior_offensivestance")
  3979. dummyMod.geartext = dummyMod:NewSpecialWarning(" %s ", nil, nil, nil, 3)
  3980. end
  3981. --Cancel any existing pull timers before creating new ones, we don't want double countdowns or mismatching blizz countdown text (cause you can't call another one if one is in progress)
  3982. if not DBM.Options.DontShowPT2 and DBM.Bars:GetBar(DBM_CORE_TIMER_PULL) then
  3983. DBM.Bars:CancelBar(DBM_CORE_TIMER_PULL)
  3984. end
  3985. if not DBM.Options.DontPlayPTCountdown then
  3986. dummyMod.countdown:Cancel()
  3987. end
  3988. if not DBM.Options.DontShowPTCountdownText then
  3989. DBM:Unschedule(countDownTextDelay)
  3990. TimerTracker_OnEvent(TimerTracker, "PLAYER_ENTERING_WORLD")--easiest way to nil out timers on TimerTracker frame. This frame just has no actual star/stop functions
  3991. end
  3992. dummyMod.text:Cancel()
  3993. if timer == 0 then return end--"/dbm pull 0" will strictly be used to cancel the pull timer (which is why we let above part of code run but not below)
  3994. DBM:FlashClientIcon()
  3995. if not DBM.Options.DontShowPT2 then
  3996. DBM.Bars:CreateBar(timer, DBM_CORE_TIMER_PULL, "Interface\\Icons\\Spell_Holy_BorrowedTime")
  3997. fireEvent("DBM_TimerStart", "pull", DBM_CORE_TIMER_PULL, timer, "Interface\\Icons\\Spell_Holy_BorrowedTime")--Most args missing because pull timer simply doesn't have them.
  3998. end
  3999. if not DBM.Options.DontPlayPTCountdown then
  4000. dummyMod.countdown:Start(timer)
  4001. end
  4002. if not DBM.Options.DontShowPTCountdownText then
  4003. TimerTracker_OnEvent(TimerTracker, "START_TIMER", 2, timer, timer)
  4004. end
  4005. if not DBM.Options.DontShowPTText then
  4006. if target then
  4007. dummyMod.text:Show(DBM_CORE_ANNOUNCE_PULL_TARGET:format(target, timer, sender))
  4008. dummyMod.text:Schedule(timer, DBM_CORE_ANNOUNCE_PULL_NOW_TARGET:format(target))
  4009. else
  4010. dummyMod.text:Show(DBM_CORE_ANNOUNCE_PULL:format(timer, sender))
  4011. dummyMod.text:Schedule(timer, DBM_CORE_ANNOUNCE_PULL_NOW)
  4012. end
  4013. end
  4014. DBM:StartLogging(timer, checkForActualPull)
  4015. if DBM.Options.CheckGear then
  4016. local bagilvl, equippedilvl = GetAverageItemLevel()
  4017. local difference = bagilvl - equippedilvl
  4018. local weapon = GetInventoryItemLink("player", 16)
  4019. local fishingPole = false
  4020. if weapon then
  4021. local _, _, _, _, _, _, type = GetItemInfo(weapon)
  4022. if type and type == DBM_CORE_GEAR_FISHING_POLE then
  4023. fishingPole = true
  4024. end
  4025. end
  4026. if IsInRaid() and difference >= 40 then
  4027. dummyMod.geartext:Show(DBM_CORE_GEAR_WARNING:format(floor(difference)))
  4028. elseif IsInRaid() and (not weapon or fishingPole) then
  4029. dummyMod.geartext:Show(DBM_CORE_GEAR_WARNING_WEAPON)
  4030. end
  4031. end
  4032. end
  4033.  
  4034. function breakTimerStart(self, timer, sender)
  4035. if not dummyMod2 then
  4036. local threshold = DBM.Options.PTCountThreshold
  4037. local adjustedThreshold = 5
  4038. if threshold > 10 then
  4039. adjustedThreshold = 10
  4040. else
  4041. adjustedThreshold = floor(threshold)
  4042. end
  4043. dummyMod2 = DBM:NewMod("BreakTimerCountdownDummy")
  4044. DBM:GetModLocalization("BreakTimerCountdownDummy"):SetGeneralLocalization{ name = DBM_CORE_MINIMAP_TOOLTIP_HEADER }
  4045. dummyMod2.countdown = dummyMod2:NewCountdown(0, 0, nil, nil, adjustedThreshold, true)
  4046. dummyMod2.text = dummyMod2:NewAnnounce("%s", 1, "Interface\\Icons\\Spell_Holy_BorrowedTime")
  4047. end
  4048. --Cancel any existing break timers before creating new ones, we don't want double countdowns or mismatching blizz countdown text (cause you can't call another one if one is in progress)
  4049. if not DBM.Options.DontShowPT2 and DBM.Bars:GetBar(DBM_CORE_TIMER_BREAK) then
  4050. DBM.Bars:CancelBar(DBM_CORE_TIMER_BREAK)
  4051. end
  4052. if not DBM.Options.DontPlayPTCountdown then
  4053. dummyMod2.countdown:Cancel()
  4054. end
  4055. dummyMod2.text:Cancel()
  4056. DBM.Options.tempBreak2 = nil
  4057. if timer == 0 then return end--"/dbm break 0" will strictly be used to cancel the break timer (which is why we let above part of code run but not below)
  4058. self.Options.tempBreak2 = timer.."/"..time()
  4059. if not self.Options.DontShowPT2 then
  4060. self.Bars:CreateBar(timer, DBM_CORE_TIMER_BREAK, "Interface\\Icons\\Spell_Holy_BorrowedTime")
  4061. fireEvent("DBM_TimerStart", "break", DBM_CORE_TIMER_BREAK, timer, "Interface\\Icons\\Spell_Holy_BorrowedTime")
  4062. end
  4063. if not self.Options.DontPlayPTCountdown then
  4064. dummyMod2.countdown:Start(timer)
  4065. end
  4066. if not self.Options.DontShowPTText then
  4067. local hour, minute = GetGameTime()
  4068. minute = minute+(timer/60)
  4069. if minute >= 60 then
  4070. hour = hour + 1
  4071. minute = minute - 60
  4072. end
  4073. minute = floor(minute)
  4074. if minute < 10 then
  4075. minute = tostring(0 .. minute)
  4076. end
  4077. dummyMod2.text:Show(DBM_CORE_BREAK_START:format(strFromTime(timer).." ("..hour..":"..minute..")", sender))
  4078. if timer/60 > 10 then dummyMod2.text:Schedule(timer - 10*60, DBM_CORE_BREAK_MIN:format(10)) end
  4079. if timer/60 > 5 then dummyMod2.text:Schedule(timer - 5*60, DBM_CORE_BREAK_MIN:format(5)) end
  4080. if timer/60 > 2 then dummyMod2.text:Schedule(timer - 2*60, DBM_CORE_BREAK_MIN:format(2)) end
  4081. if timer/60 > 1 then dummyMod2.text:Schedule(timer - 1*60, DBM_CORE_BREAK_MIN:format(1)) end
  4082. dummyMod2.text:Schedule(timer, DBM_CORE_ANNOUNCE_BREAK_OVER:format(hour..":"..minute))
  4083. end
  4084. C_TimerAfter(timer, function() self.Options.tempBreak2 = nil end)
  4085. end
  4086.  
  4087. syncHandlers["BT"] = function(sender, timer)
  4088. if DBM.Options.DontShowUserTimers then return end
  4089. timer = tonumber(timer or 0)
  4090. if timer > 3600 then return end
  4091. if (DBM:GetRaidRank(sender) == 0 and IsInGroup()) or select(2, IsInInstance()) == "pvp" or IsEncounterInProgress() then
  4092. return
  4093. end
  4094. breakTimerStart(DBM, timer, sender)
  4095. end
  4096.  
  4097. whisperSyncHandlers["BTR3"] = function(sender, timer)
  4098. if DBM.Options.DontShowUserTimers or not DBM:GetRaidUnitId(sender) then return end
  4099. timer = tonumber(timer or 0)
  4100. if timer > 3600 then return end
  4101. DBM:Unschedule(DBM.RequestTimers)--IF we got BTR3 sync, then we know immediately RequestTimers was successful, so abort others
  4102. if #inCombat >= 1 then return end
  4103. if DBM.Bars:GetBar(DBM_CORE_TIMER_BREAK) then return end--Already recovered. Prevent duplicate recovery
  4104. breakTimerStart(DBM, timer, sender)
  4105. end
  4106.  
  4107. local function SendVersion()
  4108. if DBM.Options.FakeBWVersion then
  4109. SendAddonMessage("BigWigs", versionResponseString:format(fakeBWVersion, fakeBWHash), IsInGroup(2) and "INSTANCE_CHAT" or "RAID")
  4110. return
  4111. end
  4112. --(Note, faker isn't to screw with bigwigs nor is theirs to screw with dbm, but rathor raid leaders who don't let people run WTF they want to run)
  4113. local VPVersion
  4114. local VoicePack = DBM.Options.ChosenVoicePack
  4115. if VoicePack ~= "None" then
  4116. VPVersion = "/ VP"..VoicePack..": v"..DBM.VoiceVersions[VoicePack]
  4117. end
  4118. if VPVersion then
  4119. sendSync("V", ("%d\t%s\t%s\t%s\t%s\t%s"):format(DBM.Revision, tostring(DBM.ReleaseRevision), DBM.DisplayVersion, GetLocale(), tostring(not DBM.Options.DontSetIcons), VPVersion))
  4120. else
  4121. sendSync("V", ("%d\t%s\t%s\t%s\t%s"):format(DBM.Revision, tostring(DBM.ReleaseRevision), DBM.DisplayVersion, GetLocale(), tostring(not DBM.Options.DontSetIcons)))
  4122. end
  4123. end
  4124.  
  4125. -- TODO: is there a good reason that version information is broadcasted and not unicasted?
  4126. syncHandlers["H"] = function(sender)
  4127. DBM:Unschedule(SendVersion)--Throttle so we don't needlessly send tons of comms during initial raid invites
  4128. DBM:Schedule(3, SendVersion)--Send version if 3 seconds have past since last "Hi" sync
  4129. end
  4130.  
  4131. syncHandlers["BV"] = function(sender, version, hash)--Parsed from bigwigs V7+
  4132. if version and raid[sender] then
  4133. raid[sender].bwversion = version
  4134. raid[sender].bwhash = hash or ""
  4135. end
  4136. end
  4137.  
  4138. syncHandlers["V"] = function(sender, revision, version, displayVersion, locale, iconEnabled, VPVersion)
  4139. revision, version = tonumber(revision), tonumber(version)
  4140. if revision and version and displayVersion and raid[sender] then
  4141. raid[sender].revision = revision
  4142. raid[sender].version = version
  4143. raid[sender].displayVersion = displayVersion
  4144. raid[sender].VPVersion = VPVersion
  4145. raid[sender].locale = locale
  4146. raid[sender].enabledIcons = iconEnabled or "false"
  4147. DBM:Debug("Received version info from "..sender.." : Rev - "..revision..", Ver - "..version..", Rev Diff - "..(revision - DBM.Revision), 3)
  4148. if version > DBM.Revision then -- Update reminder
  4149. if not checkEntry(newerVersionPerson, sender) then
  4150. newerVersionPerson[#newerVersionPerson + 1] = sender
  4151. DBM:Debug("Newer version detected from "..sender.." : Rev - "..revision..", Ver - "..version..", Rev Diff - "..(revision - DBM.Revision), 3)
  4152. end
  4153. if #newerVersionPerson < 4 then
  4154. if #newerVersionPerson == 2 and updateNotificationDisplayed < 2 then--Only requires 2 for update notification.
  4155. if DBM.HighestRelease < version then
  4156. DBM.HighestRelease = version
  4157. end
  4158. DBM.NewerVersion = displayVersion
  4159. --UGLY hack to get release version number instead of alpha one
  4160. if DBM.NewerVersion:find("alpha") then
  4161. local temp1, temp2 = string.split(" ", DBM.NewerVersion)--Strip down to just version, no alpha
  4162. local temp3, temp4, temp5 = string.split(".", temp1)--Strip version down to 3 numbers
  4163. if temp5 then
  4164. temp5 = tonumber(temp5)
  4165. temp5 = temp5 - 1
  4166. temp5 = tostring(temp5)
  4167. DBM.NewerVersion = temp3.."."..temp4.."."..temp5
  4168. end
  4169. end
  4170. --Find min revision.
  4171. updateNotificationDisplayed = 2
  4172. DBM:AddMsg(DBM_CORE_UPDATEREMINDER_HEADER:match("([^\n]*)"))
  4173. DBM:AddMsg(DBM_CORE_UPDATEREMINDER_HEADER:match("\n(.*)"):format(displayVersion, version))
  4174. DBM:AddMsg(("|HDBM:update:%s:%s|h|cff3588ff[%s]"):format(displayVersion, version, DBM_CORE_UPDATEREMINDER_URL or "http://www.deadlybossmods.com"))
  4175. showConstantReminder = 1
  4176. elseif #newerVersionPerson == 3 then--Requires 3 for force disable.
  4177. --Find min revision.
  4178. local revDifference = mmin((raid[newerVersionPerson[1]].revision - DBM.Revision), (raid[newerVersionPerson[2]].revision - DBM.Revision), (raid[newerVersionPerson[3]].revision - DBM.Revision))
  4179. --The following code requires at least THREE people to send that higher revision (I just upped it from 2). That should be more than adaquate.
  4180. --Disable if out of date and it's a major patch.
  4181. --[[if not testBuild and dbmToc < wowTOC then
  4182. updateNotificationDisplayed = 3
  4183. DBM:AddMsg(DBM_CORE_UPDATEREMINDER_MAJORPATCH)
  4184. DBM:Disable(true)--]]
  4185. --Disable if revision grossly out of date even if not major patch.
  4186. if revDifference > 180 then
  4187. --elseif revDifference > 180 then
  4188. if updateNotificationDisplayed < 3 then
  4189. updateNotificationDisplayed = 3
  4190. DBM:AddMsg(DBM_CORE_UPDATEREMINDER_DISABLE)
  4191. DBM:Disable(true)
  4192. end
  4193. end
  4194. end
  4195. end
  4196. end
  4197. if DBM.DisplayVersion:find("alpha") and #newerVersionPerson < 2 and #newerRevisionPerson < 2 and updateNotificationDisplayed < 2 and (revision - DBM.Revision) > 20 then
  4198. if not checkEntry(newerRevisionPerson, sender) then
  4199. newerRevisionPerson[#newerRevisionPerson + 1] = sender
  4200. DBM:Debug("Newer revision detected from "..sender.." : Rev - "..revision..", Ver - "..version..", Rev Diff - "..(revision - DBM.Revision))
  4201. end
  4202. if #newerRevisionPerson == 2 then
  4203. local revDifference = mmin((raid[newerRevisionPerson[1]].revision - DBM.Revision), (raid[newerRevisionPerson[2]].revision - DBM.Revision))
  4204. if testBuild and revDifference > 5 then
  4205. updateNotificationDisplayed = 3
  4206. DBM:AddMsg(DBM_CORE_UPDATEREMINDER_DISABLE)
  4207. DBM:Disable(true)
  4208. else
  4209. updateNotificationDisplayed = 2
  4210. DBM:AddMsg(DBM_CORE_UPDATEREMINDER_HEADER_ALPHA:format(revDifference))
  4211. end
  4212. end
  4213. end
  4214. end
  4215. DBM:GROUP_ROSTER_UPDATE()
  4216. end
  4217.  
  4218. syncHandlers["U"] = function(sender, time, text)
  4219. if select(2, IsInInstance()) == "pvp" then return end -- no pizza timers in battlegrounds
  4220. if DBM.Options.DontShowUserTimers then return end
  4221. if DBM:GetRaidRank(sender) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then return end
  4222. if sender == playerName then return end
  4223. time = tonumber(time or 0)
  4224. text = tostring(text)
  4225. if time and text then
  4226. DBM:CreatePizzaTimer(time, text, nil, sender)
  4227. end
  4228. end
  4229.  
  4230. syncHandlers["CU"] = function(sender, time, text)
  4231. if select(2, IsInInstance()) == "pvp" then return end -- no pizza timers in battlegrounds
  4232. if DBM.Options.DontShowUserTimers then return end
  4233. if DBM:GetRaidRank(sender) == 0 or difficultyIndex == 7 or difficultyIndex == 17 then return end
  4234. if sender == playerName then return end
  4235. time = tonumber(time or 0)
  4236. text = tostring(text)
  4237. if time and text then
  4238. DBM:CreatePizzaTimer(time, text, nil, sender, true)
  4239. end
  4240. end
  4241.  
  4242. -- beware, ugly and missplaced code ahead
  4243. -- todo: move this somewhere else
  4244. do
  4245. local accessList = {}
  4246. local savedSender
  4247.  
  4248. local inspopup = CreateFrame("Frame", "DBMPopupLockout", UIParent)
  4249. inspopup:SetBackdrop({bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
  4250. edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
  4251. tile = true, tileSize = 16, edgeSize = 16,
  4252. insets = {left = 1, right = 1, top = 1, bottom = 1}}
  4253. )
  4254. inspopup:SetSize(500, 120)
  4255. inspopup:SetPoint("TOP", UIParent, "TOP", 0, -200)
  4256. inspopup:SetFrameStrata("DIALOG")
  4257.  
  4258. local inspopuptext = inspopup:CreateFontString()
  4259. inspopuptext:SetFontObject(ChatFontNormal)
  4260. inspopuptext:SetWidth(470)
  4261. inspopuptext:SetWordWrap(true)
  4262. inspopuptext:SetPoint("TOP", inspopup, "TOP", 0, -15)
  4263.  
  4264. local buttonaccept = CreateFrame("Button", nil, inspopup)
  4265. buttonaccept:SetNormalTexture("Interface\\Buttons\\UI-DialogBox-Button-Up")
  4266. buttonaccept:SetPushedTexture("Interface\\Buttons\\UI-DialogBox-Button-Down")
  4267. buttonaccept:SetHighlightTexture("Interface\\Buttons\\UI-DialogBox-Button-Highlight", "ADD")
  4268. buttonaccept:SetSize(128, 35)
  4269. buttonaccept:SetPoint("BOTTOM", inspopup, "BOTTOM", -75, 0)
  4270.  
  4271. local buttonatext = buttonaccept:CreateFontString()
  4272. buttonatext:SetFontObject(ChatFontNormal)
  4273. buttonatext:SetPoint("CENTER", buttonaccept, "CENTER", 0, 5)
  4274. buttonatext:SetText(YES)
  4275.  
  4276. local buttondecline = CreateFrame("Button", nil, inspopup)
  4277. buttondecline:SetNormalTexture("Interface\\Buttons\\UI-DialogBox-Button-Up")
  4278. buttondecline:SetPushedTexture("Interface\\Buttons\\UI-DialogBox-Button-Down")
  4279. buttondecline:SetHighlightTexture("Interface\\Buttons\\UI-DialogBox-Button-Highlight", "ADD")
  4280. buttondecline:SetSize(128, 35)
  4281. buttondecline:SetPoint("BOTTOM", inspopup, "BOTTOM", 75, 0)
  4282.  
  4283. local buttondtext = buttondecline:CreateFontString()
  4284. buttondtext:SetFontObject(ChatFontNormal)
  4285. buttondtext:SetPoint("CENTER", buttondecline, "CENTER", 0, 5)
  4286. buttondtext:SetText(NO)
  4287.  
  4288. inspopup:Hide()
  4289.  
  4290. local function autoDecline(sender, force)
  4291. inspopup:Hide()
  4292. savedSender = nil
  4293. if force then
  4294. SendAddonMessage("D4", "II\t" .. "denied", "WHISPER", sender)
  4295. else
  4296. SendAddonMessage("D4", "II\t" .. "timeout", "WHISPER", sender)
  4297. end
  4298. end
  4299.  
  4300. local function showPopupInstanceIdPermission(sender)
  4301. DBM:Unschedule(autoDecline)
  4302. DBM:Schedule(59, autoDecline, sender)
  4303. inspopup:Hide()
  4304. if savedSender ~= sender then
  4305. if savedSender then
  4306. autoDecline(savedSender, 1) -- Do not allow multiple popups, so auto decline to previous sender.
  4307. end
  4308. savedSender = sender
  4309. end
  4310. inspopuptext:SetText(DBM_REQ_INSTANCE_ID_PERMISSION:format(sender, sender))
  4311. buttonaccept:SetScript("OnClick", function(f) savedSender = nil DBM:Unschedule(autoDecline) accessList[sender] = true syncHandlers["IR"](sender) f:GetParent():Hide() end)
  4312. buttondecline:SetScript("OnClick", function(f) autoDecline(sender, 1) end)
  4313. PlaySound("igMainMenuOpen")
  4314. inspopup:Show()
  4315. end
  4316.  
  4317. syncHandlers["IR"] = function(sender)
  4318. if DBM:GetRaidRank(sender) == 0 or sender == playerName then
  4319. return
  4320. end
  4321. accessList = accessList or {}
  4322. if not accessList[sender] then
  4323. -- ask for permission
  4324. showPopupInstanceIdPermission(sender)
  4325. return
  4326. end
  4327. -- okay, send data
  4328. local sentData = false
  4329. for i = 1, GetNumSavedInstances() do
  4330. local name, id, _, difficulty, locked, extended, instanceIDMostSig, isRaid, maxPlayers, textDiff, _, progress = GetSavedInstanceInfo(i)
  4331. if (locked or extended) and isRaid then -- only report locked raid instances
  4332. SendAddonMessage("D4", "II\tData\t" .. name .. "\t" .. id .. "\t" .. difficulty .. "\t" .. maxPlayers .. "\t" .. (progress or 0) .. "\t" .. textDiff, "WHISPER", sender)
  4333. sentData = true
  4334. end
  4335. end
  4336. if not sentData then
  4337. -- send something even if there is nothing to report so the receiver is able to tell you apart from someone who just didn't respond...
  4338. SendAddonMessage("D4", "II\tNoData", "WHISPER", sender)
  4339. end
  4340. end
  4341.  
  4342. syncHandlers["IRE"] = function(sender)
  4343. local popup = inspopup:IsShown()
  4344. if popup and savedSender == sender then -- found the popup with the correct data
  4345. savedSender = nil
  4346. DBM:Unschedule(autoDecline)
  4347. inspopup:Hide()
  4348. end
  4349. end
  4350.  
  4351. syncHandlers["GCB"] = function(sender, modId, ver, difficulty)
  4352. if not DBM.Options.ShowGuildMessages or not difficulty then return end
  4353. if not ver or not (ver == "2") then return end--Ignore old versions
  4354. if DBM:AntiSpam(5, "GCB") then
  4355. if IsInInstance() then return end--Simple filter, if you are inside an instance, just filter it, if not in instance, good to go.
  4356. local bossName = EJ_GetEncounterInfo(modId) or DBM_CORE_UNKNOWN
  4357. local difficultyName = DBM_CORE_UNKNOWN
  4358. difficulty = tonumber(difficulty)
  4359. if difficulty == 16 then
  4360. difficultyName = PLAYER_DIFFICULTY6
  4361. elseif difficulty == 15 then
  4362. difficultyName = PLAYER_DIFFICULTY2
  4363. else
  4364. difficultyName = PLAYER_DIFFICULTY1
  4365. end
  4366. DBM:AddMsg(DBM_CORE_GUILD_COMBAT_STARTED:format(difficultyName.."-"..bossName))
  4367. end
  4368. end
  4369.  
  4370. syncHandlers["GCE"] = function(sender, modId, ver, wipe, time, difficulty, wipeHP)
  4371. if not DBM.Options.ShowGuildMessages or not difficulty then return end
  4372. if not ver or not (ver == "3") then return end--Ignore old versions
  4373. if DBM:AntiSpam(5, "GCE") then
  4374. if IsInInstance() then return end--Simple filter, if you are inside an instance, just filter it, if not in instance, good to go.
  4375. local bossName = EJ_GetEncounterInfo(modId) or DBM_CORE_UNKNOWN
  4376. local difficultyName = DBM_CORE_UNKNOWN
  4377. difficulty = tonumber(difficulty)
  4378. if difficulty == 16 then
  4379. difficultyName = PLAYER_DIFFICULTY6
  4380. elseif difficulty == 15 then
  4381. difficultyName = PLAYER_DIFFICULTY2
  4382. else
  4383. difficultyName = PLAYER_DIFFICULTY1
  4384. end
  4385. if wipe == "1" then
  4386. DBM:AddMsg(DBM_CORE_GUILD_COMBAT_ENDED_AT:format(difficultyName.."-"..bossName, wipeHP, time))
  4387. else
  4388. DBM:AddMsg(DBM_CORE_GUILD_BOSS_DOWN:format(difficultyName.."-"..bossName, time))
  4389. end
  4390. end
  4391. end
  4392.  
  4393. syncHandlers["WBE"] = function(sender, modId, realm, health, ver, name)
  4394. if not ver or not (ver == "8") then return end--Ignore old versions
  4395. if lastBossEngage[modId..realm] and (GetTime() - lastBossEngage[modId..realm] < 30) then return end--We recently got a sync about this boss on this realm, so do nothing.
  4396. lastBossEngage[modId..realm] = GetTime()
  4397. if realm == playerRealm and DBM.Options.WorldBossAlert and not IsEncounterInProgress() then
  4398. modId = tonumber(modId)--If it fails to convert into number, this makes it nil
  4399. local bossName = modId and EJ_GetEncounterInfo(modId) or name or DBM_CORE_UNKNOWN
  4400. DBM:AddMsg(DBM_CORE_WORLDBOSS_ENGAGED:format(bossName, floor(health), sender))
  4401. end
  4402. end
  4403.  
  4404. syncHandlers["WBD"] = function(sender, modId, realm, ver, name)
  4405. if not ver or not (ver == "8") then return end--Ignore old versions
  4406. if lastBossDefeat[modId..realm] and (GetTime() - lastBossDefeat[modId..realm] < 30) then return end
  4407. lastBossDefeat[modId..realm] = GetTime()
  4408. if realm == playerRealm and DBM.Options.WorldBossAlert and not IsEncounterInProgress() then
  4409. modId = tonumber(modId)--If it fails to convert into number, this makes it nil
  4410. local bossName = modId and EJ_GetEncounterInfo(modId) or name or DBM_CORE_UNKNOWN
  4411. DBM:AddMsg(DBM_CORE_WORLDBOSS_DEFEATED:format(bossName, sender))
  4412. end
  4413. end
  4414.  
  4415. whisperSyncHandlers["WBE"] = function(sender, modId, realm, health, ver, name)
  4416. if not ver or not (ver == "8") then return end--Ignore old versions
  4417. if lastBossEngage[modId..realm] and (GetTime() - lastBossEngage[modId..realm] < 30) then return end
  4418. lastBossEngage[modId..realm] = GetTime()
  4419. if realm == playerRealm and DBM.Options.WorldBossAlert and not IsEncounterInProgress() then
  4420. local _, toonName = BNGetGameAccountInfo(sender)
  4421. modId = tonumber(modId)--If it fails to convert into number, this makes it nil
  4422. local bossName = modId and EJ_GetEncounterInfo(modId) or name or DBM_CORE_UNKNOWN
  4423. DBM:AddMsg(DBM_CORE_WORLDBOSS_ENGAGED:format(bossName, floor(health), toonName))
  4424. end
  4425. end
  4426.  
  4427. whisperSyncHandlers["WBD"] = function(sender, modId, realm, ver, name)
  4428. if not ver or not (ver == "8") then return end--Ignore old versions
  4429. if lastBossDefeat[modId..realm] and (GetTime() - lastBossDefeat[modId..realm] < 30) then return end
  4430. lastBossDefeat[modId..realm] = GetTime()
  4431. if realm == playerRealm and DBM.Options.WorldBossAlert and not IsEncounterInProgress() then
  4432. local _, toonName = BNGetGameAccountInfo(sender)
  4433. modId = tonumber(modId)--If it fails to convert into number, this makes it nil
  4434. local bossName = modId and EJ_GetEncounterInfo(modId) or name or DBM_CORE_UNKNOWN
  4435. DBM:AddMsg(DBM_CORE_WORLDBOSS_DEFEATED:format(bossName, toonName))
  4436. end
  4437. end
  4438.  
  4439. local lastRequest = 0
  4440. local numResponses = 0
  4441. local expectedResponses = 0
  4442. local allResponded = false
  4443. local results
  4444.  
  4445. local updateInstanceInfo, showResults
  4446.  
  4447. whisperSyncHandlers["II"] = function(sender, result, name, id, diff, maxPlayers, progress, textDiff)
  4448. if not DBM:GetRaidUnitId(sender) then return end
  4449. if GetTime() - lastRequest > 62 or not results then
  4450. return
  4451. end
  4452. if not result then
  4453. return
  4454. end
  4455. name = name or DBM_CORE_UNKNOWN
  4456. id = id or ""
  4457. diff = tonumber(diff or 0) or 0
  4458. maxPlayers = tonumber(maxPlayers or 0) or 0
  4459. progress = tonumber(progress or 0) or 0
  4460. textDiff = textDiff or ""
  4461.  
  4462. -- count responses
  4463. if not results.responses[sender] then
  4464. results.responses[sender] = result
  4465. numResponses = numResponses + 1
  4466. end
  4467.  
  4468. -- get localized difficulty text
  4469. if textDiff ~= "" then
  4470. results.difftext[diff] = textDiff
  4471. end
  4472.  
  4473. if result == "Data" then
  4474. -- got data in that response and not just a "no" or "i'm away"
  4475. local instanceId = name.." "..maxPlayers.." "..diff -- locale-dependant dungeon ID
  4476. results.data[instanceId] = results.data[instanceId] or {
  4477. ids = {}, -- array of all ids of all raid members
  4478. name = name,
  4479. diff = diff,
  4480. maxPlayers = maxPlayers,
  4481. }
  4482. if diff == 5 or diff == 6 or diff == 16 then
  4483. results.data[instanceId].ids[id] = results.data[instanceId].ids[id] or { progress = progress, haveid = true }
  4484. tinsert(results.data[instanceId].ids[id], sender)
  4485. else
  4486. results.data[instanceId].ids[progress] = results.data[instanceId].ids[progress] or { progress = progress }
  4487. tinsert(results.data[instanceId].ids[progress], sender)
  4488. end
  4489. end
  4490.  
  4491. if numResponses >= expectedResponses then -- unlikely, lol
  4492. DBM:Unschedule(updateInstanceInfo)
  4493. DBM:Unschedule(showResults)
  4494. if not allResponded then --Only display message once in case we get for example 4 syncs the last sender
  4495. DBM:Schedule(0.99, DBM.AddMsg, DBM, DBM_INSTANCE_INFO_ALL_RESPONSES)
  4496. allResponded = true
  4497. end
  4498. C_TimerAfter(1, showResults) --Delay results so we allow time for same sender to send more than 1 lockout, otherwise, if we get expectedResponses before all data is sent from 1 user, we clip some of their data.
  4499. end
  4500. end
  4501.  
  4502. function showResults()
  4503. local resultCount = 0
  4504. -- TODO: you could catch some localized instances by observing IDs if there are multiple players with the same instance ID but a different name ;) (not that useful if you are trying to get a fresh instance)
  4505. DBM:AddMsg(DBM_INSTANCE_INFO_RESULTS, false)
  4506. DBM:AddMsg("---", false)
  4507. for i, v in pairs(results.data) do
  4508. resultCount = resultCount + 1
  4509. DBM:AddMsg(DBM_INSTANCE_INFO_DETAIL_HEADER:format(v.name, (results.difftext[v.diff] or v.diff)), false)
  4510. for id, v in pairs(v.ids) do
  4511. if v.haveid then
  4512. DBM:AddMsg(DBM_INSTANCE_INFO_DETAIL_INSTANCE:format(id, v.progress, tconcat(v, ", ")), false)
  4513. else
  4514. DBM:AddMsg(DBM_INSTANCE_INFO_DETAIL_INSTANCE2:format(v.progress, tconcat(v, ", ")), false)
  4515. end
  4516. end
  4517. DBM:AddMsg("---", false)
  4518. end
  4519. if resultCount == 0 then
  4520. DBM:AddMsg(DBM_INSTANCE_INFO_NOLOCKOUT, false)
  4521. end
  4522. local denied = {}
  4523. local away = {}
  4524. local noResponse = {}
  4525. for i = 1, GetNumGroupMembers() do
  4526. if not UnitIsUnit("raid"..i, "player") then
  4527. tinsert(noResponse, (GetRaidRosterInfo(i)))
  4528. end
  4529. end
  4530. for i, v in pairs(results.responses) do
  4531. if v == "Data" or v == "NoData" then
  4532. elseif v == "timeout" then
  4533. tinsert(away, i)
  4534. else -- could be "clicked" or "override", in both cases we don't get the data because the dialog requesting it was dismissed
  4535. tinsert(denied, i)
  4536. end
  4537. removeEntry(noResponse, i)
  4538. end
  4539. if #denied > 0 then
  4540. DBM:AddMsg(DBM_INSTANCE_INFO_STATS_DENIED:format(tconcat(denied, ", ")), false)
  4541. end
  4542. if #away > 0 then
  4543. DBM:AddMsg(DBM_INSTANCE_INFO_STATS_AWAY:format(tconcat(away, ", ")), false)
  4544. end
  4545. if #noResponse > 0 then
  4546. DBM:AddMsg(DBM_INSTANCE_INFO_STATS_NO_RESPONSE:format(tconcat(noResponse, ", ")), false)
  4547. end
  4548. results = nil
  4549. end
  4550.  
  4551. -- called when the chat link is clicked
  4552. function DBM:ShowRaidIDRequestResults()
  4553. if not results then -- check if we are currently querying raid IDs, results will be nil if we don't
  4554. return
  4555. end
  4556. self:Unschedule(updateInstanceInfo)
  4557. self:Unschedule(showResults)
  4558. showResults() -- sets results to nil after the results are displayed, ending the current id request; future incoming data will be discarded
  4559. sendSync("IRE")
  4560. end
  4561.  
  4562. local function getResponseStats()
  4563. local numResponses = 0
  4564. local sent = 0
  4565. local denied = 0
  4566. local away = 0
  4567. for k, v in pairs(results.responses) do
  4568. numResponses = numResponses + 1
  4569. if v == "Data" or v == "NoData" then
  4570. sent = sent + 1
  4571. elseif v == "timeout" then
  4572. away = away + 1
  4573. else -- could be "clicked" or "override", in both cases we don't get the data because the dialog requesting it was dismissed
  4574. denied = denied + 1
  4575. end
  4576. end
  4577. return numResponses, sent, denied, away
  4578. end
  4579.  
  4580. local function getNumDBMUsers() -- without ourselves
  4581. local r = 0
  4582. for i, v in pairs(raid) do
  4583. if v.revision and v.name ~= playerName and UnitIsConnected(v.id) then
  4584. r = r + 1
  4585. end
  4586. end
  4587. return r
  4588. end
  4589.  
  4590. function updateInstanceInfo(timeRemaining, dontAddShowResultNowButton)
  4591. local numResponses, sent, denied, away = getResponseStats()
  4592. local dbmUsers = getNumDBMUsers()
  4593. DBM:AddMsg(DBM_INSTANCE_INFO_STATUS_UPDATE:format(numResponses, dbmUsers, sent, denied, timeRemaining), false)
  4594. if not dontAddShowResultNowButton then
  4595. if dbmUsers - numResponses <= 7 then -- waiting for 7 or less players, show their names and the early result option
  4596. -- copied from above, todo: implement a smarter way of keeping track of stuff like this
  4597. local noResponse = {}
  4598. for i = 1, GetNumGroupMembers() do
  4599. if not UnitIsUnit("raid"..i, "player") and raid[GetRaidRosterInfo(i)] and raid[GetRaidRosterInfo(i)].revision then -- only show players who actually can respond (== DBM users)
  4600. tinsert(noResponse, (GetRaidRosterInfo(i)))
  4601. end
  4602. end
  4603. for i, v in pairs(results.responses) do
  4604. removeEntry(noResponse, i)
  4605. end
  4606.  
  4607. --[[
  4608. -- this looked like the easiest way (for some reason?) to create the player string when writing this code -.-
  4609. local function dup(...) if select("#", ...) == 0 then return else return ..., ..., dup(select(2, ...)) end end
  4610. DBM:AddMsg(DBM_INSTANCE_INFO_SHOW_RESULTS:format(("|Hplayer:%s|h[%s]|h| "):rep(#noResponse):format(dup(unpack(noResponse)))), false)
  4611. ]]
  4612. -- code that one can actually read
  4613. for i, v in ipairs(noResponse) do
  4614. noResponse[i] = ("|Hplayer:%s|h[%s]|h|"):format(v, v)
  4615. end
  4616. DBM:AddMsg(DBM_INSTANCE_INFO_SHOW_RESULTS:format(tconcat(noResponse, ", ")), false)
  4617. end
  4618. end
  4619. end
  4620.  
  4621. function DBM:RequestInstanceInfo()
  4622. self:AddMsg(DBM_INSTANCE_INFO_REQUESTED)
  4623. lastRequest = GetTime()
  4624. allResponded = false
  4625. results = {
  4626. responses = { -- who responded to our request?
  4627. },
  4628. data = { -- the actual data
  4629. },
  4630. difftext = {
  4631. }
  4632. }
  4633. numResponses = 0
  4634. expectedResponses = getNumDBMUsers()
  4635. sendSync("IR")
  4636. self:Unschedule(updateInstanceInfo)
  4637. self:Unschedule(showResults)
  4638. self:Schedule(17, updateInstanceInfo, 45, true)
  4639. self:Schedule(32, updateInstanceInfo, 30)
  4640. self:Schedule(48, updateInstanceInfo, 15)
  4641. C_TimerAfter(62, showResults)
  4642. end
  4643. end
  4644.  
  4645. whisperSyncHandlers["RT"] = function(sender)
  4646. if not DBM:GetRaidUnitId(sender) then
  4647. DBM:Debug(sender.." attempted to request timers but isn't in your group")
  4648. return
  4649. end
  4650. DBM:SendTimers(sender)
  4651. end
  4652.  
  4653. whisperSyncHandlers["CI"] = function(sender, mod, time)
  4654. if not DBM:GetRaidUnitId(sender) then
  4655. DBM:Debug(sender.." attempted to send you combat info but isn't in your group")
  4656. return
  4657. end
  4658. mod = DBM:GetModByName(mod or "")
  4659. time = tonumber(time or 0)
  4660. if mod and time then
  4661. DBM:ReceiveCombatInfo(sender, mod, time)
  4662. end
  4663. end
  4664.  
  4665. whisperSyncHandlers["TI"] = function(sender, mod, timeLeft, totalTime, id, ...)
  4666. if not DBM:GetRaidUnitId(sender) then return end
  4667. mod = DBM:GetModByName(mod or "")
  4668. timeLeft = tonumber(timeLeft or 0)
  4669. totalTime = tonumber(totalTime or 0)
  4670. if mod and timeLeft and timeLeft > 0 and totalTime and totalTime > 0 and id then
  4671. DBM:ReceiveTimerInfo(sender, mod, timeLeft, totalTime, id, ...)
  4672. end
  4673. end
  4674.  
  4675. whisperSyncHandlers["VI"] = function(sender, mod, name, value)
  4676. if not DBM:GetRaidUnitId(sender) then return end
  4677. mod = DBM:GetModByName(mod or "")
  4678. value = tonumber(value) or value
  4679. if mod and name and value then
  4680. DBM:ReceiveVariableInfo(sender, mod, name, value)
  4681. end
  4682. end
  4683.  
  4684. local function handleSync(channel, sender, prefix, ...)
  4685. if not prefix then
  4686. return
  4687. end
  4688. local handler
  4689. if channel == "WHISPER" and sender ~= playerName then -- separate between broadcast and unicast, broadcast must not be sent as unicast or vice-versa
  4690. handler = whisperSyncHandlers[prefix]
  4691. else
  4692. handler = syncHandlers[prefix]
  4693. end
  4694. if handler then
  4695. return handler(sender, ...)
  4696. end
  4697. end
  4698.  
  4699. function DBM:CHAT_MSG_ADDON(prefix, msg, channel, sender)
  4700. if prefix == "D4" and msg and (channel == "PARTY" or channel == "RAID" or channel == "INSTANCE_CHAT" or channel == "WHISPER" or channel == "GUILD") then
  4701. sender = Ambiguate(sender, "none")
  4702. handleSync(channel, sender, strsplit("\t", msg))
  4703. elseif prefix == "BigWigs" and msg and (channel == "PARTY" or channel == "RAID" or channel == "INSTANCE_CHAT") then
  4704. local bwPrefix, bwMsg, extra = strsplit("^", msg)
  4705. if bwPrefix and bwMsg and extra then--Nil check all 3 to avoid errors form older versions
  4706. if bwPrefix == "V" then--Version information prefixes
  4707. local verString, hash = bwMsg, extra
  4708. local version = tonumber(verString) or 0
  4709. if version == 0 then return end--Just a query
  4710. sender = Ambiguate(sender, "none")
  4711. handleSync(channel, sender, "BV", version, hash)--Prefix changed, so it's not handled by DBMs "V" handler
  4712. if version > fakeBWVersion then--Newer revision found, upgrade!
  4713. fakeBWVersion = version
  4714. fakeBWHash = hash
  4715. end
  4716. elseif bwPrefix == "Q" then--Version request prefix
  4717. self:Unschedule(SendVersion)
  4718. self:Schedule(3, SendVersion)
  4719. end
  4720. end
  4721. elseif prefix == "Transcriptor" and msg then
  4722. if msg:find("spell:") and (DBM.Options.DebugLevel > 2 or (Transcriptor and Transcriptor:IsLogging())) then
  4723. local spellId = string.match(msg, "spell:(%d+)") or DBM_CORE_UNKNOWN
  4724. local spellName = string.match(msg, "h%[(.-)%]|h") or DBM_CORE_UNKNOWN
  4725. local message = "RAID_BOSS_WHISPER on "..sender.." with spell of "..spellName.." ("..spellId..")"
  4726. self:Debug(message)
  4727. end
  4728. end
  4729. end
  4730.  
  4731. function DBM:BN_CHAT_MSG_ADDON(prefix, msg, channel, sender)
  4732. if prefix == "D4" and msg then
  4733. handleSync(channel, sender, strsplit("\t", msg))
  4734. end
  4735. end
  4736. end
  4737.  
  4738. -----------------------
  4739. -- Update Reminder --
  4740. -----------------------
  4741. do
  4742. local frame, fontstring, fontstringFooter, editBox, urlText
  4743.  
  4744. local function createFrame()
  4745. frame = CreateFrame("Frame", "DBMUpdateReminder", UIParent)
  4746. frame:SetFrameStrata("FULLSCREEN_DIALOG") -- yes, this isn't a fullscreen dialog, but I want it to be in front of other DIALOG frames (like DBM GUI which might open this frame...)
  4747. frame:SetWidth(430)
  4748. frame:SetHeight(140)
  4749. frame:SetPoint("TOP", 0, -230)
  4750. frame:SetBackdrop({
  4751. bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
  4752. edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", tile = true, tileSize = 32, edgeSize = 32,
  4753. insets = {left = 11, right = 12, top = 12, bottom = 11},
  4754. })
  4755. fontstring = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
  4756. fontstring:SetWidth(410)
  4757. fontstring:SetHeight(0)
  4758. fontstring:SetPoint("TOP", 0, -16)
  4759. editBox = CreateFrame("EditBox", nil, frame)
  4760. do
  4761. local editBoxLeft = editBox:CreateTexture(nil, "BACKGROUND")
  4762. local editBoxRight = editBox:CreateTexture(nil, "BACKGROUND")
  4763. local editBoxMiddle = editBox:CreateTexture(nil, "BACKGROUND")
  4764. editBoxLeft:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Left")
  4765. editBoxLeft:SetHeight(32)
  4766. editBoxLeft:SetWidth(32)
  4767. editBoxLeft:SetPoint("LEFT", -14, 0)
  4768. editBoxLeft:SetTexCoord(0, 0.125, 0, 1)
  4769. editBoxRight:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Right")
  4770. editBoxRight:SetHeight(32)
  4771. editBoxRight:SetWidth(32)
  4772. editBoxRight:SetPoint("RIGHT", 6, 0)
  4773. editBoxRight:SetTexCoord(0.875, 1, 0, 1)
  4774. editBoxMiddle:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Right")
  4775. editBoxMiddle:SetHeight(32)
  4776. editBoxMiddle:SetWidth(1)
  4777. editBoxMiddle:SetPoint("LEFT", editBoxLeft, "RIGHT")
  4778. editBoxMiddle:SetPoint("RIGHT", editBoxRight, "LEFT")
  4779. editBoxMiddle:SetTexCoord(0, 0.9375, 0, 1)
  4780. end
  4781. editBox:SetHeight(32)
  4782. editBox:SetWidth(250)
  4783. editBox:SetPoint("TOP", fontstring, "BOTTOM", 0, -4)
  4784. editBox:SetFontObject("GameFontHighlight")
  4785. editBox:SetTextInsets(0, 0, 0, 1)
  4786. editBox:SetFocus()
  4787. editBox:SetText(urlText)
  4788. editBox:HighlightText()
  4789. editBox:SetScript("OnTextChanged", function(self)
  4790. editBox:SetText(urlText)
  4791. editBox:HighlightText()
  4792. end)
  4793. fontstringFooter = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
  4794. fontstringFooter:SetWidth(410)
  4795. fontstringFooter:SetHeight(0)
  4796. fontstringFooter:SetPoint("TOP", editBox, "BOTTOM", 0, 0)
  4797. local button = CreateFrame("Button", nil, frame)
  4798. button:SetHeight(24)
  4799. button:SetWidth(75)
  4800. button:SetPoint("BOTTOM", 0, 13)
  4801. button:SetNormalFontObject("GameFontNormal")
  4802. button:SetHighlightFontObject("GameFontHighlight")
  4803. button:SetNormalTexture(button:CreateTexture(nil, nil, "UIPanelButtonUpTexture"))
  4804. button:SetPushedTexture(button:CreateTexture(nil, nil, "UIPanelButtonDownTexture"))
  4805. button:SetHighlightTexture(button:CreateTexture(nil, nil, "UIPanelButtonHighlightTexture"))
  4806. button:SetText(OKAY)
  4807. button:SetScript("OnClick", function(self)
  4808. frame:Hide()
  4809. end)
  4810.  
  4811. end
  4812.  
  4813. function DBM:ShowUpdateReminder(newVersion, newRevision, text, url)
  4814. urlText = url or DBM_CORE_UPDATEREMINDER_URL or "http://www.deadlybossmods.com"
  4815. if not frame then
  4816. createFrame()
  4817. else
  4818. editBox:SetText(urlText)
  4819. editBox:HighlightText()
  4820. end
  4821. frame:Show()
  4822. if newVersion then
  4823. fontstring:SetText(DBM_CORE_UPDATEREMINDER_HEADER:format(newVersion, newRevision))
  4824. fontstringFooter:SetText(DBM_CORE_UPDATEREMINDER_FOOTER)
  4825. elseif text then
  4826. fontstring:SetText(text)
  4827. fontstringFooter:SetText(DBM_CORE_UPDATEREMINDER_FOOTER_GENERIC)
  4828. end
  4829. end
  4830. end
  4831.  
  4832. --------------------
  4833. -- Notes Editor --
  4834. --------------------
  4835. do
  4836. local frame, fontstring, fontstringFooter, editBox, button3
  4837.  
  4838. local function createFrame()
  4839. frame = CreateFrame("Frame", "DBMNotesEditor", UIParent)
  4840. frame:SetFrameStrata("FULLSCREEN_DIALOG") -- yes, this isn't a fullscreen dialog, but I want it to be in front of other DIALOG frames (like DBM GUI which might open this frame...)
  4841. frame:SetWidth(430)
  4842. frame:SetHeight(140)
  4843. frame:SetPoint("TOP", 0, -230)
  4844. frame:SetBackdrop({
  4845. bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
  4846. edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", tile = true, tileSize = 32, edgeSize = 32,
  4847. insets = {left = 11, right = 12, top = 12, bottom = 11},
  4848. })
  4849. fontstring = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
  4850. fontstring:SetWidth(410)
  4851. fontstring:SetHeight(0)
  4852. fontstring:SetPoint("TOP", 0, -16)
  4853. editBox = CreateFrame("EditBox", nil, frame)
  4854. do
  4855. local editBoxLeft = editBox:CreateTexture(nil, "BACKGROUND")
  4856. local editBoxRight = editBox:CreateTexture(nil, "BACKGROUND")
  4857. local editBoxMiddle = editBox:CreateTexture(nil, "BACKGROUND")
  4858. editBoxLeft:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Left")
  4859. editBoxLeft:SetHeight(32)
  4860. editBoxLeft:SetWidth(32)
  4861. editBoxLeft:SetPoint("LEFT", -14, 0)
  4862. editBoxLeft:SetTexCoord(0, 0.125, 0, 1)
  4863. editBoxRight:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Right")
  4864. editBoxRight:SetHeight(32)
  4865. editBoxRight:SetWidth(32)
  4866. editBoxRight:SetPoint("RIGHT", 6, 0)
  4867. editBoxRight:SetTexCoord(0.875, 1, 0, 1)
  4868. editBoxMiddle:SetTexture("Interface\\ChatFrame\\UI-ChatInputBorder-Right")
  4869. editBoxMiddle:SetHeight(32)
  4870. editBoxMiddle:SetWidth(1)
  4871. editBoxMiddle:SetPoint("LEFT", editBoxLeft, "RIGHT")
  4872. editBoxMiddle:SetPoint("RIGHT", editBoxRight, "LEFT")
  4873. editBoxMiddle:SetTexCoord(0, 0.9375, 0, 1)
  4874. end
  4875. editBox:SetHeight(32)
  4876. editBox:SetWidth(250)
  4877. editBox:SetPoint("TOP", fontstring, "BOTTOM", 0, -4)
  4878. editBox:SetFontObject("GameFontHighlight")
  4879. editBox:SetTextInsets(0, 0, 0, 1)
  4880. editBox:SetFocus()
  4881. editBox:SetText("")
  4882. fontstringFooter = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
  4883. fontstringFooter:SetWidth(410)
  4884. fontstringFooter:SetHeight(0)
  4885. fontstringFooter:SetPoint("TOP", editBox, "BOTTOM", 0, 0)
  4886. local button = CreateFrame("Button", nil, frame)
  4887. button:SetHeight(24)
  4888. button:SetWidth(75)
  4889. button:SetPoint("BOTTOM", 80, 13)
  4890. button:SetNormalFontObject("GameFontNormal")
  4891. button:SetHighlightFontObject("GameFontHighlight")
  4892. button:SetNormalTexture(button:CreateTexture(nil, nil, "UIPanelButtonUpTexture"))
  4893. button:SetPushedTexture(button:CreateTexture(nil, nil, "UIPanelButtonDownTexture"))
  4894. button:SetHighlightTexture(button:CreateTexture(nil, nil, "UIPanelButtonHighlightTexture"))
  4895. button:SetText(OKAY)
  4896. button:SetScript("OnClick", function(self)
  4897. local mod = DBM.Noteframe.mod
  4898. local modvar = DBM.Noteframe.modvar
  4899. mod.Options[modvar .. "SWNote"] = editBox:GetText() or ""
  4900. DBM.Noteframe.mod = nil
  4901. DBM.Noteframe.modvar = nil
  4902. DBM.Noteframe.abilityName = nil
  4903. frame:Hide()
  4904. end)
  4905. local button2 = CreateFrame("Button", nil, frame)
  4906. button2:SetHeight(24)
  4907. button2:SetWidth(75)
  4908. button2:SetPoint("BOTTOM", 0, 13)
  4909. button2:SetNormalFontObject("GameFontNormal")
  4910. button2:SetHighlightFontObject("GameFontHighlight")
  4911. button2:SetNormalTexture(button2:CreateTexture(nil, nil, "UIPanelButtonUpTexture"))
  4912. button2:SetPushedTexture(button2:CreateTexture(nil, nil, "UIPanelButtonDownTexture"))
  4913. button2:SetHighlightTexture(button2:CreateTexture(nil, nil, "UIPanelButtonHighlightTexture"))
  4914. button2:SetText(CANCEL)
  4915. button2:SetScript("OnClick", function(self)
  4916. DBM.Noteframe.mod = nil
  4917. DBM.Noteframe.modvar = nil
  4918. DBM.Noteframe.abilityName = nil
  4919. frame:Hide()
  4920. end)
  4921. button3 = CreateFrame("Button", nil, frame)
  4922. button3:SetHeight(24)
  4923. button3:SetWidth(75)
  4924. button3:SetPoint("BOTTOM", -80, 13)
  4925. button3:SetNormalFontObject("GameFontNormal")
  4926. button3:SetHighlightFontObject("GameFontHighlight")
  4927. button3:SetNormalTexture(button3:CreateTexture(nil, nil, "UIPanelButtonUpTexture"))
  4928. button3:SetPushedTexture(button3:CreateTexture(nil, nil, "UIPanelButtonDownTexture"))
  4929. button3:SetHighlightTexture(button3:CreateTexture(nil, nil, "UIPanelButtonHighlightTexture"))
  4930. button3:SetText(SHARE_QUEST_ABBREV)
  4931. button3:SetScript("OnClick", function(self)
  4932. local modid = DBM.Noteframe.mod.id
  4933. local modvar = DBM.Noteframe.modvar
  4934. local abilityName = DBM.Noteframe.abilityName
  4935. local syncText = editBox:GetText() or ""
  4936. if syncText == "" then
  4937. DBM:AddMsg(DBM_CORE_NOTESHAREERRORBLANK)
  4938. elseif IsInGroup(LE_PARTY_CATEGORY_INSTANCE) and IsInInstance() and not C_Garrison:IsOnGarrisonMap() then--For BGs, LFR and LFG (we also check IsInInstance() so if you're in queue but fighting something outside like a world boss, it'll sync in "RAID" instead)
  4939. DBM:AddMsg(DBM_CORE_NOTESHAREERRORGROUPFINDER)
  4940. else
  4941. local msg = modid.."\t"..modvar.."\t"..syncText.."\t"..abilityName
  4942. if IsInRaid() then
  4943. SendAddonMessage("D4", "NS\t" .. msg, "RAID")
  4944. DBM:AddMsg(DBM_CORE_NOTESHARED)
  4945. elseif IsInGroup(LE_PARTY_CATEGORY_HOME) then
  4946. SendAddonMessage("D4", "NS\t" .. msg, "PARTY")
  4947. DBM:AddMsg(DBM_CORE_NOTESHARED)
  4948. else--Solo
  4949. DBM:AddMsg(DBM_CORE_NOTESHAREERRORSOLO)
  4950. end
  4951. end
  4952. end)
  4953. end
  4954.  
  4955. function DBM:ShowNoteEditor(mod, modvar, abilityName, syncText, sender)
  4956. if not frame then
  4957. createFrame()
  4958. self.Noteframe = frame
  4959. else
  4960. if frame:IsShown() and syncText then
  4961. self:AddMsg(DBM_CORE_NOTESHAREERRORALREADYOPEN)
  4962. return
  4963. end
  4964. end
  4965. frame:Show()
  4966. fontstringFooter:SetText(DBM_CORE_NOTEFOOTER)
  4967. self.Noteframe.mod = mod
  4968. self.Noteframe.modvar = modvar
  4969. self.Noteframe.abilityName = abilityName
  4970. if syncText then
  4971. button3:Hide()--Don't show share button in shared notes
  4972. fontstring:SetText(DBM_CORE_NOTESHAREDHEADER:format(sender, abilityName))
  4973. editBox:SetText(syncText)
  4974. else
  4975. button3:Show()
  4976. fontstring:SetText(DBM_CORE_NOTEHEADER:format(abilityName))
  4977. if type(mod.Options[modvar .. "SWNote"]) == "string" then
  4978. editBox:SetText(mod.Options[modvar .. "SWNote"])
  4979. else
  4980. editBox:SetText("")
  4981. end
  4982. end
  4983. end
  4984. end
  4985.  
  4986. ----------------------
  4987. -- Pull Detection --
  4988. ----------------------
  4989. do
  4990. local targetList = {}
  4991. local function buildTargetList()
  4992. local uId = (IsInRaid() and "raid") or "party"
  4993. for i = 0, GetNumGroupMembers() do
  4994. local id = (i == 0 and "target") or uId..i.."target"
  4995. local guid = UnitGUID(id)
  4996. if guid and DBM:IsCreatureGUID(guid) then
  4997. local cId = DBM:GetCIDFromGUID(guid)
  4998. targetList[cId] = id
  4999. end
  5000. end
  5001. end
  5002.  
  5003. local function clearTargetList()
  5004. twipe(targetList)
  5005. end
  5006.  
  5007. local function scanForCombat(mod, mob, delay)
  5008. if not checkEntry(inCombat, mob) then
  5009. buildTargetList()
  5010. if targetList[mob] then
  5011. if delay > 0 and UnitAffectingCombat(targetList[mob]) and not (UnitPlayerOrPetInRaid(targetList[mob]) or UnitPlayerOrPetInParty(targetList[mob])) then
  5012. DBM:StartCombat(mod, delay, "PLAYER_REGEN_DISABLED")
  5013. elseif (delay == 0) then
  5014. DBM:StartCombat(mod, 0, "PLAYER_REGEN_DISABLED_AND_MESSAGE")
  5015. end
  5016. end
  5017. clearTargetList()
  5018. end
  5019. end
  5020.  
  5021.  
  5022. local function checkForPull(mob, combatInfo)
  5023. healthCombatInitialized = false
  5024. --This just can't be avoided, tryig to save cpu by using C_TimerAfter broke this
  5025. --This needs the redundancy and ability to pass args.
  5026. DBM:Schedule(0.5, scanForCombat, combatInfo.mod, mob, 0.5)
  5027. DBM:Schedule(2, scanForCombat, combatInfo.mod, mob, 2)
  5028. C_TimerAfter(2.1, function()
  5029. healthCombatInitialized = true
  5030. end)
  5031. end
  5032.  
  5033. -- TODO: fix the duplicate code that was added for quick & dirty support of zone IDs
  5034.  
  5035. -- detects a boss pull based on combat state, this is required for pre-ICC bosses that do not fire INSTANCE_ENCOUNTER_ENGAGE_UNIT events on engage
  5036. function DBM:PLAYER_REGEN_DISABLED()
  5037. lastCombatStarted = GetTime()
  5038. if not combatInitialized then return end
  5039. if dbmIsEnabled and combatInfo[LastInstanceMapID] then
  5040. for i, v in ipairs(combatInfo[LastInstanceMapID]) do
  5041. if v.type:find("combat") and not v.noRegenDetection then
  5042. if v.multiMobPullDetection then
  5043. for _, mob in ipairs(v.multiMobPullDetection) do
  5044. if checkForPull(mob, v) then
  5045. break
  5046. end
  5047. end
  5048. else
  5049. checkForPull(v.mob, v)
  5050. end
  5051. end
  5052. end
  5053. end
  5054. if self.Options.AFKHealthWarning and not IsEncounterInProgress() and UnitIsAFK("player") and self:AntiSpam(5, "AFK") then--You are afk and losing health, some griever is trying to kill you while you are afk/tabbed out.
  5055. self:FlashClientIcon()
  5056. local voice = DBM.Options.ChosenVoicePack
  5057. local path = "Sound\\Creature\\CThun\\CThunYouWillDIe.ogg"
  5058. if voice ~= "None" then
  5059. path = "Interface\\AddOns\\DBM-VP"..voice.."\\checkhp.ogg"
  5060. end
  5061. self:PlaySoundFile(path)
  5062. if UnitHealthMax("player") ~= 0 then
  5063. local health = UnitHealth("player") / UnitHealthMax("player") * 100
  5064. self:AddMsg(DBM_CORE_AFK_WARNING:format(health))
  5065. end
  5066. end
  5067. end
  5068.  
  5069. local function isBossEngaged(cId)
  5070. -- note that this is designed to work with any number of bosses, but it might be sufficient to check the first 5 unit ids
  5071. -- TODO: check if the client supports more than 5 boss unit IDs...just because the default boss health frame is limited to 5 doesn't mean there can't be more
  5072. local i = 1
  5073. repeat
  5074. local bossUnitId = "boss"..i
  5075. local bossGUID = not UnitIsDead(bossUnitId) and UnitGUID(bossUnitId) -- check for UnitIsVisible maybe?
  5076. local bossCId = bossGUID and DBM:GetCIDFromGUID(bossGUID)
  5077. if bossCId and (type(cId) == "number" and cId == bossCId or type(cId) == "table" and checkEntry(cId, bossCId)) then
  5078. return true
  5079. end
  5080. i = i + 1
  5081. until not bossGUID
  5082. end
  5083.  
  5084. function DBM:INSTANCE_ENCOUNTER_ENGAGE_UNIT()
  5085. if timerRequestInProgress then return end--do not start ieeu combat if timer request is progressing. (not to break Timer Recovery stuff)
  5086. if dbmIsEnabled and combatInfo[LastInstanceMapID] then
  5087. for i, v in ipairs(combatInfo[LastInstanceMapID]) do
  5088. if v.type:find("combat") and isBossEngaged(v.multiMobPullDetection or v.mob) then
  5089. self:StartCombat(v.mod, 0, "IEEU")
  5090. end
  5091. end
  5092. end
  5093. end
  5094.  
  5095. function DBM:UNIT_TARGETABLE_CHANGED(uId)
  5096. if self.Options.DebugLevel > 2 or (Transcriptor and Transcriptor:IsLogging()) then
  5097. local active = UnitExists(uId) and "true" or "false"
  5098. self:Debug("UNIT_TARGETABLE_CHANGED event fired for "..UnitName(uId)..". Active: "..active)
  5099. end
  5100. end
  5101.  
  5102. function DBM:UNIT_SPELLCAST_SUCCEEDED(uId, spellName, _, spellGUID, spellId)
  5103. local correctSpellId = tonumber(select(5, strsplit("-", spellGUID)), 10)
  5104. self:Debug("UNIT_SPELLCAST_SUCCEEDED fired: "..UnitName(uId).."'s "..spellName.."("..correctSpellId..")", 3)
  5105. end
  5106.  
  5107. function DBM:ENCOUNTER_START(encounterID, name, difficulty, size)
  5108. self:Debug("ENCOUNTER_START event fired: "..encounterID.." "..name.." "..difficulty.." "..size)
  5109. if dbmIsEnabled and combatInfo[LastInstanceMapID] then
  5110. for i, v in ipairs(combatInfo[LastInstanceMapID]) do
  5111. if not v.noESDetection then
  5112. if v.multiEncounterPullDetection then
  5113. for _, eId in ipairs(v.multiEncounterPullDetection) do
  5114. if encounterID == eId then
  5115. self:StartCombat(v.mod, 0, "ENCOUNTER_START")
  5116. return
  5117. end
  5118. end
  5119. elseif encounterID == v.eId then
  5120. self:StartCombat(v.mod, 0, "ENCOUNTER_START")
  5121. return
  5122. end
  5123. end
  5124. end
  5125. end
  5126. end
  5127.  
  5128. function DBM:ENCOUNTER_END(encounterID, name, difficulty, size, success)
  5129. self:Debug("ENCOUNTER_END event fired: "..encounterID.." "..name.." "..difficulty.." "..size.." "..success)
  5130. for i = #inCombat, 1, -1 do
  5131. local v = inCombat[i]
  5132. if not v.combatInfo then return end
  5133. if v.noEEDetection then return end
  5134. if v.respawnTime and success == 0 and self.Options.ShowRespawn and not self.Options.DontShowBossTimers then--No special hacks needed for bad wrath ENCOUNTER_END. Only mods that define respawnTime have a timer, since variable per boss.
  5135. local name = string.split(",", name)
  5136. self.Bars:CreateBar(v.respawnTime, DBM_CORE_TIMER_RESPAWN:format(name), "Interface\\Icons\\Spell_Holy_BorrowedTime")
  5137. end
  5138. if v.multiEncounterPullDetection then
  5139. for _, eId in ipairs(v.multiEncounterPullDetection) do
  5140. if encounterID == eId then
  5141. self:EndCombat(v, success == 0)
  5142. sendSync("EE", encounterID.."\t"..success.."\t"..v.id.."\t"..(v.revision or 0))
  5143. return
  5144. end
  5145. end
  5146. elseif encounterID == v.combatInfo.eId then
  5147. self:EndCombat(v, success == 0)
  5148. sendSync("EE", encounterID.."\t"..success.."\t"..v.id.."\t"..(v.revision or 0))
  5149. return
  5150. end
  5151. end
  5152. end
  5153.  
  5154. function DBM:BOSS_KILL(encounterID, name)
  5155. self:Debug("BOSS_KILL event fired: "..encounterID.." "..name)
  5156. for i = #inCombat, 1, -1 do
  5157. local v = inCombat[i]
  5158. if not v.combatInfo then return end
  5159. if v.multiEncounterPullDetection then
  5160. for _, eId in ipairs(v.multiEncounterPullDetection) do
  5161. if encounterID == eId then
  5162. self:EndCombat(v)
  5163. sendSync("EE", encounterID.."\t1\t"..v.id.."\t"..(v.revision or 0))
  5164. return
  5165. end
  5166. end
  5167. elseif encounterID == v.combatInfo.eId then
  5168. self:EndCombat(v)
  5169. sendSync("EE", encounterID.."\t1\t"..v.id.."\t"..(v.revision or 0))
  5170. return
  5171. end
  5172. end
  5173. end
  5174.  
  5175. local function checkExpressionList(exp, str)
  5176. for i, v in ipairs(exp) do
  5177. if str:match(v) then
  5178. return true
  5179. end
  5180. end
  5181. return false
  5182. end
  5183.  
  5184. -- called for all mob chat events
  5185. local function onMonsterMessage(self, type, msg)
  5186. -- pull detection
  5187. if dbmIsEnabled and combatInfo[LastInstanceMapID] then
  5188. for i, v in ipairs(combatInfo[LastInstanceMapID]) do
  5189. if v.type == type and checkEntry(v.msgs, msg) or v.type == type .. "_regex" and checkExpressionList(v.msgs, msg) then
  5190. self:StartCombat(v.mod, 0, "MONSTER_MESSAGE")
  5191. elseif v.type == "combat_" .. type .. "find" and findEntry(v.msgs, msg) or v.type == "combat_" .. type and checkEntry(v.msgs, msg) then
  5192. if IsInInstance() then--Indoor boss that uses both combat and yell for combat, so in other words (such as hodir), don't require "target" of boss for yell like scanForCombat does for World Bosses
  5193. self:StartCombat(v.mod, 0, "MONSTER_MESSAGE")
  5194. else--World Boss
  5195. scanForCombat(v.mod, v.mob, 0)
  5196. if v.mod.readyCheckQuestId and (self.Options.WorldBossNearAlert or v.mod.Options.ReadyCheck) and not IsQuestFlaggedCompleted(v.mod.readyCheckQuestId) then
  5197. self:FlashClientIcon()
  5198. self:PlaySoundFile("Sound\\interface\\levelup2.ogg", true)
  5199. end
  5200. end
  5201. end
  5202. end
  5203. end
  5204. -- kill detection (wipe detection would also be nice to have)
  5205. -- todo: add sync
  5206. for i = #inCombat, 1, -1 do
  5207. local v = inCombat[i]
  5208. if not v.combatInfo then return end
  5209. if v.combatInfo.killType == type and v.combatInfo.killMsgs[msg] then
  5210. self:EndCombat(v)
  5211. end
  5212. end
  5213. end
  5214.  
  5215. function DBM:CHAT_MSG_MONSTER_YELL(msg, npc, _, _, target)
  5216. if IsEncounterInProgress() or (IsInInstance() and InCombatLockdown()) then--Too many 5 mans/old raids don't properly return encounterinprogress
  5217. local targetName = target or "nil"
  5218. self:Debug("CHAT_MSG_MONSTER_YELL from "..npc.." while looking at "..targetName, 2)
  5219. end
  5220. return onMonsterMessage(self, "yell", msg)
  5221. end
  5222.  
  5223. function DBM:CHAT_MSG_MONSTER_EMOTE(msg)
  5224. return onMonsterMessage(self, "emote", msg)
  5225. end
  5226.  
  5227. function DBM:CHAT_MSG_RAID_BOSS_EMOTE(msg, ...)
  5228. onMonsterMessage(self, "emote", msg)
  5229. return self:FilterRaidBossEmote(msg, ...)
  5230. end
  5231.  
  5232. function DBM:RAID_BOSS_EMOTE(msg, ...)--This is a mirror of above prototype only it has less args, both still exist for some reason.
  5233. onMonsterMessage(self, "emote", msg)
  5234. return self:FilterRaidBossEmote(msg, ...)
  5235. end
  5236.  
  5237. function DBM:RAID_BOSS_WHISPER(msg)
  5238. --Make it easier for devs to detect whispers they are unable to see
  5239. --TINTERFACE\\ICONS\\ability_socererking_arcanewrath.blp:20|t You have been branded by |cFFF00000|Hspell:156238|h[Arcane Wrath]|h|r!"
  5240. if IsInGroup() then
  5241. SendAddonMessage("Transcriptor", msg, IsInGroup(2) and "INSTANCE_CHAT" or IsInRaid() and "RAID" or "PARTY")--Send any emote to transcriptor, even if no spellid
  5242. end
  5243. end
  5244.  
  5245. function DBM:CHAT_MSG_MONSTER_SAY(msg)
  5246. return onMonsterMessage(self, "say", msg)
  5247. end
  5248. end
  5249.  
  5250. ---------------------------
  5251. -- Kill/Wipe Detection --
  5252. ---------------------------
  5253.  
  5254. function checkWipe(self, confirm)
  5255. if #inCombat > 0 then
  5256. if not savedDifficulty or not difficultyText or not difficultyIndex then--prevent error if savedDifficulty or difficultyText is nil
  5257. savedDifficulty, difficultyText, difficultyIndex = self:GetCurrentInstanceDifficulty()
  5258. end
  5259. --hack for no iEEU information is provided.
  5260. if not bossuIdFound then
  5261. for i = 1, 5 do
  5262. if UnitExists("boss"..i) then
  5263. bossuIdFound = true
  5264. break
  5265. end
  5266. end
  5267. end
  5268. local wipe = 1 -- 0: no wipe, 1: normal wipe, 2: wipe by UnitExists check.
  5269. if IsInScenarioGroup() or (difficultyIndex == 11) or (difficultyIndex == 12) then -- Scenario mod uses special combat start and must be enabled before sceniro end. So do not wipe.
  5270. wipe = 0
  5271. elseif IsEncounterInProgress() then -- Encounter Progress marked, you obviously in combat with boss. So do not Wipe
  5272. wipe = 0
  5273. elseif savedDifficulty == "worldboss" and UnitIsDeadOrGhost("player") then -- On dead or ghost, unit combat status detection would be fail. If you ghost in instance, that means wipe. But in worldboss, ghost means not wipe. So do not wipe.
  5274. wipe = 0
  5275. elseif bossuIdFound and LastInstanceType == "raid" then -- Combat started by IEEU and no boss exist and no EncounterProgress marked, that means wipe
  5276. wipe = 2
  5277. for i = 1, 5 do
  5278. if UnitExists("boss"..i) then
  5279. wipe = 0 -- Boss found. No wipe
  5280. break
  5281. end
  5282. end
  5283. else -- Unit combat status detection. No combat unit in your party and no EncounterProgress marked, that means wipe
  5284. wipe = 1
  5285. local uId = (IsInRaid() and "raid") or "party"
  5286. for i = 0, GetNumGroupMembers() do
  5287. local id = (i == 0 and "player") or uId..i
  5288. if UnitAffectingCombat(id) and not UnitIsDeadOrGhost(id) then
  5289. wipe = 0 -- Someone still in combat. No wipe
  5290. break
  5291. end
  5292. end
  5293. end
  5294. if wipe == 0 then
  5295. self:Schedule(3, checkWipe, self)
  5296. elseif confirm then
  5297. for i = #inCombat, 1, -1 do
  5298. local reason = (wipe == 1 and "No combat unit found in your party." or "No boss found : "..(wipe or "nil"))
  5299. self:Debug("You wiped. Reason : "..reason)
  5300. self:EndCombat(inCombat[i], true)
  5301. end
  5302. else
  5303. local maxDelayTime = (savedDifficulty == "worldboss" and 15) or 5 --wait 10s more on worldboss do actual wipe.
  5304. for i, v in ipairs(inCombat) do
  5305. maxDelayTime = v.combatInfo and v.combatInfo.wipeTimer and v.combatInfo.wipeTimer > maxDelayTime and v.combatInfo.wipeTimer or maxDelayTime
  5306. end
  5307. self:Schedule(maxDelayTime, checkWipe, self, true)
  5308. end
  5309. end
  5310. end
  5311.  
  5312. function checkBossHealth(self)
  5313. if #inCombat > 0 then
  5314. for i, v in ipairs(inCombat) do
  5315. if not v.multiMobPullDetection or v.mainBoss then
  5316. self:GetBossHP(v.mainBoss or v.combatInfo.mob or -1)
  5317. else
  5318. for _, mob in ipairs(v.multiMobPullDetection) do
  5319. self:GetBossHP(mob)
  5320. end
  5321. end
  5322. end
  5323. self:Schedule(1, checkBossHealth, self)
  5324. end
  5325. end
  5326.  
  5327. function checkCustomBossHealth(self, mod)
  5328. mod:CustomHealthUpdate()
  5329. self:Schedule(1, checkCustomBossHealth, self, mod)
  5330. end
  5331.  
  5332. function loopCRTimer(self, timer, mod)
  5333. local crTimer = mod:NewTimer(timer, DBM_COMBAT_RES_TIMER_TEXT, "Interface\\Icons\\Spell_Nature_Reincarnation", nil, false)
  5334. crTimer:Start()
  5335. self:Schedule(timer, loopCRTimer, self, timer, mod)
  5336. end
  5337.  
  5338. do
  5339. local statVarTable = {
  5340. --6.0
  5341. ["event5"] = "normal",
  5342. ["event20"] = "lfr25",
  5343. ["event40"] = "lfr25",
  5344. ["normal5"] = "normal",
  5345. ["heroic5"] = "heroic",
  5346. ["challenge5"] = "challenge",
  5347. ["lfr"] = "lfr25",
  5348. ["normal"] = "normal",
  5349. ["heroic"] = "heroic",
  5350. ["mythic"] = "mythic",
  5351. ["worldboss"] = "normal",
  5352. ["timewalker"] = "timewalker",
  5353. --Legacy
  5354. ["lfr25"] = "lfr25",
  5355. ["normal10"] = "normal",
  5356. ["normal25"] = "normal25",
  5357. ["heroic10"] = "heroic",
  5358. ["heroic25"] = "heroic25",
  5359. }
  5360.  
  5361. function DBM:StartCombat(mod, delay, event, synced, syncedStartHp)
  5362. cSyncSender = {}
  5363. cSyncReceived = 0
  5364. if not checkEntry(inCombat, mod) then
  5365. if not mod.Options.Enabled then return end
  5366. if not mod.combatInfo then return end
  5367. if mod.combatInfo.noCombatInVehicle and UnitInVehicle("player") then -- HACK
  5368. return
  5369. end
  5370. --HACK: makes sure that we don't detect a false pull if the event fires again when the boss dies...
  5371. if mod.lastKillTime and GetTime() - mod.lastKillTime < (mod.reCombatTime or 120) and event ~= "LOADING_SCREEN_DISABLED" then return end
  5372. if mod.lastWipeTime and GetTime() - mod.lastWipeTime < (event == "ENCOUNTER_START" and 3 or mod.reCombatTime2 or 20) and event ~= "LOADING_SCREEN_DISABLED" then return end
  5373. if event then
  5374. self:Debug("StartCombat called by : "..event..". LastInstanceMapID is "..LastInstanceMapID)
  5375. if event ~= "ENCOUNTER_START" then
  5376. self:Debug("This event is started by"..event..". Review ENCOUNTER_START event to ensure if this is still needed", 2)
  5377. end
  5378. else
  5379. self:Debug("StartCombat called by individual mod or unknown reason. LastInstanceMapID is "..LastInstanceMapID)
  5380. end
  5381. --check completed. starting combat
  5382. tinsert(inCombat, mod)
  5383. if mod.inCombatOnlyEvents and not mod.inCombatOnlyEventsRegistered then
  5384. mod.inCombatOnlyEventsRegistered = 1
  5385. mod:RegisterEvents(unpack(mod.inCombatOnlyEvents))
  5386. end
  5387. --Fix for "attempt to perform arithmetic on field 'stats' (a nil value)"
  5388. if not mod.stats then
  5389. self:AddMsg(DBM_CORE_BAD_LOAD)--Warn user that they should reload ui soon as they leave combat to get their mod to load correctly as soon as possible
  5390. mod.ignoreBestkill = true--Force this to true so we don't check any more occurances of "stats"
  5391. elseif event == "TIMER_RECOVERY" then --add a lag time to delay when TIMER_RECOVERY
  5392. delay = delay + select(4, GetNetStats()) / 1000
  5393. end
  5394. --set mod default info
  5395. savedDifficulty, difficultyText, difficultyIndex, LastGroupSize = self:GetCurrentInstanceDifficulty()
  5396. local name = mod.combatInfo.name
  5397. local modId = mod.id
  5398. if C_Scenario.IsInScenario() and (mod.addon.type == "SCENARIO") then
  5399. mod.inScenario = true
  5400. end
  5401. mod.inCombat = true
  5402. mod.blockSyncs = nil
  5403. mod.combatInfo.pull = GetTime() - (delay or 0)
  5404. bossuIdFound = (event or "") == "IEEU"
  5405. if mod.minCombatTime then
  5406. self:Schedule(mmax((mod.minCombatTime - delay), 3), checkWipe, self)
  5407. else
  5408. self:Schedule(3, checkWipe, self)
  5409. end
  5410. --get boss hp at pull
  5411. if syncedStartHp and syncedStartHp < 1 then
  5412. syncedStartHp = syncedStartHp * 100
  5413. end
  5414. local startHp = syncedStartHp or mod:GetBossHP(mod.mainBoss or mod.combatInfo.mob or -1) or 100
  5415. --check boss engaged first?
  5416. if (savedDifficulty == "worldboss" and startHp < 98) or (event == "UNIT_HEALTH" and delay > 4) or event == "TIMER_RECOVERY" then--Boss was not full health when engaged, disable combat start timer and kill record
  5417. mod.ignoreBestkill = true
  5418. elseif mod.inScenario then
  5419. local _, currentStage, numStages = C_Scenario.GetInfo()
  5420. if currentStage > 1 and numStages > 1 then
  5421. mod.ignoreBestkill = true
  5422. end
  5423. else--Reset ignoreBestkill after wipe
  5424. mod.ignoreBestkill = false
  5425. --It was a clean pull, so cancel any RequestTimers which might fire after boss was pulled if boss was pulled right after mod load
  5426. --Only want timer recovery on in progress bosses, not clean pulls
  5427. if startHp > 98 and (savedDifficulty == "worldboss" or event == "IEEU") or event == "ENCOUNTER_START" then
  5428. self:Unschedule(self.RequestTimers)
  5429. end
  5430. end
  5431. --show health frame
  5432. if not mod.inScenario then
  5433. if self.Options.HideTooltips then
  5434. --Better or cleaner way?
  5435. tooltipsHidden = true
  5436. GameTooltip.Temphide = function() GameTooltip:Hide() end; GameTooltip:SetScript("OnShow", GameTooltip.Temphide)
  5437. end
  5438. if self.Options.DisableSFX and GetCVar("Sound_EnableSFX") == "1" then
  5439. self.Options.sfxDisabled = true
  5440. SetCVar("Sound_EnableSFX", 0)
  5441. end
  5442. if (self.Options.AlwaysShowHealthFrame or mod.Options.HealthFrame) then
  5443. if not self.BossHealth:IsShown() then
  5444. self.BossHealth:Show(mod.localization.general.name)
  5445. else-- pulled other boss during combat, set header text.
  5446. self.BossHealth:SetHeaderText(BOSS)
  5447. end
  5448. if mod.bossHealthInfo then
  5449. if mod.bossHealthInfo[2] and type(mod.bossHealthInfo[2]) == "number" then
  5450. for i = 1, #mod.bossHealthInfo do--boss name gets from UnitName
  5451. self.BossHealth:AddBoss(mod.bossHealthInfo[i])
  5452. end
  5453. else
  5454. for i = 1, #mod.bossHealthInfo, 2 do
  5455. self.BossHealth:AddBoss(mod.bossHealthInfo[i], mod.bossHealthInfo[i + 1])
  5456. end
  5457. end
  5458. else
  5459. self.BossHealth:AddBoss(mod.combatInfo.mob, mod.localization.general.name)
  5460. end
  5461. --boss health info update scheduler if boss health frame is not enabled.
  5462. elseif not mod.CustomHealthUpdate then
  5463. self:Schedule(1, checkBossHealth, self)
  5464. end
  5465. --this function must be scheduled if boss health frame enabled.
  5466. if mod.CustomHealthUpdate then
  5467. self:Schedule(1, checkCustomBossHealth, self, mod)
  5468. end
  5469. end
  5470. --process global options
  5471. self:HideBlizzardEvents(1)
  5472. self:StartLogging(0, nil)
  5473. if self.Options.HideObjectivesFrame and mod.addon.type ~= "SCENARIO" and GetNumTrackedAchievements() == 0 and difficultyIndex ~= 8 then
  5474. if ObjectiveTrackerFrame:IsVisible() then
  5475. ObjectiveTrackerFrame:Hide()
  5476. watchFrameRestore = true
  5477. end
  5478. end
  5479. fireEvent("pull", mod, delay, synced, startHp)
  5480. self:FlashClientIcon()
  5481. --serperate timer recovery and normal start.
  5482. if event ~= "TIMER_RECOVERY" then
  5483. --add pull count
  5484. if mod.stats then
  5485. if not mod.stats[statVarTable[savedDifficulty].."Pulls"] then mod.stats[statVarTable[savedDifficulty].."Pulls"] = 0 end
  5486. mod.stats[statVarTable[savedDifficulty].."Pulls"] = mod.stats[statVarTable[savedDifficulty].."Pulls"] + 1
  5487. end
  5488. --show speed timer
  5489. if self.Options.AlwaysShowSpeedKillTimer and mod.stats and not mod.ignoreBestkill then
  5490. --TODO, add code here to only pull best kull for CURRENT mythic+ rank
  5491. local bestTime = mod.stats[statVarTable[savedDifficulty].."BestTime"]
  5492. if bestTime and bestTime > 0 then
  5493. local speedTimer = mod:NewTimer(bestTime, DBM_SPEED_KILL_TIMER_TEXT, "Interface\\Icons\\Spell_Holy_BorrowedTime", nil, false)
  5494. speedTimer:Start()
  5495. end
  5496. end
  5497. --Combat Rez timer, if not a world boss or 5 man dungeon.
  5498. if self.Options.CRT_Enabled and difficultyIndex ~= 0 and difficultyIndex ~= 1 and difficultyIndex ~= 2 and difficultyIndex ~= 19 and difficultyIndex ~= 24 and not self.Options.DontShowBossTimers then
  5499. local charges, maxCharges, started, duration = GetSpellCharges(20484)
  5500. if charges then
  5501. local time = duration - (GetTime() - started)
  5502. loopCRTimer(self, time, mod)
  5503. self:Debug("CRT started by charges", 2)
  5504. elseif difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 17 then--Flexible difficulties
  5505. local time = 90/LastGroupSize
  5506. time = time * 60
  5507. loopCRTimer(self, time, mod)
  5508. self:Debug("CRT started by Flexible code", 2)
  5509. else--Fixed difficulties (LastGroupSize cannot be trusted, this INCLUDES mythic. If you underman mythic then it is NOT 90/20)
  5510. local realGroupSize = self:GetNumRealPlayersInZone()
  5511. if realGroupSize > 1 then
  5512. local time = 90/realGroupSize
  5513. time = time * 60
  5514. loopCRTimer(self, time, mod)
  5515. self:Debug("CRT started by iffy fixed size code", 2)
  5516. end
  5517. end
  5518. end
  5519. --update boss left
  5520. if mod.numBoss then
  5521. mod.vb.bossLeft = mod.numBoss
  5522. end
  5523. --elect icon person
  5524. if mod.findFastestComputer and not self.Options.DontSetIcons then
  5525. if self:GetRaidRank() > 0 then
  5526. for i = 1, #mod.findFastestComputer do
  5527. local option = mod.findFastestComputer[i]
  5528. if mod.Options[option] then
  5529. sendSync("IS", UnitGUID("player").."\t"..DBM.Revision.."\t"..option)
  5530. end
  5531. end
  5532. elseif not IsInGroup() then
  5533. for i = 1, #mod.findFastestComputer do
  5534. local option = mod.findFastestComputer[i]
  5535. if mod.Options[option] then
  5536. canSetIcons[option] = true
  5537. end
  5538. end
  5539. end
  5540. end
  5541. if testBuild and difficultyIndex == 16 then
  5542. self:AddMsg(DBM_CORE_NEED_LOGS)
  5543. end
  5544. --call OnCombatStart
  5545. if mod.OnCombatStart and not mod.ignoreBestkill then
  5546. mod:OnCombatStart(delay or 0, event == "PLAYER_REGEN_DISABLED_AND_MESSAGE" or event == "SPELL_CAST_SUCCESS")
  5547. end
  5548. --send "C" sync
  5549. if not synced then
  5550. sendSync("C", (delay or 0).."\t"..modId.."\t"..(mod.revision or 0).."\t"..startHp.."\t"..DBM.Revision.."\t"..(mod.hotfixNoticeRev or 0))
  5551. end
  5552. if self.Options.DisableStatusWhisper and UnitIsGroupLeader("player") and (difficultyIndex == 8 or difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16 or difficultyIndex == 23) then
  5553. sendSync("DSW")
  5554. end
  5555. --show bigbrother check
  5556. if self.Options.ShowBigBrotherOnCombatStart and BigBrother and type(BigBrother.ConsumableCheck) == "function" then
  5557. if self.Options.BigBrotherAnnounceToRaid then
  5558. BigBrother:ConsumableCheck("RAID")
  5559. else
  5560. BigBrother:ConsumableCheck("SELF")
  5561. end
  5562. end
  5563. --show enage message
  5564. if self.Options.ShowEngageMessage then
  5565. if mod.ignoreBestkill and (savedDifficulty == "worldboss") then--Should only be true on in progress field bosses, not in progress raid bosses we did timer recovery on.
  5566. self:AddMsg(DBM_CORE_COMBAT_STARTED_IN_PROGRESS:format(difficultyText..name))
  5567. elseif mod.ignoreBestkill and mod.inScenario then
  5568. self:AddMsg(DBM_CORE_SCENARIO_STARTED_IN_PROGRESS:format(difficultyText..name))
  5569. else
  5570. if mod.addon.type == "SCENARIO" then
  5571. self:AddMsg(DBM_CORE_SCENARIO_STARTED:format(difficultyText..name))
  5572. else
  5573. self:AddMsg(DBM_CORE_COMBAT_STARTED:format(difficultyText..name))
  5574. if difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16 then--Only send relevant content, not guild beating down lich king or LFR.
  5575. if InGuildParty() then--Guild Group
  5576. SendAddonMessage("D4", "GCB\t"..modId.."\t2\t"..difficultyIndex, "GUILD")
  5577. end
  5578. end
  5579. end
  5580. end
  5581. end
  5582. --stop pull count
  5583. local dummyMod = self:GetModByName("PullTimerCountdownDummy")
  5584. if dummyMod then--stop pull timer, warning, countdowns
  5585. dummyMod.countdown:Cancel()
  5586. dummyMod.text:Cancel()
  5587. self.Bars:CancelBar(DBM_CORE_TIMER_PULL)
  5588. TimerTracker_OnEvent(TimerTracker, "PLAYER_ENTERING_WORLD")
  5589. end
  5590. else
  5591. self:AddMsg(DBM_CORE_COMBAT_STATE_RECOVERED:format(difficultyText..name, strFromTime(delay)))
  5592. end
  5593. if savedDifficulty == "worldboss" and not mod.noWBEsync then
  5594. if lastBossEngage[modId..playerRealm] and (GetTime() - lastBossEngage[modId..playerRealm] < 30) then return end--Someone else synced in last 10 seconds so don't send out another sync to avoid needless sync spam.
  5595. lastBossEngage[modId..playerRealm] = GetTime()--Update last engage time, that way we ignore our own sync
  5596. if IsInGuild() then
  5597. SendAddonMessage("D4", "WBE\t"..modId.."\t"..playerRealm.."\t"..startHp.."\t8\t"..name, "GUILD")--Even guild syncs send realm so we can keep antispam the same across realid as well.
  5598. end
  5599. local _, numBNetOnline = BNGetNumFriends()
  5600. for i = 1, numBNetOnline do
  5601. local sameRealm = false
  5602. local presenceID, _, _, _, _, _, client, isOnline = BNGetFriendInfo(i)
  5603. if isOnline and client == BNET_CLIENT_WOW then
  5604. local _, _, _, userRealm = BNGetGameAccountInfo(presenceID)
  5605. if connectedServers then
  5606. for i = 1, #connectedServers do
  5607. if userRealm == connectedServers[i] then
  5608. sameRealm = true
  5609. break
  5610. end
  5611. end
  5612. else
  5613. if userRealm == playerRealm then
  5614. sameRealm = true
  5615. end
  5616. end
  5617. if sameRealm then
  5618. BNSendGameData(presenceID, "D4", "WBE\t"..modId.."\t"..userRealm.."\t"..startHp.."\t8\t"..name)--Just send users realm for pull, so we can eliminate connectedServers checks on sync handler
  5619. end
  5620. end
  5621. end
  5622. end
  5623. end
  5624. end
  5625.  
  5626. function DBM:UNIT_HEALTH(uId)
  5627. local cId = self:GetCIDFromGUID(UnitGUID(uId))
  5628. local health
  5629. if UnitHealthMax(uId) ~= 0 then
  5630. health = UnitHealth(uId) / UnitHealthMax(uId) * 100
  5631. end
  5632. if not health or health < 2 then return end -- no worthy of combat start if health is below 2%
  5633. if dbmIsEnabled and InCombatLockdown() then
  5634. if cId ~= 0 and not bossHealth[cId] and bossIds[cId] and UnitAffectingCombat(uId) and not (UnitPlayerOrPetInRaid(uId) or UnitPlayerOrPetInParty(uId)) and healthCombatInitialized then -- StartCombat by UNIT_HEALTH.
  5635. if combatInfo[LastInstanceMapID] then
  5636. for i, v in ipairs(combatInfo[LastInstanceMapID]) do
  5637. if v.mod.Options.Enabled and not v.mod.disableHealthCombat and v.type:find("combat") and (v.multiMobPullDetection and checkEntry(v.multiMobPullDetection, cId) or v.mob == cId) then
  5638. -- Delay set, > 97% = 0.5 (consider as normal pulling), max dealy limited to 20s.
  5639. self:StartCombat(v.mod, health > 97 and 0.5 or mmin(GetTime() - lastCombatStarted, 20), "UNIT_HEALTH", nil, health)
  5640. end
  5641. end
  5642. end
  5643. end
  5644. if self.Options.AFKHealthWarning and UnitIsUnit(uId, "player") and (health < 85) and not IsEncounterInProgress() and UnitIsAFK("player") and self:AntiSpam(5, "AFK") then--You are afk and losing health, some griever is trying to kill you while you are afk/tabbed out.
  5645. self:PlaySoundFile("Sound\\Creature\\CThun\\CThunYouWillDIe.ogg")--So fire an alert sound to save yourself from this person's behavior.
  5646. self:AddMsg(DBM_CORE_AFK_WARNING:format(health))
  5647. end
  5648. end
  5649. end
  5650.  
  5651. function DBM:EndCombat(mod, wipe)
  5652. if removeEntry(inCombat, mod) then
  5653. local scenario = mod.addon.type == "SCENARIO"
  5654. if mod.inCombatOnlyEvents and mod.inCombatOnlyEventsRegistered then
  5655. -- unregister all events except for SPELL_AURA_REMOVED events (might still be needed to remove icons etc...)
  5656. mod:UnregisterInCombatEvents()
  5657. self:Schedule(2, mod.UnregisterInCombatEvents, mod, true) -- 2 seconds should be enough for all auras to fade
  5658. self:Schedule(2.1, mod.Stop, mod) -- Remove accident started timers.
  5659. mod.inCombatOnlyEventsRegistered = nil
  5660. end
  5661. if mod.updateInterval then
  5662. mod:UnregisterOnUpdateHandler()
  5663. end
  5664. mod:Stop()
  5665. if enableIcons and not self.Options.DontSetIcons and not self.Options.DontRestoreIcons then
  5666. -- restore saved previous icon
  5667. for uId, icon in pairs(mod.iconRestore) do
  5668. SetRaidTarget(uId, icon)
  5669. end
  5670. twipe(mod.iconRestore)
  5671. end
  5672. mod.inCombat = false
  5673. mod.blockSyncs = true
  5674. if mod.combatInfo.killMobs then
  5675. for i, v in pairs(mod.combatInfo.killMobs) do
  5676. mod.combatInfo.killMobs[i] = true
  5677. end
  5678. end
  5679. if not savedDifficulty or not difficultyText or not difficultyIndex then--prevent error if savedDifficulty or difficultyText is nil
  5680. savedDifficulty, difficultyText, difficultyIndex = DBM:GetCurrentInstanceDifficulty()
  5681. end
  5682. if not mod.stats then--This will be nil if the mod for this intance failed to load fully because "script ran too long" (it tried to load in combat and failed)
  5683. self:AddMsg(DBM_CORE_BAD_LOAD)--Warn user that they should reload ui soon as they leave combat to get their mod to load correctly as soon as possible
  5684. return--Don't run any further, stats are nil on a bad load so rest of this code will also error out.
  5685. end
  5686. local name = mod.combatInfo.name
  5687. local modId = mod.id
  5688. if wipe then
  5689. mod.lastWipeTime = GetTime()
  5690. --Fix for "attempt to perform arithmetic on field 'pull' (a nil value)" (which was actually caused by stats being nil, so we never did getTime on pull, fixing one SHOULD fix the other)
  5691. local thisTime = GetTime() - mod.combatInfo.pull
  5692. local hp = mod.highesthealth and mod:GetHighestBossHealth() or mod:GetLowestBossHealth()
  5693. local wipeHP = mod.CustomHealthUpdate and mod:CustomHealthUpdate() or hp and ("%d%%"):format(hp) or DBM_CORE_UNKNOWN
  5694. if mod.vb.phase then
  5695. wipeHP = wipeHP.." ("..SCENARIO_STAGE:format(mod.vb.phase)..")"
  5696. end
  5697. if mod.numBoss then
  5698. local bossesKilled = mod.numBoss - mod.vb.bossLeft
  5699. wipeHP = wipeHP.." ("..BOSSES_KILLED:format(bossesKilled, mod.numBoss)..")"
  5700. end
  5701. local totalPulls = mod.stats[statVarTable[savedDifficulty].."Pulls"]
  5702. local totalKills = mod.stats[statVarTable[savedDifficulty].."Kills"]
  5703. if thisTime < 30 then -- Normally, one attempt will last at least 30 sec.
  5704. totalPulls = totalPulls - 1
  5705. mod.stats[statVarTable[savedDifficulty].."Pulls"] = totalPulls
  5706. if self.Options.ShowDefeatMessage then
  5707. if scenario then
  5708. self:AddMsg(DBM_CORE_SCENARIO_ENDED_AT:format(difficultyText..name, strFromTime(thisTime)))
  5709. else
  5710. self:AddMsg(DBM_CORE_COMBAT_ENDED_AT:format(difficultyText..name, wipeHP, strFromTime(thisTime)))
  5711. --No reason to GCE it here, so omited on purpose.
  5712. end
  5713. end
  5714. else
  5715. if self.Options.ShowDefeatMessage then
  5716. if scenario then
  5717. self:AddMsg(DBM_CORE_SCENARIO_ENDED_AT_LONG:format(difficultyText..name, strFromTime(thisTime), totalPulls - totalKills))
  5718. else
  5719. self:AddMsg(DBM_CORE_COMBAT_ENDED_AT_LONG:format(difficultyText..name, wipeHP, strFromTime(thisTime), totalPulls - totalKills))
  5720. if (difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16) and InGuildParty() then--Maybe add mythic plus/CM?
  5721. SendAddonMessage("D4", "GCE\t"..modId.."\t3\t1\t"..strFromTime(thisTime).."\t"..difficultyIndex.."\t"..wipeHP, "GUILD")
  5722. end
  5723. end
  5724. end
  5725. end
  5726. if showConstantReminder == 2 and IsInGroup() and savedDifficulty ~= "lfr" and savedDifficulty ~= "lfr25" then
  5727. showConstantReminder = 1
  5728. --Show message any time this is a mod that has a newer hotfix revision
  5729. --These people need to know the wipe could very well be their fault.
  5730. self:AddMsg(DBM_CORE_OUT_OF_DATE_NAG)
  5731. end
  5732. local msg
  5733. for k, v in pairs(autoRespondSpam) do
  5734. if self.Options.WhisperStats then
  5735. if scenario then
  5736. msg = msg or chatPrefixShort..DBM_CORE_WHISPER_SCENARIO_END_WIPE_STATS:format(playerName, difficultyText..(name or ""), totalPulls - totalKills)
  5737. else
  5738. msg = msg or chatPrefixShort..DBM_CORE_WHISPER_COMBAT_END_WIPE_STATS_AT:format(playerName, difficultyText..(name or ""), wipeHP, totalPulls - totalKills)
  5739. end
  5740. else
  5741. if scenario then
  5742. msg = msg or chatPrefixShort..DBM_CORE_WHISPER_SCENARIO_END_WIPE:format(playerName, difficultyText..(name or ""))
  5743. else
  5744. msg = msg or chatPrefixShort..DBM_CORE_WHISPER_COMBAT_END_WIPE_AT:format(playerName, difficultyText..(name or ""), wipeHP)
  5745. end
  5746. end
  5747. sendWhisper(k, msg)
  5748. end
  5749. fireEvent("wipe", mod)
  5750. else
  5751. mod.lastKillTime = GetTime()
  5752. local thisTime = GetTime() - (mod.combatInfo.pull or 0)
  5753. local lastTime = mod.stats[statVarTable[savedDifficulty].."LastTime"]
  5754. local bestTime = mod.stats[statVarTable[savedDifficulty].."BestTime"]
  5755. if not mod.stats[statVarTable[savedDifficulty].."Kills"] or mod.stats[statVarTable[savedDifficulty].."Kills"] < 0 then mod.stats[statVarTable[savedDifficulty].."Kills"] = 0 end
  5756. --Fix logical error i've seen where for some reason we have more kills then pulls for boss as seen by - stats for wipe messages.
  5757. mod.stats[statVarTable[savedDifficulty].."Kills"] = mod.stats[statVarTable[savedDifficulty].."Kills"] + 1
  5758. if mod.stats[statVarTable[savedDifficulty].."Kills"] > mod.stats[statVarTable[savedDifficulty].."Pulls"] then mod.stats[statVarTable[savedDifficulty].."Kills"] = mod.stats[statVarTable[savedDifficulty].."Pulls"] end
  5759. if not mod.ignoreBestkill and mod.combatInfo.pull then
  5760. mod.stats[statVarTable[savedDifficulty].."LastTime"] = thisTime
  5761. --Just to prevent pre mature end combat calls from broken mods from saving bad time stats.
  5762. if bestTime and bestTime > 0 and bestTime < 1.5 then
  5763. mod.stats[statVarTable[savedDifficulty].."BestTime"] = thisTime
  5764. else
  5765. if difficultyIndex == 8 then--Mythic+/Challenge Mode
  5766. --TODO, figure out how to get current mythic plus rank, compare to our best rank.
  5767. local currentMPRank = C_ChallengeMode.GetActiveKeystoneInfo() or 0
  5768. local bestMPRank = mod.stats.challengeBestRank or 0
  5769. if mod.stats.challengeBestRank > currentMPRank then--Don't save time stats at all
  5770. --DO nothing
  5771. elseif mod.stats.challengeBestRank < currentMPRank then--Update best time and best rank, even if best time is lower (for a lower rank)
  5772. mod.stats.challengeBestRank = currentMPRank--Update best rank
  5773. mod.stats[statVarTable[savedDifficulty].."BestTime"] = thisTime--Write this time no matter what.
  5774. else--Best rank must match current rank, so update time normally
  5775. mod.stats[statVarTable[savedDifficulty].."BestTime"] = mmin(bestTime or mhuge, thisTime)
  5776. end
  5777. else
  5778. mod.stats[statVarTable[savedDifficulty].."BestTime"] = mmin(bestTime or mhuge, thisTime)
  5779. end
  5780. end
  5781. end
  5782. local totalKills = mod.stats[statVarTable[savedDifficulty].."Kills"]
  5783. if self.Options.ShowDefeatMessage then
  5784. local msg = ""
  5785. if not mod.combatInfo.pull then--was a bad pull so we ignored thisTime, should never happen
  5786. if scenario then
  5787. msg = DBM_CORE_SCENARIO_COMPLETE:format(difficultyText..name, DBM_CORE_UNKNOWN)
  5788. else
  5789. msg = DBM_CORE_BOSS_DOWN:format(difficultyText..name, DBM_CORE_UNKNOWN)
  5790. end
  5791. elseif mod.ignoreBestkill then--Should never happen in a scenario so no need for scenario check.
  5792. if scenario then
  5793. msg = DBM_CORE_SCENARIO_COMPLETE_I:format(difficultyText..name, totalKills)
  5794. else
  5795. msg = DBM_CORE_BOSS_DOWN_I:format(difficultyText..name, totalKills)
  5796. end
  5797. elseif not lastTime then
  5798. if scenario then
  5799. msg = DBM_CORE_SCENARIO_COMPLETE:format(difficultyText..name, strFromTime(thisTime))
  5800. else
  5801. msg = DBM_CORE_BOSS_DOWN:format(difficultyText..name, strFromTime(thisTime))
  5802. if difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16 then
  5803. if InGuildParty() then--Guild Group
  5804. SendAddonMessage("D4", "GCE\t"..modId.."\t3\t0\t"..strFromTime(thisTime).."\t"..difficultyIndex, "GUILD")
  5805. end
  5806. end
  5807. end
  5808. elseif thisTime < (bestTime or mhuge) then
  5809. if scenario then
  5810. msg = DBM_CORE_SCENARIO_COMPLETE_NR:format(difficultyText..name, strFromTime(thisTime), strFromTime(bestTime), totalKills)
  5811. else
  5812. msg = DBM_CORE_BOSS_DOWN_NR:format(difficultyText..name, strFromTime(thisTime), strFromTime(bestTime), totalKills)
  5813. if difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16 then
  5814. if InGuildParty() then--Guild Group
  5815. SendAddonMessage("D4", "GCE\t"..modId.."\t3\t0\t"..strFromTime(thisTime).."\t"..difficultyIndex, "GUILD")
  5816. end
  5817. end
  5818. end
  5819. else
  5820. if scenario then
  5821. msg = DBM_CORE_SCENARIO_COMPLETE_L:format(difficultyText..name, strFromTime(thisTime), strFromTime(lastTime), strFromTime(bestTime), totalKills)
  5822. else
  5823. msg = DBM_CORE_BOSS_DOWN_L:format(difficultyText..name, strFromTime(thisTime), strFromTime(lastTime), strFromTime(bestTime), totalKills)
  5824. if difficultyIndex == 14 or difficultyIndex == 15 or difficultyIndex == 16 then
  5825. if InGuildParty() then--Guild Group
  5826. SendAddonMessage("D4", "GCE\t"..modId.."\t3\t0\t"..strFromTime(thisTime).."\t"..difficultyIndex, "GUILD")
  5827. end
  5828. end
  5829. end
  5830. end
  5831. self:Schedule(1, self.AddMsg, self, msg)
  5832. end
  5833. local msg
  5834. for k, v in pairs(autoRespondSpam) do
  5835. if self.Options.WhisperStats then
  5836. if scenario then
  5837. msg = msg or chatPrefixShort..DBM_CORE_WHISPER_SCENARIO_END_KILL_STATS:format(playerName, difficultyText..(name or ""), totalKills)
  5838. else
  5839. msg = msg or chatPrefixShort..DBM_CORE_WHISPER_COMBAT_END_KILL_STATS:format(playerName, difficultyText..(name or ""), totalKills)
  5840. end
  5841. else
  5842. if scenario then
  5843. msg = msg or chatPrefixShort..DBM_CORE_WHISPER_SCENARIO_END_KILL:format(playerName, difficultyText..(name or ""))
  5844. else
  5845. msg = msg or chatPrefixShort..DBM_CORE_WHISPER_COMBAT_END_KILL:format(playerName, difficultyText..(name or ""))
  5846. end
  5847. end
  5848. sendWhisper(k, msg)
  5849. end
  5850. fireEvent("kill", mod)
  5851. if savedDifficulty == "worldboss" and not mod.noWBEsync then
  5852. if lastBossDefeat[modId..playerRealm] and (GetTime() - lastBossDefeat[modId..playerRealm] < 30) then return end--Someone else synced in last 10 seconds so don't send out another sync to avoid needless sync spam.
  5853. lastBossDefeat[modId..playerRealm] = GetTime()--Update last defeat time before we send it, so we don't handle our own sync
  5854. if IsInGuild() then
  5855. SendAddonMessage("D4", "WBD\t"..modId.."\t"..playerRealm.."\t8\t"..name, "GUILD")--Even guild syncs send realm so we can keep antispam the same across realid as well.
  5856. end
  5857. local _, numBNetOnline = BNGetNumFriends()
  5858. for i = 1, numBNetOnline do
  5859. local sameRealm = false
  5860. local presenceID, _, _, _, _, _, client, isOnline = BNGetFriendInfo(i)
  5861. if isOnline and client == BNET_CLIENT_WOW then
  5862. local _, _, _, userRealm = BNGetGameAccountInfo(presenceID)
  5863. if connectedServers then
  5864. for i = 1, #connectedServers do
  5865. if userRealm == connectedServers[i] then
  5866. sameRealm = true
  5867. break
  5868. end
  5869. end
  5870. else
  5871. if userRealm == playerRealm then
  5872. sameRealm = true
  5873. end
  5874. end
  5875. if sameRealm then
  5876. BNSendGameData(presenceID, "D4", "WBD\t"..modId.."\t"..userRealm.."\t8\t"..name)
  5877. end
  5878. end
  5879. end
  5880. end
  5881. end
  5882. if mod.OnCombatEnd then mod:OnCombatEnd(wipe) end
  5883. if #inCombat == 0 then--prevent error if you pulled multiple boss. (Earth, Wind and Fire)
  5884. statusWhisperDisabled = false
  5885. self:Schedule(10, self.StopLogging, self)--small delay to catch kill/died combatlog events
  5886. self:HideBlizzardEvents(0)
  5887. self:Unschedule(checkBossHealth)
  5888. self:Unschedule(checkCustomBossHealth)
  5889. self:Unschedule(loopCRTimer)
  5890. self.BossHealth:Hide()
  5891. self.Arrow:Hide(true)
  5892. if watchFrameRestore then
  5893. ObjectiveTrackerFrame:Show()
  5894. watchFrameRestore = false
  5895. end
  5896. if tooltipsHidden then
  5897. --Better or cleaner way?
  5898. tooltipsHidden = false
  5899. GameTooltip:SetScript("OnShow", GameTooltip.Show)
  5900. end
  5901. if self.Options.sfxDisabled then
  5902. self.Options.sfxDisabled = nil
  5903. SetCVar("Sound_EnableSFX", 1)
  5904. end
  5905. --cache table
  5906. twipe(autoRespondSpam)
  5907. twipe(bossHealth)
  5908. twipe(bossHealthuIdCache)
  5909. twipe(bossuIdCache)
  5910. --sync table
  5911. twipe(canSetIcons)
  5912. twipe(iconSetRevision)
  5913. twipe(iconSetPerson)
  5914. twipe(addsGUIDs)
  5915. bossuIdFound = false
  5916. eeSyncSender = {}
  5917. eeSyncReceived = 0
  5918. targetMonitor = nil
  5919. self:CreatePizzaTimer(time, "", nil, nil, nil, nil, true)--Auto Terminate infinite loop timers on combat end
  5920. elseif self.BossHealth:IsShown() then
  5921. if mod.bossHealthInfo then
  5922. if mod.bossHealthInfo[2] and type(mod.bossHealthInfo[2]) == "number" then
  5923. for i = 1, #mod.bossHealthInfo do
  5924. self.BossHealth:RemoveBoss(mod.bossHealthInfo[i])
  5925. end
  5926. else
  5927. for i = 1, #mod.bossHealthInfo, 2 do
  5928. self.BossHealth:RemoveBoss(mod.bossHealthInfo[i])
  5929. end
  5930. end
  5931. else
  5932. self.BossHealth:RemoveBoss(mod.combatInfo.mob)
  5933. end
  5934. end
  5935. end
  5936. end
  5937. end
  5938.  
  5939. function DBM:OnMobKill(cId, synced)
  5940. for i = #inCombat, 1, -1 do
  5941. local v = inCombat[i]
  5942. if not v.combatInfo then
  5943. return
  5944. end
  5945. if v.combatInfo.killMobs and v.combatInfo.killMobs[cId] then
  5946. if not synced then
  5947. sendSync("K", cId)
  5948. end
  5949. v.combatInfo.killMobs[cId] = false
  5950. self.BossHealth:RemoveBoss(cId)
  5951. if v.numBoss then
  5952. v.vb.bossLeft = (v.vb.bossLeft or v.numBoss) - 1
  5953. self:Debug("Boss left - "..v.vb.bossLeft.."/"..v.numBoss, 2)
  5954. end
  5955. local allMobsDown = true
  5956. for i, v in pairs(v.combatInfo.killMobs) do
  5957. if v then
  5958. allMobsDown = false
  5959. break
  5960. end
  5961. end
  5962. if allMobsDown then
  5963. self:EndCombat(v)
  5964. end
  5965. elseif cId == v.combatInfo.mob and not v.combatInfo.killMobs and not v.combatInfo.multiMobPullDetection then
  5966. if not synced then
  5967. sendSync("K", cId)
  5968. end
  5969. self:EndCombat(v)
  5970. end
  5971. end
  5972. end
  5973.  
  5974. do
  5975. local autoLog = false
  5976. local autoTLog = false
  5977.  
  5978. local function isCurrentContent()
  5979. if LastInstanceMapID == 1520 or LastInstanceMapID == 1530 or LastInstanceMapID == 1220 or LastInstanceMapID == 1648 or LastInstanceMapID == 1676 then--Legion
  5980. return true
  5981. end
  5982. return false
  5983. end
  5984.  
  5985. function DBM:StartLogging(timer, checkFunc)
  5986. self:Unschedule(DBM.StopLogging)
  5987. if self.Options.LogOnlyRaidBosses and ((LastInstanceType ~= "raid") or IsPartyLFG() or not isCurrentContent()) then return end
  5988. if self.Options.AutologBosses then--Start logging here to catch pre pots.
  5989. if not LoggingCombat() then
  5990. autoLog = true
  5991. self:AddMsg("|cffffff00"..COMBATLOGENABLED.."|r")
  5992. LoggingCombat(true)
  5993. if checkFunc then
  5994. self:Unschedule(checkFunc)
  5995. self:Schedule(timer+10, checkFunc)--But if pull was canceled and we don't have a boss engaged within 10 seconds of pull timer ending, abort log
  5996. end
  5997. end
  5998. end
  5999. if self.Options.AdvancedAutologBosses and Transcriptor then
  6000. if not Transcriptor:IsLogging() then
  6001. autoTLog = true
  6002. self:AddMsg("|cffffff00"..DBM_CORE_TRANSCRIPTOR_LOG_START.."|r")
  6003. Transcriptor:StartLog(1)
  6004. end
  6005. if checkFunc then
  6006. self:Unschedule(checkFunc)
  6007. self:Schedule(timer+10, checkFunc)--But if pull was canceled and we don't have a boss engaged within 10 seconds of pull timer ending, abort log
  6008. end
  6009. end
  6010. end
  6011.  
  6012. function DBM:StopLogging()
  6013. if self.Options.AutologBosses and LoggingCombat() and autoLog then
  6014. autoLog = false
  6015. self:AddMsg("|cffffff00"..COMBATLOGDISABLED.."|r")
  6016. LoggingCombat(false)
  6017. end
  6018. if self.Options.AdvancedAutologBosses and Transcriptor and autoTLog then
  6019. if Transcriptor:IsLogging() then
  6020. autoTLog = false
  6021. self:AddMsg("|cffffff00"..DBM_CORE_TRANSCRIPTOR_LOG_END.."|r")
  6022. Transcriptor:StopLog(1)
  6023. end
  6024. end
  6025. end
  6026. end
  6027.  
  6028. function DBM:SetCurrentSpecInfo()
  6029. currentSpecGroup = GetSpecialization() or 1
  6030. currentSpecID, currentSpecName = GetSpecializationInfo(currentSpecGroup)--give temp first spec id for non-specialization char. no one should use dbm with no specialization, below level 10, should not need dbm.
  6031. currentSpecID = tonumber(currentSpecID)
  6032. end
  6033.  
  6034. function DBM:GetCurrentInstanceDifficulty()
  6035. local _, instanceType, difficulty, difficultyName, _, _, _, _, instanceGroupSize = GetInstanceInfo()
  6036. if difficulty == 0 or (difficulty == 1 and instanceType == "none") or C_Garrison:IsOnGarrisonMap() then--draenor field returns 1, causing world boss mod bug.
  6037. return "worldboss", RAID_INFO_WORLD_BOSS.." - ", difficulty
  6038. elseif difficulty == 1 then
  6039. return "normal5", difficultyName.." - ", difficulty, instanceGroupSize
  6040. elseif difficulty == 2 then
  6041. return "heroic5", difficultyName.." - ", difficulty, instanceGroupSize
  6042. elseif difficulty == 3 then
  6043. return "normal10", difficultyName.." - ", difficulty, instanceGroupSize
  6044. elseif difficulty == 4 then
  6045. return "normal25", difficultyName.." - ", difficulty, instanceGroupSize
  6046. elseif difficulty == 5 then
  6047. return "heroic10", difficultyName.." - ", difficulty, instanceGroupSize
  6048. elseif difficulty == 6 then
  6049. return "heroic25", difficultyName.." - ", difficulty, instanceGroupSize
  6050. elseif difficulty == 7 then--Fixed LFR (ie pre WoD zones)
  6051. return "lfr25", difficultyName.." - ", difficulty, instanceGroupSize
  6052. elseif difficulty == 8 then
  6053. return "challenge5", PLAYER_DIFFICULTY6.."+ - ", difficulty, instanceGroupSize
  6054. elseif difficulty == 9 then--40 man raids have their own difficulty now, no longer returned as normal 10man raids
  6055. return "normal10", difficultyName.." - ",difficulty, instanceGroupSize--Just use normal10 anyways, since that's where we been saving 40 man stuff for so long anyways, no reason to change it now, not like any 40 mans can be toggled between 10 and 40 where we NEED to tell the difference.
  6056. elseif difficulty == 11 then
  6057. return "heroic5", difficultyName.." - ", difficulty, instanceGroupSize
  6058. elseif difficulty == 12 then
  6059. return "normal5", difficultyName.." - ", difficulty, instanceGroupSize
  6060. elseif difficulty == 14 then
  6061. return "normal", difficultyName.." - ", difficulty, instanceGroupSize
  6062. elseif difficulty == 15 then
  6063. return "heroic", difficultyName.." - ", difficulty, instanceGroupSize
  6064. elseif difficulty == 16 then
  6065. return "mythic", difficultyName.." - ", difficulty, instanceGroupSize
  6066. elseif difficulty == 17 then--Variable LFR (ie post WoD zones)
  6067. return "lfr", difficultyName.." - ", difficulty, instanceGroupSize
  6068. elseif difficulty == 18 then
  6069. return "event40", difficultyName.." - ", difficulty, instanceGroupSize
  6070. elseif difficulty == 19 then
  6071. return "event5", difficultyName.." - ", difficulty, instanceGroupSize
  6072. elseif difficulty == 20 then
  6073. return "event20", difficultyName.." - ", difficulty, instanceGroupSize
  6074. elseif difficulty == 23 then
  6075. return "mythic", difficultyName.." - ", difficulty, instanceGroupSize
  6076. elseif difficulty == 24 then
  6077. return "timewalker", difficultyName.." - ", difficulty, instanceGroupSize
  6078. -- elseif difficulty == 25 then--Used by Ashran in 7.x.
  6079. -- return "pvpscenario", difficultyName.." - ", difficulty, instanceGroupSize
  6080. else--failsafe
  6081. return "normal5", "", difficulty, instanceGroupSize
  6082. end
  6083. end
  6084.  
  6085. function DBM:GetCurrentArea()
  6086. return LastInstanceMapID
  6087. end
  6088.  
  6089. function DBM:GetGroupSize()
  6090. return LastGroupSize
  6091. end
  6092.  
  6093. function DBM:HasMapRestrictions()
  6094. --Check playerX and playerY. if they are nil restrictions are active
  6095. --Restrictions active in all party, raid, pvp, arena maps. No restrictions in "none" or "scenario"
  6096. local playerX, playerY = UnitPosition("player")
  6097. if not playerX or not playerY then
  6098. return true
  6099. end
  6100. return false
  6101. end
  6102.  
  6103. function DBM:PlaySoundFile(path, ignoreSFX)
  6104. local soundSetting = self.Options.UseSoundChannel
  6105. if soundSetting == "Dialog" then
  6106. PlaySoundFile(path, "Dialog")
  6107. elseif ignoreSFX or soundSetting == "Master" then
  6108. PlaySoundFile(path, "Master")
  6109. else
  6110. PlaySoundFile(path)
  6111. end
  6112. end
  6113.  
  6114. function DBM:PlaySound(path)
  6115. local soundSetting = self.Options.UseSoundChannel
  6116. if soundSetting == "Master" then
  6117. PlaySound(path, "Master")
  6118. elseif soundSetting == "Dialog" then
  6119. PlaySound(path, "Dialog")
  6120. else
  6121. PlaySound(path)
  6122. end
  6123. end
  6124.  
  6125. function DBM:UNIT_DIED(args)
  6126. local GUID = args.destGUID
  6127. if self:IsCreatureGUID(GUID) then
  6128. self:OnMobKill(self:GetCIDFromGUID(GUID))
  6129. end
  6130. if self.Options.AFKHealthWarning and GUID == UnitGUID("player") and not IsEncounterInProgress() and UnitIsAFK("player") and self:AntiSpam(5, "AFK") then--You are afk and losing health, some griever is trying to kill you while you are afk/tabbed out.
  6131. self:FlashClientIcon()
  6132. self:PlaySoundFile("Sound\\Creature\\CThun\\CThunYouWillDIe.ogg")--So fire an alert sound to save yourself from this person's behavior.
  6133. self:AddMsg(DBM_CORE_AFK_WARNING:format(0))
  6134. end
  6135. end
  6136. DBM.UNIT_DESTROYED = DBM.UNIT_DIED
  6137.  
  6138. ----------------------
  6139. -- Timer recovery --
  6140. ----------------------
  6141. do
  6142. local requestedFrom = {}
  6143. local requestTime = 0
  6144. local clientUsed = {}
  6145. local sortMe = {}
  6146.  
  6147. local function sort(v1, v2)
  6148. if v1.revision and not v2.revision then
  6149. return true
  6150. elseif v2.revision and not v1.revision then
  6151. return false
  6152. elseif v1.revision and v2.revision then
  6153. return v1.revision > v2.revision
  6154. else
  6155. return (v1.bwversion or 0) > (v2.bwversion or 0)
  6156. end
  6157. end
  6158.  
  6159. function DBM:RequestTimers(requestNum)
  6160. twipe(sortMe)
  6161. for i, v in pairs(raid) do
  6162. tinsert(sortMe, v)
  6163. end
  6164. tsort(sortMe, sort)
  6165. self:Debug("RequestTimers Running", 2)
  6166. local selectedClient
  6167. local listNum = 0
  6168. for i, v in ipairs(sortMe) do
  6169. -- If selectedClient player's realm is not same with your's, timer recovery by selectedClient not works at all.
  6170. -- SendAddonMessage target channel is "WHISPER" and target player is other realm, no msg sends at all. At same realm, message sending works fine. (Maybe bliz bug or SendAddonMessage function restriction?)
  6171. if v.name ~= playerName and UnitIsConnected(v.id) and (not UnitIsGhost(v.id)) and UnitRealmRelationship(v.id) ~= 2 and (GetTime() - (clientUsed[v.name] or 0)) > 10 then
  6172. listNum = listNum + 1
  6173. if listNum == requestNum then
  6174. selectedClient = v
  6175. clientUsed[v.name] = GetTime()
  6176. break
  6177. end
  6178. end
  6179. end
  6180. if not selectedClient then return end
  6181. self:Debug("Requesting timer recovery to "..selectedClient.name)
  6182. requestedFrom[selectedClient.name] = true
  6183. requestTime = GetTime()
  6184. SendAddonMessage("D4", "RT", "WHISPER", selectedClient.name)
  6185. end
  6186.  
  6187. function DBM:ReceiveCombatInfo(sender, mod, time)
  6188. if dbmIsEnabled and requestedFrom[sender] and (GetTime() - requestTime) < 5 and #inCombat == 0 then
  6189. self:StartCombat(mod, time, "TIMER_RECOVERY")
  6190. --Recovery successful, someone sent info, abort other recovery requests
  6191. self:Unschedule(self.RequestTimers)
  6192. twipe(requestedFrom)
  6193. end
  6194. end
  6195.  
  6196. function DBM:ReceiveTimerInfo(sender, mod, timeLeft, totalTime, id, ...)
  6197. if requestedFrom[sender] and (GetTime() - requestTime) < 5 then
  6198. local lag = select(4, GetNetStats()) / 1000
  6199. for i, v in ipairs(mod.timers) do
  6200. if v.id == id then
  6201. v:Start(totalTime, ...)
  6202. v:Update(totalTime - timeLeft + lag, totalTime, ...)
  6203. end
  6204. end
  6205. end
  6206. end
  6207.  
  6208. function DBM:ReceiveVariableInfo(sender, mod, name, value)
  6209. if requestedFrom[sender] and (GetTime() - requestTime) < 5 then
  6210. if value == "true" then
  6211. mod.vb[name] = true
  6212. elseif value == "false" then
  6213. mod.vb[name] = false
  6214. else
  6215. mod.vb[name] = value
  6216. end
  6217. end
  6218. end
  6219. end
  6220.  
  6221. do
  6222. local spamProtection = {}
  6223. function DBM:SendTimers(target)
  6224. self:Debug("SendTimers requested by "..target, 2)
  6225. local spamForTarget = spamProtection[target] or 0
  6226. -- just try to clean up the table, that should keep the hash table at max. 4 entries or something :)
  6227. for k, v in pairs(spamProtection) do
  6228. if GetTime() - v >= 1 then
  6229. spamProtection[k] = nil
  6230. end
  6231. end
  6232. if GetTime() - spamForTarget < 1 then -- just to prevent players from flooding this on purpose
  6233. return
  6234. end
  6235. spamProtection[target] = GetTime()
  6236. if UnitInBattleground("player") then
  6237. self:SendBGTimers(target)
  6238. return
  6239. end
  6240. if #inCombat < 1 then
  6241. --Break timer is up, so send that
  6242. --But only if we are not in combat with a boss
  6243. if self.Bars:GetBar(DBM_CORE_TIMER_BREAK) then
  6244. local remaining = self.Bars:GetBar(DBM_CORE_TIMER_BREAK).timer
  6245. SendAddonMessage("D4", "BTR3\t"..remaining, "WHISPER", target)
  6246. end
  6247. return
  6248. end
  6249. local mod
  6250. for i, v in ipairs(inCombat) do
  6251. mod = not v.isCustomMod and v
  6252. end
  6253. mod = mod or inCombat[1]
  6254. self:SendCombatInfo(mod, target)
  6255. self:SendVariableInfo(mod, target)
  6256. self:SendTimerInfo(mod, target)
  6257. end
  6258. end
  6259.  
  6260. function DBM:SendBGTimers(target)
  6261. local mod
  6262. if IsActiveBattlefieldArena() then
  6263. mod = self:GetModByName("Arenas")
  6264. else
  6265. -- FIXME: this doesn't work for non-english clients
  6266. local zone = GetRealZoneText():gsub(" ", "")--Does this need updating to mapid arta?
  6267. mod = self:GetModByName(zone)
  6268. end
  6269. if mod and mod.timers then
  6270. self:SendTimerInfo(mod, target)
  6271. end
  6272. end
  6273.  
  6274. function DBM:SendCombatInfo(mod, target)
  6275. return SendAddonMessage("D4", ("CI\t%s\t%s"):format(mod.id, GetTime() - mod.combatInfo.pull), "WHISPER", target)
  6276. end
  6277.  
  6278. function DBM:SendTimerInfo(mod, target)
  6279. for i, v in ipairs(mod.timers) do
  6280. for _, uId in ipairs(v.startedTimers) do
  6281. local elapsed, totalTime, timeLeft
  6282. if select("#", string.split("\t", uId)) > 1 then
  6283. elapsed, totalTime = v:GetTime(select(2, string.split("\t", uId)))
  6284. else
  6285. elapsed, totalTime = v:GetTime()
  6286. end
  6287. timeLeft = totalTime - elapsed
  6288. if timeLeft > 0 and totalTime > 0 then
  6289. SendAddonMessage("D4", ("TI\t%s\t%s\t%s\t%s"):format(mod.id, timeLeft, totalTime, uId), "WHISPER", target)
  6290. end
  6291. end
  6292. end
  6293. end
  6294.  
  6295. function DBM:SendVariableInfo(mod, target)
  6296. for vname, v in pairs(mod.vb) do
  6297. local v2 = tostring(v)
  6298. if v2 then
  6299. SendAddonMessage("D4", ("VI\t%s\t%s\t%s"):format(mod.id, vname, v2), "WHISPER", target)
  6300. end
  6301. end
  6302. end
  6303.  
  6304. do
  6305. local soundFiles = {
  6306. "Sound\\Creature\\Rhonin\\UR_Rhonin_Event01.ogg",--5
  6307. "Sound\\Creature\\Rhonin\\UR_Rhonin_Event02.ogg",--5
  6308. "Sound\\Creature\\Rhonin\\UR_Rhonin_Event03.ogg",--5.5
  6309. "Sound\\Creature\\Rhonin\\UR_Rhonin_Event04.ogg",--9
  6310. "Sound\\Creature\\Rhonin\\UR_Rhonin_Event05.ogg",--4
  6311. "Sound\\Creature\\Rhonin\\UR_Rhonin_Event06.ogg",--10
  6312. "Sound\\Creature\\Rhonin\\UR_Rhonin_Event07.ogg",--15
  6313. "Sound\\Creature\\Rhonin\\UR_Rhonin_Event08.ogg",
  6314. }
  6315. local function playDelay(self, count)
  6316. self:PlaySoundFile(soundFiles[count])
  6317. end
  6318.  
  6319. function DBM:AprilFools()
  6320. self:Unschedule(self.AprilFools)
  6321. local currentMapId = GetPlayerMapAreaID("player")
  6322. if not currentMapId then
  6323. SetMapToCurrentZone()
  6324. currentMapId = GetCurrentMapAreaID()
  6325. end
  6326. self:Schedule(120 + math.random(0, 300) , self.AprilFools, self)
  6327. if currentMapId ~= 1014 then return end--Legion Dalaran
  6328. playDelay(self, 1)
  6329. self:Schedule(5, playDelay, self, 2)
  6330. self:Schedule(10, playDelay, self, 3)
  6331. self:Schedule(15.5, playDelay, self, 4)
  6332. self:Schedule(24.5, playDelay, self, 5)
  6333. self:Schedule(28, playDelay, self, 6)
  6334. self:Schedule(37.5, playDelay, self, 7)
  6335. self:Schedule(50.5, playDelay, self, 8)
  6336. end
  6337. function DBM:PLAYER_ENTERING_WORLD()
  6338. local weekday, month, day, year = CalendarGetDate()--Must be called after PLAYER_ENTERING_WORLD
  6339. if month == 4 and day == 1 then--April 1st
  6340. self:Schedule(180 + math.random(0, 300) , self.AprilFools, self)
  6341. end
  6342. if GetLocale() == "ptBR" or GetLocale() == "frFR" or GetLocale() == "itIT" or GetLocale() == "esES" or GetLocale() == "ruRU" then
  6343. C_TimerAfter(10, function() if self.Options.HelpMessageVersion < 4 then self.Options.HelpMessageVersion = 4 self:AddMsg(DBM_CORE_NEED_LOCALS) end end)
  6344. end
  6345. C_TimerAfter(20, function() if not self.Options.ForumsMessageShown then self.Options.ForumsMessageShown = self.ReleaseRevision self:AddMsg(DBM_FORUMS_MESSAGE) end end)
  6346. C_TimerAfter(30, function() if not self.Options.SettingsMessageShown then self.Options.SettingsMessageShown = true self:AddMsg(DBM_HOW_TO_USE_MOD) end end)
  6347. C_TimerAfter(40, function() if self.Options.NewsMessageShown < 8 then self.Options.NewsMessageShown = 8 self:AddMsg(DBM_CORE_WHATS_NEW_LINK) end end)
  6348. if type(RegisterAddonMessagePrefix) == "function" then
  6349. if not RegisterAddonMessagePrefix("D4") then -- main prefix for DBM4
  6350. self:AddMsg("Error: unable to register DBM addon message prefix (reached client side addon message filter limit), synchronization will be unavailable") -- TODO: confirm that this actually means that the syncs won't show up
  6351. end
  6352. if not RegisterAddonMessagePrefix("BigWigs") then
  6353. self:AddMsg("Error: unable to register BigWigs addon message prefix (reached client side addon message filter limit), BigWigs version checks will be unavailable")
  6354. end
  6355. if not RegisterAddonMessagePrefix("Transcriptor") then
  6356. self:AddMsg("Error: unable to register Transcriptor addon message prefix (reached client side addon message filter limit)")
  6357. end
  6358. end
  6359. if self.Options.sfxDisabled then--Check if sound was disabled by previous session and not re-enabled.
  6360. self.Options.sfxDisabled = nil
  6361. SetCVar("Sound_EnableSFX", 1)
  6362. end
  6363. if self.Options.RestoreRange then self.Options.RestoreRange = nil end--User DCed while this was true, clear it
  6364. end
  6365. end
  6366.  
  6367. ------------------------------------
  6368. -- Auto-respond/Status whispers --
  6369. ------------------------------------
  6370. do
  6371. local function getNumAlivePlayers()
  6372. local alive = 0
  6373. if IsInRaid() then
  6374. for i = 1, GetNumGroupMembers() do
  6375. alive = alive + ((UnitIsDeadOrGhost("raid"..i) and 0) or 1)
  6376. end
  6377. else
  6378. alive = (UnitIsDeadOrGhost("player") and 0) or 1
  6379. for i = 1, GetNumSubgroupMembers() do
  6380. alive = alive + ((UnitIsDeadOrGhost("party"..i) and 0) or 1)
  6381. end
  6382. end
  6383. return alive
  6384. end
  6385.  
  6386. local function getNumRealAlivePlayers()
  6387. local alive = 0
  6388. local currentMapId = GetPlayerMapAreaID("player")
  6389. if not currentMapId then
  6390. SetMapToCurrentZone()
  6391. currentMapId = GetCurrentMapAreaID()
  6392. end
  6393. local currentMapName = GetMapNameByID(currentMapId)
  6394. if IsInRaid() then
  6395. for i = 1, GetNumGroupMembers() do
  6396. if select(7, GetRaidRosterInfo(i)) == currentMapName then
  6397. alive = alive + ((UnitIsDeadOrGhost("raid"..i) and 0) or 1)
  6398. end
  6399. end
  6400. else
  6401. alive = (UnitIsDeadOrGhost("player") and 0) or 1
  6402. for i = 1, GetNumSubgroupMembers() do
  6403. if select(7, GetRaidRosterInfo(i)) == currentMapName then
  6404. alive = alive + ((UnitIsDeadOrGhost("party"..i) and 0) or 1)
  6405. end
  6406. end
  6407. end
  6408. return alive
  6409. end
  6410.  
  6411. local function isOnSameServer(presenceId)
  6412. local toonID, client = select(6, BNGetFriendInfoByID(presenceId))
  6413. if client ~= "WoW" then
  6414. return false
  6415. end
  6416. return GetRealmName() == select(4, BNGetGameAccountInfo(toonID))
  6417. end
  6418.  
  6419. -- sender is a presenceId for real id messages, a character name otherwise
  6420. local function onWhisper(msg, sender, isRealIdMessage)
  6421. if statusWhisperDisabled then return end--RL has disabled status whispers for entire raid.
  6422. if msg:find(chatPrefix) and not InCombatLockdown() and DBM:AntiSpam(60, "Ogron") and DBM.Options.AutoReplySound then
  6423. --Might need more validation if people figure out they can just whisper people with chatPrefix to trigger it.
  6424. --However if I have to add more validation it probably won't work in most languages :\ So lets hope antispam and combat check is enough
  6425. DBM:PlaySoundFile("sound\\creature\\aggron1\\VO_60_HIGHMAUL_AGGRON_1_AGGRO_1.ogg")
  6426. elseif msg == "status" and #inCombat > 0 and DBM.Options.StatusEnabled then
  6427. if not difficultyText then -- prevent error when timer recovery function worked and etc (StartCombat not called)
  6428. difficultyText = select(2, DBM:GetCurrentInstanceDifficulty())
  6429. end
  6430. if IsInScenarioGroup() then return end--status not really useful on scenario mods since there is no way to report progress as a percent. We just ignore it.
  6431. local mod
  6432. for i, v in ipairs(inCombat) do
  6433. mod = not v.isCustomMod and v
  6434. end
  6435. mod = mod or inCombat[1]
  6436. local hp = mod.highesthealth and mod:GetHighestBossHealth() or mod:GetLowestBossHealth()
  6437. local hpText = mod.CustomHealthUpdate and mod:CustomHealthUpdate() or hp and ("%d%%"):format(hp) or DBM_CORE_UNKNOWN
  6438. if mod.vb.phase then
  6439. hpText = hpText.." ("..SCENARIO_STAGE:format(mod.vb.phase)..")"
  6440. end
  6441. if mod.numBoss then
  6442. local bossesKilled = mod.numBoss - mod.vb.bossLeft
  6443. hpText = hpText.." ("..BOSSES_KILLED:format(bossesKilled, mod.numBoss)..")"
  6444. end
  6445. sendWhisper(sender, chatPrefix..DBM_CORE_STATUS_WHISPER:format(difficultyText..(mod.combatInfo.name or ""), hpText, IsInInstance() and getNumRealAlivePlayers() or getNumAlivePlayers(), DBM:GetNumRealGroupMembers()))
  6446. elseif #inCombat > 0 and DBM.Options.AutoRespond and (isRealIdMessage and (not isOnSameServer(sender) or not DBM:GetRaidUnitId(select(5, BNGetFriendInfoByID(sender)))) or not isRealIdMessage and not DBM:GetRaidUnitId(sender)) then
  6447. if not difficultyText then -- prevent error when timer recovery function worked and etc (StartCombat not called)
  6448. difficultyText = select(2, DBM:GetCurrentInstanceDifficulty())
  6449. end
  6450. local mod
  6451. for i, v in ipairs(inCombat) do
  6452. mod = not v.isCustomMod and v
  6453. end
  6454. mod = mod or inCombat[1]
  6455. local hp = mod.highesthealth and mod:GetHighestBossHealth() or mod:GetLowestBossHealth()
  6456. local hpText = mod.CustomHealthUpdate and mod:CustomHealthUpdate() or hp and ("%d%%"):format(hp) or DBM_CORE_UNKNOWN
  6457. if mod.vb.phase then
  6458. hpText = hpText.." ("..SCENARIO_STAGE:format(mod.vb.phase)..")"
  6459. end
  6460. if mod.numBoss then
  6461. local bossesKilled = mod.numBoss - mod.vb.bossLeft
  6462. hpText = hpText.." ("..BOSSES_KILLED:format(bossesKilled, mod.numBoss)..")"
  6463. end
  6464. if not autoRespondSpam[sender] then
  6465. if IsInScenarioGroup() then
  6466. sendWhisper(sender, chatPrefix..DBM_CORE_AUTO_RESPOND_WHISPER_SCENARIO:format(playerName, difficultyText..(mod.combatInfo.name or ""), getNumAlivePlayers(), DBM:GetNumGroupMembers()))
  6467. else
  6468. sendWhisper(sender, chatPrefix..DBM_CORE_AUTO_RESPOND_WHISPER:format(playerName, difficultyText..(mod.combatInfo.name or ""), hpText, IsInInstance() and getNumRealAlivePlayers() or getNumAlivePlayers(), DBM:GetNumRealGroupMembers()))
  6469. end
  6470. DBM:AddMsg(DBM_CORE_AUTO_RESPONDED)
  6471. end
  6472. autoRespondSpam[sender] = true
  6473. end
  6474. end
  6475.  
  6476. function DBM:CHAT_MSG_WHISPER(msg, name, _, _, _, status)
  6477. if status ~= "GM" then
  6478. name = Ambiguate(name, "none")
  6479. return onWhisper(msg, name, false)
  6480. end
  6481. end
  6482.  
  6483. function DBM:CHAT_MSG_BN_WHISPER(msg, ...)
  6484. local presenceId = select(12, ...) -- srsly?
  6485. return onWhisper(msg, presenceId, true)
  6486. end
  6487. end
  6488.  
  6489. -------------------
  6490. -- Chat Filter --
  6491. -------------------
  6492. do
  6493. local function filterOutgoing(self, event, ...)
  6494. local msg = ...
  6495. if not msg and self then -- compatibility mode!
  6496. -- we also check if self exists to prevent a possible freeze if the function is called without arguments at all
  6497. -- as this would be even worse than the issue with missing whisper messages ;)
  6498. return filterOutgoing(nil, nil, self, event)
  6499. end
  6500. return msg:sub(1, chatPrefix:len()) == chatPrefix or msg:sub(1, chatPrefixShort:len()) == chatPrefixShort, ...
  6501. end
  6502.  
  6503. local function filterIncoming(self, event, ...)
  6504. local msg = ...
  6505. if not msg and self then -- compatibility mode!
  6506. return filterIncoming(nil, nil, self, event)
  6507. end
  6508. if DBM.Options.SpamBlockBossWhispers then
  6509. return #inCombat > 0 and (msg == "status" or msg:sub(1, chatPrefix:len()) == chatPrefix or msg:sub(1, chatPrefixShort:len()) == chatPrefixShort), ...
  6510. else
  6511. return msg == "status" and #inCombat > 0, ...
  6512. end
  6513. end
  6514.  
  6515. local function filterSayYell(self, event, ...)
  6516. return DBM.Options.FilterSayAndYell and #inCombat > 0, ...
  6517. end
  6518.  
  6519. --This is the source of the taints. As well as function DBM:AddMsg(text, prefix) function
  6520. --Which is why we embed libchatanims to fix those taints.
  6521. ChatFrame_AddMessageEventFilter("CHAT_MSG_WHISPER_INFORM", filterOutgoing)
  6522. ChatFrame_AddMessageEventFilter("CHAT_MSG_BN_WHISPER_INFORM", filterOutgoing)
  6523. ChatFrame_AddMessageEventFilter("CHAT_MSG_WHISPER", filterIncoming)
  6524. ChatFrame_AddMessageEventFilter("CHAT_MSG_BN_WHISPER", filterIncoming)
  6525. ChatFrame_AddMessageEventFilter("CHAT_MSG_SAY", filterSayYell)
  6526. ChatFrame_AddMessageEventFilter("CHAT_MSG_YELL", filterSayYell)
  6527. end
  6528.  
  6529. --Raid Boss Emote frame handler for core and BG mods.
  6530. --This completely unregisteres or registers event so frame simply does or doesn't show events
  6531. --No dirty hooking. Least invasive way to do it. Uses lowest CPU
  6532. --Toggle is for if we are turning off or on.
  6533. --Custom is for pvp mods to call function without needing global option turned on (such as BG mods option)
  6534. --All also handled by core so both core AND pvp mods aren't trying to hook/hide it. Should all be done HERE
  6535. do
  6536. local blizzEventsUnregistered = false
  6537. function DBM:HideBlizzardEvents(toggle, custom)
  6538. if toggle == 1 and not blizzEventsUnregistered then
  6539. blizzEventsUnregistered = true
  6540. if self.Options.HideQuestTooltips then
  6541. SetCVar("showQuestTrackingTooltips", 0)
  6542. end
  6543. if self.Options.HideBossEmoteFrame or custom then
  6544. RaidBossEmoteFrame:UnregisterEvent("RAID_BOSS_EMOTE")
  6545. RaidBossEmoteFrame:UnregisterEvent("RAID_BOSS_WHISPER")
  6546. RaidBossEmoteFrame:UnregisterEvent("CLEAR_BOSS_EMOTES")
  6547. end
  6548. if self.Options.HideGarrisonToasts or custom then
  6549. AlertFrame:UnregisterEvent("GARRISON_MISSION_FINISHED")
  6550. AlertFrame:UnregisterEvent("GARRISON_BUILDING_ACTIVATABLE")
  6551. end
  6552. if self.Options.HideGuildChallengeUpdates or custom then
  6553. AlertFrame:UnregisterEvent("GUILD_CHALLENGE_COMPLETED")
  6554. end
  6555. elseif toggle == 0 and blizzEventsUnregistered then
  6556. blizzEventsUnregistered = false
  6557. if self.Options.HideQuestTooltips then
  6558. SetCVar("showQuestTrackingTooltips", 1)
  6559. end
  6560. if self.Options.HideBossEmoteFrame or custom then
  6561. RaidBossEmoteFrame:RegisterEvent("RAID_BOSS_EMOTE")
  6562. RaidBossEmoteFrame:RegisterEvent("RAID_BOSS_WHISPER")
  6563. RaidBossEmoteFrame:RegisterEvent("CLEAR_BOSS_EMOTES")
  6564. end
  6565. if self.Options.HideGarrisonToasts then
  6566. AlertFrame:RegisterEvent("GARRISON_MISSION_FINISHED")
  6567. AlertFrame:RegisterEvent("GARRISON_BUILDING_ACTIVATABLE")
  6568. end
  6569. if self.Options.HideGuildChallengeUpdates then
  6570. AlertFrame:RegisterEvent("GUILD_CHALLENGE_COMPLETED")
  6571. end
  6572. end
  6573. end
  6574. end
  6575.  
  6576. --------------------------
  6577. -- Enable/Disable DBM --
  6578. --------------------------
  6579. function DBM:Disable(forced)
  6580. unscheduleAll()
  6581. dbmIsEnabled = false
  6582. end
  6583.  
  6584. function DBM:Enable()
  6585. dbmIsEnabled = true
  6586. end
  6587.  
  6588. function DBM:IsEnabled()
  6589. return dbmIsEnabled
  6590. end
  6591.  
  6592. -----------------------
  6593. -- Misc. Functions --
  6594. -----------------------
  6595. function DBM:AddMsg(text, prefix)
  6596. local tag = prefix or (self.localization and self.localization.general.name) or "DBM"
  6597. local frame = _G[tostring(DBM.Options.ChatFrame)]
  6598. frame = frame and frame:IsShown() and frame or DEFAULT_CHAT_FRAME
  6599. if prefix ~= false then
  6600. frame:AddMessage(("|cffff7d0a<|r|cffffd200%s|r|cffff7d0a>|r %s"):format(tostring(tag), tostring(text)), 0.41, 0.8, 0.94)
  6601. else
  6602. frame:AddMessage(text, 0.41, 0.8, 0.94)
  6603. end
  6604. end
  6605.  
  6606. function DBM:Debug(text, level)
  6607. if not self.Options or not self.Options.DebugMode then return end
  6608. if (level or 1) <= DBM.Options.DebugLevel then
  6609. local frame = _G[tostring(DBM.Options.ChatFrame)]
  6610. frame = frame and frame:IsShown() and frame or DEFAULT_CHAT_FRAME
  6611. frame:AddMessage("|cffff7d0aDBM Debug:|r "..text, 1, 1, 1)
  6612. end
  6613. end
  6614.  
  6615. do
  6616. local testMod
  6617. local testWarning1, testWarning2, testWarning3
  6618. local testTimer1, testTimer2, testTimer3, testTimer4, testTimer5, testTimer6, testTimer7, testTimer8
  6619. local testCount1, testCount2
  6620. local testSpecialWarning1, testSpecialWarning2, testSpecialWarning3
  6621. function DBM:DemoMode()
  6622. if not testMod then
  6623. testMod = self:NewMod("TestMod")
  6624. self:GetModLocalization("TestMod"):SetGeneralLocalization{ name = "Test Mod" }
  6625. testWarning1 = testMod:NewAnnounce("%s", 1, "Interface\\Icons\\Spell_Nature_WispSplode")
  6626. testWarning2 = testMod:NewAnnounce("%s", 2, "Interface\\Icons\\Spell_Shadow_ShadesOfDarkness")
  6627. testWarning3 = testMod:NewAnnounce("%s", 3, "Interface\\Icons\\Spell_Fire_SelfDestruct")
  6628. testTimer1 = testMod:NewTimer(20, "%s", "Interface\\Icons\\Spell_Nature_WispSplode", nil, nil)
  6629. testTimer2 = testMod:NewTimer(20, "%s ", "Interface\\ICONS\\INV_Misc_Head_Orc_01.blp", nil, nil, 1)
  6630. testTimer3 = testMod:NewTimer(20, "%s ", "Interface\\Icons\\Spell_Shadow_ShadesOfDarkness", nil, nil, 3, DBM_CORE_MAGIC_ICON)
  6631. testTimer4 = testMod:NewTimer(20, "%s ", "Interface\\Icons\\Spell_Nature_WispSplode", nil, nil, 4, DBM_CORE_INTERRUPT_ICON)
  6632. testTimer5 = testMod:NewTimer(20, "%s ", "Interface\\Icons\\Spell_Fire_SelfDestruct", nil, nil, 2, DBM_CORE_HEALER_ICON)
  6633. testTimer6 = testMod:NewTimer(20, "%s ", "Interface\\Icons\\Spell_Nature_WispSplode", nil, nil, 5, DBM_CORE_TANK_ICON)
  6634. testTimer7 = testMod:NewTimer(20, "%s ", "Interface\\Icons\\Spell_Nature_WispSplode", nil, nil, 6)
  6635. testTimer8 = testMod:NewTimer(20, "%s ", "Interface\\Icons\\Spell_Nature_WispSplode", nil, nil, 7)
  6636. testCount1 = testMod:NewCountdown(0, 0, nil, nil, nil, true)
  6637. testCount2 = testMod:NewCountdown(0, 0, nil, nil, nil, true, true)
  6638. testSpecialWarning1 = testMod:NewSpecialWarning("%s")
  6639. testSpecialWarning2 = testMod:NewSpecialWarning(" %s ", nil, nil, nil, 2)
  6640. testSpecialWarning3 = testMod:NewSpecialWarning(" %s ", nil, nil, nil, 3) -- hack: non auto-generated special warnings need distinct names (we could go ahead and give them proper names with proper localization entries, but this is much easier)
  6641. end
  6642. testTimer1:Start(10, "Test Bar")
  6643. testTimer2:Start(30, "Adds")
  6644. testTimer3:Start(43, "Evil Debuff")
  6645. testTimer4:Start(20, "Important Interrupt")
  6646. testTimer5:Start(60, "Boom!")
  6647. testTimer6:Start(35, "Handle your Role")
  6648. testTimer7:Start(50, "Next Phase")
  6649. testTimer8:Start(55, "Custom User Bar")
  6650. testCount1:Cancel()
  6651. testCount1:Start(43)
  6652. testCount2:Cancel()
  6653. testCount2:Start(60)
  6654. testWarning1:Cancel()
  6655. testWarning2:Cancel()
  6656. testWarning3:Cancel()
  6657. testSpecialWarning1:Cancel()
  6658. testSpecialWarning2:Cancel()
  6659. testSpecialWarning3:Cancel()
  6660. testWarning1:Show("Test-mode started...")
  6661. testWarning1:Schedule(62, "Test-mode finished!")
  6662. testWarning3:Schedule(50, "Boom in 10 sec!")
  6663. testWarning3:Schedule(20, "Pew Pew Laser Owl!")
  6664. testWarning2:Schedule(38, "Evil Spell in 5 sec!")
  6665. testWarning2:Schedule(43, "Evil Spell!")
  6666. testWarning1:Schedule(10, "Test bar expired!")
  6667. testSpecialWarning1:Schedule(20, "Pew Pew Laser Owl")
  6668. testSpecialWarning2:Schedule(43, "Evil Spell!")
  6669. testSpecialWarning3:Schedule(60, "Boom!")
  6670. end
  6671. end
  6672.  
  6673. DBM.Bars:SetAnnounceHook(function(bar)
  6674. local prefix
  6675. if bar.color and bar.color.r == 1 and bar.color.g == 0 and bar.color.b == 0 then
  6676. prefix = DBM_CORE_HORDE or FACTION_HORDE
  6677. elseif bar.color and bar.color.r == 0 and bar.color.g == 0 and bar.color.b == 1 then
  6678. prefix = DBM_CORE_ALLIANCE or FACTION_ALLIANCE
  6679. end
  6680. if prefix then
  6681. return ("%s: %s %d:%02d"):format(prefix, _G[bar.frame:GetName().."BarName"]:GetText(), floor(bar.timer / 60), bar.timer % 60)
  6682. end
  6683. end)
  6684.  
  6685. function DBM:Capitalize(str)
  6686. local firstByte = str:byte(1, 1)
  6687. local numBytes = 1
  6688. if firstByte >= 0xF0 then -- firstByte & 0b11110000
  6689. numBytes = 4
  6690. elseif firstByte >= 0xE0 then -- firstByte & 0b11100000
  6691. numBytes = 3
  6692. elseif firstByte >= 0xC0 then -- firstByte & 0b11000000
  6693. numBytes = 2
  6694. end
  6695. return str:sub(1, numBytes):upper()..str:sub(numBytes + 1):lower()
  6696. end
  6697.  
  6698. --copied from big wigs with permission from funkydude. Modified by MysticalOS
  6699. function DBM:RoleCheck(ignoreLoot)
  6700. local spec = GetSpecialization()
  6701. if not spec then return end
  6702. local role = GetSpecializationRole(spec)
  6703. if not role then return end
  6704. local specID = GetLootSpecialization()
  6705. local _, _, _, _, lootrole = GetSpecializationInfoByID(specID)
  6706. if not InCombatLockdown() and ((IsPartyLFG() and (difficultyIndex == 14 or difficultyIndex == 15)) or not IsPartyLFG()) then
  6707. if UnitGroupRolesAssigned("player") ~= role then
  6708. UnitSetRole("player", role)
  6709. end
  6710. end
  6711. --Loot reminder even if spec isn't known or we are in LFR where we have a valid for role without us being ones that set us.
  6712. if not ignoreLoot and lootrole and (role ~= lootrole) and self.Options.RoleSpecAlert then
  6713. self:AddMsg(DBM_CORE_LOOT_SPEC_REMINDER:format(_G[role] or DBM_CORE_UNKNOWN, _G[lootrole]))
  6714. end
  6715. end
  6716.  
  6717. -- An anti spam function to throttle spammy events (e.g. SPELL_AURA_APPLIED on all group members)
  6718. -- @param time the time to wait between two events (optional, default 2.5 seconds)
  6719. -- @param id the id to distinguish different events (optional, only necessary if your mod keeps track of two different spam events at the same time)
  6720. function DBM:AntiSpam(time, id)
  6721. if GetTime() - (id and (self["lastAntiSpam" .. tostring(id)] or 0) or self.lastAntiSpam or 0) > (time or 2.5) then
  6722. if id then
  6723. self["lastAntiSpam" .. tostring(id)] = GetTime()
  6724. else
  6725. self.lastAntiSpam = GetTime()
  6726. end
  6727. return true
  6728. else
  6729. return false
  6730. end
  6731. end
  6732.  
  6733. function DBM:GetTOC()
  6734. return wowTOC
  6735. end
  6736.  
  6737. function DBM:FlashClientIcon()
  6738. if self:AntiSpam(5, "FLASH") then
  6739. FlashClientIcon()
  6740. end
  6741. end
  6742.  
  6743. --To speed up creating new mods.
  6744. function DBM:FindDungeonIDs()
  6745. for i=1, 2000 do
  6746. local dungeon = GetRealZoneText(i)
  6747. if dungeon and dungeon ~= "" then
  6748. self:AddMsg(i..": "..dungeon)
  6749. end
  6750. end
  6751. end
  6752.  
  6753. function DBM:FindInstanceIDs()
  6754. for i=1, 2000 do
  6755. local instance = EJ_GetInstanceInfo(i)
  6756. if instance then
  6757. self:AddMsg(i..": "..instance)
  6758. end
  6759. end
  6760. end
  6761.  
  6762. --/run DBM:FindEncounterIDs(875)--Tomb of Sargeras
  6763. --/run DBM:FindEncounterIDs(900, 23)--Cathedral of Eternal Night
  6764. function DBM:FindEncounterIDs(instanceID, diff)
  6765. if not instanceID then
  6766. self:AddMsg("Error: Function requires instanceID be provided")
  6767. end
  6768. if not diff then diff = 14 end--Default to "normal" in 6.0+ if diff arg not given.
  6769. EJ_SetDifficulty(diff)--Make sure it's set to right difficulty or it'll ignore mobs (ie ra-den if it's not set to heroic). Use user specified one as primary, with curernt zone difficulty as fallback
  6770. for i=1, 25 do
  6771. local name, _, encounterID = EJ_GetEncounterInfoByIndex(i, instanceID)
  6772. if name then
  6773. self:AddMsg(encounterID..": "..name)
  6774. end
  6775. end
  6776. end
  6777.  
  6778. --Taint the script that disables /run /dump, etc
  6779. --ScriptsDisallowedForBeta = function() return false end
  6780.  
  6781. -------------------
  6782. -- Movie Filter --
  6783. -------------------
  6784. do
  6785. local neverFilter = {
  6786. [486] = true, -- Tomb of Sarg Intro
  6787. [487] = true, -- Alliance Broken Shore cut-scene
  6788. [488] = true, -- Horde Broken Shore cut-scene
  6789. [489] = true, -- Unknown, currently encrypted
  6790. [490] = true, -- Unknown, currently encrypted
  6791. }
  6792. MovieFrame:HookScript("OnEvent", function(self, event, id)
  6793. if event == "PLAY_MOVIE" and id and not neverFilter[id] then
  6794. DBM:Debug("PLAY_MOVIE fired for ID: "..id, 2)
  6795. local isInstance, instanceType = IsInInstance()
  6796. if not isInstance or C_Garrison:IsOnGarrisonMap() or instanceType == "scenario" or DBM.Options.MovieFilter == "Never" then return end
  6797. if DBM.Options.MovieFilter == "Block" or DBM.Options.MovieFilter == "AfterFirst" and DBM.Options.MoviesSeen[id] then
  6798. MovieFrame_OnMovieFinished(self)
  6799. DBM:AddMsg(DBM_CORE_MOVIE_SKIPPED)
  6800. else
  6801. DBM.Options.MoviesSeen[id] = true
  6802. end
  6803. end
  6804. end)
  6805.  
  6806. function DBM:CINEMATIC_START()
  6807. self:Debug("CINEMATIC_START fired", 2)
  6808. local isInstance, instanceType = IsInInstance()
  6809. if not isInstance or C_Garrison:IsOnGarrisonMap() or instanceType == "scenario" or self.Options.MovieFilter == "Never" then return end
  6810. SetMapToCurrentZone()
  6811. local currentFloor = GetCurrentMapDungeonLevel() or 0
  6812. if self.Options.MovieFilter == "Block" or self.Options.MovieFilter == "AfterFirst" and self.Options.MoviesSeen[LastInstanceMapID..currentFloor] then
  6813. CinematicFrame_CancelCinematic()
  6814. self:AddMsg(DBM_CORE_MOVIE_SKIPPED)
  6815. else
  6816. self.Options.MoviesSeen[LastInstanceMapID..currentFloor] = true
  6817. end
  6818. end
  6819. end
  6820.  
  6821. ----------------------------
  6822. -- Boss Mod Constructor --
  6823. ----------------------------
  6824. do
  6825. local modsById = setmetatable({}, {__mode = "v"})
  6826. local mt = {__index = bossModPrototype}
  6827.  
  6828. function DBM:NewMod(name, modId, modSubTab, instanceId, nameModifier)
  6829. name = tostring(name) -- the name should never be a number of something as it confuses sync handlers that just receive some string and try to get the mod from it
  6830. if name == "DBM-ProfilesDummy" then return end
  6831. if modsById[name] then error("DBM:NewMod(): Mod names are used as IDs and must therefore be unique.", 2) end
  6832. local obj = setmetatable(
  6833. {
  6834. Options = {
  6835. Enabled = true,
  6836. },
  6837. DefaultOptions = {
  6838. Enabled = true,
  6839. },
  6840. subTab = modSubTab,
  6841. optionCategories = {
  6842. },
  6843. categorySort = {"announce", "announceother", "announcepersonal", "announcerole", "timer", "sound", "misc"},
  6844. id = name,
  6845. announces = {},
  6846. specwarns = {},
  6847. timers = {},
  6848. countdowns = {},
  6849. vb = {},
  6850. iconRestore = {},
  6851. modId = modId,
  6852. instanceId = instanceId,
  6853. revision = 0,
  6854. SyncThreshold = 8,
  6855. localization = self:GetModLocalization(name)
  6856. },
  6857. mt
  6858. )
  6859. for i, v in ipairs(self.AddOns) do
  6860. if v.modId == modId then
  6861. obj.addon = v
  6862. break
  6863. end
  6864. end
  6865.  
  6866. if tonumber(name) then
  6867. local t = EJ_GetEncounterInfo(tonumber(name))
  6868. if type(nameModifier) == "number" then--Get name form EJ_GetCreatureInfo
  6869. t = select(2, EJ_GetCreatureInfo(nameModifier, tonumber(name)))
  6870. elseif type(nameModifier) == "function" then--custom name modify function
  6871. t = nameModifier(t or name)
  6872. else--default name modify
  6873. t = tostring(t)
  6874. t = string.split(",", t or name)
  6875. end
  6876. obj.localization.general.name = t or name
  6877. obj.modelId = select(4, EJ_GetCreatureInfo(1, tonumber(name)))
  6878. elseif name:match("z%d+") then
  6879. local t = GetRealZoneText(string.sub(name, 2))
  6880. if type(nameModifier) == "number" then--do nothing
  6881. elseif type(nameModifier) == "function" then--custom name modify function
  6882. t = nameModifier(t or name)
  6883. else--default name modify
  6884. t = string.split(",", t or name)
  6885. end
  6886. obj.localization.general.name = t or name
  6887. elseif name:match("d%d+") then
  6888. local t = GetDungeonInfo(string.sub(name, 2))
  6889. if type(nameModifier) == "number" then--do nothing
  6890. elseif type(nameModifier) == "function" then--custom name modify function
  6891. t = nameModifier(t or name)
  6892. else--default name modify
  6893. t = string.split(",", t or name)
  6894. end
  6895. obj.localization.general.name = t or name
  6896. else
  6897. obj.localization.general.name = obj.localization.general.name or name
  6898. end
  6899. tinsert(self.Mods, obj)
  6900. if modId then
  6901. self.ModLists[modId] = self.ModLists[modId] or {}
  6902. tinsert(self.ModLists[modId], name)
  6903. end
  6904. modsById[name] = obj
  6905. obj:AddBoolOption("HealthFrame", false, "misc")
  6906. obj:SetZone()
  6907. return obj
  6908. end
  6909.  
  6910. function DBM:GetModByName(name)
  6911. return modsById[tostring(name)]
  6912. end
  6913. end
  6914.  
  6915. -----------------------
  6916. -- General Methods --
  6917. -----------------------
  6918. bossModPrototype.RegisterEvents = DBM.RegisterEvents
  6919. bossModPrototype.UnregisterInCombatEvents = DBM.UnregisterInCombatEvents
  6920. bossModPrototype.AddMsg = DBM.AddMsg
  6921. bossModPrototype.RegisterShortTermEvents = DBM.RegisterShortTermEvents
  6922. bossModPrototype.UnregisterShortTermEvents = DBM.UnregisterShortTermEvents
  6923.  
  6924. function bossModPrototype:SetZone(...)
  6925. if select("#", ...) == 0 then
  6926. self.zones = {}
  6927. if self.addon and self.addon.mapId then
  6928. for i, v in ipairs(self.addon.mapId) do
  6929. self.zones[v] = true
  6930. end
  6931. end
  6932. elseif select(1, ...) ~= DBM_DISABLE_ZONE_DETECTION then
  6933. self.zones = {}
  6934. for i = 1, select("#", ...) do
  6935. self.zones[select(i, ...)] = true
  6936. end
  6937. else -- disable zone detection
  6938. self.zones = nil
  6939. end
  6940. end
  6941.  
  6942. function bossModPrototype:Toggle()
  6943. if self.Options.Enabled then
  6944. self:DisableMod()
  6945. else
  6946. self:EnableMod()
  6947. end
  6948. end
  6949.  
  6950. function bossModPrototype:EnableMod()
  6951. self.Options.Enabled = true
  6952. end
  6953.  
  6954. function bossModPrototype:DisableMod()
  6955. self:Stop()
  6956. self.Options.Enabled = false
  6957. end
  6958.  
  6959. function bossModPrototype:Stop()
  6960. for i, v in ipairs(self.timers) do
  6961. v:Stop()
  6962. end
  6963. for i, v in ipairs(self.countdowns) do
  6964. v:Stop()
  6965. end
  6966. self:Unschedule()
  6967. end
  6968.  
  6969. function bossModPrototype:SetUsedIcons(...)
  6970. self.usedIcons = {}
  6971. for i = 1, select("#", ...) do
  6972. self.usedIcons[select(i, ...)] = true
  6973. end
  6974. end
  6975.  
  6976. function bossModPrototype:RegisterOnUpdateHandler(func, interval)
  6977. startScheduler()
  6978. if type(func) ~= "function" then return end
  6979. self.elapsed = 0
  6980. self.updateInterval = interval or 0
  6981. updateFunctions[self] = func
  6982. end
  6983.  
  6984. function bossModPrototype:UnregisterOnUpdateHandler()
  6985. self.elapsed = nil
  6986. self.updateInterval = nil
  6987. twipe(updateFunctions)
  6988. end
  6989.  
  6990. --------------
  6991. -- Events --
  6992. --------------
  6993. function bossModPrototype:RegisterEventsInCombat(...)
  6994. if self.inCombatOnlyEvents then
  6995. geterrorhandler()("combat events already set")
  6996. end
  6997. self.inCombatOnlyEvents = {...}
  6998. for k, v in pairs(self.inCombatOnlyEvents) do
  6999. if v:sub(0, 5) == "UNIT_" and v:sub(v:len() - 10) ~= "_UNFILTERED" and not v:find(" ") and v ~= "UNIT_DIED" and v ~= "UNIT_DESTROYED" then
  7000. -- legacy event, oh noes
  7001. self.inCombatOnlyEvents[k] = v .. " boss1 boss2 boss3 boss4 boss5 target focus"
  7002. end
  7003. end
  7004. end
  7005.  
  7006. -----------------------
  7007. -- Utility Methods --
  7008. -----------------------
  7009.  
  7010. function bossModPrototype:IsDifficulty(...)
  7011. local diff = DBM:GetCurrentInstanceDifficulty()
  7012. for i = 1, select("#", ...) do
  7013. if diff == select(i, ...) then
  7014. return true
  7015. end
  7016. end
  7017. return false
  7018. end
  7019.  
  7020. function bossModPrototype:IsLFR()
  7021. local diff = DBM:GetCurrentInstanceDifficulty()
  7022. if diff == "lfr" or diff == "lfr25" then
  7023. return true
  7024. end
  7025. return false
  7026. end
  7027.  
  7028. function bossModPrototype:IsEasy()
  7029. local diff = DBM:GetCurrentInstanceDifficulty()
  7030. if diff == "normal" or diff == "lfr" then
  7031. return true
  7032. end
  7033. return false
  7034. end
  7035.  
  7036. function bossModPrototype:IsHard()
  7037. local diff = DBM:GetCurrentInstanceDifficulty()
  7038. if diff == "mythic" or diff == "challenge5" then
  7039. return true
  7040. end
  7041. return false
  7042. end
  7043.  
  7044. function bossModPrototype:IsNormal()
  7045. local diff = DBM:GetCurrentInstanceDifficulty()
  7046. if diff == "normal" or diff == "normal5" or diff == "normal10" or diff == "normal25" then
  7047. return true
  7048. end
  7049. return false
  7050. end
  7051.  
  7052. function bossModPrototype:IsHeroic()
  7053. local diff = DBM:GetCurrentInstanceDifficulty()
  7054. if diff == "heroic" or diff == "heroic5" or diff == "heroic10" or diff == "heroic25" then
  7055. return true
  7056. end
  7057. return false
  7058. end
  7059.  
  7060. function bossModPrototype:IsMythic()
  7061. local diff = DBM:GetCurrentInstanceDifficulty()
  7062. if diff == "mythic" then
  7063. return true
  7064. end
  7065. return false
  7066. end
  7067.  
  7068. function bossModPrototype:IsEvent()
  7069. local diff = DBM:GetCurrentInstanceDifficulty()
  7070. if diff == "event5" or diff == "event20" or diff == "event40" then
  7071. return true
  7072. end
  7073. return false
  7074. end
  7075.  
  7076. function bossModPrototype:IsTrivial(level)
  7077. if difficultyIndex == 24 then return false end--Timewalker dungeon, ignore level and return false for trivial
  7078. if playerLevel >= level then
  7079. return true
  7080. end
  7081. return false
  7082. end
  7083.  
  7084. function bossModPrototype:CheckInterruptFilter(sourceGUID, skip)
  7085. if not DBM.Options.FilterInterrupt and not skip then return true end
  7086. if UnitGUID("target") == sourceGUID or UnitGUID("focus") == sourceGUID then
  7087. return true
  7088. end
  7089. return false
  7090. end
  7091.  
  7092. function bossModPrototype:CheckDispelFilter()
  7093. if not DBM.Options.FilterDispel then return true end
  7094. --Druid: Nature's Cure (88423), Remove Corruption (2782), Monk: Detox (115450), Priest: Purify (527), Plaadin: Cleanse (4987), Shaman: Cleanse Spirit (51886), Purify Spirit (77130), Mage: Remove Curse (475)
  7095. --start, duration, enable = GetSpellCooldown
  7096. --start & duration == 0 if spell not on cd
  7097. if (GetSpellCooldown(88423)) ~= 0 or (GetSpellCooldown(2782)) ~= 0 or (GetSpellCooldown(115450)) ~= 0 or (GetSpellCooldown(527)) ~= 0 or (GetSpellCooldown(4987)) ~= 0 or (GetSpellCooldown(51886)) ~= 0 or (GetSpellCooldown(77130)) ~= 0 or (GetSpellCooldown(475)) ~= 0 then
  7098. return false
  7099. end
  7100. return true
  7101. end
  7102.  
  7103. function bossModPrototype:IsCriteriaCompleted(criteriaIDToCheck)
  7104. if not criteriaIDToCheck then
  7105. geterrorhandler()("usage: mod:IsCriteriaComplected(criteriaId)")
  7106. return false
  7107. end
  7108. local _, _, numCriteria = C_Scenario.GetStepInfo()
  7109. for i = 1, numCriteria do
  7110. local _, _, criteriaCompleted, _, _, _, _, _, criteriaID = C_Scenario.GetCriteriaInfo(i)
  7111. if criteriaID == criteriaIDToCheck and criteriaCompleted then
  7112. return true
  7113. end
  7114. end
  7115. return false
  7116. end
  7117.  
  7118. function bossModPrototype:LatencyCheck()
  7119. return select(4, GetNetStats()) < DBM.Options.LatencyThreshold
  7120. end
  7121.  
  7122. function bossModPrototype:CheckBigWigs(name)
  7123. if raid[name] and raid[name].bwversion then
  7124. return raid[name].bwversion
  7125. else
  7126. return false
  7127. end
  7128. end
  7129.  
  7130. do
  7131. local iconStrings = {[1] = RAID_TARGET_1, [2] = RAID_TARGET_2, [3] = RAID_TARGET_3, [4] = RAID_TARGET_4, [5] = RAID_TARGET_5, [6] = RAID_TARGET_6, [7] = RAID_TARGET_7, [8] = RAID_TARGET_8,}
  7132. function bossModPrototype:IconNumToString(number)
  7133. return iconStrings[number] or number
  7134. end
  7135. end
  7136.  
  7137. bossModPrototype.AntiSpam = DBM.AntiSpam
  7138. bossModPrototype.HasMapRestrictions = DBM.HasMapRestrictions
  7139. bossModPrototype.GetUnitCreatureId = DBM.GetUnitCreatureId
  7140. bossModPrototype.GetCIDFromGUID = DBM.GetCIDFromGUID
  7141. bossModPrototype.IsCreatureGUID = DBM.IsCreatureGUID
  7142.  
  7143. do
  7144. local bossTargetuIds = {
  7145. "boss1", "boss2", "boss3", "boss4", "boss5", "focus", "target"
  7146. }
  7147. local targetScanCount = {}
  7148. local repeatedScanEnabled = {}
  7149.  
  7150. local function getBossTarget(guid, scanOnlyBoss)
  7151. local name, uid, bossuid
  7152. local cacheuid = bossuIdCache[guid] or "boss1"
  7153. if UnitGUID(cacheuid) == guid then
  7154. bossuid = cacheuid
  7155. name = DBM:GetUnitFullName(cacheuid.."target")
  7156. uid = cacheuid.."target"
  7157. bossuIdCache[guid] = bossuid
  7158. end
  7159. if name then return name, uid, bossuid end
  7160. for i, uId in ipairs(bossTargetuIds) do
  7161. if UnitGUID(uId) == guid then
  7162. bossuid = uId
  7163. name = DBM:GetUnitFullName(uId.."target")
  7164. uid = uId.."target"
  7165. bossuIdCache[guid] = bossuid
  7166. break
  7167. end
  7168. end
  7169. if name or scanOnlyBoss then return name, uid, bossuid end
  7170. -- Now lets check nameplates
  7171. for i = 1, 40 do
  7172. if UnitGUID("nameplate"..i) == guid then
  7173. bossuid = "nameplate"..i
  7174. name = DBM:GetUnitFullName("nameplate"..i.."target")
  7175. uid = "nameplate"..i.."target"
  7176. bossuIdCache[guid] = bossuid
  7177. break
  7178. end
  7179. end
  7180. if name then return name, uid, bossuid end
  7181. -- failed to detect from default uIds, scan all group members's target.
  7182. if IsInRaid() then
  7183. for i = 1, GetNumGroupMembers() do
  7184. if UnitGUID("raid"..i.."target") == guid then
  7185. bossuid = "raid"..i.."target"
  7186. name = DBM:GetUnitFullName("raid"..i.."targettarget")
  7187. uid = "raid"..i.."targettarget"
  7188. bossuIdCache[guid] = bossuid
  7189. break
  7190. end
  7191. end
  7192. elseif IsInGroup() then
  7193. for i = 1, GetNumSubgroupMembers() do
  7194. if UnitGUID("party"..i.."target") == guid then
  7195. bossuid = "party"..i.."target"
  7196. name = DBM:GetUnitFullName("party"..i.."targettarget")
  7197. uid = "party"..i.."targettarget"
  7198. bossuIdCache[guid] = bossuid
  7199. break
  7200. end
  7201. end
  7202. end
  7203. return name, uid, bossuid
  7204. end
  7205.  
  7206. function bossModPrototype:GetBossTarget(cidOrGuid, scanOnlyBoss)
  7207. local name, uid, bossuid
  7208. if type(cidOrGuid) == "number" then
  7209. local cidOrGuid = cidOrGuid or self.creatureId
  7210. local cacheuid = bossuIdCache[cidOrGuid] or "boss1"
  7211. if self:GetUnitCreatureId(cacheuid) == cidOrGuid then
  7212. bossuIdCache[cidOrGuid] = cacheuid
  7213. bossuIdCache[UnitGUID(cacheuid)] = cacheuid
  7214. name, uid, bossuid = getBossTarget(UnitGUID(cacheuid), scanOnlyBoss)
  7215. else
  7216. local found = false
  7217. for i, uId in ipairs(bossTargetuIds) do
  7218. if self:GetUnitCreatureId(uId) == cidOrGuid then
  7219. found = true
  7220. bossuIdCache[cidOrGuid] = uId
  7221. bossuIdCache[UnitGUID(uId)] = uId
  7222. name, uid, bossuid = getBossTarget(UnitGUID(uId), scanOnlyBoss)
  7223. break
  7224. end
  7225. end
  7226. if not found and not scanOnlyBoss then
  7227. if IsInRaid() then
  7228. for i = 1, GetNumGroupMembers() do
  7229. if self:GetUnitCreatureId("raid"..i.."target") == cidOrGuid then
  7230. bossuIdCache[cidOrGuid] = "raid"..i.."target"
  7231. bossuIdCache[UnitGUID("raid"..i.."target")] = "raid"..i.."target"
  7232. name, uid, bossuid = getBossTarget(UnitGUID("raid"..i.."target"))
  7233. break
  7234. end
  7235. end
  7236. elseif IsInGroup() then
  7237. for i = 1, GetNumSubgroupMembers() do
  7238. if self:GetUnitCreatureId("party"..i.."target") == cidOrGuid then
  7239. bossuIdCache[cidOrGuid] = "party"..i.."target"
  7240. bossuIdCache[UnitGUID("party"..i.."target")] = "party"..i.."target"
  7241. name, uid, bossuid = getBossTarget(UnitGUID("party"..i.."target"))
  7242. break
  7243. end
  7244. end
  7245. end
  7246. end
  7247. end
  7248. else
  7249. name, uid, bossuid = getBossTarget(cidOrGuid, scanOnlyBoss)
  7250. end
  7251. if uid then
  7252. local cid = DBM:GetUnitCreatureId(uid)
  7253. if cid == 24207 or cid == 80258 or cid == 87519 then--filter army of the dead/Garrison Footman (basically same thing as army)
  7254. return nil, nil, nil
  7255. end
  7256. end
  7257. return name, uid, bossuid
  7258. end
  7259.  
  7260. function bossModPrototype:BossTargetScannerAbort(cidOrGuid, returnFunc)
  7261. targetScanCount[cidOrGuid] = nil--Reset count for later use.
  7262. self:UnscheduleMethod("BossTargetScanner", cidOrGuid, returnFunc)
  7263. DBM:Debug("Boss target scan for "..cidOrGuid.." should be aborting.", 3)
  7264. end
  7265.  
  7266. function bossModPrototype:BossUnitTargetScannerAbort()
  7267. targetMonitor = nil
  7268. DBM:Debug("Boss unit target scan should be aborting.", 3)
  7269. end
  7270.  
  7271. function bossModPrototype:BossUnitTargetScanner(unitId, returnFunc, scanTime)
  7272. --UNIT_TARGET technique was originally used by DXE on heroic lich king back in wrath to give most accurate defile/shadow trap warnings. Recently bigwigs started using it.
  7273. --This is fastest and most accurate method for getting the target and probably should be used where it does work 100% of time.
  7274. --This method fails if boss is already looking at correct target!! This method needs to monitor a target change so it must start before that target change
  7275. --In most cases, using BossTargetScanner is probably still better, especially if boss is expected to look at target before or immediately on cast start
  7276. --Limited to only one unitTarget scanner at a time. TODO, maybe make targetMonitor a table or something to support more than one scan at a time?
  7277. --This code is much prettier if it's in mod, but then it'd require copying and pasting it all the time. SO ugly code in core more convinient.
  7278. local modId = self.id
  7279. local scanDuration = scanTime or 1.5
  7280. targetMonitor = modId.."\t"..unitId.."\t"..returnFunc
  7281. self:ScheduleMethod(scanDuration, "BossUnitTargetScannerAbort")--In case of BossUnitTargetScanner firing too late, and boss already having changed target before monitor started, it needs to abort after x seconds
  7282. end
  7283.  
  7284. function bossModPrototype:BossTargetScanner(cidOrGuid, returnFunc, scanInterval, scanTimes, scanOnlyBoss, isEnemyScan, isFinalScan, targetFilter, tankFilter)
  7285. --Increase scan count
  7286. local cidOrGuid = cidOrGuid or self.creatureId
  7287. if not cidOrGuid then return end
  7288. if not targetScanCount[cidOrGuid] then targetScanCount[cidOrGuid] = 0 end
  7289. targetScanCount[cidOrGuid] = targetScanCount[cidOrGuid] + 1
  7290. --Set default values
  7291. local scanInterval = scanInterval or 0.05
  7292. local scanTimes = scanTimes or 16
  7293. local targetname, targetuid, bossuid = self:GetBossTarget(cidOrGuid, scanOnlyBoss)
  7294. DBM:Debug("Boss target scan "..targetScanCount[cidOrGuid].." of "..scanTimes..", found target "..(targetname or "nil").." using "..(bossuid or "nil"), 3)--Doesn't hurt to keep this, as level 3
  7295. --Do scan
  7296. if targetname and targetname ~= DBM_CORE_UNKNOWN and (not targetFilter or (targetFilter and targetFilter ~= targetname)) then
  7297. if not IsInGroup() then scanTimes = 1 end--Solo, no reason to keep scanning, give faster warning. But only if first scan is actually a valid target, which is why i have this check HERE
  7298. if (isEnemyScan and UnitIsFriend("player", targetuid) or self:IsTanking(targetuid, bossuid)) and not isFinalScan then--On player scan, ignore tanks. On enemy scan, ignore friendly player.
  7299. if targetScanCount[cidOrGuid] < scanTimes then--Make sure no infinite loop.
  7300. self:ScheduleMethod(scanInterval, "BossTargetScanner", cidOrGuid, returnFunc, scanInterval, scanTimes, scanOnlyBoss, isEnemyScan, nil, targetFilter, tankFilter)--Scan multiple times to be sure it's not on something other then tank (or friend on enemy scan).
  7301. else--Go final scan.
  7302. self:BossTargetScanner(cidOrGuid, returnFunc, scanInterval, scanTimes, scanOnlyBoss, isEnemyScan, true, targetFilter, tankFilter)
  7303. end
  7304. else--Scan success. (or failed to detect right target.) But some spells can be used on tanks, anyway warns tank if player scan. (enemy scan block it)
  7305. targetScanCount[cidOrGuid] = nil--Reset count for later use.
  7306. self:UnscheduleMethod("BossTargetScanner", cidOrGuid, returnFunc)--Unschedule all checks just to be sure none are running, we are done.
  7307. if (tankFilter and self:IsTanking(targetuid, bossuid)) or (isFinalScan and isEnemyScan) then return end--If enemyScan and playerDetected, return nothing
  7308. self[returnFunc](self, targetname, targetuid, bossuid)--Return results to warning function with all variables.
  7309. end
  7310. else--target was nil, lets schedule a rescan here too.
  7311. if targetScanCount[cidOrGuid] < scanTimes then--Make sure not to infinite loop here as well.
  7312. self:ScheduleMethod(scanInterval, "BossTargetScanner", cidOrGuid, returnFunc, scanInterval, scanTimes, scanOnlyBoss, isEnemyScan, nil, targetFilter, tankFilter)
  7313. else
  7314. targetScanCount[cidOrGuid] = nil--Reset count for later use.
  7315. self:UnscheduleMethod("BossTargetScanner", cidOrGuid, returnFunc)--Unschedule all checks just to be sure none are running, we are done.
  7316. end
  7317. end
  7318. end
  7319.  
  7320. --infinite scanner. so use this carefully.
  7321. local function repeatedScanner(cidOrGuid, returnFunc, scanInterval, scanOnlyBoss, includeTank, mod)
  7322. if repeatedScanEnabled[returnFunc] then
  7323. local cidOrGuid = cidOrGuid or mod.creatureId
  7324. local scanInterval = scanInterval or 0.1
  7325. local targetname, targetuid, bossuid = mod:GetBossTarget(cidOrGuid, scanOnlyBoss)
  7326. if targetname and (includeTank or not mod:IsTanking(targetuid, bossuid)) then
  7327. mod[returnFunc](mod, targetname, targetuid, bossuid)
  7328. end
  7329. DBM:Schedule(scanInterval, repeatedScanner, cidOrGuid, returnFunc, scanInterval, scanOnlyBoss, includeTank, mod)
  7330. end
  7331. end
  7332.  
  7333. function bossModPrototype:StartRepeatedScan(cidOrGuid, returnFunc, scanInterval, scanOnlyBoss, includeTank)
  7334. repeatedScanEnabled[returnFunc] = true
  7335. repeatedScanner(cidOrGuid, returnFunc, scanInterval, scanOnlyBoss, includeTank, self)
  7336. end
  7337.  
  7338. function bossModPrototype:StopRepeatedScan(returnFunc)
  7339. repeatedScanEnabled[returnFunc] = nil
  7340. end
  7341. end
  7342.  
  7343. function bossModPrototype:CheckNearby(range, targetname)
  7344. local uId = DBM:GetRaidUnitId(targetname)
  7345. if uId and not UnitIsUnit("player", uId) then
  7346. local inRange = DBM.RangeCheck:GetDistance(uId)
  7347. if inRange and inRange < range then
  7348. return true
  7349. end
  7350. end
  7351. return false
  7352. end
  7353.  
  7354. do
  7355. local bossCache = {}
  7356. local lastTank = nil
  7357.  
  7358. function bossModPrototype:GetCurrentTank(cidOrGuid)
  7359. if lastTank and GetTime() - (bossCache[cidOrGuid] or 0) < 2 then -- return last tank within 2 seconds of call
  7360. return lastTank
  7361. else
  7362. local cidOrGuid = cidOrGuid or self.creatureId--GetBossTarget supports GUID or CID and it will automatically return correct values with EITHER ONE
  7363. local uId
  7364. local _, fallbackuId, mobuId = self:GetBossTarget(cidOrGuid)
  7365. if mobuId then--Have a valid mob unit ID
  7366. --First, use trust threat more than fallbackuId and see what we pull from it first.
  7367. --This is because for GetCurrentTank we want to know who is tanking it, not who it's targeting.
  7368. local unitId = (IsInRaid() and "raid") or "party"
  7369. for i = 0, GetNumGroupMembers() do
  7370. local id = (i == 0 and "target") or unitId..i
  7371. local tanking, status = UnitDetailedThreatSituation(id, mobuId)--Tanking may return 0 if npc is temporarily looking at an NPC (IE fracture) but status will still be 3 on true tank
  7372. if tanking or (status == 3) then uId = id end--Found highest threat target, make them uId
  7373. if uId then break end
  7374. end
  7375. --Did not get anything useful from threat, so use who the boss was looking at, at time of cast (ie fallbackuId)
  7376. if fallbackuId and not uId then
  7377. uId = fallbackuId
  7378. end
  7379. end
  7380. if uId then--Now we have a valid uId
  7381. bossCache[cidOrGuid] = GetTime()
  7382. lastTank = UnitName(uId)
  7383. return UnitName(lastTank), uId
  7384. end
  7385. return false
  7386. end
  7387. end
  7388. end
  7389.  
  7390. --Now this function works perfectly. But have some limitation due to DBM.RangeCheck:GetDistance() function.
  7391. --Unfortunely, DBM.RangeCheck:GetDistance() function cannot reflects altitude difference. This makes range unreliable.
  7392. --So, we need to cafefully check range in difference altitude (Especially, tower top and bottom)
  7393. do
  7394. local rangeCache = {}
  7395. local rangeUpdated = {}
  7396.  
  7397. function bossModPrototype:CheckTankDistance(cidOrGuid, distance, defaultReturn)
  7398. if not DBM.Options.DontShowFarWarnings then return true end--Global disable.
  7399. if rangeCache[cidOrGuid] and (GetTime() - (rangeUpdated[cidOrGuid] or 0)) < 2 then -- return same range within 2 sec call
  7400. if rangeCache[cidOrGuid] > distance then
  7401. return false
  7402. else
  7403. return true
  7404. end
  7405. else
  7406. local cidOrGuid = cidOrGuid or self.creatureId--GetBossTarget supports GUID or CID and it will automatically return correct values with EITHER ONE
  7407. local distance = distance or 40
  7408. local uId
  7409. local _, fallbackuId, mobuId = self:GetBossTarget(cidOrGuid)
  7410. if mobuId then--Have a valid mob unit ID
  7411. --First, use trust threat more than fallbackuId and see what we pull from it first.
  7412. --This is because for CheckTankDistance we want to know who is tanking it, not who it's targeting.
  7413. local unitId = (IsInRaid() and "raid") or "party"
  7414. for i = 0, GetNumGroupMembers() do
  7415. local id = (i == 0 and "target") or unitId..i
  7416. local tanking, status = UnitDetailedThreatSituation(id, mobuId)--Tanking may return 0 if npc is temporarily looking at an NPC (IE fracture) but status will still be 3 on true tank
  7417. if tanking or (status == 3) then uId = id end--Found highest threat target, make them uId
  7418. if uId then break end
  7419. end
  7420. --Did not get anything useful from threat, so use who the boss was looking at, at time of cast (ie fallbackuId)
  7421. if fallbackuId and not uId then
  7422. uId = fallbackuId
  7423. end
  7424. end
  7425. if uId then--Now we have a valid uId
  7426. if UnitIsUnit(uId, "player") then return true end--If "player" is target, avoid doing any complicated stuff
  7427. local inRange = 0
  7428. if not UnitIsPlayer(uId) then
  7429. local inRange2, checkedRange = UnitInRange(uId)
  7430. if checkedRange then--checkedRange only returns true if api worked, so if we get false, true then we are not near npc
  7431. return inRange2 and true or false
  7432. else--Its probably a totem or just something we can't assess. Fall back to no filtering
  7433. return true
  7434. end
  7435. else
  7436. inRange = DBM.RangeCheck:GetDistance("player", uId)--We check how far we are from the tank who has that boss
  7437. end
  7438. rangeCache[cidOrGuid] = inRange
  7439. rangeUpdated[cidOrGuid] = GetTime()
  7440. if inRange and (inRange > distance) then--You are not near the person tanking boss
  7441. return false
  7442. end
  7443. --Tank in range, return true.
  7444. return true
  7445. end
  7446. return (defaultReturn == nil) or defaultReturn--When we simply can't figure anything out, return true and allow warnings using this filter to fire. But some spells will prefer not to fire(i.e : Galakras tower spell), we can define it on this function calling.
  7447. end
  7448. end
  7449. end
  7450.  
  7451. do
  7452. local frame = CreateFrame("Frame", "DBMShields") -- frame for CLEU events, we don't want to run all *_MISSED events through the whole DBM event system...
  7453.  
  7454. local activeShields = {}
  7455. local shieldsByGuid = {}
  7456.  
  7457. frame:SetScript("OnEvent", function(self, _, _, subEvent, _, _, _, _, _, destGUID, _, _, _, ...)
  7458. local info = destGUID and shieldsByGuid[destGUID]
  7459. if info then
  7460. if info.maxAbsorb then
  7461. local _, absorbed
  7462. if subEvent == "SWING_MISSED" then
  7463. _, _, _, absorbed = ...
  7464. elseif subEvent == "RANGE_MISSED" or subEvent == "SPELL_MISSED" or subEvent == "SPELL_PERIODIC_MISSED" then
  7465. _, _, _, _, _, _, absorbed = ...
  7466. end
  7467. if absorbed then
  7468. info.absorbRemaining = info.absorbRemaining - absorbed
  7469. end
  7470. elseif info.maxDamage then
  7471. local _, damage
  7472. if subEvent == "SWING_DAMAGE" then
  7473. damage = ...
  7474. elseif subEvent == "RANGE_DAMAGE" or subEvent == "SPELL_DAMAGE" or subEvent == "SPELL_PERIODIC_DAMAGE" then
  7475. _, _, _, damage = ...
  7476. end
  7477. if damage then
  7478. info.damageRemaining = info.damageRemaining - damage
  7479. end
  7480. elseif info.maxHeal then
  7481. local _, absorbed
  7482. if subEvent == "SPELL_HEAL" or subEvent == "SPELL_PERIODIC_HEAL" then
  7483. _, _, _, _, _, absorbed = ...
  7484. end
  7485. if absorbed then
  7486. info.healed = info.healed + absorbed
  7487. end
  7488. end
  7489. end
  7490. end)
  7491.  
  7492. local function getShieldHPFunc(info)
  7493. return function()
  7494. return mmax(1, floor(info.absorbRemaining / info.maxAbsorb * 100))
  7495. end
  7496. end
  7497.  
  7498. function bossModPrototype:ShowShieldHealthBar(guid, name, absorb)
  7499. self:RemoveShieldHealthBar(guid, name)
  7500. if #activeShields == 0 then
  7501. frame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
  7502. end
  7503. local obj = {
  7504. mod = self.id,
  7505. name = name,
  7506. guid = guid,
  7507. absorbRemaining = absorb,
  7508. maxAbsorb = absorb,
  7509. }
  7510. obj.func = getShieldHPFunc(obj)
  7511. activeShields[#activeShields + 1] = obj
  7512. shieldsByGuid[guid] = obj
  7513. if DBM.BossHealth:IsShown() then
  7514. DBM.BossHealth:AddBoss(obj.func, name)
  7515. end
  7516. end
  7517.  
  7518. function bossModPrototype:RemoveShieldHealthBar(guid, name)
  7519. if not guid then
  7520. self:RemoveAllSpecialHealthBars()
  7521. else
  7522. shieldsByGuid[guid] = nil
  7523. for i = #activeShields, 1, -1 do
  7524. if activeShields[i].guid == guid and activeShields[i].mod == self.id and (not name or activeShields[i].name == name) then
  7525. if DBM.BossHealth:IsShown() then
  7526. DBM.BossHealth:RemoveBoss(activeShields[i].func)
  7527. end
  7528. tremove(activeShields, i)
  7529. end
  7530. end
  7531. if #activeShields == 0 then
  7532. frame:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
  7533. end
  7534. end
  7535. end
  7536.  
  7537. local function getDamagedHPFunc(info)
  7538. return function()
  7539. return mmax(1, floor(info.damageRemaining / info.maxDamage * 100))
  7540. end
  7541. end
  7542.  
  7543. function bossModPrototype:ShowDamagedHealthBar(guid, name, damage)
  7544. self:RemoveDamagedHealthBar(guid, name)
  7545. if #activeShields == 0 then
  7546. frame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
  7547. end
  7548. local obj = {
  7549. mod = self.id,
  7550. name = name,
  7551. guid = guid,
  7552. damageRemaining = damage,
  7553. maxDamage = damage,
  7554. }
  7555. obj.func = getDamagedHPFunc(obj)
  7556. activeShields[#activeShields + 1] = obj
  7557. shieldsByGuid[guid] = obj
  7558. if DBM.BossHealth:IsShown() then
  7559. DBM.BossHealth:AddBoss(obj.func, name)
  7560. end
  7561. end
  7562. bossModPrototype.RemoveDamagedHealthBar = bossModPrototype.RemoveShieldHealthBar
  7563.  
  7564. local function getHealedHPFunc(info)
  7565. return function()
  7566. return mmax(1, floor(info.healed / info.maxHeal * 100))
  7567. end
  7568. end
  7569.  
  7570. function bossModPrototype:ShowAbsorbedHealHealthBar(guid, name, heal)
  7571. self:RemoveAbsorbedHealHealthBar(guid, name)
  7572. if #activeShields == 0 then
  7573. frame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
  7574. end
  7575. local obj = {
  7576. mod = self.id,
  7577. name = name,
  7578. guid = guid,
  7579. healed = 0,
  7580. maxHeal = heal,
  7581. }
  7582. obj.func = getHealedHPFunc(obj)
  7583. activeShields[#activeShields + 1] = obj
  7584. shieldsByGuid[guid] = obj
  7585. if DBM.BossHealth:IsShown() then
  7586. DBM.BossHealth:AddBoss(obj.func, name)
  7587. end
  7588. end
  7589. bossModPrototype.RemoveAbsorbedHealHealthBar = bossModPrototype.RemoveShieldHealthBar
  7590.  
  7591. -- removes all shield, damaged, healed bars of a boss mod
  7592. function bossModPrototype:RemoveAllSpecialHealthBars()
  7593. frame:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
  7594. for i = #activeShields, 1, -1 do
  7595. if activeShields[i].mod == self.id then
  7596. if DBM.BossHealth:IsShown() then
  7597. DBM.BossHealth:RemoveBoss(activeShields[i].func)
  7598. end
  7599. shieldsByGuid[activeShields[i].guid] = nil
  7600. tremove(activeShields, i)
  7601. end
  7602. end
  7603. end
  7604. end
  7605.  
  7606. ---------------------
  7607. -- Class Methods --
  7608. ---------------------
  7609. do
  7610. --[[local specFlags ={
  7611. ["Tank"] = true,
  7612. ["Dps"] = true,
  7613. ["Healer"] = true,
  7614. ["Melee"] = true,
  7615. ["MeleeDps"] = true,
  7616. ["Physical"] = true,
  7617. ["Ranged"] = true,
  7618. ["RangedDps"] = true,
  7619. ["ManaUser"] = true,--Affected by things like mana drains, or mana detonation, etc
  7620. ["SpellCaster"] = true,--Has channeled casts, can be interrupted/spell locked by roars, etc
  7621. ["RaidCooldown"] = true,
  7622. ["RemovePoison"] = true,
  7623. ["RemoveDisease"] = true,
  7624. ["RemoveEnrage"] = true,--Depricated, no one can remove enrage anymore
  7625. ["RemoveCurse"] = true,
  7626. ["MagicDispeller"] = true--Buffs on targets, not debuffs on players
  7627. ["HasInterrupt"] = true,--Has an interrupt that is 24 seconds or less CD.
  7628. }]]
  7629.  
  7630. local specRoleTable = {
  7631. [62] = { --Arcane Mage
  7632. ["Dps"] = true,
  7633. ["Ranged"] = true,
  7634. ["RangedDps"] = true,
  7635. ["ManaUser"] = true,
  7636. ["SpellCaster"] = true,
  7637. ["MagicDispeller"] = true,
  7638. ["HasInterrupt"] = true,
  7639. },
  7640. [65] = { --Holy Paladin
  7641. ["Healer"] = true,
  7642. ["Ranged"] = true,
  7643. ["ManaUser"] = true,
  7644. ["SpellCaster"] = true,
  7645. ["RaidCooldown"] = true,--Devotion Aura
  7646. ["RemovePoison"] = true,
  7647. ["RemoveDisease"] = true,
  7648. },
  7649. [66] = { --Protection Paladin
  7650. ["Tank"] = true,
  7651. ["Melee"] = true,
  7652. ["ManaUser"] = true,
  7653. ["Physical"] = true,
  7654. ["RemovePoison"] = true,
  7655. ["RemoveDisease"] = true,
  7656. ["HasInterrupt"] = true,
  7657. },
  7658. [70] = { --Retribution Paladin
  7659. ["Dps"] = true,
  7660. ["Melee"] = true,
  7661. ["MeleeDps"] = true,
  7662. ["ManaUser"] = true,
  7663. ["Physical"] = true,
  7664. ["RemovePoison"] = true,
  7665. ["RemoveDisease"] = true,
  7666. ["HasInterrupt"] = true,
  7667. },
  7668. [71] = { --Arms Warrior
  7669. ["Dps"] = true,
  7670. ["Melee"] = true,
  7671. ["MeleeDps"] = true,
  7672. ["RaidCooldown"] = true,--Rallying Cry
  7673. ["Physical"] = true,
  7674. ["HasInterrupt"] = true,
  7675. },
  7676. [73] = { --Protection Warrior
  7677. ["Tank"] = true,
  7678. ["Melee"] = true,
  7679. ["Physical"] = true,
  7680. ["HasInterrupt"] = true,
  7681. },
  7682. [102] = { --Balance Druid
  7683. ["Dps"] = true,
  7684. ["Ranged"] = true,
  7685. ["RangedDps"] = true,
  7686. ["ManaUser"] = true,
  7687. ["SpellCaster"] = true,
  7688. ["RemoveCurse"] = true,
  7689. ["RemovePoison"] = true,
  7690. },
  7691. [103] = { --Feral Druid
  7692. ["Dps"] = true,
  7693. ["Melee"] = true,
  7694. ["MeleeDps"] = true,
  7695. ["Physical"] = true,
  7696. ["RemoveCurse"] = true,
  7697. ["RemovePoison"] = true,
  7698. ["HasInterrupt"] = true,
  7699. },
  7700. [104] = { --Guardian Druid
  7701. ["Tank"] = true,
  7702. ["Melee"] = true,
  7703. ["Physical"] = true,
  7704. ["RemoveCurse"] = true,
  7705. ["RemovePoison"] = true,
  7706. ["HasInterrupt"] = true,
  7707. },
  7708. [105] = { -- Restoration Druid
  7709. ["Healer"] = true,
  7710. ["Ranged"] = true,
  7711. ["ManaUser"] = true,
  7712. ["SpellCaster"] = true,
  7713. ["RaidCooldown"] = true,--Tranquility
  7714. ["RemoveCurse"] = true,
  7715. ["RemovePoison"] = true,
  7716. },
  7717. [250] = { --Blood DK
  7718. ["Tank"] = true,
  7719. ["Melee"] = true,
  7720. ["Physical"] = true,
  7721. ["HasInterrupt"] = true,
  7722. },
  7723. [251] = { --Frost DK
  7724. ["Dps"] = true,
  7725. ["Melee"] = true,
  7726. ["MeleeDps"] = true,
  7727. ["Physical"] = true,
  7728. ["HasInterrupt"] = true,
  7729. },
  7730. [253] = { --Beastmaster Hunter
  7731. ["Dps"] = true,
  7732. ["Ranged"] = true,
  7733. ["RangedDps"] = true,
  7734. ["Physical"] = true,
  7735. ["HasInterrupt"] = true,
  7736. },
  7737. [255] = { --Survival Hunter (Legion)
  7738. ["Dps"] = true,
  7739. ["Melee"] = true,
  7740. ["MeleeDps"] = true,
  7741. ["Physical"] = true,
  7742. ["HasInterrupt"] = true,
  7743. },
  7744. [256] = { --Discipline Priest
  7745. ["Healer"] = true,
  7746. ["Ranged"] = true,
  7747. ["ManaUser"] = true,
  7748. ["SpellCaster"] = true,
  7749. ["RaidCooldown"] = true,--Power Word: Barrier(Discipline) / Divine Hymn (Holy)
  7750. ["RemoveDisease"] = true,
  7751. ["MagicDispeller"] = true,
  7752. },
  7753. [258] = { --Shadow Priest
  7754. ["Dps"] = true,
  7755. ["Ranged"] = true,
  7756. ["RangedDps"] = true,
  7757. ["ManaUser"] = true,
  7758. ["SpellCaster"] = true,
  7759. ["MagicDispeller"] = true,
  7760. },
  7761. [259] = { --Assassination Rogue
  7762. ["Dps"] = true,
  7763. ["Melee"] = true,
  7764. ["MeleeDps"] = true,
  7765. ["Physical"] = true,
  7766. ["HasInterrupt"] = true,
  7767. },
  7768. [262] = { --Elemental Shaman
  7769. ["Dps"] = true,
  7770. ["Ranged"] = true,
  7771. ["RangedDps"] = true,
  7772. ["ManaUser"] = true,
  7773. ["SpellCaster"] = true,
  7774. ["RemoveCurse"] = true,
  7775. ["MagicDispeller"] = true,
  7776. ["HasInterrupt"] = true,
  7777. },
  7778. [263] = { --Enhancement Shaman
  7779. ["Dps"] = true,
  7780. ["Melee"] = true,
  7781. ["MeleeDps"] = true,
  7782. ["ManaUser"] = true,
  7783. ["SpellCaster"] = true,
  7784. ["Physical"] = true,
  7785. ["RemoveCurse"] = true,
  7786. ["MagicDispeller"] = true,
  7787. ["HasInterrupt"] = true,
  7788. },
  7789. [264] = { --Restoration Shaman
  7790. ["Healer"] = true,
  7791. ["Ranged"] = true,
  7792. ["ManaUser"] = true,
  7793. ["SpellCaster"] = true,
  7794. ["RaidCooldown"] = true,--Spirit Link Totem
  7795. ["RemoveCurse"] = true,
  7796. ["MagicDispeller"] = true,
  7797. ["HasInterrupt"] = true,
  7798. },
  7799. [265] = { --Affliction Warlock
  7800. ["Dps"] = true,
  7801. ["Ranged"] = true,
  7802. ["RangedDps"] = true,
  7803. ["ManaUser"] = true,
  7804. ["SpellCaster"] = true,
  7805. },
  7806. [268] = { --Brewmaster Monk
  7807. ["Tank"] = true,
  7808. ["Melee"] = true,
  7809. ["Physical"] = true,
  7810. ["RemovePoison"] = true,
  7811. ["RemoveDisease"] = true,
  7812. ["HasInterrupt"] = true,
  7813. },
  7814. [269] = { --Windwalker Monk
  7815. ["Dps"] = true,
  7816. ["Melee"] = true,
  7817. ["MeleeDps"] = true,
  7818. ["Physical"] = true,
  7819. ["RemovePoison"] = true,
  7820. ["RemoveDisease"] = true,
  7821. ["HasInterrupt"] = true,
  7822. },
  7823. [270] = { --Mistweaver Monk
  7824. ["Healer"] = true,
  7825. ["Melee"] = true,
  7826. ["Ranged"] = true,
  7827. ["ManaUser"] = true,
  7828. ["SpellCaster"] = true,
  7829. ["RaidCooldown"] = true,--Revival
  7830. ["RemovePoison"] = true,
  7831. ["RemoveDisease"] = true,
  7832. },
  7833. [577] = { --Havok Demon Hunter
  7834. ["Dps"] = true,
  7835. ["Melee"] = true,
  7836. ["MeleeDps"] = true,
  7837. ["Physical"] = true,
  7838. ["HasInterrupt"] = true,
  7839. },
  7840. [581] = { --Vengeance Demon Hunter
  7841. ["Tank"] = true,
  7842. ["Melee"] = true,
  7843. ["Physical"] = true,
  7844. ["HasInterrupt"] = true,
  7845. },
  7846. }
  7847. specRoleTable[63] = specRoleTable[62]--Frost Mage
  7848. specRoleTable[64] = specRoleTable[62]--Fire Mage
  7849. specRoleTable[72] = specRoleTable[71]--Fury Warrior
  7850. specRoleTable[252] = specRoleTable[251]--Unholy DK
  7851. specRoleTable[254] = specRoleTable[253]--Markmanship Hunter
  7852. specRoleTable[257] = specRoleTable[256]--Holy Priest
  7853. specRoleTable[260] = specRoleTable[259]--Combat Rogue
  7854. specRoleTable[261] = specRoleTable[259]--Subtlety Rogue
  7855. specRoleTable[266] = specRoleTable[265]--Demonology Warlock
  7856. specRoleTable[267] = specRoleTable[265]--Destruction Warlock
  7857.  
  7858. --[[function bossModPrototype:GetRoleFlagValue(flag)
  7859. if not flag then return false end
  7860. local flags = {strsplit("|", flag)}
  7861. for i = 1, #flags do
  7862. local flagText = flags[i]
  7863. flagText = flagText:gsub("-", "")
  7864. if not specFlags[flagText] then
  7865. print("bad flag found : "..flagText)
  7866. end
  7867. end
  7868. self:GetRoleFlagValue2(flag)
  7869. end]]
  7870.  
  7871. --to check flag is correct, remove comment block specFlags table and GetRoleFlagValue function, change this to GetRoleFlagValue2
  7872. --disable flag check normally because double flag check comsumes more cpu on mod load.
  7873. function bossModPrototype:GetRoleFlagValue(flag)
  7874. if not flag then return false end
  7875. if not currentSpecID then
  7876. DBM:SetCurrentSpecInfo()
  7877. end
  7878. local flags = {strsplit("|", flag)}
  7879. for i = 1, #flags do
  7880. local flagText = flags[i]
  7881. if flagText:match("^-") then
  7882. flagText = flagText:gsub("-", "")
  7883. if not specRoleTable[currentSpecID][flagText] then
  7884. return true
  7885. end
  7886. else
  7887. if specRoleTable[currentSpecID][flagText] then
  7888. return true
  7889. end
  7890. end
  7891. end
  7892. return false
  7893. end
  7894.  
  7895. function bossModPrototype:IsMeleeDps(uId)
  7896. if uId then--This version includes ONLY melee dps
  7897. local role = UnitGroupRolesAssigned(uId)
  7898. if role == "HEALER" or role == "TANK" then--Auto filter healer from dps check
  7899. return false
  7900. end
  7901. local _, class = UnitClass(uId)
  7902. if class == "WARRIOR" or class == "ROGUE" or class == "DEATHKNIGHT" then
  7903. return true
  7904. end
  7905. --Inspect throttle exists, so have to do it this way
  7906. if class == "DRUID" or class == "SHAMAN" or class == "PALADIN" or class == "MONK" or class == "HUNTER" then
  7907. local unitMaxPower = UnitPowerMax(uId)
  7908. --Mark and beast have 120 base focus, survival has 100 base focus. Not sure if this is best way to do it or if it breaks with talent/artifact weapon
  7909. --Elemental shaman have 100 unit power base, while enhancement have 150 power base, so a shaman with > 150 but less tha 35000 is the melee one
  7910. if (unitMaxPower < 101 and class == "HUNTER") or (unitMaxPower >= 150 and class == "SHAMAN" and unitMaxPower < 35000) or unitMaxPower < 35000 then
  7911. return true
  7912. end
  7913. end
  7914. return false
  7915. end
  7916. if not currentSpecID then
  7917. DBM:SetCurrentSpecInfo()
  7918. end
  7919. if specRoleTable[currentSpecID]["MeleeDps"] then
  7920. return true
  7921. else
  7922. return false
  7923. end
  7924. end
  7925.  
  7926. function bossModPrototype:IsMelee(uId)
  7927. if uId then--This version includes monk healers as melee and tanks as melee
  7928. local _, class = UnitClass(uId)
  7929. if class == "WARRIOR" or class == "ROGUE" or class == "DEATHKNIGHT" or class == "MONK" then
  7930. return true
  7931. end
  7932. --Inspect throttle exists, so have to do it this way
  7933. if class == "DRUID" or class == "SHAMAN" or class == "PALADIN" then
  7934. if UnitPowerMax(uId) < 35000 then
  7935. return true
  7936. end
  7937. end
  7938. return false
  7939. end
  7940. if not currentSpecID then
  7941. DBM:SetCurrentSpecInfo()
  7942. end
  7943. if specRoleTable[currentSpecID]["Melee"] then
  7944. return true
  7945. else
  7946. return false
  7947. end
  7948. end
  7949.  
  7950. function bossModPrototype:IsRanged()
  7951. if not currentSpecID then
  7952. DBM:SetCurrentSpecInfo()
  7953. end
  7954. if specRoleTable[currentSpecID]["Ranged"] then
  7955. return true
  7956. else
  7957. return false
  7958. end
  7959. end
  7960.  
  7961. function bossModPrototype:IsSpellCaster()
  7962. if not currentSpecID then
  7963. DBM:SetCurrentSpecInfo()
  7964. end
  7965. if specRoleTable[currentSpecID]["SpellCaster"] then
  7966. return true
  7967. else
  7968. return false
  7969. end
  7970. end
  7971.  
  7972. function bossModPrototype:IsMagicDispeller()
  7973. if not currentSpecID then
  7974. DBM:SetCurrentSpecInfo()
  7975. end
  7976. if specRoleTable[currentSpecID]["MagicDispeller"] then
  7977. return true
  7978. else
  7979. return false
  7980. end
  7981. end
  7982. end
  7983.  
  7984. function bossModPrototype:IsTank()
  7985. --IsTanking already handles external calls, no need here.
  7986. if not currentSpecID then
  7987. DBM:SetCurrentSpecInfo()
  7988. end
  7989. local _, _, _, _, role = GetSpecializationInfoByID(currentSpecID)
  7990. if role == "TANK" then
  7991. return true
  7992. else
  7993. return false
  7994. end
  7995. end
  7996.  
  7997. function bossModPrototype:IsDps(uId)
  7998. if uId then--External unit call.
  7999. if UnitGroupRolesAssigned(uId) == "DAMAGER" then
  8000. return true
  8001. end
  8002. return false
  8003. end
  8004. if not currentSpecID then
  8005. DBM:SetCurrentSpecInfo()
  8006. end
  8007. local _, _, _, _, role = GetSpecializationInfoByID(currentSpecID)
  8008. if role == "DAMAGER" then
  8009. return true
  8010. else
  8011. return false
  8012. end
  8013. end
  8014.  
  8015. function bossModPrototype:IsHealer(uId)
  8016. if uId then--External unit call.
  8017. if UnitGroupRolesAssigned(uId) == "HEALER" then
  8018. return true
  8019. end
  8020. return false
  8021. end
  8022. if not currentSpecID then
  8023. DBM:SetCurrentSpecInfo()
  8024. end
  8025. local _, _, _, _, role = GetSpecializationInfoByID(currentSpecID)
  8026. if role == "HEALER" then
  8027. return true
  8028. else
  8029. return false
  8030. end
  8031. end
  8032.  
  8033. function bossModPrototype:IsTanking(unit, boss)
  8034. if not unit then
  8035. DBM:Debug("IsTanking passed with invalid unit", 2)
  8036. return false
  8037. end
  8038. --Prefer threat target first
  8039. if boss and UnitExists(boss) then--Only checking one bossID as requested
  8040. local tanking, status = UnitDetailedThreatSituation(unit, boss)
  8041. if tanking or (status == 3) then
  8042. return true
  8043. end
  8044. else--Check all of them if one isn't defined
  8045. for i = 1, 5 do
  8046. if UnitExists("boss"..i) then
  8047. local tanking, status = UnitDetailedThreatSituation(unit, "boss"..i)
  8048. if tanking or (status == 3) then
  8049. return true
  8050. end
  8051. end
  8052. end
  8053. end
  8054. --Use these as fallback if threat target not found
  8055. if GetPartyAssignment("MAINTANK", unit, 1) then
  8056. return true
  8057. end
  8058. if UnitGroupRolesAssigned(unit) == "TANK" then
  8059. return true
  8060. end
  8061. return false
  8062. end
  8063.  
  8064. function bossModPrototype:GetNumAliveTanks()
  8065. if not IsInGroup() then return 1 end--Solo raid, you're the "tank"
  8066. local count = 0
  8067. local uId = (IsInRaid() and "raid") or "party"
  8068. for i = 1, DBM:GetNumRealGroupMembers() do
  8069. if UnitGroupRolesAssigned(uId..i) == "TANK" and not UnitIsDeadOrGhost(uId..i) then
  8070. count = count + 1
  8071. end
  8072. end
  8073. return count
  8074. end
  8075.  
  8076. ----------------------------
  8077. -- Boss Health Function --
  8078. ----------------------------
  8079. function DBM:GetBossHP(cId)
  8080. local uId = bossHealthuIdCache[cId] or "target"
  8081. if self:GetCIDFromGUID(UnitGUID(uId)) == cId and UnitHealthMax(uId) ~= 0 then
  8082. if bossHealth[cId] and (UnitHealth(uId) == 0 and not UnitIsDead(uId)) then return bossHealth[cId], uId, UnitName(uId) end--Return last non 0 value if value is 0, since it's last valid value we had.
  8083. local hp = UnitHealth(uId) / UnitHealthMax(uId) * 100
  8084. bossHealth[cId] = hp
  8085. return hp, uId, UnitName(uId)
  8086. elseif self:GetCIDFromGUID(UnitGUID("focus")) == cId and UnitHealthMax("focus") ~= 0 then
  8087. if bossHealth[cId] and (UnitHealth("focus") == 0 and not UnitIsDead("focus")) then return bossHealth[cId], "focus", UnitName("focus") end--Return last non 0 value if value is 0, since it's last valid value we had.
  8088. local hp = UnitHealth("focus") / UnitHealthMax("focus") * 100
  8089. bossHealth[cId] = hp
  8090. return hp, "focus", UnitName("focus")
  8091. else
  8092. for i = 1, 5 do
  8093. local guid = UnitGUID("boss"..i)
  8094. if self:GetCIDFromGUID(guid) == cId and UnitHealthMax("boss"..i) ~= 0 then
  8095. if bossHealth[cId] and (UnitHealth("boss"..i) == 0 and not UnitIsDead("boss"..i)) then return bossHealth[cId], "boss"..i, UnitName("boss"..i) end--Return last non 0 value if value is 0, since it's last valid value we had.
  8096. local hp = UnitHealth("boss"..i) / UnitHealthMax("boss"..i) * 100
  8097. bossHealth[cId] = hp
  8098. bossHealthuIdCache[cId] = "boss"..i
  8099. return hp, "boss"..i, UnitName("boss"..i)
  8100. end
  8101. end
  8102. local idType = (IsInRaid() and "raid") or "party"
  8103. for i = 0, GetNumGroupMembers() do
  8104. local unitId = ((i == 0) and "target") or idType..i.."target"
  8105. local guid = UnitGUID(unitId)
  8106. if self:GetCIDFromGUID(guid) == cId and UnitHealthMax(unitId) ~= 0 then
  8107. if bossHealth[cId] and (UnitHealth(unitId) == 0 and not UnitIsDead(unitId)) then return bossHealth[cId], unitId, UnitName(unitId) end--Return last non 0 value if value is 0, since it's last valid value we had.
  8108. local hp = UnitHealth(unitId) / UnitHealthMax(unitId) * 100
  8109. bossHealth[cId] = hp
  8110. bossHealthuIdCache[cId] = unitId
  8111. return hp, unitId, UnitName(unitId)
  8112. end
  8113. end
  8114. end
  8115. return nil
  8116. end
  8117.  
  8118. function DBM:GetBossHPByGUID(guid)
  8119. local uId = bossHealthuIdCache[guid] or "target"
  8120. if UnitGUID(uId) == guid and UnitHealthMax(uId) ~= 0 then
  8121. if bossHealth[guid] and (UnitHealth(uId) == 0 and not UnitIsDead(uId)) then return bossHealth[guid], uId, UnitName(uId) end--Return last non 0 value if value is 0, since it's last valid value we had.
  8122. local hp = UnitHealth(uId) / UnitHealthMax(uId) * 100
  8123. bossHealth[guid] = hp
  8124. return hp, uId, UnitName(uId)
  8125. elseif UnitGUID("focus") == guid and UnitHealthMax("focus") ~= 0 then
  8126. if bossHealth[guid] and (UnitHealth("focus") == 0 and not UnitIsDead("focus")) then return bossHealth[guid], "focus", UnitName("focus") end--Return last non 0 value if value is 0, since it's last valid value we had.
  8127. local hp = UnitHealth("focus") / UnitHealthMax("focus") * 100
  8128. bossHealth[guid] = hp
  8129. return hp, "focus", UnitName("focus")
  8130. else
  8131. for i = 1, 5 do
  8132. local guid2 = UnitGUID("boss"..i)
  8133. if guid == guid2 and UnitHealthMax("boss"..i) ~= 0 then
  8134. if bossHealth[guid] and (UnitHealth("boss"..i) == 0 and not UnitIsDead("boss"..i)) then return bossHealth[guid], "boss"..i, UnitName("boss"..i) end--Return last non 0 value if value is 0, since it's last valid value we had.
  8135. local hp = UnitHealth("boss"..i) / UnitHealthMax("boss"..i) * 100
  8136. bossHealth[guid] = hp
  8137. bossHealthuIdCache[guid] = "boss"..i
  8138. return hp, "boss"..i, UnitName("boss"..i)
  8139. end
  8140. end
  8141. local idType = (IsInRaid() and "raid") or "party"
  8142. for i = 0, GetNumGroupMembers() do
  8143. local unitId = ((i == 0) and "target") or idType..i.."target"
  8144. local guid2 = UnitGUID(unitId)
  8145. if guid == guid2 and UnitHealthMax(unitId) ~= 0 then
  8146. if bossHealth[guid] and (UnitHealth(unitId) == 0 and not UnitIsDead(unitId)) then return bossHealth[guid], unitId, UnitName(unitId) end--Return last non 0 value if value is 0, since it's last valid value we had.
  8147. local hp = UnitHealth(unitId) / UnitHealthMax(unitId) * 100
  8148. bossHealth[guid] = hp
  8149. bossHealthuIdCache[guid] = unitId
  8150. return hp, unitId, UnitName(unitId)
  8151. end
  8152. end
  8153. end
  8154. return nil
  8155. end
  8156.  
  8157. function DBM:GetBossHPByUnitID(uId)
  8158. if UnitHealthMax(uId) ~= 0 then
  8159. local hp = UnitHealth(uId) / UnitHealthMax(uId) * 100
  8160. bossHealth[uId] = hp
  8161. return hp, uId, UnitName(uId)
  8162. end
  8163. return nil
  8164. end
  8165.  
  8166. function bossModPrototype:SetBossHealthInfo(...)
  8167. self.bossHealthInfo = {...}
  8168. end
  8169.  
  8170. function bossModPrototype:SetMainBossID(cid)
  8171. self.mainBoss = cid
  8172. end
  8173.  
  8174. function bossModPrototype:SetBossHPInfoToHighest(numBoss)
  8175. if numBoss ~= false then
  8176. self.numBoss = numBoss or (self.multiMobPullDetection and #self.multiMobPullDetection)
  8177. end
  8178. self.highesthealth = true
  8179. end
  8180.  
  8181. function bossModPrototype:GetHighestBossHealth()
  8182. local hp
  8183. if not self.multiMobPullDetection or self.mainBoss then
  8184. hp = bossHealth[self.mainBoss or self.combatInfo.mob or -1]
  8185. if hp and (hp > 100 or hp <= 0) then
  8186. hp = nil
  8187. end
  8188. else
  8189. for _, mob in ipairs(self.multiMobPullDetection) do
  8190. if (bossHealth[mob] or 0) > (hp or 0) and (bossHealth[mob] or 0) < 100 then-- ignore full health.
  8191. hp = bossHealth[mob]
  8192. end
  8193. end
  8194. end
  8195. return hp
  8196. end
  8197.  
  8198. function bossModPrototype:GetLowestBossHealth()
  8199. local hp
  8200. if not self.multiMobPullDetection or self.mainBoss then
  8201. hp = bossHealth[self.mainBoss or self.combatInfo.mob or -1]
  8202. if hp and (hp > 100 or hp <= 0) then
  8203. hp = nil
  8204. end
  8205. else
  8206. for _, mob in ipairs(self.multiMobPullDetection) do
  8207. if (bossHealth[mob] or 100) < (hp or 100) and (bossHealth[mob] or 100) > 0 then-- ignore zero health.
  8208. hp = bossHealth[mob]
  8209. end
  8210. end
  8211. end
  8212. return hp
  8213. end
  8214.  
  8215. bossModPrototype.GetBossHP = DBM.GetBossHP
  8216. bossModPrototype.GetBossHPByGUID = DBM.GetBossHPByGUID
  8217.  
  8218. -----------------------
  8219. -- Announce Object --
  8220. -----------------------
  8221. do
  8222. local frame = CreateFrame("Frame", "DBMWarning", UIParent)
  8223. local font1u = CreateFrame("Frame", "DBMWarning1Updater", UIParent)
  8224. local font2u = CreateFrame("Frame", "DBMWarning2Updater", UIParent)
  8225. local font3u = CreateFrame("Frame", "DBMWarning3Updater", UIParent)
  8226. local font1 = frame:CreateFontString("DBMWarning1", "OVERLAY", "GameFontNormal")
  8227. font1:SetWidth(1024)
  8228. font1:SetHeight(0)
  8229. font1:SetPoint("TOP", 0, 0)
  8230. local font2 = frame:CreateFontString("DBMWarning2", "OVERLAY", "GameFontNormal")
  8231. font2:SetWidth(1024)
  8232. font2:SetHeight(0)
  8233. font2:SetPoint("TOP", font1, "BOTTOM", 0, 0)
  8234. local font3 = frame:CreateFontString("DBMWarning3", "OVERLAY", "GameFontNormal")
  8235. font3:SetWidth(1024)
  8236. font3:SetHeight(0)
  8237. font3:SetPoint("TOP", font2, "BOTTOM", 0, 0)
  8238. frame:SetMovable(1)
  8239. frame:SetWidth(1)
  8240. frame:SetHeight(1)
  8241. frame:SetFrameStrata("HIGH")
  8242. frame:SetClampedToScreen()
  8243. frame:SetPoint("CENTER", UIParent, "CENTER", 0, 300)
  8244. font1u:Hide()
  8245. font2u:Hide()
  8246. font3u:Hide()
  8247.  
  8248. local font1elapsed, font2elapsed, font3elapsed, moving
  8249.  
  8250. local function fontHide1()
  8251. local duration = DBM.Options.WarningDuration
  8252. if font1elapsed > duration * 1.3 then
  8253. font1u:Hide()
  8254. font1:Hide()
  8255. if frame.font1ticker then
  8256. frame.font1ticker:Cancel()
  8257. frame.font1ticker = nil
  8258. end
  8259. elseif font1elapsed > duration then
  8260. font1elapsed = font1elapsed + 0.05
  8261. local alpha = 1 - (font1elapsed - duration) / (duration * 0.3)
  8262. font1:SetAlpha(alpha > 0 and alpha or 0)
  8263. else
  8264. font1elapsed = font1elapsed + 0.05
  8265. font1:SetAlpha(1)
  8266. end
  8267. end
  8268.  
  8269. local function fontHide2()
  8270. local duration = DBM.Options.WarningDuration
  8271. if font2elapsed > duration * 1.3 then
  8272. font2u:Hide()
  8273. font2:Hide()
  8274. if frame.font2ticker then
  8275. frame.font2ticker:Cancel()
  8276. frame.font2ticker = nil
  8277. end
  8278. elseif font2elapsed > duration then
  8279. font2elapsed = font2elapsed + 0.05
  8280. local alpha = 1 - (font2elapsed - duration) / (duration * 0.3)
  8281. font2:SetAlpha(alpha > 0 and alpha or 0)
  8282. else
  8283. font2elapsed = font2elapsed + 0.05
  8284. font2:SetAlpha(1)
  8285. end
  8286. end
  8287.  
  8288. local function fontHide3()
  8289. local duration = DBM.Options.WarningDuration
  8290. if font3elapsed > duration * 1.3 then
  8291. font3u:Hide()
  8292. font3:Hide()
  8293. if frame.font3ticker then
  8294. frame.font3ticker:Cancel()
  8295. frame.font3ticker = nil
  8296. end
  8297. elseif font3elapsed > duration then
  8298. font3elapsed = font3elapsed + 0.05
  8299. local alpha = 1 - (font3elapsed - duration) / (duration * 0.3)
  8300. font3:SetAlpha(alpha > 0 and alpha or 0)
  8301. else
  8302. font3elapsed = font3elapsed + 0.05
  8303. font3:SetAlpha(1)
  8304. end
  8305. end
  8306.  
  8307. font1u:SetScript("OnUpdate", function(self)
  8308. local diff = GetTime() - font1.lastUpdate
  8309. local origSize = DBM.Options.WarningFontSize
  8310. if diff > 0.4 then
  8311. font1:SetTextHeight(origSize)
  8312. self:Hide()
  8313. elseif diff > 0.2 then
  8314. font1:SetTextHeight(origSize * (1.5 - (diff-0.2) * 2.5))
  8315. else
  8316. font1:SetTextHeight(origSize * (1 + diff * 2.5))
  8317. end
  8318. end)
  8319.  
  8320. font2u:SetScript("OnUpdate", function(self)
  8321. local diff = GetTime() - font2.lastUpdate
  8322. local origSize = DBM.Options.WarningFontSize
  8323. if diff > 0.4 then
  8324. font2:SetTextHeight(origSize)
  8325. self:Hide()
  8326. elseif diff > 0.2 then
  8327. font2:SetTextHeight(origSize * (1.5 - (diff-0.2) * 2.5))
  8328. else
  8329. font2:SetTextHeight(origSize * (1 + diff * 2.5))
  8330. end
  8331. end)
  8332.  
  8333. font3u:SetScript("OnUpdate", function(self)
  8334. local diff = GetTime() - font3.lastUpdate
  8335. local origSize = DBM.Options.WarningFontSize
  8336. if diff > 0.4 then
  8337. font3:SetTextHeight(origSize)
  8338. self:Hide()
  8339. elseif diff > 0.2 then
  8340. font3:SetTextHeight(origSize * (1.5 - (diff-0.2) * 2.5))
  8341. else
  8342. font3:SetTextHeight(origSize * (1 + diff * 2.5))
  8343. end
  8344. end)
  8345.  
  8346. function DBM:UpdateWarningOptions()
  8347. frame:ClearAllPoints()
  8348. frame:SetPoint(self.Options.WarningPoint, UIParent, self.Options.WarningPoint, self.Options.WarningX, self.Options.WarningY)
  8349. font1:SetFont(self.Options.WarningFont, self.Options.WarningFontSize, self.Options.WarningFontStyle == "None" and nil or self.Options.WarningFontStyle)
  8350. font2:SetFont(self.Options.WarningFont, self.Options.WarningFontSize, self.Options.WarningFontStyle == "None" and nil or self.Options.WarningFontStyle)
  8351. font3:SetFont(self.Options.WarningFont, self.Options.WarningFontSize, self.Options.WarningFontStyle == "None" and nil or self.Options.WarningFontStyle)
  8352. if self.Options.WarningFontShadow then
  8353. font1:SetShadowOffset(1, -1)
  8354. font2:SetShadowOffset(1, -1)
  8355. font3:SetShadowOffset(1, -1)
  8356. else
  8357. font1:SetShadowOffset(0, 0)
  8358. font2:SetShadowOffset(0, 0)
  8359. font3:SetShadowOffset(0, 0)
  8360. end
  8361. end
  8362.  
  8363. function DBM:AddWarning(text, force)
  8364. local added = false
  8365. if not frame.font1ticker then
  8366. font1elapsed = 0
  8367. font1.lastUpdate = GetTime()
  8368. font1:SetText(text)
  8369. font1:Show()
  8370. font1u:Show()
  8371. added = true
  8372. frame.font1ticker = frame.font1ticker or C_TimerNewTicker(0.05, fontHide1)
  8373. elseif not frame.font2ticker then
  8374. font2elapsed = 0
  8375. font2.lastUpdate = GetTime()
  8376. font2:SetText(text)
  8377. font2:Show()
  8378. font2u:Show()
  8379. added = true
  8380. frame.font2ticker = frame.font2ticker or C_TimerNewTicker(0.05, fontHide2)
  8381. elseif not frame.font3ticker or force then
  8382. font3elapsed = 0
  8383. font3.lastUpdate = GetTime()
  8384. font3:SetText(text)
  8385. font3:Show()
  8386. font3u:Show()
  8387. fontHide3()
  8388. added = true
  8389. frame.font3ticker = frame.font3ticker or C_TimerNewTicker(0.05, fontHide3)
  8390. end
  8391. if not added then
  8392. local prevText1 = font2:GetText()
  8393. local prevText2 = font3:GetText()
  8394. font1:SetText(prevText1)
  8395. font1elapsed = font2elapsed
  8396. font2:SetText(prevText2)
  8397. font2elapsed = font3elapsed
  8398. self:AddWarning(text, true)
  8399. end
  8400. end
  8401.  
  8402. do
  8403. local anchorFrame
  8404. local function moveEnd(self)
  8405. moving = false
  8406. anchorFrame:Hide()
  8407. if anchorFrame.ticker then
  8408. anchorFrame.ticker:Cancel()
  8409. anchorFrame.ticker = nil
  8410. end
  8411. font1elapsed = self.Options.WarningDuration
  8412. font2elapsed = self.Options.WarningDuration
  8413. font3elapsed = self.Options.WarningDuration
  8414. frame:SetFrameStrata("HIGH")
  8415. self:Unschedule(moveEnd)
  8416. self.Bars:CancelBar(DBM_CORE_MOVE_WARNING_BAR)
  8417. end
  8418.  
  8419. function DBM:MoveWarning()
  8420. if not anchorFrame then
  8421. anchorFrame = CreateFrame("Frame", nil, frame)
  8422. anchorFrame:SetWidth(32)
  8423. anchorFrame:SetHeight(32)
  8424. anchorFrame:EnableMouse(true)
  8425. anchorFrame:SetPoint("TOP", frame, "TOP", 0, 32)
  8426. anchorFrame:RegisterForDrag("LeftButton")
  8427. anchorFrame:SetClampedToScreen()
  8428. anchorFrame:Hide()
  8429. local texture = anchorFrame:CreateTexture()
  8430. texture:SetTexture("Interface\\Addons\\DBM-GUI\\textures\\dot.blp")
  8431. texture:SetPoint("CENTER", anchorFrame, "CENTER", 0, 0)
  8432. texture:SetWidth(32)
  8433. texture:SetHeight(32)
  8434. anchorFrame:SetScript("OnDragStart", function()
  8435. frame:StartMoving()
  8436. self:Unschedule(moveEnd)
  8437. self.Bars:CancelBar(DBM_CORE_MOVE_WARNING_BAR)
  8438. end)
  8439. anchorFrame:SetScript("OnDragStop", function()
  8440. frame:StopMovingOrSizing()
  8441. local point, _, _, xOfs, yOfs = frame:GetPoint(1)
  8442. self.Options.WarningPoint = point
  8443. self.Options.WarningX = xOfs
  8444. self.Options.WarningY = yOfs
  8445. self:Schedule(15, moveEnd, self)
  8446. self.Bars:CreateBar(15, DBM_CORE_MOVE_WARNING_BAR)
  8447. end)
  8448. end
  8449. if anchorFrame:IsShown() then
  8450. moveEnd()
  8451. else
  8452. moving = true
  8453. anchorFrame:Show()
  8454. anchorFrame.ticker = anchorFrame.ticker or C_TimerNewTicker(5, function() self:AddWarning(DBM_CORE_MOVE_WARNING_MESSAGE) end)
  8455. self:AddWarning(DBM_CORE_MOVE_WARNING_MESSAGE)
  8456. self:Schedule(15, moveEnd, self)
  8457. self.Bars:CreateBar(15, DBM_CORE_MOVE_WARNING_BAR)
  8458. frame:Show()
  8459. frame:SetFrameStrata("TOOLTIP")
  8460. frame:SetAlpha(1)
  8461. end
  8462. end
  8463. end
  8464.  
  8465. local textureCode = " |T%s:12:12|t "
  8466. local textureExp = " |T(%S+......%S+):12:12|t "--Fix texture file including blank not strips(example: Interface\\Icons\\Spell_Frost_Ring of Frost). But this have limitations. Since I'm poor at regular expressions, this is not good fix. Do you have another good regular expression, tandanu?
  8467. local announcePrototype = {}
  8468. local mt = {__index = announcePrototype}
  8469.  
  8470. -- TODO: is there a good reason that this is a weak table?
  8471. local cachedColorFunctions = setmetatable({}, {__mode = "kv"})
  8472.  
  8473. -- TODO: this function is an abomination, it needs to be rewritten. Also: check if these work-arounds are still necessary
  8474. function announcePrototype:Show(...) -- todo: reduce amount of unneeded strings
  8475. if not self.option or self.mod.Options[self.option] then
  8476. if DBM.Options.DontShowBossAnnounces then return end -- don't show the announces if the spam filter option is set
  8477. local argTable = {...}
  8478. local colorCode = ("|cff%.2x%.2x%.2x"):format(self.color.r * 255, self.color.g * 255, self.color.b * 255)
  8479. if #self.combinedtext > 0 then
  8480. --Throttle spam.
  8481. if DBM.Options.WarningAlphabetical then
  8482. tsort(self.combinedtext)
  8483. end
  8484. local combinedText = tconcat(self.combinedtext, "<, >")
  8485. if self.combinedcount == 1 then
  8486. combinedText = combinedText.." "..DBM_CORE_GENERIC_WARNING_OTHERS
  8487. elseif self.combinedcount > 1 then
  8488. combinedText = combinedText.." "..DBM_CORE_GENERIC_WARNING_OTHERS2:format(self.combinedcount)
  8489. end
  8490. --Process
  8491. for i = 1, #argTable do
  8492. if type(argTable[i]) == "string" then
  8493. argTable[i] = combinedText
  8494. end
  8495. end
  8496. end
  8497. local message = pformat(self.text, unpack(argTable))
  8498. local text = ("%s%s%s|r%s"):format(
  8499. (DBM.Options.WarningIconLeft and self.icon and textureCode:format(self.icon)) or "",
  8500. colorCode,
  8501. message,
  8502. (DBM.Options.WarningIconRight and self.icon and textureCode:format(self.icon)) or ""
  8503. )
  8504. self.combinedcount = 0
  8505. self.combinedtext = {}
  8506. if not cachedColorFunctions[self.color] then
  8507. local color = self.color -- upvalue for the function to colorize names, accessing self in the colorize closure is not safe as the color of the announce object might change (it would also prevent the announce from being garbage-collected but announce objects are never destroyed)
  8508. cachedColorFunctions[color] = function(cap)
  8509. cap = cap:sub(2, -2)
  8510. local noStrip = cap:match("noStrip ")
  8511. if not noStrip then
  8512. local name = cap
  8513. if DBM.Options.StripServerName then
  8514. cap = Ambiguate(cap, "short")
  8515. end
  8516. local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(name)] or color
  8517. if playerColor then
  8518. cap = ("|r|cff%.2x%.2x%.2x%s|r|cff%.2x%.2x%.2x"):format(playerColor.r * 255, playerColor.g * 255, playerColor.b * 255, cap, color.r * 255, color.g * 255, color.b * 255)
  8519. end
  8520. else
  8521. cap = cap:sub(9)
  8522. end
  8523. return cap
  8524. end
  8525. end
  8526. text = text:gsub(">.-<", cachedColorFunctions[self.color])
  8527. DBM:AddWarning(text)
  8528. if DBM.Options.ShowWarningsInChat then
  8529. if not DBM.Options.WarningIconChat then
  8530. text = text:gsub(textureExp, "") -- textures @ chat frame can (and will) distort the font if using certain combinations of UI scale, resolution and font size TODO: is this still true as of cataclysm?
  8531. end
  8532. self.mod:AddMsg(text, nil)
  8533. end
  8534. if self.sound then
  8535. DBM:PlaySoundFile(DBM.Options.RaidWarningSound)
  8536. end
  8537. --Message: Full message text
  8538. --Icon: Texture path for icon
  8539. --Type: Announce type
  8540. --SpellId: Raw spell or encounter journal Id if available.
  8541. --Mod ID: Encounter ID as string, or a generic string for mods that don't have encounter ID (such as trash, dummy/test mods)
  8542. fireEvent("DBM_Announce", message, self.icon, self.type, self.spellId, self.mod.id)
  8543. else
  8544. self.combinedcount = 0
  8545. self.combinedtext = {}
  8546. end
  8547. end
  8548.  
  8549. function announcePrototype:CombinedShow(delay, ...)
  8550. local argTable = {...}
  8551. for i = 1, #argTable do
  8552. if type(argTable[i]) == "string" then
  8553. if #self.combinedtext < 8 then--Throttle spam. We may not need more than 9 targets..
  8554. if not checkEntry(self.combinedtext, argTable[i]) then
  8555. self.combinedtext[#self.combinedtext + 1] = argTable[i]
  8556. end
  8557. else
  8558. self.combinedcount = self.combinedcount + 1
  8559. end
  8560. end
  8561. end
  8562. unschedule(self.Show, self.mod, self)
  8563. schedule(delay or 0.5, self.Show, self.mod, self, ...)
  8564. end
  8565.  
  8566. function announcePrototype:Schedule(t, ...)
  8567. return schedule(t, self.Show, self.mod, self, ...)
  8568. end
  8569.  
  8570. function announcePrototype:Cancel(...)
  8571. return unschedule(self.Show, self.mod, self, ...)
  8572. end
  8573.  
  8574. -- old constructor (no auto-localize)
  8575. function bossModPrototype:NewAnnounce(text, color, icon, optionDefault, optionName, noSound)
  8576. if not text then
  8577. error("NewAnnounce: you must provide announce text", 2)
  8578. return
  8579. end
  8580. local obj = setmetatable(
  8581. {
  8582. text = self.localization.warnings[text],
  8583. combinedtext = {},
  8584. combinedcount = 0,
  8585. color = DBM.Options.WarningColors[color or 1] or DBM.Options.WarningColors[1],
  8586. sound = not noSound,
  8587. mod = self,
  8588. icon = (type(icon) == "string" and icon:match("ej%d+") and select(4, EJ_GetSectionInfo(string.sub(icon, 3))) ~= "" and select(4, EJ_GetSectionInfo(string.sub(icon, 3)))) or (type(icon) == "number" and GetSpellTexture(icon)) or icon or "Interface\\Icons\\Spell_Nature_WispSplode",
  8589. },
  8590. mt
  8591. )
  8592. if optionName then
  8593. obj.option = optionName
  8594. self:AddBoolOption(obj.option, optionDefault, "announce")
  8595. elseif not (optionName == false) then
  8596. obj.option = text
  8597. self:AddBoolOption(obj.option, optionDefault, "announce")
  8598. end
  8599. tinsert(self.announces, obj)
  8600. return obj
  8601. end
  8602.  
  8603. -- new constructor (auto-localized warnings and options, yay!)
  8604. local function newAnnounce(self, announceType, spellId, color, icon, optionDefault, optionName, castTime, preWarnTime, noSound)
  8605. if not spellId then
  8606. error("newAnnounce: you must provide spellId", 2)
  8607. return
  8608. end
  8609. local optionVersion
  8610. if type(optionName) == "number" then
  8611. optionVersion = optionName
  8612. optionName = nil
  8613. end
  8614. if type(spellId) == "string" and spellId:match("OptionVersion") then
  8615. print("newAnnounce for "..color.." is using OptionVersion hack. this is depricated")
  8616. return
  8617. end
  8618. local unparsedId = spellId
  8619. local spellName
  8620. if type(spellId) == "string" and spellId:match("ej%d+") then
  8621. spellId = string.sub(spellId, 3)
  8622. spellName = EJ_GetSectionInfo(spellId) or DBM_CORE_UNKNOWN
  8623. else
  8624. spellName = GetSpellInfo(spellId) or DBM_CORE_UNKNOWN
  8625. end
  8626. icon = icon or unparsedId
  8627. local text
  8628. if announceType == "cast" then
  8629. local spellHaste = select(4, GetSpellInfo(53142)) / 10000 -- 53142 = Dalaran Portal, should have 10000 ms cast time
  8630. local timer = (select(4, GetSpellInfo(spellId)) or 1000) / spellHaste
  8631. text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName, castTime or (timer / 1000))
  8632. elseif announceType == "prewarn" then
  8633. if type(preWarnTime) == "string" then
  8634. text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName, preWarnTime)
  8635. else
  8636. text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName, DBM_CORE_SEC_FMT:format(tostring(preWarnTime or 5)))
  8637. end
  8638. elseif announceType == "phase" or announceType == "prephase" then
  8639. text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(tostring(spellId))
  8640. elseif announceType == "phasechange" then
  8641. text = DBM_CORE_AUTO_ANNOUNCE_TEXTS.spell
  8642. else
  8643. text = DBM_CORE_AUTO_ANNOUNCE_TEXTS[announceType]:format(spellName)
  8644. end
  8645. local obj = setmetatable( -- todo: fix duplicate code
  8646. {
  8647. text = text,
  8648. combinedtext = {},
  8649. combinedcount = 0,
  8650. announceType = announceType,
  8651. color = DBM.Options.WarningColors[color or 1] or DBM.Options.WarningColors[1],
  8652. mod = self,
  8653. icon = (type(icon) == "string" and icon:match("ej%d+") and select(4, EJ_GetSectionInfo(string.sub(icon, 3))) ~= "" and select(4, EJ_GetSectionInfo(string.sub(icon, 3)))) or (type(icon) == "number" and GetSpellTexture(icon)) or icon or "Interface\\Icons\\Spell_Nature_WispSplode",
  8654. sound = not noSound,
  8655. type = announceType,
  8656. spellId = unparsedId,
  8657. },
  8658. mt
  8659. )
  8660. local catType = "announce"--Default to General announce
  8661. --Change if Personal or Other
  8662. if announceType == "target" or announceType == "targetcount" or announceType == "stack" then
  8663. catType = "announceother"
  8664. end
  8665. if optionName then
  8666. obj.option = optionName
  8667. self:AddBoolOption(obj.option, optionDefault, catType)
  8668. elseif not (optionName == false) then
  8669. obj.option = catType..unparsedId..announceType..(optionVersion or "")
  8670. self:AddBoolOption(obj.option, optionDefault, catType)
  8671. self.localization.options[obj.option] = DBM_CORE_AUTO_ANNOUNCE_OPTIONS[announceType]:format(unparsedId)
  8672. end
  8673. tinsert(self.announces, obj)
  8674. return obj
  8675. end
  8676.  
  8677. function bossModPrototype:NewYouAnnounce(spellId, color, ...)
  8678. return newAnnounce(self, "you", spellId, color or 1, ...)
  8679. end
  8680.  
  8681. function bossModPrototype:NewTargetAnnounce(spellId, color, ...)
  8682. return newAnnounce(self, "target", spellId, color or 3, ...)
  8683. end
  8684.  
  8685. function bossModPrototype:NewTargetCountAnnounce(spellId, color, ...)
  8686. return newAnnounce(self, "targetcount", spellId, color or 3, ...)
  8687. end
  8688.  
  8689. function bossModPrototype:NewSpellAnnounce(spellId, color, ...)
  8690. return newAnnounce(self, "spell", spellId, color or 2, ...)
  8691. end
  8692.  
  8693. function bossModPrototype:NewEndAnnounce(spellId, color, ...)
  8694. return newAnnounce(self, "ends", spellId, color or 2, ...)
  8695. end
  8696.  
  8697. function bossModPrototype:NewEndTargetAnnounce(spellId, color, ...)
  8698. return newAnnounce(self, "endtarget", spellId, color or 2, ...)
  8699. end
  8700.  
  8701. function bossModPrototype:NewFadesAnnounce(spellId, color, ...)
  8702. return newAnnounce(self, "fades", spellId, color or 2, ...)
  8703. end
  8704.  
  8705. function bossModPrototype:NewAddsLeftAnnounce(spellId, color, ...)
  8706. return newAnnounce(self, "adds", spellId, color or 3, ...)
  8707. end
  8708.  
  8709. function bossModPrototype:NewCountAnnounce(spellId, color, ...)
  8710. return newAnnounce(self, "count", spellId, color or 2, ...)
  8711. end
  8712.  
  8713. function bossModPrototype:NewStackAnnounce(spellId, color, ...)
  8714. return newAnnounce(self, "stack", spellId, color or 2, ...)
  8715. end
  8716.  
  8717. function bossModPrototype:NewCastAnnounce(spellId, color, castTime, icon, optionDefault, optionName, noArg, noSound)
  8718. local optionVersion
  8719. if type(optionName) == "number" then
  8720. optionVersion = optionName
  8721. optionName = nil
  8722. end
  8723. if type(spellId) == "string" and spellId:match("OptionVersion") then--LEGACY hack, remove when new DBM core and other mods released
  8724. print("NewCastAnnounce is using OptionVersion and this is depricated for "..color)
  8725. return
  8726. end
  8727. return newAnnounce(self, "cast", spellId, color or 3, icon, optionDefault, optionName, castTime, nil, noSound)
  8728. end
  8729.  
  8730. function bossModPrototype:NewSoonAnnounce(spellId, color, ...)
  8731. return newAnnounce(self, "soon", spellId, color or 1, ...)
  8732. end
  8733.  
  8734. function bossModPrototype:NewPreWarnAnnounce(spellId, time, color, icon, optionDefault, optionName, noArg, noSound)
  8735. local optionVersion
  8736. if type(optionName) == "number" then
  8737. optionVersion = optionName
  8738. optionName = nil
  8739. end
  8740. if type(spellId) == "string" and spellId:match("OptionVersion") then--LEGACY hack, remove when new DBM core and other mods released
  8741. print("NewCastAnnounce is using OptionVersion and this is depricated for "..color)
  8742. return
  8743. end
  8744. return newAnnounce(self, "prewarn", spellId, color or 1, icon, optionDefault, optionName, nil, time, noSound)
  8745. end
  8746.  
  8747. function bossModPrototype:NewPhaseAnnounce(phase, color, icon, ...)
  8748. return newAnnounce(self, "phase", phase, color or 1, icon or "Interface\\Icons\\Spell_Nature_WispSplode", ...)
  8749. end
  8750.  
  8751. function bossModPrototype:NewPhaseChangeAnnounce(color, icon, ...)
  8752. return newAnnounce(self, "phasechange", 0, color or 1, icon or "Interface\\Icons\\Spell_Nature_WispSplode", ...)
  8753. end
  8754.  
  8755. function bossModPrototype:NewPrePhaseAnnounce(phase, color, icon, ...)
  8756. return newAnnounce(self, "prephase", phase, color or 1, icon or "Interface\\Icons\\Spell_Nature_WispSplode", ...)
  8757. end
  8758. end
  8759.  
  8760. --------------------
  8761. -- Sound Object --
  8762. --------------------
  8763. do
  8764. --Sound Object (basically only used by countdown now)
  8765. local soundPrototype = {}
  8766. local mt = { __index = soundPrototype }
  8767. function bossModPrototype:NewSound(spellId, optionDefault, optionName)
  8768. if not (optionName == false) then--Basically, all mods still using NewSound.
  8769. -- Because there are going to be users who update core and not old mods, we need to check and alert
  8770. --I'll try to avoid this as much as possible by removing NewSound from all old mods in advance of dbm core update
  8771. print("Error, NewSound depricated. Update your old DBM mods to remove this error")
  8772. return
  8773. end
  8774. self.numSounds = self.numSounds and self.numSounds + 1 or 1
  8775. local obj = setmetatable(
  8776. {
  8777. mod = self,
  8778. },
  8779. mt
  8780. )
  8781. return obj
  8782. end
  8783.  
  8784. function soundPrototype:Play(file)
  8785. DBM:PlaySoundFile(file)
  8786. end
  8787.  
  8788. function soundPrototype:Schedule(t, ...)
  8789. return schedule(t, self.Play, self.mod, self, ...)
  8790. end
  8791.  
  8792. function soundPrototype:Cancel(...)
  8793. return unschedule(self.Play, self.mod, self, ...)
  8794. end
  8795.  
  8796. --Voice Object
  8797. --Individual options still generated in case a person likes to enable voice, but not for ALL warnings (they can pick and choose what is enabled/disabled"
  8798. local soundPrototype2 = {}
  8799. local mt = { __index = soundPrototype2 }
  8800. function bossModPrototype:NewVoice(spellId, optionDefault, optionName, optionVersion)
  8801. if not spellId and not optionName then
  8802. print("NewVoice: you must provide either spellId or optionName", 2)
  8803. return
  8804. end
  8805. if type(spellId) == "string" and spellId:match("OptionVersion") then
  8806. print("NewVoice for "..optionDefault.." is using OptionVersion hack. this is not needed, this only has 4 args, do this properly")
  8807. return
  8808. end
  8809. self.numSounds = self.numSounds and self.numSounds + 1 or 1
  8810. local obj = setmetatable(
  8811. {
  8812. mod = self,
  8813. },
  8814. mt
  8815. )
  8816. if #DBM.Voices < 2 then optionName = false end--Hide options if no voice packs are installed
  8817. if optionName then
  8818. obj.option = optionName
  8819. self:AddBoolOption(obj.option, optionDefault, "sound")
  8820. elseif not (optionName == false) then
  8821. obj.option = "Voice"..spellId..(optionVersion or "")
  8822. self:AddBoolOption(obj.option, optionDefault, "sound")
  8823. self.localization.options[obj.option] = DBM_CORE_AUTO_VOICE_OPTION_TEXT:format(spellId)
  8824. end
  8825. return obj
  8826. end
  8827.  
  8828. --If no file at path, it should silenty fail. However, I want to try to only add NewVoice to mods for files that already exist.
  8829. function soundPrototype2:Play(name, customPath)
  8830. local voice = DBM.Options.ChosenVoicePack
  8831. local always = DBM.Options.AlwaysPlayVoice
  8832. if voice == "None" then return end
  8833. --Filter tank specific voice alerts for non tanks if tank filter enabled
  8834. --But still allow AlwaysPlayVoice to play as well.
  8835. if (name == "changemt" or name == "tauntboss") and DBM.Options.FilterTankSpec and not self.mod:IsTank() and not always then return end
  8836. if not self.option or self.mod.Options[self.option] or always then
  8837. local path = customPath or "Interface\\AddOns\\DBM-VP"..voice.."\\"..name..".ogg"
  8838. DBM:PlaySoundFile(path)
  8839. end
  8840. end
  8841.  
  8842. function soundPrototype2:Schedule(t, ...)
  8843. if DBM.Options.ChosenVoicePack == "None" then return end
  8844. return schedule(t, self.Play, self.mod, self, ...)
  8845. end
  8846.  
  8847. function soundPrototype2:Cancel(...)
  8848. if DBM.Options.ChosenVoicePack == "None" then return end
  8849. return unschedule(self.Play, self.mod, self, ...)
  8850. end
  8851. end
  8852.  
  8853. ----------------------------
  8854. -- Countdown/out object --
  8855. ----------------------------
  8856. do
  8857. local countdownProtoType = {}
  8858. local voice1, voice2, voice3 = nil, nil, nil
  8859. local voice1max, voice2max, voice3max = 5, 5, 5
  8860. local path1, path2, path3 = nil, nil, nil
  8861. local mt = {__index = countdownProtoType}
  8862.  
  8863. function DBM:BuildVoiceCountdownCache()
  8864. voice1 = self.Options.CountdownVoice
  8865. voice2 = self.Options.CountdownVoice2
  8866. voice3 = self.Options.CountdownVoice3v2
  8867. local voicesFound = 0
  8868. for i = 1, #self.Counts do
  8869. if voicesFound == 3 then return end
  8870. local curVoice = self.Counts[i]
  8871. if curVoice.value == voice1 then
  8872. path1 = curVoice.path
  8873. voice1max = curVoice.max
  8874. voicesFound = voicesFound + 1
  8875. elseif curVoice.value == voice2 then
  8876. path2 = curVoice.path
  8877. voice2max = curVoice.max
  8878. voicesFound = voicesFound + 1
  8879. elseif curVoice.value == voice3 then
  8880. path3 = curVoice.path
  8881. voice3max = curVoice.max
  8882. voicesFound = voicesFound + 1
  8883. end
  8884. end
  8885. end
  8886.  
  8887. local function showCountdown(timer)
  8888. TimerTracker_OnEvent(TimerTracker, "START_TIMER", 2, timer+1, timer+1)
  8889. end
  8890.  
  8891. local function stopCountdown()
  8892. TimerTracker_OnEvent(TimerTracker, "PLAYER_ENTERING_WORLD")
  8893. end
  8894.  
  8895. function countdownProtoType:Start(timer, count)
  8896. if not self.option or self.mod.Options[self.option] then
  8897. timer = timer or self.timer or 10
  8898. timer = timer < 2 and self.timer or timer
  8899. count = count or self.count or 5
  8900. if timer <= count then count = floor(timer) end
  8901. if DBM.Options.ShowCountdownText and not (self.textDisabled or self.alternateVoice) and self.type ~= "Countout" then
  8902. stopCountdown()
  8903. if timer >= count then
  8904. DBM:Schedule(timer-count, showCountdown, count)
  8905. else
  8906. DBM:Schedule(timer%1, showCountdown, floor(timer))
  8907. end
  8908. end
  8909. if DBM.Options.DontPlayCountdowns then return end
  8910. if not path1 or not path2 or not path3 then
  8911. DBM:Debug("Voice cache not built at time of countdownProtoType:Start. On fly caching.")
  8912. DBM:BuildVoiceCountdownCache()
  8913. end
  8914. local voice, maxCount, path
  8915. if self.alternateVoice == 2 then
  8916. voice = voice2 or DBM.DefaultOptions.CountdownVoice2
  8917. maxCount = voice2max or 10
  8918. path = path2 or "Interface\\AddOns\\DBM-Core\\Sounds\\Kolt\\"
  8919. elseif self.alternateVoice == 3 then
  8920. voice = voice3 or DBM.DefaultOptions.CountdownVoice3v2
  8921. maxCount = voice3max or 5
  8922. path = path3 or "Interface\\AddOns\\DBM-Core\\Sounds\\Heroes\\Necromancer\\"
  8923. else
  8924. voice = voice1 or DBM.DefaultOptions.CountdownVoice
  8925. maxCount = voice1max or 10
  8926. path = path1 or "Interface\\AddOns\\DBM-Core\\Sounds\\Corsica\\"
  8927. end
  8928. if not path then--Should not happen but apparently it does somehow
  8929. DBM:Debug("Voice path failed in countdownProtoType:Start.")
  8930. return
  8931. end
  8932. if self.type == "Countout" then
  8933. for i = 1, timer do
  8934. if i < maxCount then
  8935. self.sound5:Schedule(i, path..i..".ogg")
  8936. end
  8937. end
  8938. else
  8939. for i = count, 1, -1 do
  8940. if i <= maxCount then
  8941. self.sound5:Schedule(timer-i, path..i..".ogg")
  8942. end
  8943. end
  8944. end
  8945. end
  8946. end
  8947. countdownProtoType.Show = countdownProtoType.Start
  8948.  
  8949. function countdownProtoType:Schedule(t)
  8950. return schedule(t, self.Start, self.mod, self)
  8951. end
  8952.  
  8953. function countdownProtoType:Cancel()
  8954. if DBM.Options.ShowCountdownText and not self.textDisabled then
  8955. DBM:Unschedule(showCountdown)
  8956. stopCountdown()
  8957. end
  8958. self.mod:Unschedule(self.Start, self)
  8959. self.sound5:Cancel()
  8960. end
  8961. countdownProtoType.Stop = countdownProtoType.Cancel
  8962.  
  8963. local function newCountdown(self, countdownType, timer, spellId, optionDefault, optionName, count, textDisabled, altVoice)
  8964. if not spellId and not optionName then
  8965. print("NewCountdown: you must provide either spellId or optionName", 2)
  8966. return
  8967. end
  8968. if type(timer) == "string" and timer:match("OptionVersion") then
  8969. print("OptionVersion depricated for newCountdown :"..optionDefault)
  8970. return
  8971. end
  8972. local optionVersion
  8973. if type(optionName) == "number" then
  8974. optionVersion = optionName
  8975. optionName = nil
  8976. end
  8977. if altVoice == true then altVoice = 2 end--Compat
  8978. if type(timer) == "string" then
  8979. if timer:match("AltTwo") then
  8980. altVoice = 3
  8981. timer = tonumber(string.sub(timer, 7))
  8982. elseif timer:match("Alt") then
  8983. altVoice = 2
  8984. timer = tonumber(string.sub(timer, 4))
  8985. end
  8986. end
  8987. --TODO, maybe make this not use an entire sound object?
  8988. local sound5 = self:NewSound(5, true, false)
  8989. timer = timer or 10
  8990. count = count or 4
  8991. spellId = spellId or 39505
  8992. local obj = setmetatable(
  8993. {
  8994. id = optionName or countdownType..spellId..(optionVersion or ""),
  8995. type = countdownType,
  8996. sound5 = sound5,
  8997. timer = timer,
  8998. count = count,
  8999. textDisabled = textDisabled,
  9000. alternateVoice = altVoice,
  9001. mod = self
  9002. },
  9003. mt
  9004. )
  9005. if optionName then
  9006. obj.option = obj.id
  9007. self:AddBoolOption(obj.option, optionDefault, "sound")
  9008. elseif not (optionName == false) then
  9009. obj.option = obj.id
  9010. self:AddBoolOption(obj.option, optionDefault, "sound")
  9011. if countdownType == "Countdown" then
  9012. self.localization.options[obj.option] = DBM_CORE_AUTO_COUNTDOWN_OPTION_TEXT:format(spellId)
  9013. elseif countdownType == "CountdownFades" then
  9014. self.localization.options[obj.option] = DBM_CORE_AUTO_COUNTDOWN_OPTION_TEXT2:format(spellId)
  9015. elseif countdownType == "Countout" then
  9016. self.localization.options[obj.option] = DBM_CORE_AUTO_COUNTOUT_OPTION_TEXT:format(spellId)
  9017. end
  9018. end
  9019. tinsert(self.countdowns, obj)
  9020. return obj
  9021. end
  9022.  
  9023. function bossModPrototype:NewCountdown(...)
  9024. return newCountdown(self, "Countdown", ...)
  9025. end
  9026.  
  9027. function bossModPrototype:NewCountdownFades(...)
  9028. return newCountdown(self, "CountdownFades", ...)
  9029. end
  9030.  
  9031. function bossModPrototype:NewCountout(...)
  9032. return newCountdown(self, "Countout", ...)
  9033. end
  9034. end
  9035.  
  9036. --------------------
  9037. -- Yell Object --
  9038. --------------------
  9039. do
  9040. local yellPrototype = {}
  9041. local mt = { __index = yellPrototype }
  9042. local function newYell(self, yellType, spellId, yellText, optionDefault, optionName, chatType)
  9043. if not spellId and not yellText then
  9044. error("NewYell: you must provide either spellId or yellText", 2)
  9045. return
  9046. end
  9047. if type(spellId) == "string" and spellId:match("OptionVersion") then
  9048. print("newYell for: "..yellText.." is using OptionVersion hack. This is depricated")
  9049. return
  9050. end
  9051. local optionVersion
  9052. if type(optionName) == "number" then
  9053. optionVersion = optionName
  9054. optionName = nil
  9055. end
  9056. local displayText
  9057. if not yellText then
  9058. if type(spellId) == "string" and spellId:match("ej%d+") then
  9059. displayText = DBM_CORE_AUTO_YELL_ANNOUNCE_TEXT[yellType]:format(EJ_GetSectionInfo(string.sub(spellId, 3)) or DBM_CORE_UNKNOWN)
  9060. else
  9061. displayText = DBM_CORE_AUTO_YELL_ANNOUNCE_TEXT[yellType]:format(GetSpellInfo(spellId) or DBM_CORE_UNKNOWN)
  9062. end
  9063. end
  9064. --Passed spellid as yellText.
  9065. --Auto localize spelltext using yellText instead
  9066. if yellText and type(yellText) == "number" then
  9067. displayText = DBM_CORE_AUTO_YELL_ANNOUNCE_TEXT[yellType]:format(GetSpellInfo(yellText) or DBM_CORE_UNKNOWN)
  9068. end
  9069. local obj = setmetatable(
  9070. {
  9071. text = displayText or yellText,
  9072. mod = self,
  9073. chatType = chatType
  9074. },
  9075. mt
  9076. )
  9077. if optionName then
  9078. obj.option = optionName
  9079. self:AddBoolOption(obj.option, optionDefault, "misc")
  9080. elseif not (optionName == false) then
  9081. obj.option = "Yell"..(spellId or yellText)..(yellType ~= "yell" and yellType or "")..(optionVersion or "")
  9082. self:AddBoolOption(obj.option, optionDefault, "misc")
  9083. self.localization.options[obj.option] = DBM_CORE_AUTO_YELL_OPTION_TEXT[yellType]:format(spellId)
  9084. end
  9085. return obj
  9086. end
  9087.  
  9088. function yellPrototype:Yell(...)
  9089. if DBM.Options.DontSendYells then return end
  9090. if not self.option or self.mod.Options[self.option] then
  9091. SendChatMessage(pformat(self.text, ...), self.chatType or "SAY")
  9092. end
  9093. end
  9094. yellPrototype.Show = yellPrototype.Yell
  9095.  
  9096. function yellPrototype:Schedule(t, ...)
  9097. return schedule(t, self.Yell, self.mod, self, ...)
  9098. end
  9099.  
  9100. function yellPrototype:Cancel(...)
  9101. return unschedule(self.Yell, self.mod, self, ...)
  9102. end
  9103.  
  9104. function bossModPrototype:NewYell(...)
  9105. return newYell(self, "yell", ...)
  9106. end
  9107.  
  9108. function bossModPrototype:NewCountYell(...)
  9109. return newYell(self, "count", ...)
  9110. end
  9111.  
  9112. function bossModPrototype:NewFadesYell(...)
  9113. return newYell(self, "fade", ...)
  9114. end
  9115.  
  9116. function bossModPrototype:NewPosYell(...)
  9117. return newYell(self, "position", ...)
  9118. end
  9119. end
  9120.  
  9121. ------------------------------
  9122. -- Special Warning Object --
  9123. ------------------------------
  9124. do
  9125. local frame = CreateFrame("Frame", "DBMSpecialWarning", UIParent)
  9126. local font1 = frame:CreateFontString("DBMSpecialWarning1", "OVERLAY", "ZoneTextFont")
  9127. font1:SetWidth(1024)
  9128. font1:SetHeight(0)
  9129. font1:SetPoint("TOP", 0, 0)
  9130. local font2 = frame:CreateFontString("DBMSpecialWarning2", "OVERLAY", "ZoneTextFont")
  9131. font2:SetWidth(1024)
  9132. font2:SetHeight(0)
  9133. font2:SetPoint("TOP", font1, "BOTTOM", 0, 0)
  9134. frame:SetMovable(1)
  9135. frame:SetWidth(1)
  9136. frame:SetHeight(1)
  9137. frame:SetFrameStrata("HIGH")
  9138. frame:SetClampedToScreen()
  9139. frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
  9140.  
  9141. local font1elapsed, font2elapsed, moving
  9142.  
  9143. local function fontHide1()
  9144. local duration = DBM.Options.SpecialWarningDuration
  9145. if font1elapsed > duration * 1.3 then
  9146. font1:Hide()
  9147. if frame.font1ticker then
  9148. frame.font1ticker:Cancel()
  9149. frame.font1ticker = nil
  9150. end
  9151. elseif font1elapsed > duration then
  9152. font1elapsed = font1elapsed + 0.05
  9153. local alpha = 1 - (font1elapsed - duration) / (duration * 0.3)
  9154. font1:SetAlpha(alpha > 0 and alpha or 0)
  9155. else
  9156. font1elapsed = font1elapsed + 0.05
  9157. font1:SetAlpha(1)
  9158. end
  9159. end
  9160.  
  9161. local function fontHide2()
  9162. local duration = DBM.Options.SpecialWarningDuration
  9163. if font2elapsed > duration * 1.3 then
  9164. font2:Hide()
  9165. if frame.font2ticker then
  9166. frame.font2ticker:Cancel()
  9167. frame.font2ticker = nil
  9168. end
  9169. elseif font2elapsed > duration then
  9170. font2elapsed = font2elapsed + 0.05
  9171. local alpha = 1 - (font2elapsed - duration) / (duration * 0.3)
  9172. font2:SetAlpha(alpha > 0 and alpha or 0)
  9173. else
  9174. font2elapsed = font2elapsed + 0.05
  9175. font2:SetAlpha(1)
  9176. end
  9177. end
  9178.  
  9179. function DBM:UpdateSpecialWarningOptions()
  9180. frame:ClearAllPoints()
  9181. frame:SetPoint(self.Options.SpecialWarningPoint, UIParent, self.Options.SpecialWarningPoint, self.Options.SpecialWarningX, self.Options.SpecialWarningY)
  9182. font1:SetFont(self.Options.SpecialWarningFont, self.Options.SpecialWarningFontSize2, self.Options.SpecialWarningFontStyle == "None" and nil or self.Options.SpecialWarningFontStyle)
  9183. font2:SetFont(self.Options.SpecialWarningFont, self.Options.SpecialWarningFontSize2, self.Options.SpecialWarningFontStyle == "None" and nil or self.Options.SpecialWarningFontStyle)
  9184. font1:SetTextColor(unpack(self.Options.SpecialWarningFontCol))
  9185. font2:SetTextColor(unpack(self.Options.SpecialWarningFontCol))
  9186. if self.Options.SpecialWarningFontShadow then
  9187. font1:SetShadowOffset(1, -1)
  9188. font2:SetShadowOffset(1, -1)
  9189. else
  9190. font1:SetShadowOffset(0, 0)
  9191. font2:SetShadowOffset(0, 0)
  9192. end
  9193. end
  9194.  
  9195. function DBM:AddSpecialWarning(text, force)
  9196. local added = false
  9197. if not frame.font1ticker then
  9198. font1elapsed = 0
  9199. font1.lastUpdate = GetTime()
  9200. font1:SetText(text)
  9201. font1:Show()
  9202. added = true
  9203. frame.font1ticker = frame.font1ticker or C_TimerNewTicker(0.05, fontHide1)
  9204. elseif not frame.font2ticker or force then
  9205. font2elapsed = 0
  9206. font2.lastUpdate = GetTime()
  9207. font2:SetText(text)
  9208. font2:Show()
  9209. added = true
  9210. frame.font2ticker = frame.font2ticker or C_TimerNewTicker(0.05, fontHide2)
  9211. end
  9212. if not added then
  9213. local prevText1 = font2:GetText()
  9214. font1:SetText(prevText1)
  9215. font1elapsed = font2elapsed
  9216. self:AddSpecialWarning(text, true)
  9217. end
  9218. end
  9219.  
  9220. do
  9221. local anchorFrame
  9222. local function moveEnd(self)
  9223. moving = false
  9224. anchorFrame:Hide()
  9225. font1elapsed = self.Options.SpecialWarningDuration
  9226. font2elapsed = self.Options.SpecialWarningDuration
  9227. frame:SetFrameStrata("HIGH")
  9228. self:Unschedule(moveEnd)
  9229. self.Bars:CancelBar(DBM_CORE_MOVE_SPECIAL_WARNING_BAR)
  9230. end
  9231.  
  9232. function DBM:MoveSpecialWarning()
  9233. if not anchorFrame then
  9234. anchorFrame = CreateFrame("Frame", nil, frame)
  9235. anchorFrame:SetWidth(32)
  9236. anchorFrame:SetHeight(32)
  9237. anchorFrame:EnableMouse(true)
  9238. anchorFrame:SetPoint("TOP", frame, "TOP", 0, 32)
  9239. anchorFrame:RegisterForDrag("LeftButton")
  9240. anchorFrame:SetClampedToScreen()
  9241. anchorFrame:Hide()
  9242. local texture = anchorFrame:CreateTexture()
  9243. texture:SetTexture("Interface\\Addons\\DBM-GUI\\textures\\dot.blp")
  9244. texture:SetPoint("CENTER", anchorFrame, "CENTER", 0, 0)
  9245. texture:SetWidth(32)
  9246. texture:SetHeight(32)
  9247. anchorFrame:SetScript("OnDragStart", function()
  9248. frame:StartMoving()
  9249. self:Unschedule(moveEnd)
  9250. self.Bars:CancelBar(DBM_CORE_MOVE_SPECIAL_WARNING_BAR)
  9251. end)
  9252. anchorFrame:SetScript("OnDragStop", function()
  9253. frame:StopMovingOrSizing()
  9254. local point, _, _, xOfs, yOfs = frame:GetPoint(1)
  9255. self.Options.SpecialWarningPoint = point
  9256. self.Options.SpecialWarningX = xOfs
  9257. self.Options.SpecialWarningY = yOfs
  9258. self:Schedule(15, moveEnd, self)
  9259. self.Bars:CreateBar(15, DBM_CORE_MOVE_SPECIAL_WARNING_BAR)
  9260. end)
  9261. end
  9262. if anchorFrame:IsShown() then
  9263. moveEnd(self)
  9264. else
  9265. moving = true
  9266. anchorFrame:Show()
  9267. DBM:AddSpecialWarning(DBM_CORE_MOVE_SPECIAL_WARNING_TEXT)
  9268. DBM:AddSpecialWarning(DBM_CORE_MOVE_SPECIAL_WARNING_TEXT)
  9269. self:Schedule(15, moveEnd, self)
  9270. self.Bars:CreateBar(15, DBM_CORE_MOVE_SPECIAL_WARNING_BAR)
  9271. frame:Show()
  9272. frame:SetFrameStrata("TOOLTIP")
  9273. frame:SetAlpha(1)
  9274. end
  9275. end
  9276. end
  9277.  
  9278. local specialWarningPrototype = {}
  9279. local mt = {__index = specialWarningPrototype}
  9280.  
  9281. local function classColoringFunction(cap)
  9282. cap = cap:sub(2, -2)
  9283. local noStrip = cap:match("noStrip ")
  9284. if not noStrip then
  9285. local name = cap
  9286. if DBM.Options.StripServerName then
  9287. cap = Ambiguate(cap, "short")
  9288. end
  9289. if DBM.Options.SWarnClassColor then
  9290. local playerColor = RAID_CLASS_COLORS[DBM:GetRaidClass(name)]
  9291. if playerColor then
  9292. cap = ("|r|cff%.2x%.2x%.2x%s|r|cff%.2x%.2x%.2x"):format(playerColor.r * 255, playerColor.g * 255, playerColor.b * 255, cap, DBM.Options.SpecialWarningFontCol[1] * 255, DBM.Options.SpecialWarningFontCol[2] * 255, DBM.Options.SpecialWarningFontCol[3] * 255)
  9293. end
  9294. end
  9295. else
  9296. cap = cap:sub(9)
  9297. end
  9298. return cap
  9299. end
  9300.  
  9301. local textureCode = " |T%s:12:12|t "
  9302.  
  9303. function specialWarningPrototype:Show(...)
  9304. if not DBM.Options.DontShowSpecialWarnings and (not self.option or self.mod.Options[self.option]) and not moving and frame then
  9305. if self.announceType == "taunt" and DBM.Options.FilterTankSpec and not self.mod:IsTank() then return end--Don't tell non tanks to taunt, ever.
  9306. local argTable = {...}
  9307. if #self.combinedtext > 0 then
  9308. --Throttle spam.
  9309. if DBM.Options.SWarningAlphabetical then
  9310. tsort(self.combinedtext)
  9311. end
  9312. local combinedText = tconcat(self.combinedtext, "<, >")
  9313. if self.combinedcount == 1 then
  9314. combinedText = combinedText.." "..DBM_CORE_GENERIC_WARNING_OTHERS
  9315. elseif self.combinedcount > 1 then
  9316. combinedText = combinedText.." "..DBM_CORE_GENERIC_WARNING_OTHERS2:format(self.combinedcount)
  9317. end
  9318. --Process
  9319. for i = 1, #argTable do
  9320. if type(argTable[i]) == "string" then
  9321. argTable[i] = combinedText
  9322. end
  9323. end
  9324. end
  9325. local message = pformat(self.text, unpack(argTable))
  9326. local text = ("%s%s%s"):format(
  9327. (DBM.Options.SpecialWarningIcon and self.icon and textureCode:format(self.icon)) or "",
  9328. message,
  9329. (DBM.Options.SpecialWarningIcon and self.icon and textureCode:format(self.icon)) or ""
  9330. )
  9331. local noteHasName = false
  9332. if self.option then
  9333. local noteText = self.mod.Options[self.option .. "SWNote"]
  9334. if noteText and type(noteText) == "string" and noteText ~= "" then--Filter false bool and empty strings
  9335. local count1 = self.announceType == "count" or self.announceType == "switchcount" or self.announceType == "targetcount"
  9336. local count2 = self.announceType == "interruptcount"
  9337. if count1 or count2 then--Counts support different note for EACH count
  9338. local noteCount
  9339. local notesTable = {string.split("/", noteText)}
  9340. if count1 then
  9341. noteCount = argTable[1]--Count should be first arg in table
  9342. elseif count2 then
  9343. noteCount = argTable[2]--Count should be second arg in table
  9344. end
  9345. if type(noteCount) == "string" then
  9346. --Probably a hypehnated double count like inferno slice or marked for death
  9347. local mainCount = string.split("-", noteCount)
  9348. noteCount = tonumber(mainCount)
  9349. end
  9350. noteText = notesTable[noteCount]
  9351. if noteText and type(noteText) == "string" and noteText ~= "" then--Refilter after string split to make sure a note for this count exists
  9352. local hasPlayerName = noteText:find(playerName)
  9353. if DBM.Options.SWarnNameInNote and hasPlayerName then
  9354. noteHasName = 5
  9355. end
  9356. --Terminate special warning, it's an interrupt count warning without player name and filter enabled
  9357. if count2 and DBM.Options.FilterInterruptNoteName and not hasPlayerName then return end
  9358. noteText = " ("..noteText..")"
  9359. text = text..noteText
  9360. end
  9361. else--Non count warnings will have one note, period
  9362. if DBM.Options.SWarnNameInNote and noteText:find(playerName) then
  9363. noteHasName = 5
  9364. end
  9365. if self.announceType and self.announceType:find("switch") then
  9366. noteText = noteText:gsub(">.-<", classColoringFunction)--Class color note text before combining with warning text.
  9367. end
  9368. noteText = " ("..noteText..")"
  9369. text = text..noteText
  9370. end
  9371. end
  9372. end
  9373. --No stripping on switch warnings, ever. They will NEVER have player name, but often have adds with "-" in name
  9374. if self.announceType and not self.announceType:find("switch") then
  9375. text = text:gsub(">.-<", classColoringFunction)
  9376. end
  9377. DBM:AddSpecialWarning(text)
  9378. self.combinedcount = 0
  9379. self.combinedtext = {}
  9380. if DBM.Options.ShowSWarningsInChat then
  9381. local colorCode = ("|cff%.2x%.2x%.2x"):format(DBM.Options.SpecialWarningFontCol[1] * 255, DBM.Options.SpecialWarningFontCol[2] * 255, DBM.Options.SpecialWarningFontCol[3] * 255)
  9382. self.mod:AddMsg(colorCode.."["..DBM_CORE_MOVE_SPECIAL_WARNING_TEXT.."] "..text.."|r", nil)
  9383. end
  9384. if not UnitIsDeadOrGhost("player") and DBM.Options.ShowFlashFrame then
  9385. if noteHasName then
  9386. local repeatCount = DBM.Options.SpecialWarningFlashRepeat5 and DBM.Options.SpecialWarningFlashRepeatAmount or 0
  9387. DBM.Flash:Show(DBM.Options.SpecialWarningFlashCol5[1],DBM.Options.SpecialWarningFlashCol5[2], DBM.Options.SpecialWarningFlashCol5[3], DBM.Options.SpecialWarningFlashDura5, DBM.Options.SpecialWarningFlashAlph5, repeatCount)
  9388. elseif self.flash == 1 then
  9389. local repeatCount = DBM.Options.SpecialWarningFlashRepeat1 and DBM.Options.SpecialWarningFlashRepeatAmount or 0
  9390. DBM.Flash:Show(DBM.Options.SpecialWarningFlashCol1[1],DBM.Options.SpecialWarningFlashCol1[2], DBM.Options.SpecialWarningFlashCol1[3], DBM.Options.SpecialWarningFlashDura1, DBM.Options.SpecialWarningFlashAlph1, repeatCount)
  9391. elseif self.flash == 2 then
  9392. local repeatCount = DBM.Options.SpecialWarningFlashRepeat2 and DBM.Options.SpecialWarningFlashRepeatAmount or 0
  9393. DBM.Flash:Show(DBM.Options.SpecialWarningFlashCol2[1],DBM.Options.SpecialWarningFlashCol2[2], DBM.Options.SpecialWarningFlashCol2[3], DBM.Options.SpecialWarningFlashDura2, DBM.Options.SpecialWarningFlashAlph2, repeatCount)
  9394. elseif self.flash == 3 then
  9395. local repeatCount = DBM.Options.SpecialWarningFlashRepeat3 and DBM.Options.SpecialWarningFlashRepeatAmount or 0
  9396. DBM.Flash:Show(DBM.Options.SpecialWarningFlashCol3[1],DBM.Options.SpecialWarningFlashCol3[2], DBM.Options.SpecialWarningFlashCol3[3], DBM.Options.SpecialWarningFlashDura3, DBM.Options.SpecialWarningFlashAlph3, repeatCount)
  9397. elseif self.flash == 4 then
  9398. local repeatCount = DBM.Options.SpecialWarningFlashRepeat4 and DBM.Options.SpecialWarningFlashRepeatAmount or 0
  9399. DBM.Flash:Show(DBM.Options.SpecialWarningFlashCol4[1],DBM.Options.SpecialWarningFlashCol4[2], DBM.Options.SpecialWarningFlashCol4[3], DBM.Options.SpecialWarningFlashDura4, DBM.Options.SpecialWarningFlashAlph3, repeatCount)
  9400. end
  9401. end
  9402. --Text: Full message text
  9403. --Type: Announce type
  9404. --SpellId: Raw spell or encounter journal Id if available.
  9405. --Mod ID: Encounter ID as string, or a generic string for mods that don't have encounter ID (such as trash, dummy/test mods)
  9406. fireEvent("DBM_Announce", text, self.type, self.spellId, self.mod.id)
  9407. if self.sound then
  9408. local soundId = self.option and self.mod.Options[self.option .. "SWSound"] or self.flash
  9409. if noteHasName and type(soundId) == "number" then soundId = noteHasName end--Change number to 5 if it's not a custom sound, else, do nothing with it
  9410. if self.hasVoice and DBM.Options.ChosenVoicePack ~= "None" and self.hasVoice <= SWFilterDisabed and (type(soundId) == "number" and soundId < 5 and DBM.Options.VoiceOverSpecW2 == "DefaultOnly" or DBM.Options.VoiceOverSpecW2 == "All") and (DBM.Options.AlwaysPlayVoice or (self.mod.Options[self.voiceOptionId] or self.mod.Options[self.voiceOptionId.."2"] or self.mod.Options[self.voiceOptionId.."3"])) then return end
  9411. if not self.option or self.mod.Options[self.option.."SWSound"] ~= "None" then
  9412. DBM:PlaySpecialWarningSound(soundId or 1)
  9413. end
  9414. end
  9415. else
  9416. self.combinedcount = 0
  9417. self.combinedtext = {}
  9418. end
  9419. end
  9420.  
  9421. function specialWarningPrototype:CombinedShow(delay, ...)
  9422. local argTable = {...}
  9423. for i = 1, #argTable do
  9424. if type(argTable[i]) == "string" then
  9425. if #self.combinedtext < 8 then--Throttle spam. We may not need more than 9 targets..
  9426. if not checkEntry(self.combinedtext, argTable[i]) then
  9427. self.combinedtext[#self.combinedtext + 1] = argTable[i]
  9428. end
  9429. else
  9430. self.combinedcount = self.combinedcount + 1
  9431. end
  9432. end
  9433. end
  9434. unschedule(self.Show, self.mod, self)
  9435. schedule(delay or 0.5, self.Show, self.mod, self, ...)
  9436. end
  9437.  
  9438. function specialWarningPrototype:DelayedShow(delay, ...)
  9439. unschedule(self.Show, self.mod, self, ...)
  9440. schedule(delay or 0.5, self.Show, self.mod, self, ...)
  9441. end
  9442.  
  9443. function specialWarningPrototype:Schedule(t, ...)
  9444. return schedule(t, self.Show, self.mod, self, ...)
  9445. end
  9446.  
  9447. function specialWarningPrototype:Cancel(t, ...)
  9448. return unschedule(self.Show, self.mod, self, ...)
  9449. end
  9450.  
  9451. function bossModPrototype:NewSpecialWarning(text, optionDefault, optionName, optionVersion, runSound, hasVoice)
  9452. if not text then
  9453. error("NewSpecialWarning: you must provide special warning text", 2)
  9454. return
  9455. end
  9456. if type(text) == "string" and text:match("OptionVersion") then
  9457. print("NewSpecialWarning: you must provide remove optionversion hack for "..optionDefault)
  9458. return
  9459. end
  9460. if runSound == true then
  9461. runSound = 2
  9462. elseif not runSound then
  9463. runSound = 1
  9464. end
  9465. if hasVoice == true then--if not a number, set it to 2, old mods that don't use new numbered system
  9466. hasVoice = 2
  9467. end
  9468. local obj = setmetatable(
  9469. {
  9470. text = self.localization.warnings[text],
  9471. combinedtext = {},
  9472. combinedcount = 0,
  9473. mod = self,
  9474. sound = runSound>0,
  9475. flash = runSound,--Set flash color to hard coded runsound (even if user sets custom sounds)
  9476. hasVoice = hasVoice,
  9477. },
  9478. mt
  9479. )
  9480. local optionId = optionName or optionName ~= false and text
  9481. if optionId then
  9482. obj.voiceOptionId = hasVoice and "Voice"..optionId or nil
  9483. obj.option = optionId..(optionVersion or "")
  9484. self:AddSpecialWarningOption(optionId, optionDefault, runSound, "announce")
  9485. end
  9486. tinsert(self.specwarns, obj)
  9487. return obj
  9488. end
  9489.  
  9490. local function newSpecialWarning(self, announceType, spellId, stacks, optionDefault, optionName, optionVersion, runSound, hasVoice)
  9491. if not spellId then
  9492. error("newSpecialWarning: you must provide spellId", 2)
  9493. return
  9494. end
  9495. if type(spellId) == "string" and spellId:match("OptionVersion") then
  9496. print("NewSpecialWarning: you must provide remove optionversion hack for"..stacks)
  9497. return
  9498. end
  9499. if runSound == true then
  9500. runSound = 2
  9501. elseif not runSound then
  9502. runSound = 1
  9503. end
  9504. if hasVoice == true then--if not a number, set it to 2, old mods that don't use new numbered system
  9505. hasVoice = 2
  9506. end
  9507. local spellName
  9508. local unparsedId = spellId
  9509. if type(spellId) == "string" and spellId:match("ej%d+") then
  9510. spellName = EJ_GetSectionInfo(string.sub(spellId, 3)) or DBM_CORE_UNKNOWN
  9511. else
  9512. spellName = GetSpellInfo(spellId) or DBM_CORE_UNKNOWN
  9513. end
  9514. local text
  9515. if announceType == "prewarn" then
  9516. if type(stacks) == "string" then
  9517. text = DBM_CORE_AUTO_SPEC_WARN_TEXTS[announceType]:format(spellName, stacks)
  9518. else
  9519. text = DBM_CORE_AUTO_SPEC_WARN_TEXTS[announceType]:format(spellName, DBM_CORE_SEC_FMT:format(tostring(stacks or 5)))
  9520. end
  9521. else
  9522. text = DBM_CORE_AUTO_SPEC_WARN_TEXTS[announceType]:format(spellName)
  9523. end
  9524. local obj = setmetatable( -- todo: fix duplicate code
  9525. {
  9526. text = text,
  9527. combinedtext = {},
  9528. combinedcount = 0,
  9529. announceType = announceType,
  9530. mod = self,
  9531. sound = runSound>0,
  9532. flash = runSound,--Set flash color to hard coded runsound (even if user sets custom sounds)
  9533. hasVoice = hasVoice,
  9534. type = announceType,
  9535. spellId = unparsedId,
  9536. icon = (type(spellId) == "string" and spellId:match("ej%d+") and select(4, EJ_GetSectionInfo(string.sub(spellId, 3))) ~= "" and select(4, EJ_GetSectionInfo(string.sub(spellId, 3)))) or (type(spellId) == "number" and GetSpellTexture(spellId)) or nil
  9537. },
  9538. mt
  9539. )
  9540. if optionName then
  9541. obj.option = optionName
  9542. elseif not (optionName == false) then
  9543. obj.option = "SpecWarn"..spellId..announceType..(optionVersion or "")
  9544. if announceType == "stack" then
  9545. self.localization.options[obj.option] = DBM_CORE_AUTO_SPEC_WARN_OPTIONS[announceType]:format(stacks or 3, spellId)
  9546. elseif announceType == "prewarn" then
  9547. self.localization.options[obj.option] = DBM_CORE_AUTO_SPEC_WARN_OPTIONS[announceType]:format(tostring(stacks or 5), spellId)
  9548. else
  9549. self.localization.options[obj.option] = DBM_CORE_AUTO_SPEC_WARN_OPTIONS[announceType]:format(spellId)
  9550. end
  9551. end
  9552. if obj.option then
  9553. local catType = "announce"--Default to General announce
  9554. --Directly affects another target (boss or player) that you need to know about
  9555. if announceType == "target" or announceType == "targetcount" or announceType == "close" or announceType == "reflect" or announceType == "switch" or announceType == "switchcount" then
  9556. catType = "announceother"
  9557. --Directly affects you
  9558. elseif announceType == "you" or announceType == "youcount" or announceType == "move" or announceType == "dodge" or announceType == "moveaway" or announceType == "run" or announceType == "stack" or announceType == "moveto" then
  9559. catType = "announcepersonal"
  9560. --Things you have to do to fulfil your role
  9561. elseif announceType == "taunt" or announceType == "dispel" or announceType == "interrupt" or announceType == "interruptcount" then
  9562. catType = "announcerole"
  9563. end
  9564. self:AddSpecialWarningOption(obj.option, optionDefault, runSound, catType)
  9565. end
  9566. obj.voiceOptionId = hasVoice and "Voice"..spellId or nil
  9567. tinsert(self.specwarns, obj)
  9568. return obj
  9569. end
  9570.  
  9571. function bossModPrototype:NewSpecialWarningSpell(text, optionDefault, ...)
  9572. return newSpecialWarning(self, "spell", text, nil, optionDefault, ...)
  9573. end
  9574.  
  9575. function bossModPrototype:NewSpecialWarningEnd(text, optionDefault, ...)
  9576. return newSpecialWarning(self, "ends", text, nil, optionDefault, ...)
  9577. end
  9578.  
  9579. function bossModPrototype:NewSpecialWarningFades(text, optionDefault, ...)
  9580. return newSpecialWarning(self, "fades", text, nil, optionDefault, ...)
  9581. end
  9582.  
  9583. function bossModPrototype:NewSpecialWarningSoon(text, optionDefault, ...)
  9584. return newSpecialWarning(self, "soon", text, nil, optionDefault, ...)
  9585. end
  9586.  
  9587. function bossModPrototype:NewSpecialWarningDispel(text, optionDefault, ...)
  9588. return newSpecialWarning(self, "dispel", text, nil, optionDefault, ...)
  9589. end
  9590.  
  9591. function bossModPrototype:NewSpecialWarningInterrupt(text, optionDefault, ...)
  9592. return newSpecialWarning(self, "interrupt", text, nil, optionDefault, ...)
  9593. end
  9594.  
  9595. function bossModPrototype:NewSpecialWarningInterruptCount(text, optionDefault, ...)
  9596. return newSpecialWarning(self, "interruptcount", text, nil, optionDefault, ...)
  9597. end
  9598.  
  9599. function bossModPrototype:NewSpecialWarningYou(text, optionDefault, ...)
  9600. return newSpecialWarning(self, "you", text, nil, optionDefault, ...)
  9601. end
  9602.  
  9603. function bossModPrototype:NewSpecialWarningYouCount(text, optionDefault, ...)
  9604. return newSpecialWarning(self, "youcount", text, nil, optionDefault, ...)
  9605. end
  9606.  
  9607. function bossModPrototype:NewSpecialWarningYouPos(text, optionDefault, ...)
  9608. return newSpecialWarning(self, "youpos", text, nil, optionDefault, ...)
  9609. end
  9610.  
  9611. function bossModPrototype:NewSpecialWarningSoakPos(text, optionDefault, ...)
  9612. return newSpecialWarning(self, "soakpos", text, nil, optionDefault, ...)
  9613. end
  9614.  
  9615. function bossModPrototype:NewSpecialWarningTarget(text, optionDefault, ...)
  9616. return newSpecialWarning(self, "target", text, nil, optionDefault, ...)
  9617. end
  9618.  
  9619. function bossModPrototype:NewSpecialWarningTargetCount(text, optionDefault, ...)
  9620. return newSpecialWarning(self, "targetcount", text, nil, optionDefault, ...)
  9621. end
  9622.  
  9623. function bossModPrototype:NewSpecialWarningDefensive(text, optionDefault, ...)
  9624. return newSpecialWarning(self, "defensive", text, nil, optionDefault, ...)
  9625. end
  9626.  
  9627. function bossModPrototype:NewSpecialWarningTaunt(text, optionDefault, ...)
  9628. return newSpecialWarning(self, "taunt", text, nil, optionDefault, ...)
  9629. end
  9630.  
  9631. function bossModPrototype:NewSpecialWarningClose(text, optionDefault, ...)
  9632. return newSpecialWarning(self, "close", text, nil, optionDefault, ...)
  9633. end
  9634.  
  9635. function bossModPrototype:NewSpecialWarningMove(text, optionDefault, ...)
  9636. return newSpecialWarning(self, "move", text, nil, optionDefault, ...)
  9637. end
  9638.  
  9639. function bossModPrototype:NewSpecialWarningDodge(text, optionDefault, ...)
  9640. return newSpecialWarning(self, "dodge", text, nil, optionDefault, ...)
  9641. end
  9642.  
  9643. function bossModPrototype:NewSpecialWarningMoveAway(text, optionDefault, ...)
  9644. return newSpecialWarning(self, "moveaway", text, nil, optionDefault, ...)
  9645. end
  9646.  
  9647. function bossModPrototype:NewSpecialWarningMoveTo(text, optionDefault, ...)
  9648. return newSpecialWarning(self, "moveto", text, nil, optionDefault, ...)
  9649. end
  9650.  
  9651. function bossModPrototype:NewSpecialWarningJump(text, optionDefault, ...)
  9652. return newSpecialWarning(self, "jump", text, nil, optionDefault, ...)
  9653. end
  9654.  
  9655. function bossModPrototype:NewSpecialWarningRun(text, optionDefault, ...)
  9656. return newSpecialWarning(self, "run", text, nil, optionDefault, ...)
  9657. end
  9658.  
  9659. function bossModPrototype:NewSpecialWarningCast(text, optionDefault, ...)
  9660. return newSpecialWarning(self, "cast", text, nil, optionDefault, ...)
  9661. end
  9662.  
  9663. function bossModPrototype:NewSpecialWarningReflect(text, optionDefault, ...)
  9664. return newSpecialWarning(self, "reflect", text, nil, optionDefault, ...)
  9665. end
  9666.  
  9667. function bossModPrototype:NewSpecialWarningCount(text, optionDefault, ...)
  9668. return newSpecialWarning(self, "count", text, nil, optionDefault, ...)
  9669. end
  9670.  
  9671. function bossModPrototype:NewSpecialWarningStack(text, optionDefault, stacks, ...)
  9672. if type(text) == "string" and text:match("OptionVersion") then
  9673. print("NewSpecialWarning: you must provide remove optionversion hack for "..optionDefault)
  9674. end
  9675. return newSpecialWarning(self, "stack", text, stacks, optionDefault, ...)
  9676. end
  9677.  
  9678. function bossModPrototype:NewSpecialWarningSwitch(text, optionDefault, ...)
  9679. return newSpecialWarning(self, "switch", text, nil, optionDefault, ...)
  9680. end
  9681.  
  9682. function bossModPrototype:NewSpecialWarningSwitchCount(text, optionDefault, ...)
  9683. return newSpecialWarning(self, "switchcount", text, nil, optionDefault, ...)
  9684. end
  9685.  
  9686. function bossModPrototype:NewSpecialWarningPreWarn(text, optionDefault, time, ...)
  9687. if type(text) == "string" and text:match("OptionVersion") then
  9688. print("NewSpecialWarning: you must provide remove optionversion hack for "..optionDefault)
  9689. end
  9690. return newSpecialWarning(self, "prewarn", text, time, optionDefault, ...)
  9691. end
  9692.  
  9693. function DBM:PlayCountSound(number, forceVoice)
  9694. if number > 10 then return end
  9695. local voice
  9696. if forceVoice then--For options example
  9697. voice = forceVoice
  9698. else
  9699. voice = self.Options.CountdownVoice
  9700. end
  9701. local path
  9702. local maxCount = 5
  9703. for i = 1, #self.Counts do
  9704. if self.Counts[i].value == voice then
  9705. path = self.Counts[i].path
  9706. maxCount = self.Counts[i].max
  9707. break
  9708. end
  9709. end
  9710. if not path or (number > maxCount) then return end
  9711. self:PlaySoundFile(path..number..".ogg")
  9712. end
  9713.  
  9714. function DBM:RegisterCountSound(t, v, p, m)
  9715. --Prevent duplicate insert
  9716. for i = 1, #self.Counts do
  9717. if self.Counts[i].value == v then return end
  9718. end
  9719. --Insert into counts table.
  9720. if t and v and p and m then
  9721. tinsert(self.Counts, { text = t, value = v, path = p, max = m })
  9722. end
  9723. end
  9724.  
  9725. function DBM:CheckVoicePackVersion(value)
  9726. local activeVP = self.Options.ChosenVoicePack
  9727. --Check if voice pack out of date
  9728. if activeVP ~= "None" and activeVP == value then
  9729. if self.VoiceVersions[value] < 6 then--Version will be bumped when new voice packs released that contain new voices.
  9730. self:AddMsg(DBM_CORE_VOICE_PACK_OUTDATED)
  9731. SWFilterDisabed = self.VoiceVersions[value]--Set disable to version on current voice pack
  9732. else
  9733. SWFilterDisabed = 6
  9734. end
  9735. end
  9736. end
  9737.  
  9738. function DBM:PlaySpecialWarningSound(soundId)
  9739. local sound = type(soundId) == "number" and self.Options["SpecialWarningSound" .. (soundId == 1 and "" or soundId)] or soundId or self.Options.SpecialWarningSound
  9740. self:PlaySoundFile(sound)
  9741. end
  9742.  
  9743. local function testWarningEnd()
  9744. frame:SetFrameStrata("HIGH")
  9745. end
  9746.  
  9747. function DBM:ShowTestSpecialWarning(text, number, noSound)
  9748. if moving then
  9749. return
  9750. end
  9751. self:AddSpecialWarning(DBM_CORE_MOVE_SPECIAL_WARNING_TEXT)
  9752. frame:SetFrameStrata("TOOLTIP")
  9753. self:Unschedule(testWarningEnd)
  9754. self:Schedule(self.Options.SpecialWarningDuration * 1.3, testWarningEnd)
  9755. if number and not noSound then
  9756. self:PlaySpecialWarningSound(number)
  9757. end
  9758. if self.Options.ShowFlashFrame and number then
  9759. if number == 1 then
  9760. local repeatCount = self.Options.SpecialWarningFlashRepeat1 and self.Options.SpecialWarningFlashRepeatAmount or 0
  9761. self.Flash:Show(self.Options.SpecialWarningFlashCol1[1],self.Options.SpecialWarningFlashCol1[2], self.Options.SpecialWarningFlashCol1[3], self.Options.SpecialWarningFlashDura1, self.Options.SpecialWarningFlashAlph1, repeatCount)
  9762. elseif number == 2 then
  9763. local repeatCount = self.Options.SpecialWarningFlashRepeat2 and self.Options.SpecialWarningFlashRepeatAmount or 0
  9764. self.Flash:Show(self.Options.SpecialWarningFlashCol2[1],self.Options.SpecialWarningFlashCol2[2], self.Options.SpecialWarningFlashCol2[3], self.Options.SpecialWarningFlashDura2, self.Options.SpecialWarningFlashAlph2, repeatCount)
  9765. elseif number == 3 then
  9766. local repeatCount = self.Options.SpecialWarningFlashRepeat3 and self.Options.SpecialWarningFlashRepeatAmount or 0
  9767. self.Flash:Show(self.Options.SpecialWarningFlashCol3[1],self.Options.SpecialWarningFlashCol3[2], self.Options.SpecialWarningFlashCol3[3], self.Options.SpecialWarningFlashDura3, self.Options.SpecialWarningFlashAlph3, repeatCount)
  9768. elseif number == 4 then
  9769. local repeatCount = self.Options.SpecialWarningFlashRepeat4 and self.Options.SpecialWarningFlashRepeatAmount or 0
  9770. self.Flash:Show(self.Options.SpecialWarningFlashCol4[1],self.Options.SpecialWarningFlashCol4[2], self.Options.SpecialWarningFlashCol4[3], self.Options.SpecialWarningFlashDura4, self.Options.SpecialWarningFlashAlph4, repeatCount)
  9771. elseif number == 5 then
  9772. local repeatCount = self.Options.SpecialWarningFlashRepeat5 and self.Options.SpecialWarningFlashRepeatAmount or 0
  9773. self.Flash:Show(self.Options.SpecialWarningFlashCol5[1],self.Options.SpecialWarningFlashCol5[2], self.Options.SpecialWarningFlashCol5[3], self.Options.SpecialWarningFlashDura5, self.Options.SpecialWarningFlashAlph5, repeatCount)
  9774. end
  9775. end
  9776. end
  9777.  
  9778. function DBM:ShowTestHUD()
  9779. local x, y = UnitPosition("player")
  9780. DBMHudMap:RegisterPositionMarker(10000, "Test1", "highlight", x, y-20, 5, 10, 1, 1, 0, 0.5, nil, 1):Pulse(0.5, 0.5)
  9781. DBMHudMap:RegisterPositionMarker(20000, "Test2", "highlight", x-20, y, 5, 10, 1, 0, 0, 0.5, nil, 2):Pulse(0.5, 0.5)
  9782. DBMHudMap:RegisterPositionMarker(30000, "Test3", "highlight", x+20, y, 5, 10, 1, 0.5, 0, 0.5, nil, 3):Pulse(0.5, 0.5)
  9783. DBMHudMap:RegisterPositionMarker(40000, "Test4", "highlight", x, y+20, 5, 10, 0, 1, 0, 0.5, nil, 4):Pulse(0.5, 0.5)
  9784. end
  9785. end
  9786.  
  9787. --------------------
  9788. -- Timer Object --
  9789. --------------------
  9790. do
  9791. local timerPrototype = {}
  9792. local mt = {__index = timerPrototype}
  9793.  
  9794. function timerPrototype:Start(timer, ...)
  9795. if DBM.Options.DontShowBossTimers then return end
  9796. if timer and type(timer) ~= "number" then
  9797. return self:Start(nil, timer, ...) -- first argument is optional!
  9798. end
  9799. if not self.option or self.mod.Options[self.option] then
  9800. if self.type and self.type:find("count") and not self.allowdouble then--cdcount, nextcount. remove previous timer.
  9801. for i = #self.startedTimers, 1, -1 do
  9802. if DBM.Options.AutoCorrectTimer or (DBM.Options.DebugMode and DBM.Options.DebugLevel > 1) then
  9803. local bar = DBM.Bars:GetBar(self.startedTimers[i])
  9804. if bar then
  9805. local remaining = ("%.1f"):format(bar.timer)
  9806. local ttext = _G[bar.frame:GetName().."BarName"]:GetText() or ""
  9807. ttext = ttext.."("..self.id..")"
  9808. if bar.timer > 0.2 then
  9809. if timer then
  9810. self.correctedCast = timer - bar.timer--Store what lowest timer is in timer object
  9811. self.correctedDiff = difficultyIndex--Store index of correction to ensure the change is only used in one difficulty (so a mythic timer doesn't alter heroic for example)
  9812. end
  9813. DBM:Debug("Timer "..ttext.. " refreshed before expired. Remaining time is : "..remaining, 2)
  9814. end
  9815. end
  9816. end
  9817. DBM.Bars:CancelBar(self.startedTimers[i])
  9818. self.startedTimers[i] = nil
  9819. end
  9820. end
  9821. local timer = timer and ((timer > 0 and timer) or self.timer + timer) or self.timer
  9822. --AI timer api:
  9823. --Starting ai timer with (1) indicates it's a first timer after pull
  9824. --Starting timer with (2) or (3) indicates it's a phase 2 or phase 3 first timer
  9825. --Starting AI timer with anything above 3 indicarets it's a regular timer and to use shortest time in between two regular casts
  9826. if self.type == "ai" then--A learning timer
  9827. if not DBM.Options.AITimer then return end
  9828. if timer > 4 then--Normal behavior.
  9829. if self.firstCastTimer and type(self.firstCastTimer) == "string" then--This is first cast of spell, we need to generate self.firstPullTimer
  9830. self.firstCastTimer = tonumber(self.firstCastTimer)
  9831. self.firstCastTimer = GetTime() - self.firstCastTimer--We have generated a self.firstCastTimer! Next pull, DBM should know timer for first cast next pull. FANCY!
  9832. DBM:Debug("AI timer learned a first timer for pull of "..self.firstCastTimer, 2)
  9833. end
  9834. if self.phase4CastTimer and type(self.phase4CastTimer) == "string" then--This is first cast of spell after a phase transition, we need to generate self.phaseCastTimer
  9835. self.phase4CastTimer = tonumber(self.phase4CastTimer)
  9836. self.phase4CastTimer = GetTime() - self.phase4CastTimer--We have generated a self.phaseCastTimer!
  9837. DBM:Debug("AI timer learned a first timer for phase of "..self.phase4CastTimer, 2)
  9838. end
  9839. if self.phase3CastTimer and type(self.phase3CastTimer) == "string" then--This is first cast of spell after a phase transition, we need to generate self.phaseCastTimer
  9840. self.phase3CastTimer = tonumber(self.phase3CastTimer)
  9841. self.phase3CastTimer = GetTime() - self.phase3CastTimer--We have generated a self.phaseCastTimer!
  9842. DBM:Debug("AI timer learned a first timer for phase of "..self.phase3CastTimer, 2)
  9843. end
  9844. if self.phase2CastTimer and type(self.phase2CastTimer) == "string" then--This is first cast of spell after a phase transition, we need to generate self.phaseCastTimer
  9845. self.phase2CastTimer = tonumber(self.phase2CastTimer)
  9846. self.phase2CastTimer = GetTime() - self.phase2CastTimer--We have generated a self.phaseCastTimer!
  9847. DBM:Debug("AI timer learned a first timer for phase of "..self.phase2CastTimer, 2)
  9848. end
  9849. if self.lastCast then--We have a GetTime() on last cast
  9850. local timeLastCast = GetTime() - self.lastCast--Get time between current cast and last cast
  9851. if timeLastCast > 4 then--Prevent infinite loop cpu hang. Plus anything shorter than 5 seconds doesn't need a timer
  9852. if not self.lowestSeenCast or (self.lowestSeenCast and self.lowestSeenCast > timeLastCast) then--Always use lowest seen cast for a timer
  9853. self.lowestSeenCast = timeLastCast
  9854. DBM:Debug("AI timer learned a new lowest timer of "..self.lowestSeenCast, 2)
  9855. end
  9856. end
  9857. end
  9858. self.lastCast = GetTime()
  9859. if self.lowestSeenCast then--Always use lowest seen cast for timer
  9860. timer = self.lowestSeenCast
  9861. else
  9862. return--Don't start the bogus timer shoved into timer field in the mod
  9863. end
  9864. elseif timer == 4 then
  9865. if self.phase4CastTimer and type(self.phase4CastTimer) == "number" then
  9866. timer = self.phase4CastTimer
  9867. else--No first pull timer generated yet, set it to GetTime, as a string
  9868. self.phase4CastTimer = tostring(GetTime())
  9869. return--Don't start the 4 second timer
  9870. end
  9871. elseif timer == 3 then
  9872. if self.phase3CastTimer and type(self.phase3CastTimer) == "number" then
  9873. timer = self.phase3CastTimer
  9874. else--No first pull timer generated yet, set it to GetTime, as a string
  9875. self.phase3CastTimer = tostring(GetTime())
  9876. return--Don't start the 3 second timer
  9877. end
  9878. elseif timer == 2 then
  9879. if self.phase2CastTimer and type(self.phase2CastTimer) == "number" then
  9880. timer = self.phase2CastTimer
  9881. else--No first pull timer generated yet, set it to GetTime, as a string
  9882. self.phase2CastTimer = tostring(GetTime())
  9883. return--Don't start the 2 second timer
  9884. end
  9885. else--1 was sent, trigger a first Cast timer
  9886. if self.firstCastTimer and type(self.firstCastTimer) == "number" then
  9887. timer = self.firstCastTimer
  9888. else--No first pull timer generated yet, set it to GetTime, as a string
  9889. self.firstCastTimer = tostring(GetTime())
  9890. return--Don't start the 1 second timer
  9891. end
  9892. end
  9893. end
  9894. local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
  9895. if DBM.Options.AutoCorrectTimer or (DBM.Options.DebugMode and DBM.Options.DebugLevel > 1) then
  9896. if not self.type or (self.type ~= "target" and self.type ~= "active" and self.type ~= "fades" and self.type ~= "ai") then
  9897. local bar = DBM.Bars:GetBar(id)
  9898. if bar then
  9899. local remaining = ("%.1f"):format(bar.timer)
  9900. local ttext = _G[bar.frame:GetName().."BarName"]:GetText() or ""
  9901. ttext = ttext.."("..self.id..")"
  9902. if bar.timer > 0.2 then
  9903. self.correctedCast = timer - bar.timer--Store what lowest timer is for advanced user feature
  9904. self.correctedDiff = difficultyIndex--Store index of correction to ensure the change is only used in one difficulty (so a mythic timer doesn't alter heroic for example
  9905. DBM:Debug("Timer "..ttext.. " refreshed before expired. Remaining time is : "..remaining, 2)
  9906. end
  9907. end
  9908. end
  9909. end
  9910. if DBM.Options.AutoCorrectTimer and self.correctedCast and self.correctedDiff and self.correctedDiff == difficultyIndex and self.correctedCast < timer then
  9911. local debugtemp = timer - self.correctedCast
  9912. DBM:Debug("Timer autocorrected by "..debugtemp, 2)
  9913. timer = self.correctedCast
  9914. end
  9915. local colorId = 0
  9916. if self.option then
  9917. colorId = self.mod.Options[self.option .. "TColor"]
  9918. end
  9919. local bar = DBM.Bars:CreateBar(timer, id, self.icon, nil, nil, nil, nil, colorId)
  9920. if not bar then
  9921. return false, "error" -- creating the timer failed somehow, maybe hit the hard-coded timer limit of 15
  9922. end
  9923. local msg = ""
  9924. if self.type and not self.text then
  9925. msg = pformat(self.mod:GetLocalizedTimerText(self.type, self.spellId), ...)
  9926. else
  9927. if type(self.text) == "number" then
  9928. msg = pformat(self.mod:GetLocalizedTimerText(self.type, self.text), ...)
  9929. else
  9930. msg = pformat(self.text, ...)
  9931. end
  9932. end
  9933. msg = msg:gsub(">.-<", stripServerName)
  9934. bar:SetText(msg, self.inlineIcon)
  9935. --ID: Internal DBM timer ID
  9936. --msg: Timer Text
  9937. --timer: Raw timer value (number).
  9938. --Icon: Texture Path for Icon
  9939. --type: Timer type (Cooldowns: cd, cdcount, nextcount, nextsource, cdspecial, nextspecial, phase, ai. Durations: target, active, fades, roleplay. Casting: cast)
  9940. --spellId: Raw spellid if available (most timers will have spellId or EJ ID unless it's a specific timer not tied to ability such as pull or combat start or rez timers. EJ id will be in format ej%d
  9941. --colorID: Type classification (1-Add, 2-Aoe, 3-targeted ability, 4-Interrupt, 5-Role, 6-Phase, 7-User(custom))
  9942. --Mod ID: Encounter ID as string, or a generic string for mods that don't have encounter ID (such as trash, dummy/test mods)
  9943. fireEvent("DBM_TimerStart", id, msg, timer, self.icon, self.type, self.spellId, colorId, self.mod.id)
  9944. tinsert(self.startedTimers, id)
  9945. self.mod:Unschedule(removeEntry, self.startedTimers, id)
  9946. self.mod:Schedule(timer, removeEntry, self.startedTimers, id)
  9947. return bar
  9948. else
  9949. return false, "disabled"
  9950. end
  9951. end
  9952. timerPrototype.Show = timerPrototype.Start
  9953.  
  9954. function timerPrototype:DelayedStart(delay, ...)
  9955. unschedule(self.Start, self.mod, self, ...)
  9956. schedule(delay or 0.5, self.Start, self.mod, self, ...)
  9957. end
  9958. timerPrototype.DelayedShow = timerPrototype.DelayedStart
  9959.  
  9960. function timerPrototype:Schedule(t, ...)
  9961. return schedule(t, self.Start, self.mod, self, ...)
  9962. end
  9963.  
  9964. function timerPrototype:Unschedule(...)
  9965. return unschedule(self.Start, self.mod, self, ...)
  9966. end
  9967.  
  9968. function timerPrototype:Stop(...)
  9969. if select("#", ...) == 0 then
  9970. for i = #self.startedTimers, 1, -1 do
  9971. --This callback sucks, it needs useful information for external mods to listen to it better, such as mod and spellid
  9972. fireEvent("DBM_TimerStop", self.startedTimers[i])
  9973. DBM.Bars:CancelBar(self.startedTimers[i])
  9974. self.startedTimers[i] = nil
  9975. end
  9976. else
  9977. local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
  9978. for i = #self.startedTimers, 1, -1 do
  9979. if self.startedTimers[i] == id then
  9980. --This callback sucks, it needs useful information for external mods to listen to it better, such as mod and spellid
  9981. fireEvent("DBM_TimerStop", id)
  9982. DBM.Bars:CancelBar(id)
  9983. tremove(self.startedTimers, i)
  9984. end
  9985. end
  9986. end
  9987. end
  9988.  
  9989. function timerPrototype:Cancel(...)
  9990. self:Stop(...)
  9991. self:Unschedule(...)
  9992. end
  9993.  
  9994. function timerPrototype:GetTime(...)
  9995. local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
  9996. local bar = DBM.Bars:GetBar(id)
  9997. return bar and (bar.totalTime - bar.timer) or 0, (bar and bar.totalTime) or 0
  9998. end
  9999.  
  10000. function timerPrototype:GetRemaining(...)
  10001. local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
  10002. local bar = DBM.Bars:GetBar(id)
  10003. return bar and bar.timer or 0
  10004. end
  10005.  
  10006. function timerPrototype:IsStarted(...)
  10007. local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
  10008. local bar = DBM.Bars:GetBar(id)
  10009. return bar and true
  10010. end
  10011.  
  10012. function timerPrototype:SetTimer(timer)
  10013. self.timer = timer
  10014. end
  10015.  
  10016. function timerPrototype:Update(elapsed, totalTime, ...)
  10017. if DBM.Options.DontShowBossTimers then return end
  10018. if self:GetTime(...) == 0 then
  10019. self:Start(totalTime, ...)
  10020. end
  10021. local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
  10022. return DBM.Bars:UpdateBar(id, elapsed, totalTime)
  10023. end
  10024.  
  10025. function timerPrototype:UpdateIcon(icon, ...)
  10026. local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
  10027. local bar = DBM.Bars:GetBar(id)
  10028. if bar then
  10029. return bar:SetIcon((type(icon) == "string" and icon:match("ej%d+") and select(4, EJ_GetSectionInfo(string.sub(icon, 3))) ~= "" and select(4, EJ_GetSectionInfo(string.sub(icon, 3)))) or (type(icon) == "number" and GetSpellTexture(icon)) or icon or "Interface\\Icons\\Spell_Nature_WispSplode")
  10030. end
  10031. end
  10032.  
  10033. function timerPrototype:UpdateName(name, ...)
  10034. local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
  10035. local bar = DBM.Bars:GetBar(id)
  10036. if bar then
  10037. return bar:SetText(name, self.inlineIcon)
  10038. end
  10039. end
  10040.  
  10041. function timerPrototype:SetColor(c, ...)
  10042. local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
  10043. local bar = DBM.Bars:GetBar(id)
  10044. if bar then
  10045. return bar:SetColor(c)
  10046. end
  10047. end
  10048.  
  10049. function timerPrototype:DisableEnlarge(...)
  10050. local id = self.id..pformat((("\t%s"):rep(select("#", ...))), ...)
  10051. local bar = DBM.Bars:GetBar(id)
  10052. if bar then
  10053. bar.small = true
  10054. end
  10055. end
  10056.  
  10057. function timerPrototype:AddOption(optionDefault, optionName, colorType)
  10058. if optionName ~= false then
  10059. self.option = optionName or self.id
  10060. self.mod:AddBoolOption(self.option, optionDefault, "timer", nil, colorType)
  10061. end
  10062. end
  10063.  
  10064. function bossModPrototype:NewTimer(timer, name, icon, optionDefault, optionName, colorType, inlineIcon, r, g, b)
  10065. local icon = (type(icon) == "string" and icon:match("ej%d+") and select(4, EJ_GetSectionInfo(string.sub(icon, 3))) ~= "" and select(4, EJ_GetSectionInfo(string.sub(icon, 3)))) or (type(icon) == "number" and GetSpellTexture(icon)) or icon or "Interface\\Icons\\Spell_Nature_WispSplode"
  10066. local obj = setmetatable(
  10067. {
  10068. text = self.localization.timers[name],
  10069. timer = timer,
  10070. id = name,
  10071. icon = icon,
  10072. colorType = colorType,
  10073. inlineIcon = inlineIcon,
  10074. r = r,
  10075. g = g,
  10076. b = b,
  10077. startedTimers = {},
  10078. mod = self,
  10079. },
  10080. mt
  10081. )
  10082. obj:AddOption(optionDefault, optionName, colorType)
  10083. tinsert(self.timers, obj)
  10084. return obj
  10085. end
  10086.  
  10087. -- new constructor for the new auto-localized timer types
  10088. -- note that the function might look unclear because it needs to handle different timer types, especially achievement timers need special treatment
  10089. local function newTimer(self, timerType, timer, spellId, timerText, optionDefault, optionName, colorType, texture, inlineIcon, r, g, b)
  10090. if type(timer) == "string" and timer:match("OptionVersion") then
  10091. DBM:Debug("OptionVersion hack depricated, remove it from: "..spellId)
  10092. return
  10093. end
  10094. if type(colorType) == "number" and colorType > 6 then
  10095. DBM:Debug("texture is in the colorType arg for: "..spellId)
  10096. end
  10097. --Use option optionName for optionVersion as well, no reason to split.
  10098. --This ensures that remaining arg positions match for auto generated and regular NewTimer
  10099. local optionVersion
  10100. if type(optionName) == "number" then
  10101. optionVersion = optionName
  10102. optionName = nil
  10103. end
  10104. local allowdouble
  10105. if type(timer) == "string" and timer:match("d%d+") then
  10106. allowdouble = true
  10107. timer = tonumber(string.sub(timer, 2))
  10108. end
  10109. local spellName, icon
  10110. local unparsedId = spellId
  10111. if timerType == "achievement" then
  10112. spellName = select(2, GetAchievementInfo(spellId))
  10113. icon = type(texture) == "number" and select(10, GetAchievementInfo(texture)) or texture or spellId and select(10, GetAchievementInfo(spellId))
  10114. elseif timerType == "cdspecial" or timerType == "nextspecial" or timerType == "phase" then
  10115. icon = type(texture) == "number" and GetSpellTexture(texture) or texture or type(spellId) == "string" and select(4, EJ_GetSectionInfo(string.sub(spellId, 3))) ~= "" and select(4, EJ_GetSectionInfo(string.sub(spellId, 3))) or (type(spellId) == "number" and GetSpellTexture(spellId)) or "Interface\\Icons\\Spell_Nature_WispSplode"
  10116. if timerType == "phase" then
  10117. colorType = 6
  10118. end
  10119. elseif timerType == "roleplay" then
  10120. icon = "Interface\\Icons\\Spell_Holy_BorrowedTime"
  10121. colorType = 6
  10122. else
  10123. if type(spellId) == "string" and spellId:match("ej%d+") then
  10124. spellName = EJ_GetSectionInfo(string.sub(spellId, 3)) or ""
  10125. else
  10126. spellName = GetSpellInfo(spellId or 0)
  10127. end
  10128. if spellName then
  10129. icon = type(texture) == "number" and GetSpellTexture(texture) or texture or type(spellId) == "string" and select(4, EJ_GetSectionInfo(string.sub(spellId, 3))) ~= "" and select(4, EJ_GetSectionInfo(string.sub(spellId, 3))) or (type(spellId) == "number" and GetSpellTexture(spellId))
  10130. else
  10131. icon = nil
  10132. end
  10133. end
  10134. spellName = spellName or tostring(spellId)
  10135. local timerTextValue
  10136. --If timertext is a number, accept it as a secondary auto translate spellid
  10137. if timerText and type(timerText) == "number" and DBM.Options.ShortTimerText then
  10138. timerTextValue = timerText
  10139. else
  10140. timerTextValue = self.localization.timers[timerText]
  10141. end
  10142. local id = "Timer"..(spellId or 0)..timerType..(optionVersion or "")
  10143. local obj = setmetatable(
  10144. {
  10145. text = timerTextValue,
  10146. type = timerType,
  10147. spellId = spellId,
  10148. timer = timer,
  10149. id = id,
  10150. icon = icon,
  10151. colorType = colorType,
  10152. inlineIcon = inlineIcon,
  10153. r = r,
  10154. g = g,
  10155. b = b,
  10156. allowdouble = allowdouble,
  10157. startedTimers = {},
  10158. mod = self,
  10159. },
  10160. mt
  10161. )
  10162. obj:AddOption(optionDefault, optionName, colorType)
  10163. tinsert(self.timers, obj)
  10164. -- todo: move the string creation to the GUI with SetFormattedString...
  10165. if timerType == "achievement" then
  10166. self.localization.options[id] = DBM_CORE_AUTO_TIMER_OPTIONS[timerType]:format(GetAchievementLink(spellId):gsub("%[(.+)%]", "%1"))
  10167. elseif timerType == "cdspecial" or timerType == "nextspecial" or timerType == "phase" or timerType == "roleplay" then--Timers without spellid, generic
  10168. self.localization.options[id] = DBM_CORE_AUTO_TIMER_OPTIONS[timerType]--Using more than 1 phase timer or more than 1 special timer will break this, fortunately you should NEVER use more than 1 of either in a mod
  10169. else
  10170. self.localization.options[id] = DBM_CORE_AUTO_TIMER_OPTIONS[timerType]:format(unparsedId)
  10171. end
  10172. return obj
  10173. end
  10174.  
  10175. function bossModPrototype:NewTargetTimer(...)
  10176. return newTimer(self, "target", ...)
  10177. end
  10178.  
  10179. function bossModPrototype:NewBuffActiveTimer(...)
  10180. return newTimer(self, "active", ...)
  10181. end
  10182.  
  10183. function bossModPrototype:NewBuffFadesTimer(...)
  10184. return newTimer(self, "fades", ...)
  10185. end
  10186.  
  10187. function bossModPrototype:NewCastTimer(timer, ...)
  10188. if tonumber(timer) and timer > 1000 then -- hehe :) best hack in DBM. This makes the first argument optional, so we can omit it to use the cast time from the spell id ;)
  10189. local spellId = timer
  10190. timer = select(4, GetSpellInfo(spellId)) or 1000 -- GetSpellInfo takes YOUR spell haste into account...WTF?
  10191. local spellHaste = select(4, GetSpellInfo(53142)) / 10000 -- 53142 = Dalaran Portal, should have 10000 ms cast time
  10192. timer = timer / spellHaste -- calculate the real cast time of the spell...
  10193. return self:NewCastTimer(timer / 1000, spellId, ...)
  10194. end
  10195. return newTimer(self, "cast", timer, ...)
  10196. end
  10197.  
  10198. function bossModPrototype:NewCastSourceTimer(timer, ...)
  10199. if tonumber(timer) and timer > 1000 then -- hehe :) best hack in DBM. This makes the first argument optional, so we can omit it to use the cast time from the spell id ;)
  10200. local spellId = timer
  10201. timer = select(4, GetSpellInfo(spellId)) or 1000 -- GetSpellInfo takes YOUR spell haste into account...WTF?
  10202. local spellHaste = select(4, GetSpellInfo(53142)) / 10000 -- 53142 = Dalaran Portal, should have 10000 ms cast time
  10203. timer = timer / spellHaste -- calculate the real cast time of the spell...
  10204. return self:NewCastSourceTimer(timer / 1000, spellId, ...)
  10205. end
  10206. return newTimer(self, "castsource", timer, ...)
  10207. end
  10208.  
  10209. function bossModPrototype:NewCDTimer(...)
  10210. return newTimer(self, "cd", ...)
  10211. end
  10212.  
  10213. function bossModPrototype:NewCDCountTimer(...)
  10214. return newTimer(self, "cdcount", ...)
  10215. end
  10216.  
  10217. function bossModPrototype:NewCDSourceTimer(...)
  10218. return newTimer(self, "cdsource", ...)
  10219. end
  10220.  
  10221. function bossModPrototype:NewNextTimer(...)
  10222. return newTimer(self, "next", ...)
  10223. end
  10224.  
  10225. function bossModPrototype:NewNextCountTimer(...)
  10226. return newTimer(self, "nextcount", ...)
  10227. end
  10228.  
  10229. function bossModPrototype:NewNextSourceTimer(...)
  10230. return newTimer(self, "nextsource", ...)
  10231. end
  10232.  
  10233. function bossModPrototype:NewAchievementTimer(...)
  10234. return newTimer(self, "achievement", ...)
  10235. end
  10236.  
  10237. function bossModPrototype:NewCDSpecialTimer(...)
  10238. return newTimer(self, "cdspecial", ...)
  10239. end
  10240.  
  10241. function bossModPrototype:NewNextSpecialTimer(...)
  10242. return newTimer(self, "nextspecial", ...)
  10243. end
  10244.  
  10245. function bossModPrototype:NewPhaseTimer(...)
  10246. return newTimer(self, "phase", ...)
  10247. end
  10248.  
  10249. function bossModPrototype:NewRPTimer(...)
  10250. return newTimer(self, "roleplay", ...)
  10251. end
  10252.  
  10253. function bossModPrototype:NewAITimer(...)
  10254. return newTimer(self, "ai", ...)
  10255. end
  10256.  
  10257. function bossModPrototype:GetLocalizedTimerText(timerType, spellId)
  10258. local spellName
  10259. if timerType == "achievement" then
  10260. spellName = select(2, GetAchievementInfo(spellId))
  10261. elseif type(spellId) == "string" and spellId:match("ej%d+") then
  10262. spellName = EJ_GetSectionInfo(string.sub(spellId, 3))
  10263. else
  10264. spellName = GetSpellInfo(spellId)
  10265. end
  10266. return pformat(DBM_CORE_AUTO_TIMER_TEXTS[timerType], spellName)
  10267. end
  10268. end
  10269.  
  10270. ------------------------------
  10271. -- Berserk/Combat Objects --
  10272. ------------------------------
  10273. do
  10274. local enragePrototype = {}
  10275. local mt = {__index = enragePrototype}
  10276.  
  10277. function enragePrototype:Start(timer)
  10278. timer = timer or self.timer or 600
  10279. timer = timer <= 0 and self.timer - timer or timer
  10280. self.bar:SetTimer(timer)
  10281. self.bar:Start()
  10282. if self.warning1 then
  10283. if timer > 660 then self.warning1:Schedule(timer - 600, 10, DBM_CORE_MIN) end
  10284. if timer > 300 then self.warning1:Schedule(timer - 300, 5, DBM_CORE_MIN) end
  10285. if timer > 180 then self.warning2:Schedule(timer - 180, 3, DBM_CORE_MIN) end
  10286. end
  10287. if self.warning2 then
  10288. if timer > 60 then self.warning2:Schedule(timer - 60, 1, DBM_CORE_MIN) end
  10289. if timer > 30 then self.warning2:Schedule(timer - 30, 30, DBM_CORE_SEC) end
  10290. if timer > 10 then self.warning2:Schedule(timer - 10, 10, DBM_CORE_SEC) end
  10291. end
  10292. if self.countdown then
  10293. if not DBM.Options.DontPlayPTCountdown then
  10294. self.countdown:Start(timer)
  10295. end
  10296. if not DBM.Options.DontShowPTCountdownText then
  10297. local threshold = DBM.Options.PTCountThreshold
  10298. if timer > threshold then
  10299. DBM:Schedule(timer-threshold, countDownTextDelay, threshold)
  10300. else
  10301. TimerTracker_OnEvent(TimerTracker, "START_TIMER", 2, timer, timer)
  10302. end
  10303. end
  10304. end
  10305. end
  10306.  
  10307. function enragePrototype:Schedule(t)
  10308. return self.owner:Schedule(t, self.Start, self)
  10309. end
  10310.  
  10311. function enragePrototype:Cancel()
  10312. self.owner:Unschedule(self.Start, self)
  10313. if self.warning1 then
  10314. self.warning1:Cancel()
  10315. end
  10316. if self.warning2 then
  10317. self.warning2:Cancel()
  10318. end
  10319. if self.countdown then
  10320. DBM:Unschedule(countDownTextDelay)
  10321. self.countdown:Cancel()
  10322. TimerTracker_OnEvent(TimerTracker, "PLAYER_ENTERING_WORLD")
  10323. end
  10324. self.bar:Stop()
  10325. end
  10326. enragePrototype.Stop = enragePrototype.Cancel
  10327.  
  10328. function bossModPrototype:NewBerserkTimer(timer, text, barText, barIcon)
  10329. timer = timer or 600
  10330. local warning1 = self:NewAnnounce(text or DBM_CORE_GENERIC_WARNING_BERSERK, 1, nil, "warning_berserk", false)
  10331. local warning2 = self:NewAnnounce(text or DBM_CORE_GENERIC_WARNING_BERSERK, 4, nil, "warning_berserk", false)
  10332. local bar = self:NewTimer(timer, barText or DBM_CORE_GENERIC_TIMER_BERSERK, barIcon or 28131, nil, "timer_berserk")
  10333. local obj = setmetatable(
  10334. {
  10335. warning1 = warning1,
  10336. warning2 = warning2,
  10337. bar = bar,
  10338. timer = timer,
  10339. owner = self
  10340. },
  10341. mt
  10342. )
  10343. return obj
  10344. end
  10345.  
  10346. function bossModPrototype:NewCombatTimer(timer, text, barText, barIcon)
  10347. timer = timer or 10
  10348. local bar = self:NewTimer(timer, barText or DBM_CORE_GENERIC_TIMER_COMBAT, barIcon or "Interface\\Icons\\ability_warrior_offensivestance", nil, "timer_combat")
  10349. local countdown = self:NewCountdown(0, 0, nil, false, nil, true)
  10350. local obj = setmetatable(
  10351. {
  10352. bar = bar,
  10353. timer = timer,
  10354. countdown = countdown,
  10355. owner = self
  10356. },
  10357. mt
  10358. )
  10359. return obj
  10360. end
  10361. end
  10362.  
  10363. ---------------
  10364. -- Options --
  10365. ---------------
  10366. function bossModPrototype:AddBoolOption(name, default, cat, func, extraOption)
  10367. cat = cat or "misc"
  10368. self.DefaultOptions[name] = (default == nil) or default
  10369. if cat == "timer" then
  10370. self.DefaultOptions[name.."TColor"] = extraOption or 0
  10371. end
  10372. if default and type(default) == "string" then
  10373. default = self:GetRoleFlagValue(default)
  10374. end
  10375. self.Options[name] = (default == nil) or default
  10376. if cat == "timer" then
  10377. self.Options[name.."TColor"] = extraOption or 0
  10378. end
  10379. self:SetOptionCategory(name, cat)
  10380. if func then
  10381. self.optionFuncs = self.optionFuncs or {}
  10382. self.optionFuncs[name] = func
  10383. end
  10384. end
  10385.  
  10386. function bossModPrototype:AddSpecialWarningOption(name, default, defaultSound, cat)
  10387. cat = cat or "misc"
  10388. self.DefaultOptions[name] = (default == nil) or default
  10389. self.DefaultOptions[name.."SWSound"] = defaultSound or 1
  10390. self.DefaultOptions[name.."SWNote"] = true
  10391. if default and type(default) == "string" then
  10392. default = self:GetRoleFlagValue(default)
  10393. end
  10394. self.Options[name] = (default == nil) or default
  10395. self.Options[name.."SWSound"] = defaultSound or 1
  10396. self.Options[name.."SWNote"] = true
  10397. self:SetOptionCategory(name, cat)
  10398. end
  10399.  
  10400. function bossModPrototype:AddSetIconOption(name, spellId, default, isHostile)
  10401. self.DefaultOptions[name] = (default == nil) or default
  10402. if default and type(default) == "string" then
  10403. default = self:GetRoleFlagValue(default)
  10404. end
  10405. self.Options[name] = (default == nil) or default
  10406. self:SetOptionCategory(name, "misc")
  10407. if isHostile then
  10408. if not self.findFastestComputer then
  10409. self.findFastestComputer = {}
  10410. end
  10411. self.findFastestComputer[#self.findFastestComputer + 1] = name
  10412. self.localization.options[name] = DBM_CORE_AUTO_ICONS_OPTION_TEXT2:format(spellId)
  10413. else
  10414. self.localization.options[name] = DBM_CORE_AUTO_ICONS_OPTION_TEXT:format(spellId)
  10415. end
  10416. end
  10417.  
  10418. function bossModPrototype:AddArrowOption(name, spellId, default, isRunTo)
  10419. if isRunTo == true then isRunTo = 2 end--Support legacy
  10420. self.DefaultOptions[name] = (default == nil) or default
  10421. if default and type(default) == "string" then
  10422. default = self:GetRoleFlagValue(default)
  10423. end
  10424. self.Options[name] = (default == nil) or default
  10425. self:SetOptionCategory(name, "misc")
  10426. if isRunTo == 2 then
  10427. self.localization.options[name] = DBM_CORE_AUTO_ARROW_OPTION_TEXT:format(spellId)
  10428. elseif isRunTo == 3 then
  10429. self.localization.options[name] = DBM_CORE_AUTO_ARROW_OPTION_TEXT3:format(spellId)
  10430. else
  10431. self.localization.options[name] = DBM_CORE_AUTO_ARROW_OPTION_TEXT2:format(spellId)
  10432. end
  10433. end
  10434.  
  10435. function bossModPrototype:AddRangeFrameOption(range, spellId, default)
  10436. self.DefaultOptions["RangeFrame"] = (default == nil) or default
  10437. if default and type(default) == "string" then
  10438. default = self:GetRoleFlagValue(default)
  10439. end
  10440. self.Options["RangeFrame"] = (default == nil) or default
  10441. self:SetOptionCategory("RangeFrame", "misc")
  10442. if spellId then
  10443. self.localization.options["RangeFrame"] = DBM_CORE_AUTO_RANGE_OPTION_TEXT:format(range, spellId)
  10444. else
  10445. self.localization.options["RangeFrame"] = DBM_CORE_AUTO_RANGE_OPTION_TEXT_SHORT:format(range)
  10446. end
  10447. end
  10448.  
  10449. function bossModPrototype:AddHudMapOption(name, spellId, default)
  10450. self.DefaultOptions[name] = (default == nil) or default
  10451. if default and type(default) == "string" then
  10452. default = self:GetRoleFlagValue(default)
  10453. end
  10454. self.Options[name] = (default == nil) or default
  10455. self:SetOptionCategory(name, "misc")
  10456. if spellId then
  10457. self.localization.options[name] = DBM_CORE_AUTO_HUD_OPTION_TEXT:format(spellId)
  10458. else
  10459. self.localization.options[name] = DBM_CORE_AUTO_HUD_OPTION_TEXT_MULTI
  10460. end
  10461. end
  10462.  
  10463. function bossModPrototype:AddInfoFrameOption(spellId, default)
  10464. self.DefaultOptions["InfoFrame"] = (default == nil) or default
  10465. if default and type(default) == "string" then
  10466. default = self:GetRoleFlagValue(default)
  10467. end
  10468. self.Options["InfoFrame"] = (default == nil) or default
  10469. self:SetOptionCategory("InfoFrame", "misc")
  10470. self.localization.options["InfoFrame"] = DBM_CORE_AUTO_INFO_FRAME_OPTION_TEXT:format(spellId)
  10471. end
  10472.  
  10473. function bossModPrototype:AddReadyCheckOption(questId, default)
  10474. self.readyCheckQuestId = questId
  10475. self.DefaultOptions["ReadyCheck"] = (default == nil) or default
  10476. if default and type(default) == "string" then
  10477. default = self:GetRoleFlagValue(default)
  10478. end
  10479. self.Options["ReadyCheck"] = (default == nil) or default
  10480. self.localization.options["ReadyCheck"] = DBM_CORE_AUTO_READY_CHECK_OPTION_TEXT
  10481. self:SetOptionCategory("ReadyCheck", "misc")
  10482. end
  10483.  
  10484. function bossModPrototype:AddSliderOption(name, minValue, maxValue, valueStep, default, cat, func)
  10485. cat = cat or "misc"
  10486. self.DefaultOptions[name] = {type = "slider", value = default or 0}
  10487. self.Options[name] = default or 0
  10488. self:SetOptionCategory(name, cat)
  10489. self.sliders = self.sliders or {}
  10490. self.sliders[name] = {
  10491. minValue = minValue,
  10492. maxValue = maxValue,
  10493. valueStep = valueStep,
  10494. }
  10495. if func then
  10496. self.optionFuncs = self.optionFuncs or {}
  10497. self.optionFuncs[name] = func
  10498. end
  10499. end
  10500.  
  10501. function bossModPrototype:AddButton(name, onClick, cat, func)
  10502. cat = cat or "misc"
  10503. self:SetOptionCategory(name, cat)
  10504. self.buttons = self.buttons or {}
  10505. self.buttons[name] = onClick
  10506. if func then
  10507. self.optionFuncs = self.optionFuncs or {}
  10508. self.optionFuncs[name] = func
  10509. end
  10510. end
  10511.  
  10512. -- FIXME: this function does not reset any settings to default if you remove an option in a later revision and a user has selected this option in an earlier revision were it still was available
  10513. -- this will be fixed as soon as it is necessary due to removed options ;-)
  10514. function bossModPrototype:AddDropdownOption(name, options, default, cat, func)
  10515. cat = cat or "misc"
  10516. self.DefaultOptions[name] = {type = "dropdown", value = default}
  10517. self.Options[name] = default
  10518. self:SetOptionCategory(name, cat)
  10519. self.dropdowns = self.dropdowns or {}
  10520. self.dropdowns[name] = options
  10521. if func then
  10522. self.optionFuncs = self.optionFuncs or {}
  10523. self.optionFuncs[name] = func
  10524. end
  10525. end
  10526.  
  10527. function bossModPrototype:AddOptionSpacer(cat)
  10528. cat = cat or "misc"
  10529. if self.optionCategories[cat] then
  10530. tinsert(self.optionCategories[cat], DBM_OPTION_SPACER)
  10531. end
  10532. end
  10533.  
  10534. function bossModPrototype:AddOptionLine(text, cat)
  10535. cat = cat or "misc"
  10536. if not self.optionCategories[cat] then
  10537. self.optionCategories[cat] = {}
  10538. end
  10539. if self.optionCategories[cat] then
  10540. tinsert(self.optionCategories[cat], {line = true, text = text})
  10541. end
  10542. end
  10543.  
  10544. function bossModPrototype:AddAnnounceSpacer()
  10545. return self:AddOptionSpacer("announce")
  10546. end
  10547.  
  10548. function bossModPrototype:AddTimerSpacer()
  10549. return self:AddOptionSpacer("timer")
  10550. end
  10551.  
  10552. function bossModPrototype:AddAnnounceLine(text)
  10553. return self:AddOptionLine(text, "announce")
  10554. end
  10555.  
  10556. function bossModPrototype:AddTimerLine(text)
  10557. return self:AddOptionLine(text, "timer")
  10558. end
  10559.  
  10560. function bossModPrototype:RemoveOption(name)
  10561. self.Options[name] = nil
  10562. for i, options in pairs(self.optionCategories) do
  10563. removeEntry(options, name)
  10564. if #options == 0 then
  10565. self.optionCategories[i] = nil
  10566. end
  10567. end
  10568. if self.optionFuncs then
  10569. self.optionFuncs[name] = nil
  10570. end
  10571. end
  10572.  
  10573. function bossModPrototype:SetOptionCategory(name, cat)
  10574. for _, options in pairs(self.optionCategories) do
  10575. removeEntry(options, name)
  10576. end
  10577. if not self.optionCategories[cat] then
  10578. self.optionCategories[cat] = {}
  10579. end
  10580. tinsert(self.optionCategories[cat], name)
  10581. end
  10582.  
  10583. --------------
  10584. -- Combat --
  10585. --------------
  10586. function bossModPrototype:RegisterCombat(cType, ...)
  10587. if cType then
  10588. cType = cType:lower()
  10589. end
  10590. local info = {
  10591. type = cType,
  10592. mob = self.creatureId,
  10593. eId = self.encounterId,
  10594. name = self.localization.general.name or self.id,
  10595. msgs = (cType ~= "combat") and {...},
  10596. mod = self
  10597. }
  10598. if self.multiMobPullDetection then
  10599. info.multiMobPullDetection = self.multiMobPullDetection
  10600. end
  10601. if self.multiEncounterPullDetection then
  10602. info.multiEncounterPullDetection = self.multiEncounterPullDetection
  10603. end
  10604. if self.noESDetection then
  10605. info.noESDetection = self.noESDetection
  10606. end
  10607. if self.noEEDetection then
  10608. info.noEEDetection = self.noEEDetection
  10609. end
  10610. if self.noRegenDetection then
  10611. info.noRegenDetection = self.noRegenDetection
  10612. end
  10613. if self.noWBEsync then
  10614. info.noWBEsync = self.noWBEsync
  10615. end
  10616. -- use pull-mobs as kill mobs by default, can be overriden by RegisterKill
  10617. if self.multiMobPullDetection then
  10618. for i, v in ipairs(self.multiMobPullDetection) do
  10619. info.killMobs = info.killMobs or {}
  10620. info.killMobs[v] = true
  10621. end
  10622. end
  10623. self.combatInfo = info
  10624. if not self.zones then return end
  10625. for v in pairs(self.zones) do
  10626. combatInfo[v] = combatInfo[v] or {}
  10627. tinsert(combatInfo[v], info)
  10628. end
  10629. end
  10630.  
  10631. -- needs to be called _AFTER_ RegisterCombat
  10632. function bossModPrototype:RegisterKill(msgType, ...)
  10633. if not self.combatInfo then
  10634. error("mod.combatInfo not yet initialized, use mod:RegisterCombat before using this method", 2)
  10635. end
  10636. if msgType == "kill" then
  10637. if select("#", ...) > 0 then -- calling this method with 0 IDs means "use the values from SetCreatureID", this is already done by RegisterCombat as calling RegisterKill should be optional --> mod:RegisterKill("kill") with no IDs is never necessary
  10638. self.combatInfo.killMobs = {}
  10639. for i = 1, select("#", ...) do
  10640. local v = select(i, ...)
  10641. if type(v) == "number" then
  10642. self.combatInfo.killMobs[v] = true
  10643. end
  10644. end
  10645. end
  10646. else
  10647. self.combatInfo.killType = msgType
  10648. self.combatInfo.killMsgs = {}
  10649. for i = 1, select("#", ...) do
  10650. local v = select(i, ...)
  10651. self.combatInfo.killMsgs[v] = true
  10652. end
  10653. end
  10654. end
  10655.  
  10656. function bossModPrototype:SetDetectCombatInVehicle(flag)
  10657. if not self.combatInfo then
  10658. error("mod.combatInfo not yet initialized, use mod:RegisterCombat before using this method", 2)
  10659. end
  10660. self.combatInfo.noCombatInVehicle = not flag
  10661. end
  10662.  
  10663. function bossModPrototype:SetCreatureID(...)
  10664. self.creatureId = ...
  10665. if select("#", ...) > 1 then
  10666. self.multiMobPullDetection = {...}
  10667. if self.combatInfo then
  10668. self.combatInfo.multiMobPullDetection = self.multiMobPullDetection
  10669. end
  10670. for i = 1, select("#", ...) do
  10671. local cId = select(i, ...)
  10672. bossIds[cId] = true
  10673. end
  10674. else
  10675. local cId = ...
  10676. bossIds[cId] = true
  10677. end
  10678. end
  10679.  
  10680. function bossModPrototype:SetEncounterID(...)
  10681. self.encounterId = ...
  10682. if select("#", ...) > 1 then
  10683. self.multiEncounterPullDetection = {...}
  10684. if self.combatInfo then
  10685. self.combatInfo.multiEncounterPullDetection = self.multiEncounterPullDetection
  10686. end
  10687. end
  10688. end
  10689.  
  10690. function bossModPrototype:DisableESCombatDetection()
  10691. self.noESDetection = true
  10692. if self.combatInfo then
  10693. self.combatInfo.noESDetection = true
  10694. end
  10695. end
  10696.  
  10697. function bossModPrototype:DisableEEKillDetection()
  10698. self.noEEDetection = true
  10699. if self.combatInfo then
  10700. self.combatInfo.noEEDetection = true
  10701. end
  10702. end
  10703.  
  10704. function bossModPrototype:DisableRegenDetection()
  10705. self.noRegenDetection = true
  10706. if self.combatInfo then
  10707. self.combatInfo.noRegenDetection = true
  10708. end
  10709. end
  10710.  
  10711. function bossModPrototype:DisableWBEngageSync()
  10712. self.noWBEsync = true
  10713. if self.combatInfo then
  10714. self.combatInfo.noWBEsync = true
  10715. end
  10716. end
  10717.  
  10718. function bossModPrototype:IsInCombat()
  10719. return self.inCombat
  10720. end
  10721.  
  10722. function bossModPrototype:IsAlive()
  10723. return not UnitIsDeadOrGhost("player")
  10724. end
  10725.  
  10726. function bossModPrototype:SetMinCombatTime(t)
  10727. self.minCombatTime = t
  10728. end
  10729.  
  10730. -- needs to be called after RegisterCombat
  10731. function bossModPrototype:SetWipeTime(t)
  10732. if not self.combatInfo then
  10733. error("mod.combatInfo not yet initialized, use mod:RegisterCombat before using this method", 2)
  10734. end
  10735. self.combatInfo.wipeTimer = t
  10736. end
  10737.  
  10738. -- fix for LFR ToES Tsulong combat detection bug after killed.
  10739. function bossModPrototype:SetReCombatTime(t, t2)--T1, after kill. T2 after wipe
  10740. self.reCombatTime = t
  10741. self.reCombatTime2 = t2
  10742. end
  10743.  
  10744. -----------------------
  10745. -- Synchronization --
  10746. -----------------------
  10747. function bossModPrototype:SendSync(event, ...)
  10748. event = event or ""
  10749. local arg = select("#", ...) > 0 and strjoin("\t", tostringall(...)) or ""
  10750. local str = ("%s\t%s\t%s\t%s"):format(self.id, self.revision or 0, event, arg)
  10751. local spamId = self.id .. event .. arg -- *not* the same as the sync string, as it doesn't use the revision information
  10752. local time = GetTime()
  10753. --Mod syncs are more strict and enforce latency threshold always.
  10754. --Do not put latency check in main sendSync local function (line 313) though as we still want to get version information, etc from these users.
  10755. if not modSyncSpam[spamId] or (time - modSyncSpam[spamId]) > 8 then
  10756. self:ReceiveSync(event, nil, self.revision or 0, tostringall(...))
  10757. sendSync("M", str)
  10758. end
  10759. end
  10760.  
  10761. function bossModPrototype:ReceiveSync(event, sender, revision, ...)
  10762. local spamId = self.id .. event .. strjoin("\t", ...)
  10763. local time = GetTime()
  10764. if (not modSyncSpam[spamId] or (time - modSyncSpam[spamId]) > self.SyncThreshold) and self.OnSync and (not (self.blockSyncs and sender)) and (not sender or (not self.minSyncRevision or revision >= self.minSyncRevision)) then
  10765. modSyncSpam[spamId] = time
  10766. -- we have to use the sender as last argument for compatibility reasons (stupid old API...)
  10767. -- avoid table allocations for frequently used number of arguments
  10768. if select("#", ...) <= 1 then
  10769. -- syncs with no arguments have an empty argument (also for compatibility reasons)
  10770. self:OnSync(event, ... or "", sender)
  10771. elseif select("#", ...) == 2 then
  10772. self:OnSync(event, ..., select(2, ...), sender)
  10773. else
  10774. local tmp = { ... }
  10775. tmp[#tmp + 1] = sender
  10776. self:OnSync(event, unpack(tmp))
  10777. end
  10778. end
  10779. end
  10780.  
  10781. function bossModPrototype:SetRevision(revision)
  10782. revision = tonumber(revision or "")
  10783. if not revision then
  10784. -- bad revision: either forgot the svn keyword or using git svn
  10785. revision = DBM.Revision
  10786. end
  10787. self.revision = revision
  10788. end
  10789.  
  10790. function bossModPrototype:SetMinSyncRevision(revision)
  10791. self.minSyncRevision = revision
  10792. end
  10793.  
  10794. function bossModPrototype:SetHotfixNoticeRev(revision)
  10795. self.hotfixNoticeRev = revision
  10796. end
  10797.  
  10798. -----------------
  10799. -- Scheduler --
  10800. -----------------
  10801. function bossModPrototype:Schedule(t, f, ...)
  10802. return schedule(t, f, self, ...)
  10803. end
  10804.  
  10805. function bossModPrototype:Unschedule(f, ...)
  10806. return unschedule(f, self, ...)
  10807. end
  10808.  
  10809. function bossModPrototype:ScheduleMethod(t, method, ...)
  10810. if not self[method] then
  10811. error(("Method %s does not exist"):format(tostring(method)), 2)
  10812. end
  10813. return self:Schedule(t, self[method], self, ...)
  10814. end
  10815. bossModPrototype.ScheduleEvent = bossModPrototype.ScheduleMethod
  10816.  
  10817. function bossModPrototype:UnscheduleMethod(method, ...)
  10818. if not self[method] then
  10819. error(("Method %s does not exist"):format(tostring(method)), 2)
  10820. end
  10821. return self:Unschedule(self[method], self, ...)
  10822. end
  10823. bossModPrototype.UnscheduleEvent = bossModPrototype.UnscheduleMethod
  10824.  
  10825. -------------
  10826. -- Icons --
  10827. -------------
  10828.  
  10829. do
  10830. local scanExpires = {}
  10831. local addsIcon = {}
  10832. local addsIconSet = {}
  10833.  
  10834. function bossModPrototype:SetIcon(target, icon, timer)
  10835. if not target then return end--Fix a rare bug where target becomes nil at last second (end combat fires and clears targets)
  10836. if DBM.Options.DontSetIcons or not enableIcons or DBM:GetRaidRank(playerName) == 0 then
  10837. return
  10838. end
  10839. self:UnscheduleMethod("SetIcon", target)
  10840. if type(icon) ~= "number" or type(target) ~= "string" then--icon/target probably backwards.
  10841. DBM:Debug("SetIcon is being used impropperly. Check icon/target order")
  10842. return--Fail silently instead of spamming icon lua errors if we screw up
  10843. end
  10844. icon = icon and icon >= 0 and icon <= 8 and icon or 8
  10845. local uId = DBM:GetRaidUnitId(target)
  10846. if uId and UnitIsUnit(uId, "player") and DBM:GetNumRealGroupMembers() < 2 then return end--Solo raid, no reason to put icon on yourself.
  10847. if uId or UnitExists(target) then--target accepts uid, unitname both.
  10848. uId = uId or target
  10849. --save previous icon into a table.
  10850. local oldIcon = self:GetIcon(uId) or 0
  10851. if not self.iconRestore[uId] then
  10852. self.iconRestore[uId] = oldIcon
  10853. end
  10854. --set icon
  10855. if oldIcon ~= icon then--Don't set icon if it's already set to what we're setting it to
  10856. SetRaidTarget(uId, self.iconRestore[uId] and icon == 0 and self.iconRestore[uId] or icon)
  10857. end
  10858. --schedule restoring old icon if timer enabled.
  10859. if timer then
  10860. self:ScheduleMethod(timer, "SetIcon", target, 0)
  10861. end
  10862. end
  10863. end
  10864.  
  10865. do
  10866. local iconSortTable = {}
  10867. local iconSet = 0
  10868.  
  10869. local function sort_by_group(v1, v2)
  10870. return DBM:GetRaidSubgroup(DBM:GetUnitFullName(v1)) < DBM:GetRaidSubgroup(DBM:GetUnitFullName(v2))
  10871. end
  10872.  
  10873. local function clearSortTable()
  10874. twipe(iconSortTable)
  10875. iconSet = 0
  10876. end
  10877.  
  10878. function bossModPrototype:SetIconByAlphaTable(returnFunc)
  10879. tsort(iconSortTable)--Sorted alphabetically
  10880. for i = 1, #iconSortTable do
  10881. local target = iconSortTable[i]
  10882. if i > 8 then
  10883. DBM:Debug("Too many players to set icons, reconsider where using icons", 2)
  10884. return
  10885. end
  10886. if not self.iconRestore[target] then
  10887. local oldIcon = self:GetIcon(target) or 0
  10888. self.iconRestore[target] = oldIcon
  10889. end
  10890. SetRaidTarget(target, i)--Icons match number in table in alpha sort
  10891. if returnFunc then
  10892. self[returnFunc](self, target, i)--Send icon and target to returnFunc. (Generally used by announce icon targets to raid chat feature)
  10893. end
  10894. end
  10895. C_TimerAfter(1.5, clearSortTable)--Table wipe delay so if icons go out too early do to low fps or bad latency, when they get new target on table, resort and reapplying should auto correct teh icon within .2-.4 seconds at most.
  10896. end
  10897.  
  10898. function bossModPrototype:SetAlphaIcon(delay, target, maxIcon, returnFunc)
  10899. if not target then return end
  10900. if DBM.Options.DontSetIcons or not enableIcons or DBM:GetRaidRank(playerName) == 0 then
  10901. return
  10902. end
  10903. local uId = DBM:GetRaidUnitId(target)
  10904. if uId or UnitExists(target) then--target accepts uid, unitname both.
  10905. uId = uId or target
  10906. local foundDuplicate = false
  10907. for i = #iconSortTable, 1, -1 do
  10908. if iconSortTable[i] == uId then
  10909. foundDuplicate = true
  10910. break
  10911. end
  10912. end
  10913. if not foundDuplicate then
  10914. iconSet = iconSet + 1
  10915. tinsert(iconSortTable, uId)
  10916. end
  10917. self:UnscheduleMethod("SetIconByAlphaTable")
  10918. if maxIcon and iconSet == maxIcon then
  10919. self:SetIconByAlphaTable(returnFunc)
  10920. elseif self:LatencyCheck() then--lag can fail the icons so we check it before allowing.
  10921. self:ScheduleMethod(delay or 0.5, "SetIconByAlphaTable", returnFunc)
  10922. end
  10923. end
  10924. end
  10925.  
  10926. function bossModPrototype:SetIconBySortedTable(startIcon, reverseIcon, returnFunc)
  10927. tsort(iconSortTable, sort_by_group)
  10928. local icon = startIcon or 1
  10929. for i, v in ipairs(iconSortTable) do
  10930. if not self.iconRestore[v] then
  10931. local oldIcon = self:GetIcon(v) or 0
  10932. self.iconRestore[v] = oldIcon
  10933. end
  10934. SetRaidTarget(v, icon)--do not use SetIcon function again. It already checked in SetSortedIcon function.
  10935. if reverseIcon then
  10936. icon = icon - 1
  10937. else
  10938. icon = icon + 1
  10939. end
  10940. if returnFunc then
  10941. self[returnFunc](self, v, icon)--Send icon and target to returnFunc. (Generally used by announce icon targets to raid chat feature)
  10942. end
  10943. end
  10944. C_TimerAfter(1.5, clearSortTable)--Table wipe delay so if icons go out too early do to low fps or bad latency, when they get new target on table, resort and reapplying should auto correct teh icon within .2-.4 seconds at most.
  10945. end
  10946.  
  10947. function bossModPrototype:SetSortedIcon(delay, target, startIcon, maxIcon, reverseIcon, returnFunc)
  10948. if not target then return end
  10949. if DBM.Options.DontSetIcons or not enableIcons or DBM:GetRaidRank(playerName) == 0 then
  10950. return
  10951. end
  10952. if not startIcon then startIcon = 1 end
  10953. startIcon = startIcon and startIcon >= 0 and startIcon <= 8 and startIcon or 8
  10954. local uId = DBM:GetRaidUnitId(target)
  10955. if uId or UnitExists(target) then--target accepts uid, unitname both.
  10956. uId = uId or target
  10957. local foundDuplicate = false
  10958. for i = #iconSortTable, 1, -1 do
  10959. if iconSortTable[i] == uId then
  10960. foundDuplicate = true
  10961. break
  10962. end
  10963. end
  10964. if not foundDuplicate then
  10965. iconSet = iconSet + 1
  10966. tinsert(iconSortTable, uId)
  10967. end
  10968. self:UnscheduleMethod("SetIconBySortedTable")
  10969. if maxIcon and iconSet == maxIcon then
  10970. self:SetIconBySortedTable(startIcon, reverseIcon, returnFunc)
  10971. elseif self:LatencyCheck() then--lag can fail the icons so we check it before allowing.
  10972. self:ScheduleMethod(delay or 0.5, "SetIconBySortedTable", startIcon, reverseIcon, returnFunc)
  10973. end
  10974. end
  10975. end
  10976. end
  10977.  
  10978. function bossModPrototype:GetIcon(uId)
  10979. return UnitExists(uId) and GetRaidTargetIndex(uId)
  10980. end
  10981.  
  10982. function bossModPrototype:RemoveIcon(target)
  10983. return self:SetIcon(target, 0)
  10984. end
  10985.  
  10986. function bossModPrototype:ClearIcons()
  10987. if IsInRaid() then
  10988. for i = 1, GetNumGroupMembers() do
  10989. if UnitExists("raid"..i) and GetRaidTargetIndex("raid"..i) then
  10990. SetRaidTarget("raid"..i, 0)
  10991. end
  10992. end
  10993. else
  10994. for i = 1, GetNumSubgroupMembers() do
  10995. if UnitExists("party"..i) and GetRaidTargetIndex("party"..i) then
  10996. SetRaidTarget("party"..i, 0)
  10997. end
  10998. end
  10999. end
  11000. end
  11001.  
  11002. function bossModPrototype:CanSetIcon(optionName)
  11003. if canSetIcons[optionName] then
  11004. return true
  11005. end
  11006. return false
  11007. end
  11008.  
  11009. local mobUids = {"mouseover", "target", "boss1", "boss2", "boss3", "boss4", "boss5"}
  11010. function bossModPrototype:ScanForMobs(creatureID, iconSetMethod, mobIcon, maxIcon, scanInterval, scanningTime, optionName, isFriendly, secondCreatureID)
  11011. if not optionName then optionName = self.findFastestComputer[1] end
  11012. if canSetIcons[optionName] then
  11013. --Declare variables.
  11014. local timeNow = GetTime()
  11015. local creatureID = creatureID--This function must not be used to boss, so remove self.creatureId. Accepts cid, guid and cid table
  11016. local iconSetMethod = iconSetMethod or 0--Set IconSetMethod -- 0: Descending / 1:Ascending / 2: Force Set / 9:Force Stop
  11017. --With different scanID, this function can support multi scanning same time. Required for Nazgrim.
  11018. local scanID = 0
  11019. if type(creatureID) == "number" then
  11020. scanID = creatureID --guid and table no not supports multi scanning. only cid supports multi scanning
  11021. end
  11022. if iconSetMethod == 9 then--Force stop scanning
  11023. --clear variables
  11024. scanExpires[scanID] = nil
  11025. addsIcon[scanID] = nil
  11026. addsIconSet[scanID] = nil
  11027. return
  11028. end
  11029. if not addsIcon[scanID] then addsIcon[scanID] = mobIcon or 8 end
  11030. if not addsIconSet[scanID] then addsIconSet[scanID] = 0 end
  11031. if not scanExpires[scanID] then scanExpires[scanID] = timeNow + scanningTime end
  11032. local maxIcon = maxIcon or 8 --We only have 8 icons.
  11033. local isFriendly = isFriendly or false
  11034. local secondCreatureID = secondCreatureID or 0
  11035. local scanInterval = scanInterval or 0.2
  11036. local scanningTime = scanningTime or 8
  11037. --DO SCAN NOW
  11038. for uId in DBM:GetGroupMembers() do
  11039. if not self.iconRestore[uId] then
  11040. local oldIcon = self:GetIcon(uId) or 0
  11041. self.iconRestore[uId] = oldIcon
  11042. end
  11043. local unitid = uId.."target"
  11044. local guid = UnitGUID(unitid)
  11045. local cid = self:GetCIDFromGUID(guid)
  11046. local isEnemy = UnitIsEnemy("player", unitid) or true--If api returns nil, assume it's an enemy
  11047. local isFiltered = false
  11048. if not isFriendly and not isEnemy then
  11049. isFiltered = true
  11050. DBM:Debug("ScanForMobs aborting because friendly mob", 2)
  11051. end
  11052. if not isFiltered then
  11053. if guid and type(creatureID) == "table" and creatureID[cid] and not addsGUIDs[guid] then
  11054. if type(creatureID[cid]) == "number" then
  11055. SetRaidTarget(unitid, creatureID[cid])
  11056. else
  11057. SetRaidTarget(unitid, addsIcon[scanID])
  11058. if iconSetMethod == 1 then
  11059. addsIcon[scanID] = addsIcon[scanID] + 1
  11060. else
  11061. addsIcon[scanID] = addsIcon[scanID] - 1
  11062. end
  11063. end
  11064. addsGUIDs[guid] = true
  11065. addsIconSet[scanID] = addsIconSet[scanID] + 1
  11066. if addsIconSet[scanID] >= maxIcon then--stop scan immediately to save cpu
  11067. --clear variables
  11068. scanExpires[scanID] = nil
  11069. addsIcon[scanID] = nil
  11070. addsIconSet[scanID] = nil
  11071. return
  11072. end
  11073. elseif guid and ((guid == creatureID) or (cid == creatureID or cid == secondCreatureID)) and not addsGUIDs[guid] then
  11074. if iconSetMethod == 2 then
  11075. SetRaidTarget(unitid, mobIcon)
  11076. else
  11077. SetRaidTarget(unitid, addsIcon[scanID])
  11078. if iconSetMethod == 1 then
  11079. addsIcon[scanID] = addsIcon[scanID] + 1
  11080. else
  11081. addsIcon[scanID] = addsIcon[scanID] - 1
  11082. end
  11083. end
  11084. addsGUIDs[guid] = true
  11085. addsIconSet[scanID] = addsIconSet[scanID] + 1
  11086. if addsIconSet[scanID] >= maxIcon then--stop scan immediately to save cpu
  11087. --clear variables
  11088. scanExpires[scanID] = nil
  11089. addsIcon[scanID] = nil
  11090. addsIconSet[scanID] = nil
  11091. return
  11092. end
  11093. end
  11094. end
  11095. end
  11096. for _, unitid2 in ipairs(mobUids) do
  11097. local guid2 = UnitGUID(unitid2)
  11098. local cid2 = self:GetCIDFromGUID(guid2)
  11099. local isEnemy = UnitIsEnemy("player", unitid2)
  11100. local isFiltered = false
  11101. if not isFriendly and not isEnemy then
  11102. isFiltered = true
  11103. end
  11104. if not isFiltered then
  11105. if guid2 and type(creatureID) == "table" and creatureID[cid2] and not addsGUIDs[guid2] then
  11106. if type(creatureID[cid2]) == "number" then
  11107. SetRaidTarget(unitid2, creatureID[cid2])
  11108. else
  11109. SetRaidTarget(unitid2, addsIcon[scanID])
  11110. if iconSetMethod == 1 then
  11111. addsIcon[scanID] = addsIcon[scanID] + 1
  11112. else
  11113. addsIcon[scanID] = addsIcon[scanID] - 1
  11114. end
  11115. end
  11116. addsGUIDs[guid2] = true
  11117. addsIconSet[scanID] = addsIconSet[scanID] + 1
  11118. if addsIconSet[scanID] >= maxIcon then--stop scan immediately to save cpu
  11119. --clear variables
  11120. scanExpires[scanID] = nil
  11121. addsIcon[scanID] = nil
  11122. addsIconSet[scanID] = nil
  11123. return
  11124. end
  11125. elseif guid2 and ((guid2 == creatureID) or (cid2 == creatureID or cid2 == secondCreatureID)) and not addsGUIDs[guid2] then
  11126. if iconSetMethod == 2 then
  11127. SetRaidTarget(unitid2, mobIcon)
  11128. else
  11129. SetRaidTarget(unitid2, addsIcon[scanID])
  11130. if iconSetMethod == 1 then
  11131. addsIcon[scanID] = addsIcon[scanID] + 1
  11132. else
  11133. addsIcon[scanID] = addsIcon[scanID] - 1
  11134. end
  11135. end
  11136. addsGUIDs[guid2] = true
  11137. addsIconSet[scanID] = addsIconSet[scanID] + 1
  11138. if addsIconSet[scanID] >= maxIcon then--stop scan immediately to save cpu
  11139. --clear variables
  11140. scanExpires[scanID] = nil
  11141. addsIcon[scanID] = nil
  11142. addsIconSet[scanID] = nil
  11143. return
  11144. end
  11145. end
  11146. end
  11147. end
  11148. if timeNow < scanExpires[scanID] then--scan for limited times.
  11149. self:ScheduleMethod(scanInterval, "ScanForMobs", creatureID, iconSetMethod, mobIcon, maxIcon, scanInterval, scanningTime, optionName, isFriendly, secondCreatureID)
  11150. else
  11151. DBM:Debug("Stopping ScanForMobs for: "..(optionName or "nil"), 2)
  11152. --clear variables
  11153. scanExpires[scanID] = nil
  11154. addsIcon[scanID] = nil
  11155. addsIconSet[scanID] = nil
  11156. --Do not wipe adds GUID table here, it's wiped by :Stop() which is called by EndCombat
  11157. end
  11158. end
  11159. end
  11160. end
  11161.  
  11162. -----------------------
  11163. -- Model Functions --
  11164. -----------------------
  11165. function bossModPrototype:SetModelScale(scale)
  11166. self.modelScale = scale
  11167. end
  11168.  
  11169. function bossModPrototype:SetModelOffset(x, y, z)
  11170. self.modelOffsetX = x
  11171. self.modelOffsetY = y
  11172. self.modelOffsetZ = z
  11173. end
  11174.  
  11175. function bossModPrototype:SetModelRotation(r)
  11176. self.modelRotation = r
  11177. end
  11178.  
  11179. function bossModPrototype:SetModelMoveSpeed(v)
  11180. self.modelMoveSpeed = v
  11181. end
  11182.  
  11183. function bossModPrototype:SetModelID(id)
  11184. self.modelId = id
  11185. end
  11186.  
  11187. function bossModPrototype:SetModelSound(long, short)--PlaySoundFile prototype for model viewer, long is long sound, short is a short clip, configurable in UI, both sound paths defined in boss mods.
  11188. self.modelSoundLong = long
  11189. self.modelSoundShort = short
  11190. end
  11191.  
  11192. function bossModPrototype:EnableModel()
  11193. self.modelEnabled = true
  11194. end
  11195.  
  11196. function bossModPrototype:DisableModel()
  11197. self.modelEnabled = nil
  11198. end
  11199.  
  11200. --------------------
  11201. -- Localization --
  11202. --------------------
  11203. function bossModPrototype:GetLocalizedStrings()
  11204. self.localization.miscStrings.name = self.localization.general.name
  11205. return self.localization.miscStrings
  11206. end
  11207.  
  11208. -- Not really good, needs a few updates
  11209. do
  11210. local modLocalizations = {}
  11211. local modLocalizationPrototype = {}
  11212. local mt = {__index = modLocalizationPrototype}
  11213. local returnKey = {__index = function(t, k) return k end}
  11214. local defaultCatLocalization = {
  11215. __index = setmetatable({
  11216. timer = DBM_CORE_OPTION_CATEGORY_TIMERS,
  11217. announce = DBM_CORE_OPTION_CATEGORY_WARNINGS,
  11218. announceother = DBM_CORE_OPTION_CATEGORY_WARNINGS_OTHER,
  11219. announcepersonal = DBM_CORE_OPTION_CATEGORY_WARNINGS_YOU,
  11220. announcerole = DBM_CORE_OPTION_CATEGORY_WARNINGS_ROLE,
  11221. sound = DBM_CORE_OPTION_CATEGORY_SOUNDS,
  11222. misc = MISCELLANEOUS
  11223. }, returnKey)
  11224. }
  11225. local defaultTimerLocalization = {
  11226. __index = setmetatable({
  11227. timer_berserk = DBM_CORE_GENERIC_TIMER_BERSERK,
  11228. timer_combat = DBM_CORE_GENERIC_TIMER_COMBAT,
  11229. TimerSpeedKill = DBM_CORE_ACHIEVEMENT_TIMER_SPEED_KILL
  11230. }, returnKey)
  11231. }
  11232. local defaultAnnounceLocalization = {
  11233. __index = setmetatable({
  11234. warning_berserk = DBM_CORE_GENERIC_WARNING_BERSERK
  11235. }, returnKey)
  11236. }
  11237. local defaultOptionLocalization = {
  11238. __index = setmetatable({
  11239. timer_berserk = DBM_CORE_OPTION_TIMER_BERSERK,
  11240. timer_combat = DBM_CORE_OPTION_TIMER_COMBAT,
  11241. HealthFrame = DBM_CORE_OPTION_HEALTH_FRAME,
  11242. }, returnKey)
  11243. }
  11244. local defaultMiscLocalization = {
  11245. __index = {}
  11246. }
  11247.  
  11248. function modLocalizationPrototype:SetGeneralLocalization(t)
  11249. for i, v in pairs(t) do
  11250. self.general[i] = v
  11251. end
  11252. end
  11253.  
  11254. function modLocalizationPrototype:SetWarningLocalization(t)
  11255. for i, v in pairs(t) do
  11256. self.warnings[i] = v
  11257. end
  11258. end
  11259.  
  11260. function modLocalizationPrototype:SetTimerLocalization(t)
  11261. for i, v in pairs(t) do
  11262. self.timers[i] = v
  11263. end
  11264. end
  11265.  
  11266. function modLocalizationPrototype:SetOptionLocalization(t)
  11267. for i, v in pairs(t) do
  11268. self.options[i] = v
  11269. end
  11270. end
  11271.  
  11272. function modLocalizationPrototype:SetOptionCatLocalization(t)
  11273. for i, v in pairs(t) do
  11274. self.cats[i] = v
  11275. end
  11276. end
  11277.  
  11278. function modLocalizationPrototype:SetMiscLocalization(t)
  11279. for i, v in pairs(t) do
  11280. self.miscStrings[i] = v
  11281. end
  11282. end
  11283.  
  11284. function DBM:CreateModLocalization(name)
  11285. name = tostring(name)
  11286. local obj = {
  11287. general = setmetatable({}, returnKey),
  11288. warnings = setmetatable({}, defaultAnnounceLocalization),
  11289. options = setmetatable({}, defaultOptionLocalization),
  11290. timers = setmetatable({}, defaultTimerLocalization),
  11291. miscStrings = setmetatable({}, defaultMiscLocalization),
  11292. cats = setmetatable({}, defaultCatLocalization),
  11293. }
  11294. setmetatable(obj, mt)
  11295. modLocalizations[name] = obj
  11296. return obj
  11297. end
  11298.  
  11299. function DBM:GetModLocalization(name)
  11300. name = tostring(name)
  11301. return modLocalizations[name] or self:CreateModLocalization(name)
  11302. end
  11303. end
Add Comment
Please, Sign In to add comment